Android通知發送原理之Framework實現(基於Android 10)

原創不易,轉載請標明出處。若閱讀過程當中發現問題,請不吝指教,比心~java

前言

這是一個基於 Android 10 源碼,全面分析 Android通知系統實現原理 的系列,這是第三篇,全系列將覆蓋:android

寫在前面

前面兩篇文章咱們介紹了 Notification 的簡單使用 和 經常使用接口介紹 以及 系統通知服務的啓動流程 和 功能實現,相信你已經對系統通知服務有了初步的印象了,這一篇咱們將全面分析通知發送在框架層(服務端)的一系列處理數據庫

說明:設計模式

    1. 下文出現的簡寫
NM -> NotificationManager
NMS -> NotificationManagerService
Sysui -> SystemUI
複製代碼
  • 2.通知總體發送流程:Android 通知的發送涉及到兩個進程:System進程(NMS所在的進程) 和 SystemUI, 流程以下:

服務端(System進程):NM發送 -> NMS處理 -> NMS 將通知post給監聽器 ->
客戶端(SystemUI):Sysui接收 -> 根據通知類型加載對應通知佈局 -> 顯示數組

這一篇咱們分析的是服務端的實現,因爲 服務端的整個處理流程很是複雜,故細分爲以下幾個小節:bash

  • 1.NotificationManager 預處理通知
  • 2.入列預處理
  • 3.通知入列
  • 4.通知post流程
    • 4.1 通知post預處理
    • 4.2 通知排序預處理
    • 4.3 通知排序
    • 4.4 通知震動、音效和呼吸燈處理
    • 4.5 通知post

1、NotificationManager 預處理通知

通知發送的入口是NotificationManager.notify(...)方法,因此咱們從這裏開始閱讀源碼,在咱們調用notify發送通知後,NotificationManager先調用了fixNotification(Notification notification)對通知作了預處理,而後直接調用了NMS的enqueueNotificationWithTag()函數去處理咱們的請求:微信

/*frameworks/base/core/java/android/app/NotificationManager.java*/
    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    fixNotification(notification), user.getIdentifier());
        }
    }

    private Notification fixNotification(Notification notification) {
        // 修正 sound
        ......
        // 兼容舊版本smallIcon設置接口
        fixLegacySmallIcon(notification, pkg);
        // 步驟1:異常處理
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        // 裁剪通知中包含的圖片的大小,避免用戶設置的圖片太大
        notification.reduceImageSizes(mContext);
        // 低內存設備兼容
        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        boolean isLowRam = am.isLowRamDevice();
        return Builder.maybeCloneStrippedForDelivery(notification, isLowRam, mContext);
    }
複製代碼

fixNotification方法對通知作了簡單修正,如 smallIcon 處理,圖片資源裁剪處理,低內存兼容等,說明下步驟1:在SDK版本大於22以後,Android強制要求用戶設置 smallIcon 了,不然會報異常,因此咱們發送通知的時候,smallIcon必須設置。數據結構

作完簡單修正處理後,NotificationManager就直接將流程交給NMS了,接下來看NMS的處理流程。app

2、入列預處理

前面NotificationManager中調用了NMS的enqueueNotificationWithTag(...)方法,這個方法最終會走到NMS.enqueueNotificationInternal(...)方法,這個方法挺長的,下面給出簡化後的代碼,說明幾個開發者須要注意的問題:框架

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java*/
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,final int callingPid, final String tag, final int id, final Notification notification,int incomingUserId) {
        // 權限檢查
        ......
        // 步驟1
        try {
            fixNotification(notification, pkg, userId);
        }
        // 步驟2
        String channelId = notification.getChannelId();
        final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,notificationUid, channelId, false);
        if (channel == null) {
            ......
            return;
        }
        // 步驟3
        final StatusBarNotification n = new StatusBarNotification(pkg, opPkg, id, tag, notificationUid,callingPid, notification,user, null, System.currentTimeMillis());
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
        // 前臺服務channel處理
        if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
            ......
        }
        // 步驟4
        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,r.sbn.getOverrideGroupKey() != null)) {
            return;
        }
        ......
        // 步驟5
        mHandler.post(new EnqueueNotificationRunnable(userId, r));
    }
複製代碼

這個方法進一步作了各類權限檢查、應用設置檢查、應用通知發送速率限制等,下面分別解釋:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java*/
    protected void fixNotification(Notification notification, String pkg, int userId) throws NameNotFoundException {
        // 將通知發送方的 ApplicationInfo 存進 notification.extras 中,key = Notification.EXTRA_BUILDER_APPLICATION_INFO
        final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
        Notification.addFieldsFromContext(ai, notification);
        // 通知着色權限 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS 處理
        int canColorize = mPackageManagerClient.checkPermission(android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
        if (canColorize == PERMISSION_GRANTED) {
            notification.flags |= Notification.FLAG_CAN_COLORIZE;
        } else {
            notification.flags &= ~Notification.FLAG_CAN_COLORIZE;
        }
        // fullScreenIntent 的處理
        if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) {
            int fullscreenIntentPermission = mPackageManagerClient.checkPermission(
                    android.Manifest.permission.USE_FULL_SCREEN_INTENT, pkg);
            if (fullscreenIntentPermission != PERMISSION_GRANTED) {
                notification.fullScreenIntent = null;
                Slog.w(TAG, "Package " + pkg +
                        ": Use of fullScreenIntent requires the USE_FULL_SCREEN_INTENT permission");
            }
        }
    }
複製代碼
  • 步驟1:fixNotification(...)方法進一步修正通知,須要咱們特別關注的有android.Manifest.permission.USE_FULL_SCREEN_INTENT這個權限,當咱們在Android Q(29)及以上給通知設置fullScreenIntent時,須要聲明該權限,不然咱們設置的fullScreenIntent將是無效的,不是報錯,而是fullScreenIntent被置爲null了
/*NotificationManagerService.enqueueNotificationInternal()*/
        // 步驟2
        String channelId = notification.getChannelId();
        final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,
                notificationUid, channelId, false /* includeDeleted */);
        if (channel == null) {
            ......
            return;
        }
複製代碼
  • 步驟2:前面咱們說過。Android 8.0以後不設置channel的通知是沒法發送的,源碼就是在這裏作的限制
