Notification之---NotificationList

作者: Hly_Coder | 来源:发表于2017-03-01 17:36 被阅读1226次

    概述

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

    相关文章

      网友评论

        本文标题:Notification之---NotificationList

        本文链接:https://www.haomeiwen.com/subject/llnqgttx.html