學(xué)習(xí)筆記:篇幅比較長(zhǎng)請(qǐng)耐心看愈涩,之前有寫(xiě)過(guò)部分栽渴,但不是很詳細(xì)芥备。
一、NotificationChannel 的創(chuàng)建
這部分我覺(jué)得三方應(yīng)用使用的較多坤塞,分析的時(shí)候也是源碼與三方應(yīng)用
結(jié)合分析的冯勉。
在源碼中,我看到了一個(gè)很怪的類(lèi):NotificationChannels.java摹芙。這個(gè)類(lèi)繼承了 CoreStartable灼狰。
注:CoreStartable 就是 SystemUI,只是我這的源碼的命名不一樣浮禾,下面為了便于他人閱讀交胚,就以 SystemUI 來(lái)叫。
NotificationChannels.java 就百十行代碼盈电,很簡(jiǎn)單蝴簇,一起看看這個(gè)類(lèi):
NotificationChannels
// NotificationChannels.java
public class NotificationChannels extends CoreStartable {
// ...
// 省略代碼
public static void createAll(Context context) {
final NotificationManager nm = context.getSystemService(NotificationManager.class);
// 創(chuàng)建通道
final NotificationChannel batteryChannel = new NotificationChannel(BATTERY,
context.getString(R.string.notification_channel_battery),
NotificationManager.IMPORTANCE_MAX);
final String soundPath = Settings.Global.getString(context.getContentResolver(),
Settings.Global.LOW_BATTERY_SOUND);
batteryChannel.setSound(Uri.parse("file://" + soundPath), new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
.build());
batteryChannel.setBlockable(true);
// 創(chuàng)建通道
final NotificationChannel alerts = new NotificationChannel(
ALERTS,
context.getString(R.string.notification_channel_alerts),
NotificationManager.IMPORTANCE_HIGH);
// 創(chuàng)建通道
final NotificationChannel general = new NotificationChannel(
GENERAL,
context.getString(R.string.notification_channel_general),
NotificationManager.IMPORTANCE_MIN);
// 創(chuàng)建通道
final NotificationChannel storage = new NotificationChannel(
STORAGE,
context.getString(R.string.notification_channel_storage),
isTv(context)
? NotificationManager.IMPORTANCE_DEFAULT
: NotificationManager.IMPORTANCE_LOW);
// 創(chuàng)建通道
final NotificationChannel hint = new NotificationChannel(
HINTS,
context.getString(R.string.notification_channel_hints),
NotificationManager.IMPORTANCE_DEFAULT);
// No need to bypass DND.
// 注冊(cè)通道
nm.createNotificationChannels(Arrays.asList(
alerts,
general,
storage,
createScreenshotChannel(
context.getString(R.string.notification_channel_screenshot),
nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
batteryChannel,
hint
));
// Delete older SS channel if present.
// Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
// This line can be deleted in Q.
nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
if (isTv(context)) {
// TV specific notification channel for TV PIP controls.
// Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
// priority, so it can be shown in all times.
// 注冊(cè)通道
nm.createNotificationChannel(new NotificationChannel(
TVPIP,
context.getString(R.string.notification_channel_tv_pip),
NotificationManager.IMPORTANCE_MAX));
}
}
/**
* Set up screenshot channel, respecting any previously committed user settings on legacy
* channel.
* @return
*/
@VisibleForTesting static NotificationChannel createScreenshotChannel(
String name, NotificationChannel legacySS) {
NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
screenshotChannel.setSound(null, // silent
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
screenshotChannel.setBlockable(true);
if (legacySS != null) {
// Respect any user modified fields from the old channel.
int userlock = legacySS.getUserLockedFields();
if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
screenshotChannel.setImportance(legacySS.getImportance());
}
if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0) {
screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
}
if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0) {
screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
}
if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0) {
screenshotChannel.setLightColor(legacySS.getLightColor());
}
// skip show_badge, irrelevant for system channel
}
return screenshotChannel;
}
@Override
public void start() {
createAll(mContext);
}
private static boolean isTv(Context context) {
PackageManager packageManager = context.getPackageManager();
return packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
}
NotificationChannels 擴(kuò)展自 SystemUI 并重寫(xiě)了 start()
方法,它執(zhí)行了 createAll()
方法匆帚,創(chuàng)建了通知通道有 batteryChannel(電池)熬词、alerts(提醒)、storage(存儲(chǔ)空間)、screenshot(屏幕截圖)互拾、hint (提示)歪今、general(常規(guī)消息)。
此外颜矿,如果是 TV 設(shè)備的話還會(huì)創(chuàng)建畫(huà)中畫(huà)通知通道寄猩。
- 怪在哪呢:為什么在這個(gè)類(lèi)去創(chuàng)建注冊(cè)那些通知通道,而且并沒(méi)有提示消息什么的或衡,意義在哪焦影?
注:下面我把該類(lèi)當(dāng)作三方應(yīng)用。
下面圍繞 NotificationChannels 一步一步的分析封断,上面調(diào)用 new NotificationChannel()
創(chuàng)建通知通道斯辰,然后 調(diào)用 nm.createNotificationChannels()
方法注冊(cè)通道。
nm 其實(shí)是 NotificationManager 的對(duì)象坡疼,這樣就轉(zhuǎn)到了 NotificationManager 中彬呻。這里我作了一個(gè)流程圖:
從 NotificationManager.createNotificationChannel() 到 NotificationManagerService.createNotificationChannelsImpl() 都是正常流程,也好理解柄瑰。創(chuàng)建的關(guān)鍵代碼在 mPreferencesHelper.createNotificationChannel() 中闸氮,具體如下:
// PreferencesHelper.java
@Override
public boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel,
boolean fromTargetApp, boolean hasDndAccess) {
Objects.requireNonNull(pkg);
Objects.requireNonNull(channel);
Objects.requireNonNull(channel.getId());
Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
boolean needsPolicyFileChange = false, wasUndeleted = false, needsDndChange = false;
synchronized (mPackagePreferences) {
PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
if (r == null) {
throw new IllegalArgumentException("Invalid package");
}
if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
}
if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
throw new IllegalArgumentException("Reserved id");
}
// 前面是各種條件檢查,下面這行是關(guān)鍵點(diǎn)教沾,先檢索這個(gè) channel 是否已經(jīng)存在蒲跨,以 channel id 為標(biāo)志位。
NotificationChannel existing = r.channels.get(channel.getId());
// 如果通道已經(jīng)存在就更新通道
// 更新通道保留大部分已存在的設(shè)置授翻,只更新了 name或悲,description 等幾項(xiàng)
if (existing != null && fromTargetApp) {
// 省略部分代碼......
} else {
// 省略部分代碼......
// channel 未創(chuàng)建過(guò),把用戶(hù)創(chuàng)建的 channel 加入到系統(tǒng)的 cache 里
r.channels.put(channel.getId(), channel);
if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
needsDndChange = true;
}
MetricsLogger.action(getChannelLog(channel, pkg).setType(
com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
mNotificationChannelLogger.logNotificationChannelCreated(channel, uid, pkg);
}
}
if (needsDndChange) {
updateChannelsBypassingDnd();
}
return needsPolicyFileChange;
}
至此堪唐,一個(gè)通知完整的創(chuàng)建完成巡语。
其實(shí)通過(guò)mPreferencesHelper.createNotificationChannel() 方法還能看出 NotificationChannel 一旦創(chuàng)建,那么能更改的東西就很少了(只有名字淮菠,描述男公,blocksystem,以及優(yōu)先級(jí))合陵,而 blocksystem 屬性只有在系統(tǒng)源碼里面才能使用(hide)枢赔;
NotificationChannel 不會(huì)重復(fù)創(chuàng)建。
Android官方是這么解釋這個(gè)設(shè)計(jì)的:NotificationChannel 就像是開(kāi)發(fā)者送給用戶(hù)的一個(gè)精美禮物拥知,一旦送出去糠爬,控制權(quán)就在用戶(hù)那里了。即使用戶(hù)把通知鈴聲設(shè)置成《江南style》举庶,你可以知道,但不可以更改揩抡。
二户侥、Notification 的顯示過(guò)程
這里代碼有點(diǎn)多镀琉,我制作了一個(gè)通知傳遞的方法調(diào)用流程圖:
上述流程圖中,我們可能更比較關(guān)注 NotificationManagerService 是怎么與 SystemUI 交互的蕊唐。
其實(shí)SystemUI向 NotificationManagerService 注冊(cè)一個(gè)"服務(wù)"(一個(gè)Binder)屋摔。這個(gè)"服務(wù)"就相當(dāng)于客戶(hù)端 SystemUI 在服務(wù)端 NotificationManagerService 注冊(cè)的一個(gè)回調(diào)。當(dāng)有通知來(lái)臨的時(shí)候替梨,就會(huì)通過(guò)這個(gè)"服務(wù)"通知SystemUI钓试,這個(gè)注冊(cè)是在StatusBar#setUpPresenter()中完成的:
// StatusBar.java
private void setUpPresenter() {
// 省略部分代碼......
// 這位置調(diào)用了NotificationsControllerImpl#initialize()的方法
mNotificationsController.initialize(
mPresenter,
mNotifListContainer,
mStackScrollerController.getNotifStackController(),
mNotificationActivityStarter,
mCentralSurfacesComponent.getBindRowCallback());
}
在 NotificationsControllerImpl#initialize()中進(jìn)行注冊(cè):
// NotificationsControllerImpl.kt
override fun initialize(
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
bindRowCallback: NotificationRowBinderImpl.BindRowCallback
) {
// 注冊(cè)回調(diào)
notificationListener.registerAsSystemService()
}
上述注冊(cè)了之后,每當(dāng)有通知來(lái)時(shí)就會(huì)回調(diào)到:NotificationListener#onNotificationPosted() 中副瀑,接著就會(huì)到 NotificationEntryManager中弓熏。
下面分析通知視圖的加載,這里就直接從 NotificationEntryManager#onNotificationPosted() 開(kāi)始糠睡。
// NotificationEntryManager.java
private final NotificationHandler mNotifListener = new NotificationHandler() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey());
// 通過(guò)key值進(jìn)行判斷挽鞠,通知是否已存在
if (isUpdateToInflatedNotif) {
updateNotification(sbn, rankingMap);
} else {
addNotification(sbn, rankingMap);
}
}
}
通過(guò)上述源碼可以知道,通知到后狈孔,首先會(huì)進(jìn)行判斷該通知是否存在信认,存在則刷新,不存在則添加均抽;這里以添加為例去分析嫁赏。
先看兩張圖,可以知道下面分析的方向:
SystemUI組件思維導(dǎo)圖:
SystemUI 關(guān)鍵布局圖:
根布局
:super_status_bar.xml,頂上狀態(tài)欄
: status_bar.xml, 通過(guò)CollapsedStatusBarFragment.java加載油挥;PhoneStatusBarView(FrameLayout,)是里面的父控件; 對(duì)應(yīng) R.id.status_bar_container 潦蝇。下拉狀態(tài)欄
:(包括通知為status_bar_expanded.xml),最外層布局NotificationPanelView;qs_frame.xml 為下拉后的狀態(tài)欄部分(用QSFragment管理喘漏,布局控件為QSContainerImpl)护蝶,其高度更新在QSContainerImpl.java中;NotificationStackScrollLayout
用于下拉的通知的相關(guān)問(wèn)題(占滿(mǎn)全屏翩迈,包括導(dǎo)航欄持灰,會(huì)處理點(diǎn)擊狀態(tài)欄空白區(qū)的邏輯)。
NotificationStackScrollLayout:是一個(gè)滑動(dòng)布局负饲,里面嵌套著 ExpandableNotificationRow 堤魁,即通知。
接著上面分析:上面我們只關(guān)注 addNotification(sbn, rankingMap) 返十,而它內(nèi)部時(shí)調(diào)用 addNotificationInternal() 方法實(shí)現(xiàn)的妥泉。
NotificationEntryManager#addNotificationInternal()
// NotificationEntryManager.java
private void addNotificationInternal(
StatusBarNotification notification,
RankingMap rankingMap) throws InflationException {
// 省略部分代碼 ...
NotificationEntry entry = mPendingNotifications.get(key);
// 省略部分代碼 ...
// 構(gòu)造視圖
if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
// NotificationRowBinderImpl 為 NotificationRowBinder 的實(shí)現(xiàn)類(lèi)
mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback);
}
// 省略部分代碼 ...
}
首先為通知?jiǎng)?chuàng)建一個(gè) NotificationEntry 通知實(shí)例,然后再通過(guò) NotificationRowBinderImpl 中的 inflateViews() 加載通知視圖洞坑,綁定通知信息盲链,并在通知欄添加通知視圖,以及在狀態(tài)欄添加通知圖標(biāo)。
NotificationRowBinderImpl#inflateViews()
// NotificationRowBinderImpl.java
@Override
public void inflateViews(
NotificationEntry entry,
NotifInflater.Params params,
NotificationRowContentBinder.InflationCallback callback)
throws InflationException {
if (params == null) {
// weak assert that the params should always be passed in the new pipeline
mNotifPipelineFlags.checkLegacyPipelineEnabled();
}
// 獲取查看父布局
ViewGroup parent = mListContainer.getViewParentForNotification(entry);
// 通知是否存在
if (entry.rowExists()) {
mIconManager.updateIcons(entry);
ExpandableNotificationRow row = entry.getRow();
row.reset();
updateRow(entry, row);
inflateContentViews(entry, params, row, callback);
} else {
// 創(chuàng)建圖標(biāo)
mIconManager.createIcons(entry);
mRowInflaterTaskProvider.get().inflate(mContext, parent, entry,
row -> {
// 為視圖設(shè)置控制器.
ExpandableNotificationRowComponent component =
mExpandableNotificationRowComponentBuilder
.expandableNotificationRow(row)
.notificationEntry(entry)
.onExpandClickListener(mPresenter)
.listContainer(mListContainer)
.build();
ExpandableNotificationRowController rowController =
component.getExpandableNotificationRowController();
rowController.init(entry);
entry.setRowController(rowController);
bindRow(entry, row);
updateRow(entry, row);
inflateContentViews(entry, params, row, callback);
});
}
}
上面無(wú)論走哪個(gè)分支刽沾,最后進(jìn)入到inflateContentViews(entry, row, callback);這是一個(gè)回調(diào):
NotificationRowBinderImpl#inflateContentViews()
// NotificationRowBinderImpl.java
// 加載該行的基本內(nèi)容視圖
private void inflateContentViews(
NotificationEntry entry,
NotifInflater.Params inflaterParams,
ExpandableNotificationRow row,
@Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
// 省略部分代碼......
params.rebindAllContentViews();
mRowContentBindStage.requestRebind(entry, en -> {
row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
row.setIsLowPriority(isLowPriority);
if (inflationCallback != null) {
inflationCallback.onAsyncInflationFinished(en);
}
});
}
inflationCallback 是 NotificationRowContentBinder 的一個(gè)內(nèi)部接口本慕;在 NotificationEntryManager 中被實(shí)現(xiàn),所以將回調(diào)到 NotificationEntryManager#onAsyncInflationFinished() 中侧漓。
// NotificationEntryManager.java
@Override
public void onAsyncInflationFinished(NotificationEntry entry) {
Trace.beginSection("NotificationEntryManager.onAsyncInflationFinished");
mPendingNotifications.remove(entry.getKey());
// If there was an async task started after the removal, we don't want to add it back to
// the list, otherwise we might get leaks.
if (!entry.isRowRemoved()) {
boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null;
// 省略部分代碼......
if (isNew) {
// 省略部分代碼......
// 添加一個(gè)notification會(huì)走到這里锅尘、
// 包括一開(kāi)機(jī)就顯示出來(lái)的那些notification
addActiveNotification(entry);
// 更新視圖
updateNotifications("onAsyncInflationFinished");
// 省略部分代碼......
} else {
// 省略部分代碼......
}
}
Trace.endSection();
}
這里直接看 updateNotifications("onAsyncInflationFinished") 方法;
NotificationEntryManager#updateNotification()
// NotificationEntryManager.java
public void updateNotifications(String reason) {
// 省略部分代碼......
if (mPresenter != null) {
// 更新視圖
mPresenter.updateNotificationViews(reason);
}
// 省略部分代碼......
}
mPresenter 的實(shí)現(xiàn)類(lèi)是 StatusBarNotificationPresenter布蔗,所以接著看其里面的 updateNotificationViews() 方法藤违。
StatusBarNotificationPresenter#updateNotificationViews()
// StatusBarNotificationPresenter.java
@Override
public void updateNotificationViews(final String reason) {
if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
return;
}
// The function updateRowStates depends on both of these being non-null, so check them here.
// We may be called before they are set from DeviceProvisionedController's callback.
if (mScrimController == null) return;
// 不要在折疊期間修改通知。.
if (isCollapsing()) {
mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason));
return;
}
// 把通知視圖添加到通知面版的通知欄中
mViewHierarchyManager.updateNotificationViews();
// 這里不僅僅更新了通知面版的通知視圖纵揍,也更新了狀態(tài)欄的通知圖標(biāo)
mNotificationPanel.updateNotificationViews(reason);
}
我們這里看通知面板更新顿乒,即 mNotificationPanel.updateNotificationViews(reason) 方法。mNotificationPanel 為 NotificationPanelViewController 的對(duì)象骡男。
NotificationPanelViewController#updateNotificationViews(reason)
// NotificationPanelViewController.java
// 更新通知視圖的部分和狀態(tài)欄圖標(biāo)淆游。每當(dāng)顯示的基礎(chǔ)通知數(shù)據(jù)發(fā)生更改時(shí),
// 這由 NotificationPresenter 觸發(fā)隔盛。
public void updateNotificationViews(String reason) {
// 更新NotificationStackScrollLayout 這個(gè)視圖類(lèi)的各種信息
// updateSectionBoundaries() 這個(gè)方法還沒(méi)弄明白犹菱,但我估計(jì)是添加/刪除視圖后布局重新定位,以及一個(gè)
mNotificationStackScrollLayoutController.updateSectionBoundaries(reason);
// Footer 其實(shí)就是通知面板底部的兩個(gè)按鈕:“管理”吮炕、“全部清除”腊脱。
mNotificationStackScrollLayoutController.updateFooter();
// 更新?tīng)顟B(tài)欄的通知圖標(biāo)
mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList());
}
至此通知面板的視圖完成添加、更新龙亲。
下面接著看下?tīng)顟B(tài)欄的通知圖標(biāo)更新:
NotificationIconAreaController#updateNotificationIcons()
// NotificationIconAreaController.java
public void updateNotificationIcons(List<ListEntry> entries) {
mNotificationEntries = entries;
updateNotificationIcons();
}
private void updateNotificationIcons() {
Trace.beginSection("NotificationIconAreaController.updateNotificationIcons");
// 更新?tīng)顟B(tài)欄圖標(biāo)
updateStatusBarIcons();
updateShelfIcons();
// 更新 Aod 通知圖標(biāo)
updateAodNotificationIcons();
// 應(yīng)用通知圖標(biāo)色調(diào)
applyNotificationIconsTint();
Trace.endSection();
}
下面都是調(diào)用 update XXX Icons() 這種類(lèi)似的方法陕凹,接著調(diào)用 updateIconsForLayout() 方法,我們直接分析 NotificationIconAreaController#updateIconsForLayout():
// NotificationIconAreaController.java
private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function,
NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority,
boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia,
boolean hideCenteredIcon) {
// toShow保存即將顯示的圖標(biāo)
ArrayList<StatusBarIconView> toShow = new ArrayList<>(
mNotificationScrollLayout.getChildCount());
// 過(guò)濾通知鳄炉,并保存需要顯示的通知圖標(biāo)
for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
// 獲取一個(gè)通知視圖
View view = mNotificationScrollLayout.getChildAt(i);
if (view instanceof ExpandableNotificationRow) {
NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry();
if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed,
hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) {
// 獲取圖標(biāo)
StatusBarIconView iconView = function.apply(ent);
if (iconView != null) {
toShow.add(iconView);
}
}
}
}
// ...
// 把需要顯示的圖標(biāo)添加到hostLayout中
final FrameLayout.LayoutParams params = generateIconLayoutParams();
for (int i = 0; i < toShow.size(); i++) {
StatusBarIconView v = toShow.get(i);
// 如果剛剛刪除并再次添加杜耙,視圖可能仍會(huì)暫時(shí)添加
hostLayout.removeTransientView(v);
if (v.getParent() == null) {
if (hideDismissed) {
v.setOnDismissListener(mUpdateStatusBarIcons);
}
// 執(zhí)行到最后是 NotificationIconContainer.addView 添加視圖
// NotificationIconContainer本身沒(méi)有addView、removeView方法拂盯,
// 最終走的是其多層下去的父類(lèi)ViewGroup的方法
hostLayout.addView(v, i, params);
}
}
// ...
}
到這里整個(gè) Notification 流程分析完畢佑女。