/*NotificationManagerService.enqueueNotificationInternal()*/
        // 步驟3
        final StatusBarNotification n = new StatusBarNotification(pkg, opPkg, id, tag, notificationUid,callingPid, notification,user, null, System.currentTimeMillis());
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
複製代碼
  • 步驟3:這裏說明兩個數據結構:
    • StatusBarNotification是一個面向客戶端的Notification的包裝類,僅包含用戶須要知道的通知相關的信息,如通知包名、id、key等信息,最終NMS將新通知回調給監聽者的時候,給客戶端的就是該對象
    • NotificationRecord面向服務端的Notification的包裝類,除了持有StatusBarNotification實例外,還封裝了各類通知相關的信息,如channel、sound(通知鈴聲)、vibration(震動效果)等等,這些信息在服務端處理通知的時候須要用到,但客戶端並不須要關心這些
/*NotificationManagerService.enqueueNotificationInternal()*/
        // 步驟4
        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,r.sbn.getOverrideGroupKey() != null)) {
            return;
        }
複製代碼
private boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag, NotificationRecord r, boolean isAutogroup) {
            ......
            if (!isSystemNotification && !isNotificationFromListener) {
                synchronized (mNotificationLock) {
                    // 限制更新類型通知的更新速率
                    if (mNotificationsByKey.get(r.sbn.getKey()) != null && !r.getNotification().hasCompletedProgress() && !isAutogroup) {
                        final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                        if (appEnqueueRate > mMaxPackageEnqueueRate) {
                            ......
                            return false;
                        }
                    }
                    // 限制普通應用可發送的通知數
                    int count = getNotificationCountLocked(pkg, userId, id, tag);
                    if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                        return false;
                    }
                }
            }
            // 不發送Snoozed類型的通知
            if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
                return false;
            }
            // blocked檢查
            if (isBlocked(r, mUsageStats)) {
                return false;
            }
            return true;
        }
複製代碼
  • 步驟4:進一步處理通知:
    • 限制更新類型通知的更新速率(如顯示下載進度的通知)
    • 限制普通應用可發送的通知數,Android Q上每一個應用最多容許發送25條,因Android版本而異
    • 不發送Snoozed類型的通知
    • 當用戶在設置中設置了不容許顯示某應用的通知(blocked)時,再也不發送
/*NotificationManagerService.enqueueNotificationInternal()*/
        // 步驟5
        mHandler.post(new EnqueueNotificationRunnable(userId, r));
複製代碼
  • 步驟5:通知入列,將通知發送流程交給EnqueueNotificationRunnable

在進入下一節以前這裏先解釋幾個概念:

1.下文會出現 舊通知 和 新通知 這兩個說法,指的是兩條"相同"的通知,這裏的相同指的是StatusBarNotification.key相同,NMS中維護了一個mNotificationsByKey數據集合,該集合以StatusBarNotification.key爲key,以NotificationRecord爲value,維護者當前的通知列表,其中key的構造是在StatusBarNotification的構造函數中完成的,構造過程以下:

/*frameworks/base/core/java/android/service/notification/StatusBarNotification.java*/
    private String key() {
        String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
        if (overrideGroupKey != null && getNotification().isGroupSummary()) {
            sbnKey = sbnKey + "|" + overrideGroupKey;
        }
        return sbnKey;
    }
複製代碼

也就是咱們先後以相同的key發送一條通知時,系統根據這個key就能夠從mNotificationsByKey中獲取到舊通知,例如更新類型的通知(微信同我的的消息,音樂軟件的通知等);而新通知天然就是新來的這條要更新的通知了。

2.NMS維護了幾個主要的數據結構,分別用在不一樣的場景下,先說明下,後續閱讀源碼的時候若是困惑就回頭再來看看這個總結吧:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java*/
// 服務端維護的 已排序 的通知
    final ArrayList<NotificationRecord> mNotificationList = new ArrayList<>();
// 服務端維護的 未排序 的通知
    final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();
//  "入列通知",通知入列的時候被記錄,當某通知成功發送後則會被從該集合中移除,因此最終該集合記錄的是全部入列成功但發送不成功的通知
    final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();
// 前面咱們說過,當應用未主動爲通知設置組別時,系統也會去作這件事,該集合記錄的就是這些系統成組的父通知,
// 也就是每次系統幫某用戶的某個應用建立了一條父通知,則該父通知會被記錄進該集合,
// key - value 爲:ArrayMap<userId, ArrayMap<pkg, summarySbnKey>>
    final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
// 服務端根據groupKey,維護着全部用戶主動成組的父通知,主要在`EnqueueNotificationRunnable`中處理分組通知的時候使用
    final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
複製代碼

3.分組的概念: Android容許應用主動將發送出來的多條通知以組的形式顯示在一塊兒,並經過goupKey區分組別,這樣能夠避免同個應用的多條通知佔據了通知面板的大量顯示空間,同時,若是應用未主動將多條通知成組,則系統也會去作這個事情,例如Android Q上面在同個應用的通知數達到4條的時候就會將其成組顯示。


3、通知入列

前面 入列預處理 enqueueNotificationInternal(...)方法的最後將通知的發送進一步交給EnqueueNotificationRunnable這個Runable去處理,咱們來看看它的run方法,一樣是作過精簡的:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService$EnqueueNotificationRunnable.java*/
        public void run() {
            synchronized (mNotificationLock) {
                // 存進集合,後續會用到
                mEnqueuedNotifications.add(r);
                // 當用戶設置了 Builder.setTimeoutAfter(long durationMs) 則會在這裏作處理
                scheduleTimeoutLocked(r);
                // 從集合mNotificationsByKey中取出舊通知
                final StatusBarNotification n = r.sbn;
                NotificationRecord old = mNotificationsByKey.get(n.getKey());
                ......
                // 分組處理
                handleGroupedNotificationLocked(r, old, callingUid, callingPid);
                ......
                // 準備工做作好了,下一步準備post通知
                if (mAssistants.isEnabled()) {
                    mAssistants.onNotificationEnqueuedLocked(r);
                    mHandler.postDelayed(new PostNotificationRunnable(r.getKey()), DELAY_FOR_ASSISTANT_TIME);
                } else {
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
        }
複製代碼

重點關注下分組處理的內容:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java*/
    private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old,
            int callingUid, int callingPid) {
        StatusBarNotification sbn = r.sbn;
        Notification n = sbn.getNotification();
        // 步驟1
        if (n.isGroupSummary() && !sbn.isAppGroup())  {
            n.flags &= ~Notification.FLAG_GROUP_SUMMARY;
        }

        String group = sbn.getGroupKey();
        boolean isSummary = n.isGroupSummary();
        Notification oldN = old != null ? old.sbn.getNotification() : null;
        String oldGroup = old != null ? old.sbn.getGroupKey() : null;
        boolean oldIsSummary = old != null && oldN.isGroupSummary();
        boolean oldIsSummary = old != null && oldN.isGroupSummary();
        // 步驟2
        if (oldIsSummary) {
            NotificationRecord removedSummary = mSummaryByGroupKey.remove(oldGroup);
            if (removedSummary != old) {
                String removedKey =
                        removedSummary != null ? removedSummary.getKey() : "<null>";
                Slog.w(TAG, "Removed summary didn't match old notification: old=" + old.getKey() +
                        ", removed=" + removedKey);
            }
        }
        if (isSummary) {
            mSummaryByGroupKey.put(group, r);
        }
        // 步驟3 
        if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
            cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */,null);
        }
    }
