androidnotificationsforeground-serviceheads-up-notifications

How to cancel an ongoing notification of another app?


Background

I've found an app that somehow hides heads-up notifications, including even ongoing notifications (used by foreground services), called NCleaner .

I was wondering how such a thing works.

The problem

There isn't much information of how to control notifications of other apps. Only of the current app, and for some reason I've failed to cancel an ongoing notification, but I've succeeded canceling a normal one.

What I've found

I've found this sample showing how to monitor notifications. After searching on the docs about NotificationListenerService, I've also found how to cancel specific notifications of other apps.

However, it doesn't work for ongoing notifications.

For testing of normal notifications I just sent myself an email or wrote something on PushBullet app via my PC.

For testing of ongoing notifications, I've installed and ran my spare time app (here), which shows an ongoing notification right on the beginning of the app, starting from Android O. In fact, it can even hide the notifications of the OS, of USB connected and debugging (just connect the device to the PC via USB) ...

Here's the code I've made, based on the sample, to cancel all notifications it gets:

NotificationListenerExampleService.kt

class NotificationListenerExampleService : NotificationListenerService() {

    override fun onNotificationPosted(sbn: StatusBarNotification) {
        Log.d("AppLog", "sbn:" + sbn.toString())
        cancelNotification(sbn.key)
    }

    override fun onListenerConnected() {
        super.onListenerConnected()
        Log.d("AppLog", "onListenerConnected")
    }

    override fun onNotificationRemoved(sbn: StatusBarNotification) {}
}

manifest:

<application
  android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"
  tools:ignore="AllowBackup,GoogleAppIndexingWarning">
  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN"/>

      <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
  </activity>

  <service
    android:name=".NotificationListenerExampleService" android:label="@string/service_label" android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
    <intent-filter>
      <action android:name="android.service.notification.NotificationListenerService"/>
    </intent-filter>
  </service>
</application>

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private val isNotificationServiceEnabled: Boolean
        get() {
            val pkgName = packageName
            val flat = Settings.Secure.getString(contentResolver, "enabled_notification_listeners"")
            if (!TextUtils.isEmpty(flat)) {
                val names = flat.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                for (i in names.indices) {
                    val cn = ComponentName.unflattenFromString(names[i])
                    if (cn != null) {
                        if (TextUtils.equals(pkgName, cn.packageName)) {
                            return true
                        }
                    }
                }
            }
            return false
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (!isNotificationServiceEnabled) {
            startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
        }
    }

}

The questions

  1. How come my code can't remove ongoing notifications?
  2. How come the app I've found can do it? What does it do?
  3. Just how much does this API provide in terms of notifications? Looking at the docs, it seems it can do a lot: read notifications, dismiss them, get all kinds of information about them, ... But I can't find some other features: Is it possible to modify a notification? To change its priority? To make it avoid being a heads-up-notification and just be in the notification drawer? Is it possible to trigger an action of a notification?

Solution

  • I used this code for notification building. It has title, text, icon, contentAction as Broadcast Message. It is enough for answering your questions, I hope)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val notManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                val notChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
                notManager.createNotificationChannel(notChannel)
            }
    
            val builder = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
    
            val contentIntent = Intent(NOT_ACTION)
            contentIntent.putExtra(EXTRA_NOT_ID, notificationId)
    
            val contentAction = PendingIntent.getBroadcast(this@MainActivity, NOT_REQ_CODE, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)
    
            builder.setContentTitle("Notification $notificationId")
                    .setContentText("Awesome text for $notificationId notification")
                    .setContentIntent(contentAction)
                    .setSmallIcon(R.drawable.ic_launcher_foreground)
                    .setAutoCancel(true)
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setCategory(NotificationCompat.CATEGORY_MESSAGE)
                    .setDefaults(NotificationCompat.DEFAULT_ALL)
    
            NotificationManagerCompat
                    .from(this@MainActivity)
                    .notify(notificationId++, builder.build())
    

    1. Can you trigger an actions of notification?

    Yes, you can. You can get Notification from StatusBarNotification, and then get PendingIntents for actions and use them.
    For example, I want to trigger a contentAction of notification

    override fun onNotificationPosted(sbn: StatusBarNotification) {
        super.onNotificationPosted(sbn)
    
        Log.d(TAG, "Gotcha notification from ${sbn.packageName}")
    
        if (sbn.packageName == TARGET_PACKAGE) {
            val contentIntent = sbn.notification.contentIntent
            contentIntent.send()
        }
    }
    

    This code will trigger sending of Broadcast message. BUT it will ignore a .setAutoCancel(true) of notification, so you need to handle it manually like this

        if (sbn.packageName == TARGET_PACKAGE) {
            val contentIntent = sbn.notification.contentIntent
            contentIntent.send()
            if (sbn.notification.flags and Notification.FLAG_AUTO_CANCEL != 0) {
                cancelNotification(sbn.key)
            }
        }
    

    2. How NCleaner cancels ongoing notifications?

    The first way which comes on my mind is usage of NotificationListenerService.snoozeNotification like this

    if (sbn.packageName == TARGET_PACKAGE) {
            snoozeNotification(sbn.key, Long.MAX_VALUE - System.currentTimeMillis())
    }
    

    I'm 99% percent sure, that NCLeaner uses this code. Because it doesn't support this feature for pre-O devices.

    Note. You can't just use Long.MAX_VALUE, notification will be snoozed for a couple of seconds, and then appear again. Tested on Pixel 2 8.1.

    3. Can you modify an existing notification?

    No, you cannot. Only notification owner app can modify it.

    P.S.

    I also tried to overcome limits with reflection via hidden methods and fields of NotificationManager and NotificationListenerService. The result of all attempts is:

    Caused by: java.lang.SecurityException: Calling uid 10210 gave package com.ns.notificationsender which is owned by uid 10211

    So you app need to be system one, or maybe root can help to succeed. But I don't have such device.