概述
NotificationListenerService是android api18(Android 4.3)引入的一个类。主要作用就是用来监听系统接收到的通知。 具体可以做什么事情大家可以发挥想象,比如:红包插件中就可以使用该类。
本文来解释下该service的实现原理
使用
首先看下如何使用
- AndroidManifest.xml中注册
<service android:name=".MonitorNotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
- 继承NotificationListenerService
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class MonitorNotificationService extends NotificationListenerService {
...
@Override
public void onNotificationPosted(StatusBarNotification sbn) {...}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {}
}
只要这2步,就完成了对通知栏监听的准备工作。接下来只要在系统设置中打开开关就可以监听通知了。笔者尝试了许多机型,在设置中找到通知栏权限太难了。所以都是通知如下代码帮助用户进入系统设置指定界面,然后引导用户打开开关
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
startActivity(intent);
至此,所有工作都做完了,在onNotificationPosted
的回调里面就可以收到系统的所有通知栏消息了。
抛砖
这里,先抛出2个问题供大家思考,然后下文再给出答案
- AndroidManifest中的service声明了permission和action,这个有什么用?
- 当我们的程序启动的时候,MonitorNotificationService自动就启动了,但是代码里面并没有对该service做显示启动,那它是如何启动的呢?
对于如何研究NotificationListenerService的实现原理,笔者是从系统设置界面开始的,毕竟这个地方的开关决定了该功能是否可用
setting
通过Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS
可以进入到setting指定界面,我们就从这里入手,找到该界面,继承关系如下
NotificationAccessSettings extends ManagedServiceSettings extends ListFragment
那么先看下该界面的list数据是如何填充的
private static int getServices(Config c, ArrayAdapter<ServiceInfo> adapter, PackageManager pm) {
...
List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
new Intent(c.intentAction),
PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
user);
for (int i = 0, count = installedServices.size(); i < count; i++) {
ResolveInfo resolveInfo = installedServices.get(i);
ServiceInfo info = resolveInfo.serviceInfo;
if (!c.permission.equals(info.permission)) {
Slog.w(c.tag, "Skipping " + c.noun + " service "
+ info.packageName + "/" + info.name
+ ": it does not require the permission "
+ c.permission);
continue;
}
if (adapter != null) {
adapter.add(info);
}
...
}
return services;
}
- 通过pm查找指定service,该service需要满足符合参数
new Intent(c.intentAction)
- 对查找出来的service进行遍历,如果没有配置
c.permission
的service则不显示在列表中
那这个c.intentAction和c.permission的值是多少呢?答案在NotificationAccessSettings
中
private static Config getNotificationListenerConfig() {
...
c.intentAction = NotificationListenerService.SERVICE_INTERFACE;
c.permission = android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE;
...
return c;
}
这2个值分别对应我们之前在AndroidManifest.xml中的service配置的<intent-filter>中的action和android:permission的值。如果我们在开发过程中service少配了一个选项,就没有办法在setting找到服务并开启,所以之前抛砖中的问题1也就迎刃而解了。
接下来再看看点开该服务后,是不是启动了我们配置的service。
先找到点击后的代码
private void saveEnabledServices() {
StringBuilder sb = null;
for (ComponentName cn : mEnabledServices) {
if (sb == null) {
sb = new StringBuilder();
} else {
sb.append(':');
}
sb.append(cn.flattenToString());
}
Settings.Secure.putString(mCR,
mConfig.setting,
sb != null ? sb.toString() : "");
}
what?!, 居然只是往setting的Secure
表中写了一个值而已?并没有启动service
其中mConfig.setting
也是在NotificationAccessSettings
中配置的
c.setting = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS; //-->enabled_notification_listeners
(插曲
我们读取setting中Secure表的ENABLED_NOTIFICATION_LISTENERS字段的值
String flat = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
该值是包含了系统当前所有授权了的服务列表,以:
作为分割,如下所示
com.qihoo360.mobilesafe/com.qihoo360.mobilesafe.service.NLService: //360
com.huajiao/com.huajiao.service.AppStoreNotificationListenerService: //花椒
)
既然只是往数据库中写了一个值就开启了服务,那么一定是采用了观察者模式,其他地方对该数据库进行了监听,得到回调。
在源码中全局搜索ENABLED_NOTIFICATION_LISTENERS
,最后定位到NotificationManagerService.java
中
NotificationManagerService
public class NotificationListeners extends ManagedServices {
...
@Override
protected Config getConfig() {
...
c.secureSettingName = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS;
...
return c;
}
...
}
这个写法是不是相当熟悉,在系统的设置界面就是使用的该写法。
我们到父类ManagedServices
中看看是如何使用getConfig
的
ManagedServices
public ManagedServices(Context context, Handler handler, Object mutex,
UserProfiles userProfiles) {
...
mConfig = getConfig();
mSettingsObserver = new SettingsObserver(handler);
}
private class SettingsObserver extends ContentObserver {
private final Uri mSecureSettingsUri = Settings.Secure.getUriFor(mConfig.secureSettingName);
...
private void observe() {
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(mSecureSettingsUri,
false, this, UserHandle.USER_ALL);
update(null);
}
...
}
构造方法中给Config和ContentObserver对象赋值.
看到ContentObserver是不是豁然开朗,它所监听的Uri正好又是Settings.Secure.ENABLED_NOTIFICATION_LISTENERS
已经越来越接近答案了,我们看看ContentObserver的回调函数
@Override
public void onChange(boolean selfChange, Uri uri) {
update(uri);
}
private void update(Uri uri) {
if (uri == null || mSecureSettingsUri.equals(uri)) {
if (DEBUG) Slog.d(TAG, "Setting changed: mSecureSettingsUri=" + mSecureSettingsUri +
" / uri=" + uri);
rebindServices();
}
}
这里只响应Null和Settings.Secure.ENABLED_NOTIFICATION_LISTENERS
rebindServices看名字就能猜到是一个bind services的操作
rebindServices
private void rebindServices() {
...
final SparseArray<String> flat = new SparseArray<String>();
//根据不同用户,读取setting数据库中对应的值
for (int i = 0; i < nUserIds; ++i) {
flat.put(userIds[i], Settings.Secure.getStringForUser(
mContext.getContentResolver(),
mConfig.secureSettingName,
userIds[i]));
}
...
for (int i = 0; i < nUserIds; ++i) {
String toDecode = flat.get(userIds[i]);
if (toDecode != null) {
//使用冒号作为分割符号,保存已经开启了服务的ComponentName
String[] components = toDecode.split(ENABLED_SERVICES_SEPARATOR);
for (int j = 0; j < components.length; j++) {
final ComponentName component
= ComponentName.unflattenFromString(components[j]);
if (component != null) {
...
add.add(component);
}
}
}
}
...
final int N = add.size();
for (int j = 0; j < N; j++) {
final ComponentName component = add.get(j);
Slog.v(TAG, "enabling " + getCaption() + " for user " + userIds[i] + ": "
+ component);
//注册每一个授权了的ComponentName
registerService(component, userIds[i]);
}
...
}
由插曲可以知道,数据库中的字符串是以冒号的形式的拼接的,所以这里读取出来后会以冒号的形式进行分割。
从代码可以看出,这里是区分了不同用户的,毕竟android现在已经支持了多用户。
registerService
这个方法就不细说了,实现十分简单,就是调用了bindServiceAsUser
方法,正式启动了服务
mContext.bindServiceAsUser(intent,
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
...
try {
...
info = newServiceInfo(mService, name,
userid, false /*isSystem*/, this, targetSdkVersion);
added = mServices.add(info);
} catch (RemoteException e) {}
...
}
},
...)
关于问题二的答案就是在上面.
服务bind成功以后,app中的监听服务代理对象会保存在ManagedServices
的mServices(ArrayList数据结构)中.
流程图
接受通知
上面讲解了三方app中监听通知栏服务启动的过程,那么系统中有了通知来了以后,是如何回调到三方app中的呢?
这就不得不看下Notification之----Android5.0实现原理(二),由于篇幅原因这里简单说下。
- app通过notify方法 借助NotificationMange(NM)将通知传递给NotificationManagerService(NMS)
- NMS接受到该通知后,遍历ManagedServices中注册了的listener,并且调用回调方法
- 监听方回调onNotificationPosted方法
相关阅读
Notification之----Android5.0实现原理(二)
Notification之----Android5.0实现原理(一)
Notification之----自定义样式
Notification之----默认样式
Notification之----任务栈