複製代碼
  • 步驟1:修正處理,當用戶調用Builder setGroupSummary(boolean isGroupSummary)設置了Notification.FLAG_GROUP_SUMMARY這個flag,可是沒調用 Builder.setGroup(String groupKey)設置對應的groupKey,則Notification.FLAG_GROUP_SUMMARY這個flag會被去掉,不然會致使後續系統的自動成組出錯。這個處理糾正了一些用戶的錯誤操做,例如用戶但願發送一條父通知,可是隻調用了Builder setGroupSummary(boolean isGroupSummary)而忘了設置相應的groupKey
  • 步驟2:若是必要的話更新集合mSummaryByGroupKey,這個集合咱們前面總結過,忘記的回頭看吧
  • 步驟3:若是舊通知是一條父通知,新通知變成了非父通知;或者舊通知新通知均是父通知,可是group key已經發生了變化,則原來父通知下的全部子通知會被移除

能夠看到,EnqueueNotificationRunnable只是對通知作進一步的處理和糾偏,重點處理了通知成組相關的內容,該Runnable的最後是將通知發送流程進一步交給了PostNotificationRunnable去處理,這個Runnable真正作了通知在服務端的發送(post)操做,下一節~


4、通知post流程

PostNotificationRunnable.run(),這個方法處理的事情很是多,主要是作通知發送前的最後處理,先給出這一步的代碼,後面再一一分析:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService$PostNotificationRunnable.java*/
        public void run() {
            synchronized (mNotificationLock) {
                try {
                    NotificationRecord r = null;
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            r = enqueued;
                            break;
                        }
                    }
                    // 步驟1 
                    if (r == null) { return; }
                    // 步驟2 
                    if (isBlocked(r)) { return; }
                    // 步驟3:
                    final boolean isPackageSuspended = isPackagePausedOrSuspended(r.sbn.getPackageName(), r.getUid());
                    r.setHidden(isPackageSuspended);
                    NotificationRecord old = mNotificationsByKey.get(key);
                    final StatusBarNotification n = r.sbn;
                    final Notification notification = n.getNotification();
                    int index = indexOfNotificationLocked(n.getKey());
                    // 步驟4
                    if (index < 0) {
                        mNotificationList.add(r);
                    } else {
                        old = mNotificationList.get(index);
                        mNotificationList.set(index, r);
                        // 避免通知更新過程當中前臺服務標誌丟失
                        notification.flags |=
                                old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                        // 記錄通知是更新類型的,後續決定是否播放通知聲音、震動等提醒的時候會用到
                        r.isUpdate = true;
                    }
                    // 步驟5 記錄 未排序 通知
                    mNotificationsByKey.put(n.getKey(), r);
                    // 步驟6
                    if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) { notification.flags |= FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; }
                    // 步驟7
                    mRankingHelper.extractSignals(r);
                    // 步驟8
                    mRankingHelper.sort(mNotificationList);
                    // 步驟9
                    if (!r.isHidden()) { buzzBeepBlinkLocked(r); }
                    // 步驟:10
                    if (notification.getSmallIcon() != null) {
                        StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                        // 步驟10.2
                        mListeners.notifyPostedLocked(r, old);
                        if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) && !isCritical(r)) {
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    // 步驟10.3
                                    mGroupHelper.onNotificationPosted(n, hasAutoGroupSummaryLocked(n)); 
                                }
                            });
                        }
                    } else {
                        // 步驟10.1
                        if (old != null && !old.isCanceled) {
                            mListeners.notifyRemovedLocked(r, NotificationListenerService.REASON_ERROR, r.getStats());
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationRemoved(n);
                                }
                            });
                        }
                    }
                } finally {
                    // 將前面入列的通知從 mEnqueuedNotifications 移除,因此最終該集合記錄的是全部入列成功但發送不成功的通知
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
複製代碼

4.1 通知post預處理

/*PostNotificationRunnable.run()*/
                    // 步驟1 
                    if (r == null) { return; }
複製代碼
  • 步驟1:若通知已入列可是沒走到這裏的時候就被取消了,則中止發送處理,由於存在通知處理一半就被取消的狀況,而取消通知時會從mEnqueuedNotifications將通知移除,因此將入列期間的通知存在mEnqueuedNotifications中可讓咱們在處理通知的不一樣階段去檢查通知是否已經被移除
/*PostNotificationRunnable.run()*/
                    // 步驟2 
                    if (isBlocked(r)) { return; }
複製代碼
  • 步驟2:前面 enqueueNotificationInternal 已經作過一次 blocked 檢查,這裏再次檢查是避免在中間處理過程當中 blocked 屬性發生了改變,因此整個通知發送過程當中存在兩次 blocked 狀態檢查
/*PostNotificationRunnable.run()*/
                    // 步驟3:
                    final boolean isPackageSuspended = isPackagePausedOrSuspended(r.sbn.getPackageName(), r.getUid());
                    r.setHidden(isPackageSuspended);
複製代碼
  • 步驟3:該應用是否被系統限制了,是的話hidden爲true,這個屬性在後面決定是否播放通知聲音、震動等提醒的時候會用到
/*PostNotificationRunnable.run()*/
                    NotificationRecord old = mNotificationsByKey.get(key);
                    final StatusBarNotification n = r.sbn;
                    final Notification notification = n.getNotification();
                    int index = indexOfNotificationLocked(n.getKey());
                    // 步驟4
                    if (index < 0) {
                        mNotificationList.add(r);
                    } else {
                        old = mNotificationList.get(index);
                        mNotificationList.set(index, r);
                        // 避免通知更新過程當中前臺服務標誌丟失
                        notification.flags |=
                                old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                        // 記錄通知是更新類型的,後續決定是否播放通知聲音、震動等提醒的時候會用到
                        r.isUpdate = true;
                    }
複製代碼
  • 步驟4:前面咱們總結過,mNotificationList是存儲 已排序 的通知,這裏判斷新來的通知是否是更新類型的,不是的話就直接add進mNotificationList,是的話則會將舊的通知替換掉,排序不變
/*PostNotificationRunnable.run()*/
                    // 步驟5 記錄 未排序 通知
                    mNotificationsByKey.put(n.getKey(), r);
複製代碼
  • 步驟5:將即將發送的通知存進集合mNotificationsByKey,這也是爲何前面咱們能夠經過mNotificationsByKey獲取到某通知是否存在舊通知的緣由
/*PostNotificationRunnable.run()*/
                    // 步驟6
                    if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) { notification.flags |= FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; }
