使用NotificationListenerService监控通知栏

介绍

NotificationListenerService是Android API Level18新增加的一个服务,当系统通知栏有通知弹出,移除以及位置改变时,会调用这个服务相关的回调方法。因此,我们可以利用这个服务来监控系统通知栏的行为。

如何在App中注册NotificationListenerService,方法很简单,首先在AndroidManifest.xml中添加一个这样的服务:

1
2
3
4
5
6
7
<service android:name=".NotificationListener"
android:label="@string/service_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>

注意的是服务需要申请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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// registerService(final ComponentName name, final int userid)中bind NotificationListenerService的源码
...
if (!mContext.bindServiceAsUser(intent,
new ServiceConnection() {
IInterface mService;

@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
boolean added = false;
ManagedServiceInfo info = null;
synchronized (mMutex) {
mServicesBinding.remove(servicesBindingTag);
try {
mService = asInterface(binder);
info = newServiceInfo(mService, name,
userid, false /*isSystem*/, this, targetSdkVersion);
binder.linkToDeath(info, 0);
added = mServices.add(info);
} catch (RemoteException e) {
// already dead
}
}
if (added) {
onServiceAdded(info);
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
Slog.v(TAG, getCaption() + " connection lost: " + name);
}
},
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
new UserHandle(userid)))
{
mServicesBinding.remove(servicesBindingTag);
Slog.w(TAG, "Unable to bind " + getCaption() + " service: " + intent);
return;
}
...

onServiceConnected(ComponentName name, IBinder binder)中,bind成功后会回调onServiceAdded(ManagedServiceInfo info)方法,这里的info是对binder的简单封装。在这里面又会回调listener.onListenerConnected(update)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onServiceAdded(ManagedServiceInfo info) {
final INotificationListener listener = (INotificationListener) info.service;
final NotificationRankingUpdate update;
synchronized (mNotificationList) {
updateNotificationGroupsDesiredLocked();
update = makeRankingUpdateLocked(info);
}
try {
listener.onListenerConnected(update);
} catch (RemoteException e) {
// we tried
}
}

listener是一个INotificationListener接口的对象。这个接口会通过IPC传递给NotificationManagerService。在NotificationListenerService的onBind方法中

1
2
3
4
5
6
7
@Override
public IBinder onBind(Intent intent) {
if (mWrapper == null) {
mWrapper = new INotificationListenerWrapper();
}
return mWrapper;
}

到这里,就回到了最普通的bindService的流程了。

监控通知

首先回到NotificationManager中。当我们发送一个通知栏时,需要调用notify(int id, Notification notification)这个方法。其中会调用NotificationManagerService的enqueueNotificationWithTag

1
2
3
4
5
6
7
8
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
stripped, idOut, UserHandle.myUserId());
if (id != idOut[0]) {
Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
}
} catch (RemoteException e) {
}

然后看一下NotificationManagerService做了些什么。
enqueueNotificationWithTag -> enqueueNotificationInternal -> notifyPostedLocked -> notifyPosted
当我们来到notifyPosted方法中时,我们会看到

1
2
3
4
5
6
7
8
9
10
private void notifyPosted(final ManagedServiceInfo info,
final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
final INotificationListener listener = (INotificationListener)info.service;
StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn);
try {
listener.onNotificationPosted(sbnHolder, rankingUpdate);
} catch (RemoteException ex) {
Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
}
}

它调用了listener.onNotificationPosted(sbnHolder, rankingUpdate)。而这个listener接口的onNotificationPosted方法的实现就在NotificationListenerService中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private class INotificationListenerWrapper extends INotificationListener.Stub {
@Override
public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
NotificationRankingUpdate update) {
StatusBarNotification sbn;
try {
sbn = sbnHolder.get();
} catch (RemoteException e) {
Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
return;
}

try {
Notification.Builder.rebuild(getContext(), sbn.getNotification());
// convert icon metadata to legacy format for older clients
createLegacyIconExtras(sbn.getNotification());
} catch (IllegalArgumentException e) {
// drop corrupt notification
sbn = null;
Log.w(TAG, "onNotificationPosted: can't rebuild notification from " +
sbn.getPackageName());
}

// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized (mWrapper) {
applyUpdate(update);
try {
if (sbn != null) {
NotificationListenerService.this.onNotificationPosted(sbn, mRankingMap);
} else {
// still pass along the ranking map, it may contain other information
NotificationListenerService.this.onNotificationRankingUpdate(mRankingMap);
}
} catch (Throwable t) {
Log.w(TAG, "Error running onNotificationPosted", t);
}
}
}

