Notification之---NotificationListenerService5.0實(shí)現(xiàn)原理

概述

NotificationListenerService是android api18(Android 4.3)引入的一個(gè)類傻铣。主要作用就是用來監(jiān)聽系統(tǒng)接收到的通知。 具體可以做什么事情大家可以發(fā)揮想象九串,比如:紅包插件中就可以使用該類残吩。
本文來解釋下該service的實(shí)現(xiàn)原理

使用

首先看下如何使用

  1. AndroidManifest.xml中注冊(cè)
        <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步,就完成了對(duì)通知欄監(jiān)聽的準(zhǔn)備工作哺眯。接下來只要在系統(tǒng)設(shè)置中打開開關(guān)就可以監(jiān)聽通知了。筆者嘗試了許多機(jī)型扒俯,在設(shè)置中找到通知欄權(quán)限太難了奶卓。所以都是通知如下代碼幫助用戶進(jìn)入系統(tǒng)設(shè)置指定界面一疯,然后引導(dǎo)用戶打開開關(guān)

   Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
   startActivity(intent);

至此,所有工作都做完了夺姑,在onNotificationPosted的回調(diào)里面就可以收到系統(tǒng)的所有通知欄消息了墩邀。

拋磚

這里,先拋出2個(gè)問題供大家思考盏浙,然后下文再給出答案

  1. AndroidManifest中的service聲明了permissionaction磕蒲,這個(gè)有什么用?
  2. 當(dāng)我們的程序啟動(dòng)的時(shí)候只盹,MonitorNotificationService自動(dòng)就啟動(dòng)了,但是代碼里面并沒有對(duì)該service做顯示啟動(dòng)兔院,那它是如何啟動(dòng)的呢殖卑?

對(duì)于如何研究NotificationListenerService的實(shí)現(xiàn)原理,筆者是從系統(tǒng)設(shè)置界面開始的坊萝,畢竟這個(gè)地方的開關(guān)決定了該功能是否可用

setting

通過Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS可以進(jìn)入到setting指定界面孵稽,我們就從這里入手,找到該界面十偶,繼承關(guān)系如下

NotificationAccessSettings  extends ManagedServiceSettings extends ListFragment

那么先看下該界面的list數(shù)據(jù)是如何填充的

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需要滿足符合參數(shù)new Intent(c.intentAction)
  2. 對(duì)查找出來的service進(jìn)行遍歷,如果沒有配置c.permission的service則不顯示在列表中

那這個(gè)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個(gè)值分別對(duì)應(yīng)我們之前在AndroidManifest.xml中的service配置的<intent-filter>中的action和android:permission的值接校。如果我們?cè)陂_發(fā)過程中service少配了一個(gè)選項(xiàng),就沒有辦法在setting找到服務(wù)并開啟狮崩,所以之前拋磚中的問題1也就迎刃而解了蛛勉。

接下來再看看點(diǎn)開該服務(wù)后,是不是啟動(dòng)了我們配置的service睦柴。
先找到點(diǎn)擊后的代碼

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表中寫了一個(gè)值而已?并沒有啟動(dòng)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");

該值是包含了系統(tǒng)當(dāng)前所有授權(quán)了的服務(wù)列表坦敌,以:作為分割侣诵,如下所示

com.qihoo360.mobilesafe/com.qihoo360.mobilesafe.service.NLService: //360
com.huajiao/com.huajiao.service.AppStoreNotificationListenerService: //花椒

既然只是往數(shù)據(jù)庫中寫了一個(gè)值就開啟了服務(wù),那么一定是采用了觀察者模式狱窘,其他地方對(duì)該數(shù)據(jù)庫進(jìn)行了監(jiān)聽杜顺,得到回調(diào)。
在源碼中全局搜索ENABLED_NOTIFICATION_LISTENERS训柴,最后定位到NotificationManagerService.java

NotificationManagerService

public class NotificationListeners extends ManagedServices {
    ...
    @Override
    protected Config getConfig() {
        ...
        c.secureSettingName = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS;
        ...
        return c;
    }
    ...
}

這個(gè)寫法是不是相當(dāng)熟悉哑舒,在系統(tǒng)的設(shè)置界面就是使用的該寫法。
我們到父類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);
    }
    ...
}