複製代碼
  • 步驟6:前臺服務通知是強制常駐通知面板的,無論你發送的時候是否設置了相關的常駐標誌(FLAG_ONGOING_EVENT / FLAG_NO_CLEAR),系統都會幫你加上

4.2 通知排序預處理

這一步是在對通知進行排序前利用各類規則更新通知的各類屬性, 這裏涉及到幾個類:

frameworks/base/services/core/java/com/android/server/notification/RankingConfig.java
frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper.java

frameworks/base/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
frameworks/base/services/core/java/com/android/server/notification/BadgeExtractor.java
frameworks/base/services/core/java/com/android/server/notification/ZenModeExtractor.java
frameworks/base/services/core/java/com/android/server/notification/XXXExtractor.java
......
複製代碼

RankingConfig,接口類,定義了各類通知屬性的操做接口,例如:

public interface RankingConfig {
    void setImportance(String packageName, int uid, int importance);
    int getImportance(String packageName, int uid);
    void setShowBadge(String packageName, int uid, boolean showBadge);
    boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,boolean fromTargetApp, boolean hasDndAccess);
    void updateNotificationChannel(String pkg, int uid, NotificationChannel channel, boolean fromUser);
    ......
}
複製代碼

而接口的實現類爲PreferencesHelper,咱們挑兩個來看看:

/*frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper.java*/
    @Override
    public boolean canShowBadge(String packageName, int uid) {
        synchronized (mPackagePreferences) {
            return getOrCreatePackagePreferencesLocked(packageName, uid).showBadge;
        }
    }
    @Override
    public void setShowBadge(String packageName, int uid, boolean showBadge) {
        synchronized (mPackagePreferences) {
            getOrCreatePackagePreferencesLocked(packageName, uid).showBadge = showBadge;
        }
        updateConfig();
    }
複製代碼

badge表示通知圓點,也就是應用桌面圖標右上角上那個 告訴你該應用來通知了的小圓點,當咱們在設置中設置某應用的圓點開關的時候,請求會被從 設置 跨應用發送到 NMS,NMS則調用 PreferencesHelper.setShowBadge(String packageName, int uid, boolean showBadge)來執行該更新事件,更新結果保存在PreferencesHelper中一個叫PackagePreferences的數據結構中,並在更新完成的時候調用updateConfig去更新配置,以便咱們後續使用RankingConfig時能讀到最新的狀態

/*frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper$PackagePreferences.java*/
    private static class PackagePreferences {
        String pkg; // 包名
        int uid = UNKNOWN_UID;
        int importance = DEFAULT_IMPORTANCE; // 通知重要程度
        int priority = DEFAULT_PRIORITY; // 通知優先級
        int visibility = DEFAULT_VISIBILITY; // 通知可見性
        boolean showBadge = DEFAULT_SHOW_BADGE; // 通知圓點
        boolean allowBubble = DEFAULT_ALLOW_BUBBLE; // 氣泡通知
        int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS;
        boolean oemLockedImportance = DEFAULT_OEM_LOCKED_IMPORTANCE;
        List<String> futureOemLockedChannels = new ArrayList<>();
        boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;
        Delegate delegate = null;
        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
        Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>();
        public boolean isValidDelegate(String pkg, int uid) {
            return delegate != null && delegate.isAllowed(pkg, uid);
        }
    }
複製代碼

NotificationChannelExtractor,規則處理器抽象類,定義各類規則的抽象接口,具體規則則由各類類型的子類去實現,後面會舉例,先看看這個接口:

public interface NotificationSignalExtractor {
    // 初始化接口
    public void initialize(Context context, NotificationUsageStats usageStats);
    // 每次通知發送或更新的時候調用,若是`process`方法處理完以後還有其餘東西須要作進一步處理,則返回一個`RankingReconsideration`
    public RankingReconsideration process(NotificationRecord notification);
    // 讓規則處理器持有規則`RankingConfig`
    void setConfig(RankingConfig config);
    // 讓規則處理器持有免打擾輔助類`ZenModeHelper`
    void setZenHelper(ZenModeHelper helper);
}
複製代碼

該接口有多個實現類,一套規則一個實現類,咱們挑一個來看看:

public class BadgeExtractor implements NotificationSignalExtractor {
    private RankingConfig mConfig;
    public void initialize(Context ctx, NotificationUsageStats usageStats) { }
    public RankingReconsideration process(NotificationRecord record) {
        if (record == null || record.getNotification() == null) { return null; }
        if (mConfig == null) { return null; }
        boolean userWantsBadges = mConfig.badgingEnabled(record.sbn.getUser());
        boolean appCanShowBadge = mConfig.canShowBadge(record.sbn.getPackageName(), record.sbn.getUid());
        if (!userWantsBadges || !appCanShowBadge) {
            record.setShowBadge(false);
        } else {
            if (record.getChannel() != null) {
                record.setShowBadge(record.getChannel().canShowBadge() && appCanShowBadge);
            } else {
                record.setShowBadge(appCanShowBadge);
            }
        }
        if (record.isIntercepted()
                && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_BADGE) != 0) {
            record.setShowBadge(false);
        }
        return null;
    }

    @Override
    public void setConfig(RankingConfig config) {
        mConfig = config;
    }

    @Override
    public void setZenHelper(ZenModeHelper helper) {
    }
}
複製代碼

