原創不易,轉載請標明出處。若閱讀過程當中發現問題,請不吝指教,比心~java
這是一個基於 Android 10 源碼,全面分析 Android通知系統實現原理 的系列,這是第三篇,全系列將覆蓋:android
前面兩篇文章咱們介紹了 Notification 的簡單使用 和 經常使用接口介紹 以及 系統通知服務的啓動流程 和 功能實現,相信你已經對系統通知服務有了初步的印象了,這一篇咱們將全面分析通知發送在框架層(服務端)的一系列處理數據庫
說明:設計模式
NM -> NotificationManager
NMS -> NotificationManagerService
Sysui -> SystemUI
複製代碼
服務端(System進程):NM發送 -> NMS處理 -> NMS 將通知post給監聽器 ->
客戶端(SystemUI):Sysui接收 -> 根據通知類型加載對應通知佈局 -> 顯示數組
這一篇咱們分析的是服務端的實現,因爲 服務端的整個處理流程很是複雜,故細分爲以下幾個小節:bash
通知發送的入口是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
前面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");
}
}
}
複製代碼
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;
}
複製代碼
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);
複製代碼
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;
}
複製代碼
/*NotificationManagerService.enqueueNotificationInternal()*/
// 步驟5
mHandler.post(new EnqueueNotificationRunnable(userId, r));
複製代碼
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條的時候就會將其成組顯示。
前面 入列預處理 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);
}
}
複製代碼
Builder setGroupSummary(boolean isGroupSummary)
設置了Notification.FLAG_GROUP_SUMMARY
這個flag,可是沒調用 Builder.setGroup(String groupKey)
設置對應的groupKey
,則Notification.FLAG_GROUP_SUMMARY
這個flag會被去掉,不然會致使後續系統的自動成組出錯。這個處理糾正了一些用戶的錯誤操做,例如用戶但願發送一條父通知,可是隻調用了Builder setGroupSummary(boolean isGroupSummary)
而忘了設置相應的groupKey
;mSummaryByGroupKey
,這個集合咱們前面總結過,忘記的回頭看吧group key
已經發生了變化,則原來父通知下的全部子通知會被移除能夠看到,EnqueueNotificationRunnable
只是對通知作進一步的處理和糾偏,重點處理了通知成組相關的內容,該Runnable
的最後是將通知發送流程進一步交給了PostNotificationRunnable
去處理,這個Runnable
真正作了通知在服務端的發送(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;
}
}
}
}
}
複製代碼
/*PostNotificationRunnable.run()*/
// 步驟1
if (r == null) { return; }
複製代碼
mEnqueuedNotifications
將通知移除,因此將入列期間的通知存在mEnqueuedNotifications
中可讓咱們在處理通知的不一樣階段去檢查通知是否已經被移除/*PostNotificationRunnable.run()*/
// 步驟2
if (isBlocked(r)) { return; }
複製代碼
enqueueNotificationInternal
已經作過一次 blocked 檢查,這裏再次檢查是避免在中間處理過程當中 blocked 屬性發生了改變,因此整個通知發送過程當中存在兩次 blocked 狀態檢查/*PostNotificationRunnable.run()*/
// 步驟3:
final boolean isPackageSuspended = isPackagePausedOrSuspended(r.sbn.getPackageName(), r.getUid());
r.setHidden(isPackageSuspended);
複製代碼
/*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;
}
複製代碼
mNotificationList
是存儲 已排序 的通知,這裏判斷新來的通知是否是更新類型的,不是的話就直接add進mNotificationList
,是的話則會將舊的通知替換掉,排序不變/*PostNotificationRunnable.run()*/
// 步驟5 記錄 未排序 通知
mNotificationsByKey.put(n.getKey(), r);
複製代碼
mNotificationsByKey
,這也是爲何前面咱們能夠經過mNotificationsByKey
獲取到某通知是否存在舊通知的緣由/*PostNotificationRunnable.run()*/
// 步驟6
if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) { notification.flags |= FLAG_ONGOING_EVENT | FLAG_NO_CLEAR; }
複製代碼
FLAG_ONGOING_EVENT / FLAG_NO_CLEAR
),系統都會幫你加上這一步是在對通知進行排序前利用各類規則更新通知的各類屬性, 這裏涉及到幾個類:
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) {
}
}
複製代碼
這是一套決定通知是否顯示圓點的規則,規則包括:
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
不爲空時,則進一步處理其餘規則,不展開講。
這裏主要學習源碼的這種實現思路:將賦值過程複雜的屬性的處理經過抽象隔離開來分別處理,達到修改某個屬性的規則時不影響其餘屬性的目的,同時還保證了良好的可擴展性,當咱們須要定義新的規則的時候,只須要擴展咱們本身的一套規則便可.
這裏體現了設計模式中多個基本原則,如單一職責原則(一個類應只包含單一的職責)、依賴倒轉原則(抽象不該該依賴於細節,細節應當依賴於抽象)和迪米特原則(一個類儘可能不要與其餘類發生關係)等,整個通知系統的設計是十分複雜的,這個過程當中有不少設計模式的體現,讀者閱讀的時候可多加思考並學習其應用。
/*PostNotificationRunnable.run()*/
// 步驟8
mRankingHelper.sort(mNotificationList);
複製代碼
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.java
的 compare
方法。
對於初步排序這裏只強調一點,因爲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的大小」:
因此正常狀況下,當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);
}
複製代碼
sortKey
作了統一處理:
sortKey
時,groupSortKeyPortion = "nsk"sortKey
爲空(也就是"")時,groupSortKeyPortion = "esk";sortKey
不爲null時,groupSortKeyPortion = "gsk=" + groupSortKey;crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x
這個格式爲record
設置mGlobalSortKey
,也就是系統將這幾個屬性組合成一個字符串,賦值給mGlobalSortKey
,裏面就包括前面步驟1中規範化出來的sortKey
,而這一整個字符串將在最終排序中影響通知排序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
並不能生效了,由於sortKey
的排序優先級不是最高的,還受crtcl、intrsv、grnk、gsmry
這幾個屬性影響。
/*PostNotificationRunnable.run()*/
// 步驟9
if (!r.isHidden()) { buzzBeepBlinkLocked(r); }
複製代碼
NotificationManagerService.buzzBeepBlinkLocked(NotificationRecord record)
方法,下面重點看看通知的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;
}
}
}
}
}
複製代碼
mSmallIcon
作了檢查,避免前面在處理通知的過程當中mSmallIcon
丟失了,而沒有mSmallIcon
的通知是必定不能發送的,這也看出了Google對流氓通知是零容忍的。若是沒有mSmallIcon
,則走進else
。else
的狀況,此時若是舊通知已成功發送但新通知沒smallIcon
,則舊通知會被移除,因此移除通知不必定要調用cancel
接口,在這種狀況下舊通知也是會被移除的。/*PostNotificationRunnable.run()*/
// 步驟10.2
mListeners.notifyPostedLocked(r, old);
複製代碼
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);
}
});
}
}
複製代碼
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
類型的數組,而後構建一個RankingMap
,RankingMap
是一個key=StatusBarNotification.key,value=NotificationListenerService.Ranking
的ArrayMap
,後面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]);
}
}
複製代碼
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));
}
});
}
複製代碼
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將回調註冊到GroupHelper
,GroupHelper
則在必要的時候通知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());
}
}
}
複製代碼
GroupHelper
持有一個集合mUngroupedNotifications
,存儲內容爲:<user, <packageName, notificationsForPackage>>,這樣就能將每一個用戶的每一個應用下的通知存儲起來,在條件知足的時候去執行系統成組操做mGroupKey
或者mSortKey
,則系統會嘗試去走建立父通知的邏輯config
中的,路徑爲frameworks/base/core/res/res/values/config.xml 下的 config_autoGroupAtCount
字段,因此若是咱們想要修改系統的通知自動成組數條件,修改該變量便可。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));
}
}
複製代碼
Notification
的mGroupKey = GroupHelper.AUTOGROUP_KEY
,也就是ranker_group
,同時指定了StatusBarNotification
的tag = GroupHelper.AUTOGROUP_KEY
和 overrideGroupKey = GroupHelper.AUTOGROUP_KEY
,這些信息在後續分析客戶端通知顯示的時候會用到/*GroupHelper.onNotificationPosted(...)*/
// 步驟10.3.5
adjustNotificationBundling(notificationsToGroup, true);
複製代碼
StatusBarNotification
的setOverrideGroupKey()
方法,將該值指定爲GroupHelper.AUTOGROUP_KEY
,因此到這裏,因此成組的通知,包括父通知的overrideGroupKey
就都變成了ranker_group
,一樣,這個屬性將在SystemUI
顯示時發揮做用/*GroupHelper.onNotificationPosted(...)*/
// 步驟10.3.6
maybeUngroup(sbn, false, sbn.getUserId());
複製代碼
至此,整個 PostNotificationRunnable.run()
方法就都分析完了,通知會發送給各個監聽者,包括咱們後面要講的 SystemUI
,第四篇在路上~