这里调用了NotificationListenerService.this.onNotificationPosted。而它的实现就是我们创建的NotificationListenerService中重写的方法。也就是说,一个收到通知的消息最终传递到了我们的NotificationListenerService中。

获取通知信息

NotificationListenerService有两个抽象的回调方法(API 21以上不是抽象方法了)需要我们实现。

1
2
onNotificationPosted(StatusBarNotification sbn)
onNotificationRemoved(StatusBarNotification sbn)

这两个方法分别在系统收到通知和移除通知的时候回调。StatusBarNotification这个类是对Notification类的封装,包含了idpkgpostTime等非常有用的属性。

如果我们想要获取更多关于当前收到或移除通知的信息的话,需要我们对Android Notification的机制有更多的了解。在Notification类中有一个名为extrasBundle成员,它的注释是这样的:

1
2
3
4
5
6
7
8
/**
* Additional semantic data to be carried around with this Notification.
* <p>
* The extras keys defined here are intended to capture the original inputs to {@link Builder}
* APIs, and are intended to be used by
* {@link android.service.notification.NotificationListenerService} implementations to extract
* detailed information from notification objects.
*/

这个extras内部存储了Notification.Builder在build通知过程中的属性,包括title, content, largeIcon, smallIcon等。而它可以用于获取NotificationListenerService回调方法中Notification的信息。我们可以通过这样的方式来得到这些信息:(注意:extras需要API Level 19以上)

1
2
3
4
sbn.getNotification().extras.get(Notification.EXTRA_TITLE)
sbn.getNotification().extras.get(Notification.EXTRA_TEXT)
sbn.getNotification().extras.get(Notification.EXTRA_LARGE_ICON)
......

然而,有一些用户自定义的通知,如果想要获取其中的文案,则不能用上面的方法。用户自定义的通知是使用了RemoteViews来自定义界面。我们如何能获取到RemoteViews里面控件的属性呢。

RemoteViews

RemoteViews顾名思义是一种在远程绘制的View,它不在自己的进程绘制,而是通过IPC将RemoteViews发送到其他进程更新界面。RemoteViews的主要应用场景是Notification和AppWidget。在通知栏上应用的方式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Notification notification = new Notification();
notification.icon = R.mipmap.ic_launcher;
notification.tickerText = "hello notification";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;

Intent intent = new Intent(this, RemoteViewsActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.tv, "这是一个Test");
remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));
remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);
PendingIntent openActivity2Pending = PendingIntent.getActivity
(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//设置RemoveViews点击后启动界面
remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending);

notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);

在调用manager.notify(2, notification)之后,NotificationManager通过Binder和SystemService进程中的NotificationManagerService通信,将RemoteViews传递过去。在SystemService进程中,RemoteViews通过之前一系列setXYZ操作添加的Actions一步一步地完成界面更新,有点类似于状态机日志,这样可以避免大量的IPC操作。而我们可以通过反射来获取当前Notification中RemoteViews的mActions成员,从中获取这个通知栏RemoteViews界面更新过程中需要设置的文案,字体颜色等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List actions;
RemoteViews rv = sbn.getNotification().contentView;
if (rv == null) {
return;
}

try {
Field field = rv.getClass().getDeclaredField("mActions");
field.setAccessible(true);
actions = field.get(rv);
} catch (Exception e) {
e.printStackTrace();
}

for (Object action : actions) {
// 反射得到methodName,viewId和value...
}

当然,对于没有经过setXYZ操作直接将文案,颜色等内容写在layout.xml中的RemoteViews,这个方法是行不通的。

一些细节

  1. 在实际监控过程中,有时会遇到收到一个通知回调多次的现象。后来发现这些sbn的postTime是一样的。我们可以增加一个LastPostTime时间戳将这些postTime相同的通知过滤掉。
  2. 由于是后台服务,如果单独在一个进程的话,很容易被系统在内存吃紧的时候杀死,这样我们的监控功能就会失效。如果可以将服务变成前台服务的话,存活的时间将会大大增长。
  3. 系统在什么时候会触发rebind操作。查看rebindServices()的引用会发现在ManagedServicesonPackageChangedonUserSwitched以及ManagedServices.SettingsObserverupdate三处会rebind。也就是在应用安装卸载时,系统用户切换时以及Settings.Secure.getString(getContentResolver(), ENABLED_NOTIFICATION_LISTENERS)有变化时。