這是一套決定通知是否顯示圓點的規則,規則包括:

  • 系統是否容許顯示圓點(Config.badgingEnabled),受一個系統全局變量影響,該變量寫在 Settings 數據庫字段中:NOTIFICATION_BADGING = "notification_badging",改變該值的地方是 設置中的開關
  • 用戶是否在設置中打開了容許通知(Config.canShowBadge),受咱們前面說的setShowBadge接口影響,也就是會去查詢PreferencesHelper類中的數據結構PackagePreferences
  • 此外還可能受通知channel影響等等

以上只是處理了一個通知屬性,而其餘各類屬性則分別在不一樣的規則處理器中處理,Android定義了一個配置列表,聲明瞭全部的規則處理器,同時容許咱們去擴展咱們本身的規則處理器。

咱們看看這個規則處理器配置列表:

/*frameworks/base/core/res/res/values/config.xml*/

    <string-array name="config_notificationSignalExtractors">
        <!-- many of the following extractors depend on the notification channel, so this
        extractor must come first -->
        <item>com.android.server.notification.NotificationChannelExtractor</item>
        <item>com.android.server.notification.NotificationAdjustmentExtractor</item>
        <item>com.android.server.notification.BubbleExtractor</item>
        <!-- depends on AdjustmentExtractor-->
        <item>com.android.server.notification.ValidateNotificationPeople</item>
        <item>com.android.server.notification.PriorityExtractor</item>
        <!-- depends on PriorityExtractor -->
        <item>com.android.server.notification.ZenModeExtractor</item>
        <item>com.android.server.notification.ImportanceExtractor</item>
        <!-- depends on ImportanceExtractor-->
        <item>com.android.server.notification.NotificationIntrusivenessExtractor</item>
        <item>com.android.server.notification.VisibilityExtractor</item>
        <!-- Depends on ZenModeExtractor -->
        <item>com.android.server.notification.BadgeExtractor</item>
        <item>com.android.server.notification.CriticalNotificationExtractor</item>
    </string-array>
複製代碼

當咱們須要新增規則時,只須要在這個配置列表中指定咱們的規則實現類,並讓咱們的規則實現類實現NotificationSignalExtractor這個接口,完成咱們特定的規則制定便可。

/*PostNotificationRunnable.run()*/
                    // 步驟7
                    mRankingHelper.extractSignals(r);
複製代碼

咱們接着看PostNotificationRunnable.run()中的步驟7

/*frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java*/
    public void extractSignals(NotificationRecord r) {
        final int N = mSignalExtractors.length;
        for (int i = 0; i < N; i++) {
            NotificationSignalExtractor extractor = mSignalExtractors[i];
            try {
                RankingReconsideration recon = extractor.process(r);
                if (recon != null) {
                    mRankingHandler.requestReconsideration(recon);
                }
            }
        }
    }
複製代碼

也就是遍歷各個規則處理器,觸發其process方法去設置通知的各類屬性,當返回值RankingReconsideration不爲空時,則進一步處理其餘規則,不展開講。

這裏主要學習源碼的這種實現思路:將賦值過程複雜的屬性的處理經過抽象隔離開來分別處理,達到修改某個屬性的規則時不影響其餘屬性的目的,同時還保證了良好的可擴展性,當咱們須要定義新的規則的時候,只須要擴展咱們本身的一套規則便可.

這裏體現了設計模式中多個基本原則,如單一職責原則(一個類應只包含單一的職責)、依賴倒轉原則(抽象不該該依賴於細節,細節應當依賴於抽象)和迪米特原則(一個類儘可能不要與其餘類發生關係)等,整個通知系統的設計是十分複雜的,這個過程當中有不少設計模式的體現,讀者閱讀的時候可多加思考並學習其應用。

4.3 通知排序

/*PostNotificationRunnable.run()*/
                    // 步驟8
                    mRankingHelper.sort(mNotificationList);
複製代碼
  • 步驟8:更新完通知的各類屬性後,就能夠對通知進行排序了,能夠看到傳進去排序的集合爲mNotificationList,這也就是爲何咱們前面說mNotificationList是已排序的通知集合

服務端通知排序分爲兩次,初步排序與最終排序,具體排序規則受兩個排序類影響:

初步排序:frameworks/base/services/core/java/com/android/server/notification/NotificationComparator.java
最終排序:frameworks/base/services/core/java/com/android/server/notification/GlobalSortKeyComparator.java
複製代碼

其中初步排序主要是 根據Importance / 彩色通知(受Notification.setColorized()接口影響) / 是否常駐通知(ongoing) / 是否重要消息 / 是否重要聯繫人 / priority / 通知發送時間 等因素影響,其中通知發送時間在排序規則中是最後被考慮的,這也是爲何常常咱們看到的最新通知不必定是顯示在最頂端的緣由。具體規則和代碼不展開講,感興趣的本身閱讀下frameworks/base/services/core/java/com/android/server/notification/NotificationComparator.javacompare 方法。

對於初步排序這裏只強調一點,因爲Comparator比較器默認是升序的,若是不作處理會致使mNotificationList中的通知的排序是按照重要程度從低到高排序,這與咱們的預期結果是相反的,源碼的處理是在返回比較結果前作一次反序處理,舉個例子:

/*frameworks/base/services/core/java/com/android/server/notification/NotificationComparator.java*/
    public int compare(NotificationRecord left, NotificationRecord right) {
        final int leftImportance = left.getImportance();
        final int rightImportance = right.getImportance();
        final boolean isLeftHighImportance = leftImportance >= IMPORTANCE_DEFAULT;
        final boolean isRightHighImportance = rightImportance >= IMPORTANCE_DEFAULT;
        if (Settings.Secure.getInt(mContext.getContentResolver(),
                Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) == 1) {
            if (isLeftHighImportance != isRightHighImportance) {
                return -1 * Boolean.compare(isLeftHighImportance, isRightHighImportance);
            }
        }
        ......
    }
