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之----任务栈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容