Pebble App Modding to Include Notification Filters

Pebble getting filters for notifications 
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());
    }
}

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);
}

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;

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);
     }
}

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)) {

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...

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

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

We change that jump to our code:

    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

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) { }
			}
		}
	}
}

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

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.

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:

Pebble application modded with a notification filter

You can get a modded 4.3.0 version here.

8 thoughts to “Pebble App Modding to Include Notification Filters”

  1. Hey, are you maybe thinking in patching the last pebble app version (4.3.0)? As the company doesn’t exist anymore we won’t see any more updates, so having the last version patched will be interesting for a lot of users I think…

    1. Good idea! I didn’t even know there was a 4.3.0. Since I’m using my own versions I never receive the official updates and I always forget to check if there was any.

  2. This is fantastic! I’ve been wrestling with Pebble Notifications for ages now and this is the ammunition I need to finally manage them. Have you done one for 4.3.0? If not, I might do it, and add in some additional features that I like from NC while I do.

    1. Well, I’ve been trying this for a while now and I’m simply not experienced enough to do this kind of thing. Some of this parsing code has changed in the new 4.3.0 app, and I think I just about managed to follow it to insert the new assembler. However, you also added a menu option etc and that, for now, is beyond me.

      Graciously awaiting you to try your hand at the 4.3.0 version.

  3. I updated the modded version to 4.3.0, I guess that’s the last official Pebble client for Android we will ever had. If Pebble opens the client code, I could do way more stuff, and improve the Pebble a lot, with things like these but with a more friendly GUI and stuff. I hope they open the code soon.

    1. Any chance you could release an updated version (4.4.2 I believe) which is compatible with the Rebble Web Services?

Leave a Reply