複製代碼

咱們知道,int compare(T o1, T o2)是「比較o1和o2的大小」:

  • 返回 負數 表示 o1 比 o2小
  • 返回 0 表示 o1 等於 o2
  • 返回 正數 表示 o1 大於 o2

因此正常狀況下,當isLeftHighImportance的值大於isRightHighImportance時,因爲是升序,Importance較大的通知會被排在後面,而這裏執行了 -1 * result 後,Importance較大的通知就排在前面了

接下來思考:爲何初步排序還不夠呢,這裏就涉及到咱們發送通知時可能會用到的一個接口了:Builder.setSortKey(),有時候咱們發送通知會調用Builder.setSortKey()設置一個排序鍵值,去對當前應用的通知進行排序,系統就是在最終排序裏面對咱們經過setSortKey設置的排序規則作受理的,而在最終排序前,系統會去規範咱們設置的鍵值

那當咱們設置了setSortKey以後,系統是怎麼排序的呢?這塊可能有些童鞋有點糊迷,下面看看步驟8的sort方法:

/*frameworks/base/services/core/java/com/android/server/notification/RankingHelper.java*/
    public void sort(ArrayList<NotificationRecord> notificationList) {
        final int N = notificationList.size();
        // clear global sort keys
        for (int i = N - 1; i >= 0; i--) {
            notificationList.get(i).setGlobalSortKey(null);
        }
        // 初步排序,詳見 `NotificationComparator`
        Collections.sort(notificationList, mPreliminaryComparator);
        // 最終排序前的預處理
        synchronized (mProxyByGroupTmp) {
            for (int i = 0; i < N; i++) {
                final NotificationRecord record = notificationList.get(i);
                record.setAuthoritativeRank(i);
                final String groupKey = record.getGroupKey();
                NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
                if (existingProxy == null) {
                    mProxyByGroupTmp.put(groupKey, record);
                }
            }
            for (int i = 0; i < N; i++) {
                final NotificationRecord record = notificationList.get(i);
                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
                String groupSortKey = record.getNotification().getSortKey();
                // 步驟8.1,執行預處理
                String groupSortKeyPortion;
                if (groupSortKey == null) {
                    groupSortKeyPortion = "nsk";
                } else if (groupSortKey.equals("")) {
                    groupSortKeyPortion = "esk";
                } else {
                    groupSortKeyPortion = "gsk=" + groupSortKey;
                }
                boolean isGroupSummary = record.getNotification().isGroupSummary();
                // 步驟8.2,執行預處理
               record.setGlobalSortKey(String.format("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
                        record.getCriticality(), record.isRecentlyIntrusive() && record.getImportance() > NotificationManager.IMPORTANCE_MIN ? '0' : '1',
                        groupProxy.getAuthoritativeRank(), isGroupSummary ? '0' : '1', groupSortKeyPortion, record.getAuthoritativeRank()));
            }
            mProxyByGroupTmp.clear();
        }
        // 步驟8.3,執行最終排序
        Collections.sort(notificationList, mFinalComparator);
    }
複製代碼
  • 步驟8.1:最終排序預處理,能夠看到系統對sortKey作了統一處理:
    • 當咱們沒設置sortKey時,groupSortKeyPortion = "nsk"
    • 當咱們設置的sortKey爲空(也就是"")時,groupSortKeyPortion = "esk";
    • 當咱們設置的sortKey不爲null時,groupSortKeyPortion = "gsk=" + groupSortKey;
  • 步驟8.2:最終排序預處理,以crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x這個格式爲record設置mGlobalSortKey,也就是系統將這幾個屬性組合成一個字符串,賦值給mGlobalSortKey,裏面就包括前面步驟1中規範化出來的sortKey,而這一整個字符串將在最終排序中影響通知排序
  • 步驟8.3:執行最終排序,這裏咱們直接看最終排序用的這個對比器mFinalComparator裏面的規則:
/*frameworks/base/services/core/java/com/android/server/notification/GlobalSortKeyComparator.java*/
public class GlobalSortKeyComparator implements Comparator<NotificationRecord> {
    @Override
    public int compare(NotificationRecord left, NotificationRecord right) {
        if (left.getGlobalSortKey() == null) { return 1; }
        if (right.getGlobalSortKey() == null) { return  -1; }
        return left.getGlobalSortKey().compareTo(right.getGlobalSortKey());
    }
}
複製代碼

能夠看到系統直接比較了前面設置的mGlobalSortKey值,mGlobalSortKey是一個字符串,也就是這裏的排序規則是由字典序排序規則決定的。結合前面兩點咱們能夠得出,當兩條通知的crtcl、intrsv、grnk、gsmry這幾個屬性的值都同樣的狀況下,咱們經過Builder.setSortKey()設置的排序鍵值就會生效了。

通過前面系統的規範,不一樣的鍵值類型對應的字典序排序結果爲:esk類型 > gsk=xxx類型 > nsk類型,即sortKey類型爲**""** 的通知會排在最前面,接着是設置了sortKey的通知,這一類通知的排序則根據用戶指定的sortKey而定,接着纔是沒設置sortKey的,本地寫了個demo驗證了下,結果以下:

sortKey.jpg

到這裏咱們也就明白了爲何有時候設置sortKey並不能生效了,由於sortKey的排序優先級不是最高的,還受crtcl、intrsv、grnk、gsmry這幾個屬性影響。

4.4 通知震動、音效和呼吸燈處理

/*PostNotificationRunnable.run()*/
                    // 步驟9
                    if (!r.isHidden()) { buzzBeepBlinkLocked(r); }
複製代碼
  • 步驟9:這裏開始處理通知的震動、音效和呼吸燈效果,這裏主要根據通知的 重要程度、是否當前用戶、是否更新類型的通知等 信息共同決定當前這條通知是否須要 震動、音效和呼吸燈效果,代碼較簡單,不展開講,感興趣的童鞋直接看下NotificationManagerService.buzzBeepBlinkLocked(NotificationRecord record)方法,下面重點看看通知的post

4.5 通知post

分析了這麼久,咱們終於來到通知的發送步驟了,別急,還有不少事情沒處理呢~例如咱們前面說過,當用戶未主動給應用通知設置組別時,系統會幫咱們最這件事,可是到目前爲止都沒有見到相關處理,答案就在下面,接着往下看:

