Notification之---NotificationListenerService5.0实现原理

概述

NotificationListenerService是android api18(Android 4.3)引入的一个类。主要作用就是用来监听系统接收到的通知。 具体可以做什么事情大家可以发挥想象,比如:红包插件中就可以使用该类。
本文来解释下该service的实现原理

使用

首先看下如何使用

  1. 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>
  1. 继承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个问题供大家思考,然后下文再给出答案

  1. AndroidManifest中的service声明了permissionaction,这个有什么用?
  2. 当我们的程序启动的时候,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;
}
  1. 通过pm查找指定service,该service需要满足符合参数new Intent(c.intentAction)
  2. 对查找出来的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中的监听服务代理对象会保存在ManagedServicesmServices(ArrayList数据结构)中.

流程图

app_monitor_flow.jpg

接受通知

上面讲解了三方app中监听通知栏服务启动的过程,那么系统中有了通知来了以后,是如何回调到三方app中的呢?
这就不得不看下Notification之----Android5.0实现原理(二),由于篇幅原因这里简单说下。

  1. app通过notify方法 借助NotificationMange(NM)将通知传递给NotificationManagerService(NMS)
  2. NMS接受到该通知后,遍历ManagedServices中注册了的listener,并且调用回调方法
  3. 监听方回调onNotificationPosted方法
app_callback_flow.jpg

相关阅读
Notification之----Android5.0实现原理(二)
Notification之----Android5.0实现原理(一)
Notification之----自定义样式
Notification之----默认样式
Notification之----任务栈

推荐阅读更多精彩内容

  • 原文出处: http://www.androidchina.net/6174.html Notification在...
    木木00阅读 9,475评论 3 31
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 160,209评论 24 690
  • CLLocationManager The CLLocationManager class is the cent...
    没刀的大佐阅读 915评论 1 4
  • 前段时间读完格雷厄姆《聪明的投资者》、《证券分析》后、接着读了芒格的《穷查理宝典》和彼得林奇的《战胜华尔街》,读完...
    成为自己的CEO阅读 234评论 0 4
  • 2017年1月7日、地球に来てもう満百日、ここでの生活にはやっと慣れたと感じ、やや違和感あるけど、言葉も通じるよう...
    阿豆荚阅读 35评论 0 0
  • 无人问我粥可温,无人与我立黄昏。突然很喜欢这一句。非常喜欢。大概是因为我心底还是蛮寂寞的吧。感觉有共鸣的人都正在别...
    急急又慢慢阅读 71评论 0 0
  • 落日汀头接水烟,柴门正对大江边。婆娑小屋围新柳,欵乃清波载酒船。黄鸟多情飞款款,鲈鱼有味梦娟娟。山中小住浑无事,独...
    倚剑白云天阅读 34评论 0 1
  • 初看书本题目时 估摸着这应该是一本存粹关于生命题材的小说 初文竟给人一丝欢快的感觉,作者去乡间收集民歌偶然遇到一位...
    Pink39阅读 96评论 0 0