How to reply to Android App Notifications — the magic of Can’t Talk

  • by

One of the things I’ve always loved about Android is it’s openness. It’s a real operating system allowing you to create, conceptualise & develop innovative apps that enhance the users experience. That’s where I’ve found my niche when it comes to my own personal apps like ReadItToMe, Flotifications, Super Simple Sleep Timer — almost all of them are what I call utility apps, you set them up one time & they’ll run quietly in the background, enhancing the user experience, as if they were a part of the Android system itself. At least that’s my goal, and my latest app Can’t Talk is no different.

The first message I got in my XDA inbox when the app got mentioned on XDA Portal was how is my app able to reply to Whatsapp “without root”. Okay, I tell a lie — the first message I got was a list of corrected English translations because from all the typos in the app someone had presumed English wasn’t my “native language”. 🙈 That’s what happens when you don’t properly test for form over functionality.. but I digress.

Since then I’ve had a few more requests on how I did it so that’s what I’m going to go through here. I’ve also got a library on my GitHub, that both ReadItToMe & Can’t Talk share to separate out a lot of the notification handling heavy work, which anyone is welcome to use. (I’d recommend using it as a Gradle dependency via JitPack, which is what I do)

How to listen to notifications

This is the first bit you need to get set up in order to retrieve notifications & it’s done by implementing the NotificationListener service. I’m not going to reinvent wheel explaining how to set this up, the API documentation for it is pretty straightforward.

I will however say my library contains a class called BaseNotificationListener which I’d highly recommend subclassing as it handles a number of issues you’ll find working with messaging app notifications. The most relevant being the fact you might receive duplicate notifications, some of which will be replyable while others won’t, which can be a pain to figure out. This is usually caused by one notification being a device notification, one might be part of a notification group, the other might be a wear notification etc.. So many notifications!

Admittedly, my listener class uses a rather primitive method to detect possible duplicates, by using a delay of 200 milliseconds before handling them, & then selects which it believes to be the “real” notification to forward to the onNotificationPosted method. But it’s solved the issue for me so..

Find notifications that can be replied to

There’s a lot that you can use notifications for outside of replying to them but for the purpose of this article I’m going to talk about how to retrieve only messages that can be replied to from the NotificationListener service. Again the method I use to do this is in the library, inside NotificationUtils calls getQuickReplyAction.

public static Action getQuickReplyAction(Notification n, String packageName) {
    NotificationCompat.Action action = null;
    if(Build.VERSION.SDK_INT >= 24)
        action = getQuickReplyAction(n);
    if(action == null)
        action = getWearReplyAction(n);
    if(action == null)
        return null;
    return new Action(action, packageName, true);
}

This method check if the Notification object you’ve just received from your NotificationListener service has an Action, if so does that action have a RemoteInput (the method the notification system uses to receive some input) & if so does it’s RemoteInput have a resultKey with the word “reply” in it. Again, this is a pretty primitive method of going about things but everything we’re doing here is not how any of this was intended to be used.

private static NotificationCompat.Action getQuickReplyAction(Notification n) {
    for(int i = 0; i < NotificationCompat.getActionCount(n); i++) {
        NotificationCompat.Action action = NotificationCompat.getAction(n, i);
        for(int x = 0; x < action.getRemoteInputs().length; x++) {
            RemoteInput remoteInput = action.getRemoteInputs()[x];
            if(remoteInput.getResultKey().toLowerCase().contains(REPLY_KEYWORD))
                return action;
        }
    }
    return null;
}

I use multiple methods to try & find a way to reply to a notification, the first being looking for the quick reply option introduced in Nougat. If no replyable action is found I then check for Android Wear actions as these can often be replied to from the watch and therefore we can make use of that functionality to allow our app to reply.

private static NotificationCompat.Action getWearReplyAction(Notification n) {
    NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(n);
    for (NotificationCompat.Action action : wearableExtender.getActions()) {
        for (int x = 0; x < action.getRemoteInputs().length; x++) {
            RemoteInput remoteInput = action.getRemoteInputs()[x];
            if (remoteInput.getResultKey().toLowerCase().contains(REPLY_KEYWORD))
                return action;
        }
    }
    return null;
}

If we found an Action object that matched our criteria we can now use it to reply to whichever notification it belongs to.