/*PostNotificationRunnable.run()*/
                    // 步驟:10
                    if (notification.getSmallIcon() != null) {
                        StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                        // 步驟10.2
                        mListeners.notifyPostedLocked(r, old);
                        if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) && !isCritical(r)) {
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    // 步驟10.3
                                    mGroupHelper.onNotificationPosted(n, hasAutoGroupSummaryLocked(n)); 
                                }
                            });
                        }
                    } else {
                        // 步驟10.1
                        if (old != null && !old.isCanceled) {
                            mListeners.notifyRemovedLocked(r, NotificationListenerService.REASON_ERROR, r.getStats());
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationRemoved(n);
                                }
                            });
                        }
                    }
                } finally {
                    // 將前面入列的通知從 mEnqueuedNotifications 移除,因此最終該集合記錄的是全部入列成功但發送不成功的通知
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
複製代碼
  • 步驟10:這裏再次對通知是否有mSmallIcon作了檢查,避免前面在處理通知的過程當中mSmallIcon丟失了,而沒有mSmallIcon的通知是必定不能發送的,這也看出了Google對流氓通知是零容忍的。若是沒有mSmallIcon,則走進else
  • 步驟10.1:先看else 的狀況,此時若是舊通知已成功發送但新通知沒smallIcon,則舊通知會被移除,因此移除通知不必定要調用cancel接口,在這種狀況下舊通知也是會被移除的。

通知各監聽者

/*PostNotificationRunnable.run()*/
                        // 步驟10.2
                        mListeners.notifyPostedLocked(r, old);
複製代碼
  • 步驟10.2:mSmallIcon不爲空,終於能夠發送了,mListeners.notifyPostedLocked(r, old)將以異步的形式,將該消息通知給各個listeners,其中就包括咱們後面主要分析的SystemUI:
/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java$*/
        private void notifyPostedLocked(NotificationRecord r, NotificationRecord old, boolean notifyAllListeners) {
            for (final ManagedServiceInfo info : getServices()) {
                // 過濾掉部分listener.如不可見用戶,Android P如下hidden類型的通知等
                ......
                // 步驟10.2.1
                final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
                // 移除原來可見如今不可見的通知
                if (oldSbnVisible && !sbnVisible) {
                    final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            notifyRemoved(
                                    info, oldSbnLightClone, update, null, REASON_USER_STOPPED);
                        }
                    });
                    continue;
                }
                // 步驟10.2.2
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyPosted(info, sbnToPost, update);
                    }
                });
            }
        }
複製代碼
  • 步驟10.2.1:構建一個包含全部通知排序信息和關鍵屬性的映射表,其中key=StatusBarNotification.key,value=NotificationListenerService.Ranking
/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java$*/
    private NotificationRankingUpdate makeRankingUpdateLocked(ManagedServiceInfo info) {
        final int N = mNotificationList.size();
        final ArrayList<NotificationListenerService.Ranking> rankings = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            NotificationRecord record = mNotificationList.get(i);
            // 過濾掉當前用戶不可見的通知
            if (!isVisibleToListener(record.sbn, info)) {
                continue;
            }
            final String key = record.sbn.getKey();
            // 一條通知對應一個 Ranking
            final NotificationListenerService.Ranking ranking =
                    new NotificationListenerService.Ranking();
            // 將通知的關鍵信息,包括排序、關鍵屬性等存進 Ranking
            ranking.populate(
                    key,
                    rankings.size(),
                    !record.isIntercepted(),
                    record.getPackageVisibilityOverride(),
                    record.getSuppressedVisualEffects(),
                    record.getImportance(),
                    record.getImportanceExplanation(),
                    record.sbn.getOverrideGroupKey(),
                    record.getChannel(),
                    record.getPeopleOverride(),
                    record.getSnoozeCriteria(),
                    record.canShowBadge(),
                    record.getUserSentiment(),
                    record.isHidden(),
                    record.getLastAudiblyAlertedMs(),
                    record.getSound() != null || record.getVibration() != null,
                    record.getSystemGeneratedSmartActions(),
                    record.getSmartReplies(),
                    record.canBubble()
            );
            rankings.add(ranking);
        }
        // 構建`RankingMap`
        return new NotificationRankingUpdate(
                rankings.toArray(new NotificationListenerService.Ranking[0]));
    }
複製代碼

關注下最後一步:將列表轉爲 NotificationListenerService.Ranking 類型的數組,而後構建一個RankingMapRankingMap是一個key=StatusBarNotification.key,value=NotificationListenerService.RankingArrayMap,後面SystemUI會根據這個映射表的排序信息顯示通知

/*frameworks/base/core/java/android/service/notification/NotificationRankingUpdate.java*/
    public NotificationRankingUpdate(NotificationListenerService.Ranking[] rankings) {
        mRankingMap = new NotificationListenerService.RankingMap(rankings);
    }

/*frameworks/base/core/java/android/service/notification/NotificationRankingUpdate$RankingMap.java*/
        public RankingMap(Ranking[] rankings) {
            for (int i = 0; i < rankings.length; i++) {
                final String key = rankings[i].getKey();
                mOrderedKeys.add(key);
                // key=StatusBarNotification.key,value=NotificationListenerService.Ranking
                mRankings.put(key, rankings[i]);
            }
        }
複製代碼
  • 步驟10.2.2:執行listener.onNotificationPosted(sbnHolder, rankingUpdate);接口通知各個監聽器

至此,各個監聽器就能收到來新通知的消息了

嘗試構建系統分組

/*PostNotificationRunnable.run()*/
                        if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) && !isCritical(r)) {
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    // 步驟10.3
                                    mGroupHelper.onNotificationPosted(n, hasAutoGroupSummaryLocked(n)); 
                                }
                            });
                        }
複製代碼
  • 步驟10.3:這裏將利用分組通知的輔助類GroupHelper在必要的狀況下構建一個父通知,前面咱們說的用戶未主動將通知分組時系統會幫咱們去作這件事,就是在這裏完成的。但GroupHelper只是負責判斷是否須要建立或者移除系統建立的通知,具體的操做是在NMS中完成的,涉及到下面這個回調:
