

UPDATE 01/22/2017: You can find a modded 4.3.0 version with the modifications from this post clicking here.

I really like my Pebble. Also, I really like notifications on my Pebble. But I really hate that I can’t filter those annoying WhatsApp groups notifications in my Pebble without losing all my WhatsApp notifications. The Pebble really needs a notification filtering system.

Most people solve this problem by deactivating the stock notifications and installing a third-party application for that. But usually, those applications are really slow sending the notifications and lose a lot of them. I tried several but I didn’t like any of them. So I decided I wanted the stock notifications but with regular expression filtering and went ahead to modify the original Pebble application for Android.

Basically what I needed is to check the notification against some regular expressions before the Pebble application sends it to the watch. An Android application needs to create a NotificationListenerService in order to get the notifications from the phone, and implement the method onNotificationPosted to receive the notification and process it. We’re going to find where this is using a Dex to Java decompiler called jadx.

For this article we’re going to use the Pebble for Android v4.0.0-1209-98b6e71.

Now we look for the method onNotificationPosted to see where it is implemented using the search button on jadx. We can find it quickly in the class com.getpebble.android.notifications.PblNotificationService :

public void onNotificationPosted(StatusBarNotification statusBarNotification) { if (statusBarNotification == null) { try { z.b("PblNotificationService", "onNotificationPosted: sbn is null"); } catch (Throwable e) { z.a("PblNotificationService", "Error handling notification", e); l.a("PblNotificationService", false, e); } catch (Throwable e2) { z.a("PblNotificationService", "Unrecoverable error occurred handling notification", e2); l.a("PblNotificationService", true, e2); } } else { z.d("PblNotificationService", "onNotificationPosted(" + statusBarNotification + ")"); com.getpebble.android.bluetooth.b.d.a(new b(this, statusBarNotification)); z.e("PblNotificationService", "end onNotificationPosted id = " + statusBarNotification.getId()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void onNotificationPosted ( StatusBarNotification statusBarNotification ) { if ( statusBarNotification == null ) { try { z . b ( "PblNotificationService" , "onNotificationPosted: sbn is null" ) ; } catch ( Throwable e ) { z . a ( "PblNotificationService" , "Error handling notification" , e ) ; l . a ( "PblNotificationService" , false , e ) ; } catch ( Throwable e2 ) { z . a ( "PblNotificationService" , "Unrecoverable error occurred handling notification" , e2 ) ; l . a ( "PblNotificationService" , true , e2 ) ; } } else { z . d ( "PblNotificationService" , "onNotificationPosted(" + statusBarNotification + ")" ) ; com . getpebble . android . bluetooth . b . d . a ( new b ( this , statusBarNotification ) ) ; z . e ( "PblNotificationService" , "end onNotificationPosted id = " + statusBarNotification . getId ( ) ) ; } }

This is a little obfuscated but it’s still very readable. You can immediately realize that z.d() is used for logging, and the notification is processed in a thread defined in the class com.getpebble.android.notifications.b() before sending it via BlueTooth to the watch. This class b has just one method called run :

public void run() { l.a(this.b.getApplicationContext(), false); o a = o.a(this.a, s.NOTIFICATION, System.currentTimeMillis()); String g = a.g(); if (!(TextUtils.isEmpty(g) || a.o() || g.equals("com.getpebble.android.basalt"))) { cf.a(g, System.currentTimeMillis(), PebbleApplication.D().getContentResolver()); } e.a(a); } 1 2 3 4 5 6 7 8 9 public void run ( ) { l . a ( this . b . getApplicationContext ( ) , false ) ; o a = o . a ( this . a , s . NOTIFICATION , System . currentTimeMillis ( ) ) ; String g = a . g ( ) ; if ( ! ( TextUtils . isEmpty ( g ) || a . o ( ) || g . equals ( "com.getpebble.android.basalt" ) ) ) { cf . a ( g , System . currentTimeMillis ( ) , PebbleApplication . D ( ) . getContentResolver ( ) ) ; } e . a ( a ) ; }

The class o is the notification parsed and the class e will process it. This class belongs to:

import com.getpebble.android.framework.i.e; 1 import com . getpebble . android . framework . i . e ;

And that’s finally what we were looking for. This class is where the application decides to send the notification to the watch depending on things like quiet time, or if the application that is sending the notification is not selected by the user, etc. Eventually we reach this code in the method d :

de parseRecordFrom = dd.parseRecordFrom(oVar); eq a2 = a(a(oVar, parseRecordFrom.notificationUuid)); if (oVar.p()) { z.d("PebbleNotificationProcessor", "Notification is duplicate; skipping"); dd.markAsDup(PebbleApplication.D().getContentResolver(), parseRecordFrom.notificationUuid); a(oVar, false); } else if (m.a(oVar)) { z.d("PebbleNotificationProcessor", "Notification is calendar invite via gmail; not sending notification"); a(oVar, false); } else { z.d("PebbleNotificationProcessor", "sending timelineModel to Pebble."); if (a(a2)) { b(parseRecordFrom); a(oVar, parseRecordFrom); a(oVar, true); } else { a(oVar, false); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 de parseRecordFrom = dd . parseRecordFrom ( oVar ) ; eq a2 = a ( a ( oVar , parseRecordFrom . notificationUuid ) ) ; if ( oVar . p ( ) ) { z . d ( "PebbleNotificationProcessor" , "Notification is duplicate; skipping" ) ; dd . markAsDup ( PebbleApplication . D ( ) . getContentResolver ( ) , parseRecordFrom . notificationUuid ) ; a ( oVar , false ) ; } else if ( m . a ( oVar ) ) { z . d ( "PebbleNotificationProcessor" , "Notification is calendar invite via gmail; not sending notification" ) ; a ( oVar , false ) ; } else { z . d ( "PebbleNotificationProcessor" , "sending timelineModel to Pebble." ) ; if ( a ( a2 ) ) { b ( parseRecordFrom ) ; a ( oVar , parseRecordFrom ) ; a ( oVar , true ) ; } else { a ( oVar , false ) ; } }

Let’s change that to:

if (oVar.p()) { z.d("PebbleNotificationProcessor", "Notification is duplicate; skipping"); dd.markAsDup(PebbleApplication.D().getContentResolver(), parseRecordFrom.notificationUuid); a(oVar, false); } else if (hacks.matchesExcludeList(parseRecordFrom.androidPackageName, parseRecordFrom.title, parseRecordFrom.body)) { z.d("PebbleNotificationProcessor", "Notification title/body matches an exclude string; skipping"); a(oVar, false); } else if (m.a(oVar)) { 1 2 3 4 5 6 7 8 if ( oVar . p ( ) ) { z . d ( "PebbleNotificationProcessor" , "Notification is duplicate; skipping" ) ; dd . markAsDup ( PebbleApplication . D ( ) . getContentResolver ( ) , parseRecordFrom . notificationUuid ) ; a ( oVar , false ) ; } else if ( hacks . matchesExcludeList ( parseRecordFrom . androidPackageName , parseRecordFrom . title , parseRecordFrom . body ) ) { z . d ( "PebbleNotificationProcessor" , "Notification title/body matches an exclude string; skipping" ) ; a ( oVar , false ) ; } else if ( m . a ( oVar ) ) {

Just adding our new check there that if the notification matches and exclude list, it won’t be sent to the watch.

Now how can we do that? It’s not like we can compile all this code again in Java and then convert it to Dalvik. We have to modify this class in assembler, and that’s the fun part of it!

First install apktool (if you use Debian it’s as easy as apt-get install apktool) and unpack the apk:

naikel@debian ~/Pebble $ apktool d original/Pebble_4.0.0-1209-98b6e71.apk I: Using Apktool 2.2.0-dirty on Pebble_4.0.0-1209-98b6e71.apk I: Loading resource table... I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: /home/naikel/.local/share/apktool/framework/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files... 1 2 3 4 5 6 7 8 9 10 11 12 naikel @ debian ~ / Pebble $ apktool d original / Pebble_4 . 0.0 - 1209 - 98b6e71.apk I : Using Apktool 2.2.0 - dirty on Pebble_4 . 0.0 - 1209 - 98b6e71.apk I : Loading resource table . . . I : Decoding AndroidManifest .xml with resources . . . I : Loading resource table from file : / home / naikel / .local / share / apktool / framework / 1.apk I : Regular manifest package . . . I : Decoding file - resources . . . I : Decoding values * / * XMLs . . . I : Baksmaling classes .dex . . . I : Copying assets and libs . . . I : Copying unknown files . . . I : Copying original files . . .

And then modify the smali file for com.getpebble.android.framework.i.e . First raise the number of registers in the method:

.method public declared-synchronized d(Lcom/getpebble/android/notifications/a/o;)V .locals 8 1 2 .method public declared - synchronized d ( Lcom / getpebble / android / notifications / a / o ; ) V .locals 8

And then before checking the next condition in the if above:

if-eqz v2, :cond_3 .line 222 const-string v1, "PebbleNotificationProcessor" const-string v2, "Notification is duplicate; skipping" invoke-static {v1, v2}, Lcom/getpebble/android/common/b/a/z;->;d(Ljava/lang/String;Ljava/lang/String;)V 1 2 3 4 5 6 if - eqz v2 , : cond _ 3 .line 222 const - string v1 , "PebbleNotificationProcessor" const - string v2 , "Notification is duplicate; skipping" invoke - static { v1 , v2 } , Lcom / getpebble / android / common / b / a / z ; -> ; d ( Ljava / lang / String ; Ljava / lang / String ; ) V

We change that jump to our code:

if-eqz v2, :cond_5150 1 if - eqz v2 , : cond_5150

Here’s our code in smali for the check above:

:cond_5150 iget-object v7, v0, Lcom/getpebble/android/common/model/de;->androidPackageName:Ljava/lang/String; iget-object v4, v0, Lcom/getpebble/android/common/model/de;->title:Ljava/lang/String; iget-object v5, v0, Lcom/getpebble/android/common/model/de;->body:Ljava/lang/String; invoke-static {v7, v4, v5}, Lcom/scorpius/hacks;->matchesExcludeList(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z move-result v6 if-eqz v6, :cond_3 const-string v0, "PebbleNotificationProcessor" const-string v1, "Notification title/body matches an exclude string; skipping" invoke-static {v0, v1}, Lcom/getpebble/android/common/b/a/z;->d(Ljava/lang/String;Ljava/lang/String;)V const/4 v0, 0x0 invoke-virtual {p0, p1, v0}, Lcom/getpebble/android/framework/i/e;->a(Lcom/getpebble/android/notifications/a/o;Z)V goto :goto_0 1 2 3 4 5 6 7 8 9 10 11 12 13 : cond_5150 iget - object v7 , v0 , Lcom / getpebble / android / common / model / de ; -> androidPackageName : Ljava / lang / String ; iget - object v4 , v0 , Lcom / getpebble / android / common / model / de ; -> title : Ljava / lang / String ; iget - object v5 , v0 , Lcom / getpebble / android / common / model / de ; -> body : Ljava / lang / String ; invoke - static { v7 , v4 , v5 } , Lcom / scorpius / hacks ; -> matchesExcludeList ( Ljava / lang / String ; Ljava / lang / String ; Ljava / lang / String ; ) Z move - result v6 if - eqz v6 , : cond_3 const - string v0 , "PebbleNotificationProcessor" const - string v1 , "Notification title/body matches an exclude string; skipping" invoke - static { v0 , v1 } , Lcom / getpebble / android / common / b / a / z ; -> d ( Ljava / lang / String ; Ljava / lang / String ; ) V const / 4 v0 , 0x0 invoke - virtual { p0 , p1 , v0 } , Lcom / getpebble / android / framework / i / e ; -> a ( Lcom / getpebble / android / notifications / a / o ; Z ) V goto : goto_0

And now you can define the static method com.scorpius.hacks.matchesExcludeList however you want. This time you can write it in Java. This is what I did: I put all my rules in regular expressions in a file in /sdcard/pblExcludeList.txt with the following format:

application-regexp:content-regexp

For example:

.*whatsapp.*:.*annoying group title.*

And then I wrote the following code to read that file and check on my rules on every single notification before sending it to the watch:

package com.scorpius; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.os.Environment; public class hacks { private static List regexps = null; public static boolean matchesExcludeList(String packageName, String title, String body) { BufferedWriter bw = null; if (packageName == null) return false; if (regexps == null) readFile(); for (String rule : regexps) { // Validate that the rule is a valid rule // Rule format: // Application:Regexp if (rule != null) { int divider; if ((divider = rule.indexOf(":")) >= 0) { String application = rule.substring(0, divider); String regexp = rule.substring(divider + 1); Pattern p = Pattern.compile(application); Matcher m = p.matcher(packageName); if (m.matches()) { p = Pattern.compile(regexp, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); m = p.matcher(title); if (m.matches()) return true; m = p.matcher(body); if (m.matches()) return true; } } } } return false; } public static void readFile() { regexps = new LinkedList<String>(); BufferedReader br = null; try { File sdcard = Environment.getExternalStorageDirectory(); File file = new File(sdcard, "pblExcludeList.txt"); br = new BufferedReader(new FileReader(file)); String line; while ((line = br.readLine()) != null) { regexps.add(line); } } catch (Exception e) { // What could I do? } finally { if (br != null) { try { br.close(); } catch (IOException e) { } } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package com . scorpius ; import java . io . BufferedReader ; import java . io . BufferedWriter ; import java . io . File ; import java . io . FileNotFoundException ; import java . io . FileReader ; import java . io . FileWriter ; import java . io . IOException ; import java . util . LinkedList ; import java . util . List ; import java . util . regex . Matcher ; import java . util . regex . Pattern ; import android . os . Environment ; public class hacks { private static List regexps = null ; public static boolean matchesExcludeList ( String packageName , String title , String body ) { BufferedWriter bw = null ; if ( packageName == null ) return false ; if ( regexps == null ) readFile ( ) ; for ( String rule : regexps ) { // Validate that the rule is a valid rule // Rule format: // Application:Regexp if ( rule != null ) { int divider ; if ( ( divider = rule . indexOf ( ":" ) ) >= 0 ) { String application = rule . substring ( 0 , divider ) ; String regexp = rule . substring ( divider + 1 ) ; Pattern p = Pattern . compile ( application ) ; Matcher m = p . matcher ( packageName ) ; if ( m . matches ( ) ) { p = Pattern . compile ( regexp , Pattern . DOTALL | Pattern . CASE_INSENSITIVE ) ; m = p . matcher ( title ) ; if ( m . matches ( ) ) return true ; m = p . matcher ( body ) ; if ( m . matches ( ) ) return true ; } } } } return false ; } public static void readFile ( ) { regexps = new LinkedList <String> ( ) ; BufferedReader br = null ; try { File sdcard = Environment . getExternalStorageDirectory ( ) ; File file = new File ( sdcard , "pblExcludeList.txt" ) ; br = new BufferedReader ( new FileReader ( file ) ) ; String line ; while ( ( line = br . readLine ( ) ) != null ) { regexps . add ( line ) ; } } catch ( Exception e ) { // What could I do? } finally { if ( br != null ) { try { br . close ( ) ; } catch ( IOException e ) { } } } } }

Now compile it, convert it to dex, convert it to smali and include it in the Pebble application. Somehow it has to be Java 7:

naikel@debian ~/Pebble/hacks $ ~/jdk1.7.0_03/bin/javac -cp android.jar com/scorpius/hacks.java naikel@debian ~/Pebble/hacks $ dalvik-exchange --dex --output=classes.dex com/scorpius/hacks.class naikel@debian ~/Pebble/hacks $ java -jar baksmali-2.1.3.jar classes.dex naikel@debian ~/Pebble/hacks $ cp -r out/com/scorpius ~/Pebble/Pebble_4.0.0-1209-98b6e71/smali/com 1 2 3 4 naikel @ debian ~ / Pebble / hacks $ ~ / jdk1 . 7.0_03 / bin / javac - cp android .jar com / scorpius / hacks .java naikel @ debian ~ / Pebble / hacks $ dalvik - exchange -- dex -- output = classes .dex com / scorpius / hacks .class naikel @ debian ~ / Pebble / hacks $ java - jar baksmali - 2.1.3.jar classes .dex naikel @ debian ~ / Pebble / hacks $ cp - r out / com / scorpius ~ / Pebble / Pebble_4 . 0.0 - 1209 - 98b6e71 / smali / com

Finally build your APK and sign it:

naikel@debian ~/Pebble $ apktool b Pebble_4.0.0-1209-98b6e71 I: Using Apktool 2.2.0-dirty I: Checking whether sources has changed... I: Smaling smali folder into classes.dex... I: Checking whether resources has changed... I: Building resources... I: Copying libs... (/lib) I: Building apk file... I: Copying unknown files/dir... naikel@debian ~/Pebble $ jarsigner -storepass testing -keystore pebble-modded.keystore Pebble_4.0.0-1209-98b6e71/dist/Pebble_4.0.0-1209-98b6e71.apk pebble-modded jar signed. Warning: No -tsa or -tsacert is provided and this jar is not timestamped. Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date (2044-01-14) or after any future revocation date. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 naikel @ debian ~ / Pebble $ apktool b Pebble_4 . 0.0 - 1209 - 98b6e71 I : Using Apktool 2.2.0 - dirty I : Checking whether sources has changed . . . I : Smaling smali folder into classes .dex . . . I : Checking whether resources has changed . . . I : Building resources . . . I : Copying libs . . . ( / lib ) I : Building apk file . . . I : Copying unknown files / dir . . . naikel @ debian ~ / Pebble $ jarsigner - storepass testing - keystore pebble - modded .keystore Pebble_4 . 0.0 - 1209 - 98b6e71 / dist / Pebble_4 . 0.0 - 1209 - 98b6e71.apk pebble - modded jar signed . Warning : No - tsa or - tsacert is provided and this jar is not timestamped . Without a timestamp , users may not be able to validate this jar after the signer certificate ' s expiration date ( 2044 - 01 - 14 ) or after any future revocation date .

And now you can install it in your phone. Remember that you have to uninstall the official Pebble application first since the signatures are different. Sadly you will lose the past health data in your phone but it will still be there in the watch. Also, you have to enable the option to allow installation of apps from unknown sources in your phone. If you are not getting any notifications you will need to turn off and then back on the notification access of the Pebble app in your phone, but I really prefer to reboot the phone after installing the app.

Want to try my version? I added a menu option to reload the file anytime:

You can get a modded 4.3.0 version here.