構(gòu)造方法中給Config和ContentObserver對(duì)象賦值.
看到ContentObserver是不是豁然開朗幻馁,它所監(jiān)聽的Uri正好又是Settings.Secure.ENABLED_NOTIFICATION_LISTENERS
已經(jīng)越來越接近答案了洗鸵,我們看看ContentObserver的回調(diào)函數(shù)

@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();
    }
}

這里只響應(yīng)Null和Settings.Secure.ENABLED_NOTIFICATION_LISTENERS
rebindServices看名字就能猜到是一個(gè)bind services的操作

rebindServices

private void rebindServices() {
    ...
    final SparseArray<String> flat = new SparseArray<String>();
    //根據(jù)不同用戶越锈,讀取setting數(shù)據(jù)庫中對(duì)應(yīng)的值
    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) {
            //使用冒號(hào)作為分割符號(hào),保存已經(jīng)開啟了服務(wù)的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);
        //注冊(cè)每一個(gè)授權(quán)了的ComponentName 
        registerService(component, userIds[i]);
    }
    ...
}

由插曲可以知道膘滨,數(shù)據(jù)庫中的字符串是以冒號(hào)的形式的拼接的甘凭,所以這里讀取出來后會(huì)以冒號(hào)的形式進(jìn)行分割。
從代碼可以看出火邓,這里是區(qū)分了不同用戶的丹弱,畢竟android現(xiàn)在已經(jīng)支持了多用戶。

registerService

這個(gè)方法就不細(xì)說了铲咨,實(shí)現(xiàn)十分簡單躲胳,就是調(diào)用了bindServiceAsUser方法,正式啟動(dòng)了服務(wù)

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) {}
            ...
        }
    },
    ...)

關(guān)于問題二的答案就是在上面.
服務(wù)bind成功以后纤勒,app中的監(jiān)聽服務(wù)代理對(duì)象會(huì)保存在ManagedServicesmServices(ArrayList數(shù)據(jù)結(jié)構(gòu))中.

流程圖

app_monitor_flow.jpg

接受通知

上面講解了三方app中監(jiān)聽通知欄服務(wù)啟動(dòng)的過程坯苹,那么系統(tǒng)中有了通知來了以后,是如何回調(diào)到三方app中的呢摇天?
這就不得不看下Notification之----Android5.0實(shí)現(xiàn)原理(二)粹湃,由于篇幅原因這里簡單說下。

  1. app通過notify方法 借助NotificationMange(NM)將通知傳遞給NotificationManagerService(NMS)
  2. NMS接受到該通知后泉坐,遍歷ManagedServices中注冊(cè)了的listener为鳄,并且調(diào)用回調(diào)方法
  3. 監(jiān)聽方回調(diào)onNotificationPosted方法
app_callback_flow.jpg

相關(guān)閱讀
Notification之----Android5.0實(shí)現(xiàn)原理(二)
Notification之----Android5.0實(shí)現(xiàn)原理(一)
Notification之----自定義樣式
Notification之----默認(rèn)樣式
Notification之----任務(wù)棧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腕让,隨后出現(xiàn)的幾起案子孤钦,更是在濱河造成了極大的恐慌,老刑警劉巖记某,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件司训,死亡現(xiàn)場離奇詭異,居然都是意外死亡液南,警方通過查閱死者的電腦和手機(jī)壳猜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滑凉,“玉大人统扳,你說我怎么就攤上這事〕╂ⅲ” “怎么了咒钟?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長若未。 經(jīng)常有香客問我朱嘴,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任萍嬉,我火速辦了婚禮乌昔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘壤追。我一直安慰自己磕道,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布行冰。 她就那樣靜靜地躺著溺蕉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悼做。 梳的紋絲不亂的頭發(fā)上疯特,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音肛走,去河邊找鬼辙芍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛羹与,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庶灿,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼纵搁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了往踢?” 一聲冷哼從身側(cè)響起腾誉,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎峻呕,沒想到半個(gè)月后利职,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘦癌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年猪贪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片讯私。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡热押,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斤寇,到底是詐尸還是另有隱情桶癣,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布娘锁,位于F島的核電站牙寞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏莫秆。R本人自食惡果不足惜间雀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一悔详、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雷蹂,春花似錦伟端、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至萎庭,卻和暖如春霜医,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驳规。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國打工肴敛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吗购。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓医男,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捻勉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子镀梭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容