/*frameworks/base/services/core/java/com/android/server/notification/GroupHelper.java*/
    protected interface Callback {
        void addAutoGroup(String key);
        void removeAutoGroup(String key);
        void addAutoGroupSummary(int userId, String pkg, String triggeringKey);
        void removeAutoGroupSummary(int user, String pkg);
    }
複製代碼

NMS將回調註冊到GroupHelperGroupHelper則在必要的時候通知NMS去完成相關操做,來看看 步驟10.3 的具體操做,代碼作過簡化

/*frameworks/base/services/core/java/com/android/server/notification/GroupHelper.java*/
    // 步驟10.3.1
    Map<Integer, Map<String, LinkedHashSet<String>>> mUngroupedNotifications = new HashMap<>();

    public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
        try {
            List<String> notificationsToGroup = new ArrayList<>();
            // 步驟10.3.2
            if (!sbn.isAppGroup()) {
                synchronized (mUngroupedNotifications) {
                    // 步驟10.3.3
                    if (notificationsForPackage.size() >= mAutoGroupAtCount
                            || autogroupSummaryExists) {
                        notificationsToGroup.addAll(notificationsForPackage);
                    }
                }
                if (notificationsToGroup.size() > 0) {
                    // 步驟10.3.4
                    adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(), notificationsToGroup.get(0), true);
                    // 步驟10.3.5
                    adjustNotificationBundling(notificationsToGroup, true);
                }
            } else {
                // 步驟10.3.6
                maybeUngroup(sbn, false, sbn.getUserId());
            }
        }
    }
複製代碼
  • 步驟10.3.1:GroupHelper持有一個集合mUngroupedNotifications,存儲內容爲:<user, <packageName, notificationsForPackage>>,這樣就能將每一個用戶的每一個應用下的通知存儲起來,在條件知足的時候去執行系統成組操做
  • 步驟10.3.2:若用戶建立該通知的時候未指定mGroupKey或者mSortKey,則系統會嘗試去走建立父通知的邏輯
  • 步驟10.3.3:系統嘗試建立父通知,建立的條件是該應用的通知數達到了4條 或者 以前已經存在該應用的父通知了,這個的 4 是在配置在config中的,路徑爲frameworks/base/core/res/res/values/config.xml 下的 config_autoGroupAtCount字段,因此若是咱們想要修改系統的通知自動成組數條件,修改該變量便可。
  • 步驟10.3.4:知足建立父通知的條件,走adjustAutogroupingSummary邏輯,該方法最終調了NMS中的addAutoGroupSummary方法,這裏關注下建立的這條父通知的內容:
/*frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java*/
    private void createAutoGroupSummary(int userId, String pkg, String triggeringKey) {
        NotificationRecord summaryRecord = null;
        synchronized (mNotificationLock) {
            // 步驟10.3.4.1
            NotificationRecord notificationRecord = mNotificationsByKey.get(triggeringKey);
            ......
            if (!summaries.containsKey(pkg)) {
                ......
                final Notification summaryNotification =
                        new Notification.Builder(getContext(), channelId)
                                .setSmallIcon(adjustedSbn.getNotification().getSmallIcon())
                                .setGroupSummary(true)
                                .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN)
                                // 步驟10.3.4.2
                                .setGroup(GroupHelper.AUTOGROUP_KEY)
                                .setFlag(FLAG_AUTOGROUP_SUMMARY, true)
                                .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
                                .setColor(adjustedSbn.getNotification().color)
                                .setLocalOnly(true)
                                .build();
                ......
                final StatusBarNotification summarySbn =
                        new StatusBarNotification(adjustedSbn.getPackageName(), adjustedSbn.getOpPkg(), Integer.MAX_VALUE,
                                // 步驟10.3.4.2
                                GroupHelper.AUTOGROUP_KEY, 
                                adjustedSbn.getUid(), adjustedSbn.getInitialPid(), summaryNotification, adjustedSbn.getUser(),
                                // 步驟10.3.4.2
                                GroupHelper.AUTOGROUP_KEY,
                                System.currentTimeMillis());
                ......
            }
        }
        if (summaryRecord != null && checkDisqualifyingFeatures(userId, MY_UID, summaryRecord.sbn.getId(), summaryRecord.sbn.getTag(), summaryRecord, true)) {
            //  步驟10.3.4.3
            mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
        }
    }
複製代碼
  • 步驟10.3.4.1:拿到欲成組的子通知裏面的第一條,而後將該子通知的各類屬性複製給父通知,包括子通知的userId、extras、contentIntent等信息
  • 步驟10.3.4.2:這裏建立父通知時指定了NotificationmGroupKey = GroupHelper.AUTOGROUP_KEY,也就是ranker_group,同時指定了StatusBarNotificationtag = GroupHelper.AUTOGROUP_KEYoverrideGroupKey = GroupHelper.AUTOGROUP_KEY,這些信息在後續分析客戶端通知顯示的時候會用到
  • 步驟10.3.4.3:父通知構建完成後,執行入列操做,這個就跟前面分析的新通知入列的流程是同樣的了
/*GroupHelper.onNotificationPosted(...)*/
                    // 步驟10.3.5
                    adjustNotificationBundling(notificationsToGroup, true);
複製代碼
  • 步驟10.3.5:看回前面的 步驟10.3.5,這裏會去刷新全部剛被成組的全部子通知的屬性,主要操做是執行了StatusBarNotificationsetOverrideGroupKey()方法,將該值指定爲GroupHelper.AUTOGROUP_KEY,因此到這裏,因此成組的通知,包括父通知的overrideGroupKey就都變成了ranker_group,一樣,這個屬性將在SystemUI顯示時發揮做用
/*GroupHelper.onNotificationPosted(...)*/
                // 步驟10.3.6
                maybeUngroup(sbn, false, sbn.getUserId());
複製代碼
  • 步驟10.3.6:存在一種狀況:某通知原來未指定group,而後被加進了系統建立的父通知裏,但如今用戶更新了該通知,併爲其指定了group,也就是用戶告訴系統,接下我本身要建立分組了,你把通知還給我。對於這種狀況,系統須要將該通知從系統分組裏面移除出來,避免出錯

至此,整個 PostNotificationRunnable.run() 方法就都分析完了,通知會發送給各個監聽者,包括咱們後面要講的 SystemUI,第四篇在路上~

相關文章
相關標籤/搜索