You might also notice the getQuickReplyAction method wraps the action it returns in it’s own Action object, the reason for this being the Notification.Action object isn’t parcelable, & for my own apps this is how I want to move the object around before I’m ready to reply, so I created a handy class that essentially recreates the Notification.Action object as a parcelable. Pretty handy in real world use.

Replying to the notification

So now that we have our Action object from our desired notification we want to reply to, how do we reply to it? Well, if you’re using my library then my parcelable Action class has a handy method called reply that does the work for you.

If you’re not, or you just want to know how it works then what you want to do is create a new Bundle object, loop through the RemoteInput’s from your Action object, for each one putting a CharSequence into the bundle with the RemoteInput’s key as the bundle key, and the text you want to send as a reply as the bundle’s value.

You also want to create a new array of RemoteInput’s and then using the RemoteInput.Builder create RemoteInput’s in the same format as they’d be created if a user had replied via the orthodox route (either from the notification itself as a quick reply or via their Android Wear device if it was a Wear notification we got the Action from).

Once that’s all done we create an intent with the data using the RemoteInput.addResultsToIntent method and then pass that intent to your Action’s actionIntent send method.

public void sendReply(Context context, String msg) throws PendingIntent.CanceledException {
   Intent intent = new Intent();
   Bundle bundle = new Bundle();
   ArrayList actualInputs = new ArrayList<>();

   for (RemoteInputParcel input : remoteInputs) {
      Log.i("", "RemoteInput: " + input.getLabel());
      bundle.putCharSequence(input.getResultKey(), msg);
      RemoteInput.Builder builder = new RemoteInput.Builder(input.getResultKey());
      builder.setLabel(input.getLabel());
      builder.setChoices(input.getChoices());
      builder.setAllowFreeFormInput(input.isAllowFreeFormInput());
      builder.addExtras(input.getExtras());
      actualInputs.add(builder.build());
   }

   RemoteInput[] inputs = actualInputs.toArray(new RemoteInput[actualInputs.size()]);
   RemoteInput.addResultsToIntent(inputs, intent, bundle);
   p.send(context, 0, intent);
}

And that’s it. It’s actually pretty straightforward minus the fact that almost all of this was reverse engineered & a lot of it isn’t well documented, if at all, in terms of how it functions.

Pitfalls to be aware of

One thing to note when playing with Notifications in general is there are a lot of unexpected scenarios I’ve noticed that have caused me no end of headaches in my apps. The most notable ones to watch out for are:

  • Duplicate notifications — I highly suggest you use a timed delay before handling any notifications as almost always they’ll be duplicates of different notification types, especially for messaging apps like Whatsapp, and it could very well be that the best notification for your use case isn’t the first one.
  • When you reply to a notification it will be dismissed & the app you’re replying to will treat it as a user interaction & therefore in an app like Whatsapp it’ll mark the conversation as read. This is the correct functionality but just something to be aware of.
  • Timing — Apps like Whatsapp, for example, often resend notifications ie if a user swiped a notification away without reading it, when the next new notification comes in, the previous one will be resent. I use the following code in my apps to try & determine if the notification has just been received or if it’s an old one that’s been resent (NOTIF_TIME_TOLERANCE is whatever you deem to be an appropriate amount of time between a message being sent & it’s notification being received — I use 30 seconds)
boolean isCurrent = sbn.getNotification().when > 0 && System.currentTimeMillis() - sbn.getNotification().when <= TimeUnit.SECONDS.toMillis(NOTIF_TIME_TOLERANCE);
  • Contacts — Some apps will attach an array of contacts that are involved in the conversation the notification relates too as an extra called NotificationCompat.EXTRA_PEOPLE . Whatsapp specifically only includes this if the device is in priority mode (don’t ask). Other apps might be more accommodating.

And that’s how it’s done..

Simple(ish) right? 😛 Gotta love Android.. I do hope you found this useful & if you have any questions or know of a better way to do anything I’ve mentioned in this article feel free to hit me up in the comments or on social media (@lowcarbrob everywhere).

Have fun!

PS: It’s also pertinent to note the library I use might have bugs in it as it was specifically built for ReadItToMe & adapter to fit Can’t Talk also. There’s also a lot in there not related to replying but particularly useful for my own use cases around notifications. If you do find any issues feel free to make a pull request or raise an issue.

Leave a Reply

Your email address will not be published. Required fields are marked *