In this article, we’ll explore an interesting trick to circumvent Android’s Do Not Disturb. I found this some time ago while going through the notification service source code in Android.
Disclaimer: If you are an app developer, please don’t use this in your app! It’s not a security vulnerability, but it’s still unintended behavior made possible due to undocumented APIs. The Android feature team has been notified of this behavior and will likely fix it in the future. I’m not responsible if you implement this in your app and it gets banned from Play Store.
Overview of Android DND
“Do Not Disturb” (DND) is a feature on Android that allows a user to silence notifications, calls, and other interruptions. This can be useful when you’re sleeping, attending a meeting, or when you just want some quiet time. Some highlights from DND functionality:
- Notification silencing: When DND is enabled, most notifications will be silenced. This means your device won’t make noise or vibrate, and the screen won’t light up. You can still see those notifications if you open the notification shade.
- Exceptions: Android allows you to set exceptions for DND. For example, you can allow calls or messages from specific contacts or notifications from specific apps.
- Automatic Rules: You can set automatic rules that turn on DND at certain times of the day or during certain events. For example, you could set DND to automatically turn on every night while you’re sleeping.
- Priority Conversations: You can mark some conversations as “priority” conversations. Notifications associated with those conversations will come through even when DND is on unless you explicitly disable this in DND settings.
Wait, what’s a conversation again?
Android has an extensive People and Conversations system, integrated with many other subsystems like contacts, notifications, shortcuts, etc.
To shamelessly oversimplify, Android conversations are a special notification category, meant to separate people-related notifications and make them more accessible and manageable for the user.
To include a notification in a conversation, you need to build it as a messaging-style notification using the MessagingStyle
class. It should also be associated with a “long-lived” dynamic shortcut. Dynamic shortcuts are the button-like things that pop up when you long-press the app icon in the launcher; such shortcuts can be registered by any app using ShortcutManager.
Whole notification channels can be marked as conversation-centric, using the setConversationId()
API. (please check this out to learn what notification channels are, it’s very in-depth) Such notification channels should have a “parent channel” specified through setConversationId()
. They should also have a dynamic shortcut associated with them, just like conversation-style notifications should.
Priority conversations
A “priority conversation”, also known as an “important conversation”, is a conversation that has been explicitly given a higher priority by the user through Android settings. As mentioned above, DND ignores notifications associated with such priority conversations and lets them through. These notifications also get special visual treatment, like appearing on the lock screen and lighting up the device when the device is locked.
Internal mechanisms
Now on to the technical stuff! The private boolean field mImportantConvo
in NotificationChannel
determines whether a channel is associated with a priority conversation or not. This field can be changed with setImportantConversation()
.
Still, this is an undocumented API intended for the system itself, so you can’t just call it in your app. Priority conversations are strictly a user-centric feature, so there’s no documented function in the API that allows you to view or handle priority conversations.
Android apps create notification channels by building a NotificationChannel
object and sending it to the system using NotificationManager.createNotificationChannel()
. It goes through lots of IPC, wrappers, abstractions, etc., but the important stuff happens in the internal class PreferencesHelper
.
if (existing != null && fromTargetApp) {
// Actually modifying an existing channel - keep most of the existing settings
...
} else {
...
// Reset fields that apps aren't allowed to set.
if (fromTargetApp && !hasDndAccess) {
channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
}
if (fromTargetApp) {
channel.setLockscreenVisibility(r.visibility);
channel.setAllowBubbles(existing != null
? existing.getAllowBubbles()
: NotificationChannel.DEFAULT_ALLOW_BUBBLE);
}
clearLockedFieldsLocked(channel);
channel.setImportanceLockedByCriticalDeviceFunction(
r.defaultAppLockedImportance || r.fixedImportance);
if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
channel.setLockscreenVisibility(
NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
}
if (!r.showBadge) {
channel.setShowBadge(false);
}
...
}
As we can see here, after receiving a NotificationChannel
object from an app, PreferencesHelper
resets some fields that apps aren’t allowed to change, before internally registering the notification channel. This includes the “bypass DND” attribute that specifies whether it can directly circumvent DND, and the “locked fields” flag used to determine the properties explicitly defined by the user. But interestingly, it does not reset mImportantConvo
. If you are familiar with Android framework security, you might see where this is going already…
Enter: Reflection
As mentioned above, there’s no way to directly change mImportantConvo
. But lucky for us, Java/Kotlin has a feature called reflection. Apps can use reflection to “introspect” and modify their own structure during runtime, and one of the things it allows is modifying private fields in classes.
So, nothing is preventing us from simply building a conversation-centric notification channel, marking it as a priority conversation using reflection, and then passing it on to the system!
Well, there’s actually one thing: Android restricts apps from using reflection to modify undocumented fields in classes. (more information here) But this happens to be just a discouragement rather than a real restriction, and it can be easily bypassed with one of the many methods available on the internet. I personally prefer this library.
Final steps
- Create two
NotificationChannel
objects - the parent channel and the exploit channel. - Build a long-lived dynamic shortcut. (use
ShortcutInfo.Builder.setLongLived()
) Push it to the system usingShortcutManager.pushDynamicShortcut()
. - Use
NotificationChannel.setConversationId()
to mark the exploit channel as a proper conversation-centric channel, by associating it with the parent channel and the shortcut. - Use reflection to mark the private field
mImportantConvo
in the exploit channel as accessible. - Set
mImportantConvo
to true. - Register both the parent channel and the exploit channel, in that order.
- The exploit channel would then be marked by the system as a priority conversation, so conversation-style notifications posted to it would go through Do Not Disturb.
You can find a proof-of-concept implementation of this trick here.
If you have any questions, please leave a comment below or reach out on Twitter!!