介绍
NotificationListenerService是Android API Level18新增加的一个服务,当系统通知栏有通知弹出,移除以及位置改变时,会调用这个服务相关的回调方法。因此,我们可以利用这个服务来监控系统通知栏的行为。
如何在App中注册NotificationListenerService,方法很简单,首先在AndroidManifest.xml中添加一个这样的服务:
1 | <service android:name=".NotificationListener" |
注意的是服务需要申请BIND_NOTIFICATION_LISTENER_SERVICE
权限也就是所谓的通知读取权限,并且在intent-filter
中加上SERVICE_INTERFACE
这个Action。然后创建一个名为@stirng/service_name
的服务继承NotificationListenerService。这样在App启动后并且通知读取权限已开启的情况下,我们的NotificationListenerService就可以监控通知栏事件了。
还有一个注意的地方在文档里面也说得很清楚了,除了requestRebind(ComponentName)
以外,不应该在onListenerConnected()
方法回调之前做任何操作。
启动流程
首先,NotificationListenerService是由NotificationManagerService启动的。在用户开机后,Zygote进程fork出SystemService进程,NotificationManagerService在SystemService进程中初始化。
在API Level 23中,NotificationManagerService的mListeners
成员通过调用rebindServices()
-> registerService(final ComponentName name, final int userid)
完成bind NotificationListenerService。
1 | // registerService(final ComponentName name, final int userid)中bind NotificationListenerService的源码 |
在onServiceConnected(ComponentName name, IBinder binder)
中,bind成功后会回调onServiceAdded(ManagedServiceInfo info)
方法,这里的info
是对binder
的简单封装。在这里面又会回调listener.onListenerConnected(update)
。
1 |
|
listener是一个INotificationListener接口的对象。这个接口会通过IPC传递给NotificationManagerService。在NotificationListenerService的onBind
方法中
1 |
|
到这里,就回到了最普通的bindService的流程了。
监控通知
首先回到NotificationManager中。当我们发送一个通知栏时,需要调用notify(int id, Notification notification)
这个方法。其中会调用NotificationManagerService的enqueueNotificationWithTag
。
1 | try { |
然后看一下NotificationManagerService做了些什么。enqueueNotificationWithTag
-> enqueueNotificationInternal
-> notifyPostedLocked
-> notifyPosted
。
当我们来到notifyPosted
方法中时,我们会看到
1 | private void notifyPosted(final ManagedServiceInfo info, |
它调用了listener.onNotificationPosted(sbnHolder, rankingUpdate)
。而这个listener
接口的onNotificationPosted
方法的实现就在NotificationListenerService中
1 | private class INotificationListenerWrapper extends INotificationListener.Stub { |
这里调用了NotificationListenerService.this.onNotificationPosted
。而它的实现就是我们创建的NotificationListenerService中重写的方法。也就是说,一个收到通知的消息最终传递到了我们的NotificationListenerService中。
获取通知信息
NotificationListenerService有两个抽象的回调方法(API 21以上不是抽象方法了)需要我们实现。
1 | onNotificationPosted(StatusBarNotification sbn) |
这两个方法分别在系统收到通知和移除通知的时候回调。StatusBarNotification
这个类是对Notification
类的封装,包含了id
,pkg
,postTime
等非常有用的属性。
如果我们想要获取更多关于当前收到或移除通知的信息的话,需要我们对Android Notification的机制有更多的了解。在Notification
类中有一个名为extras
的Bundle
成员,它的注释是这样的:
1 | /** |
这个extras内部存储了Notification.Builder
在build通知过程中的属性,包括title
, content
, largeIcon
, smallIcon
等。而它可以用于获取NotificationListenerService回调方法中Notification的信息。我们可以通过这样的方式来得到这些信息:(注意:extras
需要API Level 19以上)
1 | sbn.getNotification().extras.get(Notification.EXTRA_TITLE) |
然而,有一些用户自定义的通知,如果想要获取其中的文案,则不能用上面的方法。用户自定义的通知是使用了RemoteViews来自定义界面。我们如何能获取到RemoteViews里面控件的属性呢。
RemoteViews
RemoteViews顾名思义是一种在远程绘制的View,它不在自己的进程绘制,而是通过IPC将RemoteViews发送到其他进程更新界面。RemoteViews的主要应用场景是Notification和AppWidget。在通知栏上应用的方式是这样的:
1 | Notification notification = new Notification(); |
在调用manager.notify(2, notification)
之后,NotificationManager通过Binder和SystemService进程中的NotificationManagerService通信,将RemoteViews传递过去。在SystemService进程中,RemoteViews通过之前一系列setXYZ操作添加的Actions一步一步地完成界面更新,有点类似于状态机日志,这样可以避免大量的IPC操作。而我们可以通过反射来获取当前Notification中RemoteViews的mActions
成员,从中获取这个通知栏RemoteViews界面更新过程中需要设置的文案,字体颜色等信息。
1 | List actions; |
当然,对于没有经过setXYZ操作直接将文案,颜色等内容写在layout.xml中的RemoteViews,这个方法是行不通的。
一些细节
- 在实际监控过程中,有时会遇到收到一个通知回调多次的现象。后来发现这些sbn的
postTime
是一样的。我们可以增加一个LastPostTime
时间戳将这些postTime相同的通知过滤掉。 - 由于是后台服务,如果单独在一个进程的话,很容易被系统在内存吃紧的时候杀死,这样我们的监控功能就会失效。如果可以将服务变成前台服务的话,存活的时间将会大大增长。
- 系统在什么时候会触发rebind操作。查看
rebindServices()
的引用会发现在ManagedServices
的onPackageChanged
,onUserSwitched
以及ManagedServices.SettingsObserver
的update
三处会rebind。也就是在应用安装卸载时,系统用户切换时以及Settings.Secure.getString(getContentResolver(), ENABLED_NOTIFICATION_LISTENERS)
有变化时。