一、NotificationChannel 的创建
这部分我觉得三方应用使用的较多,分析的时候也是源码与三方应用
结合分析的。
在源码中,我看到了一个很怪的类:NotificationChannels.java。这个类继承了 CoreStartable。
注:CoreStartable 就是 SystemUI,只是我这的源码的命名不一样,下面为了便于他人阅读,就以 SystemUI 来叫。
NotificationChannels.java 就百十行代码,很简单,一起看看这个类:
NotificationChannels
// NotificationChannels.java public class NotificationChannels extends CoreStartable { // ... // 省略代码 public static void createAll(Context context) { final NotificationManager nm = context.getSystemService(NotificationManager.class); // 创建通道 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); // 创建通道 final NotificationChannel alerts = new NotificationChannel( ALERTS, context.getString(R.string.notification_channel_alerts), NotificationManager.IMPORTANCE_HIGH); // 创建通道 final NotificationChannel general = new NotificationChannel( GENERAL, context.getString(R.string.notification_channel_general), NotificationManager.IMPORTANCE_MIN); // 创建通道 final NotificationChannel storage = new NotificationChannel( STORAGE, context.getString(R.string.notification_channel_storage), isTv(context) ? NotificationManager.IMPORTANCE_DEFAULT : NotificationManager.IMPORTANCE_LOW); // 创建通道 final NotificationChannel hint = new NotificationChannel( HINTS, context.getString(R.string.notification_channel_hints), NotificationManager.IMPORTANCE_DEFAULT); // No need to bypass DND. // 注册通道 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. // 注册通道 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 扩展自 SystemUI 并重写了 start()
方法,它执行了 createAll()
方法,创建了通知通道有 batteryChannel(电池)、alerts(提醒)、storage(存储空间)、screenshot(屏幕截图)、hint (提示)、general(常规消息)。
此外,如果是 TV 设备的话还会创建画中画通知通道。
- 怪在哪呢:为什么在这个类去创建注册那些通知通道,而且并没有提示消息什么的,意义在哪?
注:下面我把该类当作三方应用。
下面围绕 NotificationChannels 一步一步的分析,上面调用 new NotificationChannel()
创建通知通道,然后 调用 nm.createNotificationChannels()
方法注册通道。
nm 其实是 NotificationManager 的对象,这样就转到了 NotificationManager 中。这里我作了一个流程图:
从 NotificationManager.createNotificationChannel() 到 NotificationManagerService.createNotificationChannelsImpl() 都是正常流程,也好理解。创建的关键代码在 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"); } // 前面是各种条件检查,下面这行是关键点,先检索这个 channel 是否已经存在,以 channel id 为标志位。 NotificationChannel existing = r.channels.get(channel.getId()); // 如果通道已经存在就更新通道 // 更新通道保留大部分已存在的设置,只更新了 name,description 等几项 if (existing != null && fromTargetApp) { // 省略部分代码...... } else { // 省略部分代码...... // channel 未创建过,把用户创建的 channel 加入到系统的 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; }
至此,一个通知完整的创建完成。
其实通过mPreferencesHelper.createNotificationChannel() 方法还能看出 NotificationChannel 一旦创建,那么能更改的东西就很少了(只有名字,描述,blocksystem,以及优先级),而 blocksystem 属性只有在系统源码里面才能使用(hide);
NotificationChannel 不会重复创建。
Android官方是这么解释这个设计的:NotificationChannel 就像是开发者送给用户的一个精美礼物,一旦送出去,控制权就在用户那里了。即使用户把通知铃声设置成《江南style》,你可以知道,但不可以更改。
二、Notification 的显示过程
这里代码有点多,我制作了一个通知传递的方法调用流程图:
上述流程图中,我们可能更比较关注 NotificationManagerService 是怎么与 SystemUI 交互的。
其实SystemUI向 NotificationManagerService 注册一个"服务"(一个Binder)。这个"服务"就相当于客户端 SystemUI 在服务端 NotificationManagerService 注册的一个回调。当有通知来临的时候,就会通过这个"服务"通知SystemUI,这个注册是在StatusBar#setUpPresenter()中完成的:
// StatusBar.java private void setUpPresenter() { // 省略部分代码...... // 这位置调用了NotificationsControllerImpl#initialize()的方法 mNotificationsController.initialize( mPresenter, mNotifListContainer, mStackScrollerController.getNotifStackController(), mNotificationActivityStarter, mCentralSurfacesComponent.getBindRowCallback()); }
在 NotificationsControllerImpl#initialize()中进行注册:
// NotificationsControllerImpl.kt override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, bindRowCallback: NotificationRowBinderImpl.BindRowCallback ) { // 注册回调 notificationListener.registerAsSystemService() }
上述注册了之后,每当有通知来时就会回调到:NotificationListener#onNotificationPosted() 中,接着就会到 NotificationEntryManager中。
下面分析通知视图的加载,这里就直接从 NotificationEntryManager#onNotificationPosted() 开始。
// NotificationEntryManager.java private final NotificationHandler mNotifListener = new NotificationHandler() { @Override public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey()); // 通过key值进行判断,通知是否已存在 if (isUpdateToInflatedNotif) { updateNotification(sbn, rankingMap); } else { addNotification(sbn, rankingMap); } } }
通过上述源码可以知道,通知到后,首先会进行判断该通知是否存在,存在则刷新,不存在则添加;这里以添加为例去分析。
先看两张图,可以知道下面分析的方向:
SystemUI组件思维导图:
SystemUI 关键布局图
根布局
:super_status_bar.xml,
顶上状态栏
: status_bar.xml, 通过CollapsedStatusBarFragment.java加载;PhoneStatusBarView(FrameLayout,)是里面的父控件; 对应 R.id.status_bar_container 。
下拉状态栏
:(包括通知为status_bar_expanded.xml),最外层布局NotificationPanelView;qs_frame.xml 为下拉后的状态栏部分(用QSFragment管理,布局控件为QSContainerImpl),其高度更新在QSContainerImpl.java中;
NotificationStackScrollLayout
用于下拉的通知的相关问题(占满全屏,包括导航栏,会处理点击状态栏空白区的逻辑)。
NotificationStackScrollLayout:是一个滑动布局,里面嵌套着 ExpandableNotificationRow ,即通知。
接着上面分析:上面我们只关注 addNotification(sbn, rankingMap) ,而它内部时调用 addNotificationInternal() 方法实现的。
NotificationEntryManager#addNotificationInternal()
// NotificationEntryManager.java private void addNotificationInternal( StatusBarNotification notification, RankingMap rankingMap) throws InflationException { // 省略部分代码 ... NotificationEntry entry = mPendingNotifications.get(key); // 省略部分代码 ... // 构造视图 if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { // NotificationRowBinderImpl 为 NotificationRowBinder 的实现类 mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback); } // 省略部分代码 ... }
首先为通知创建一个 NotificationEntry 通知实例,然后再通过 NotificationRowBinderImpl 中的 inflateViews() 加载通知视图,绑定通知信息,并在通知栏添加通知视图,以及在状态栏添加通知图标。
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 { // 创建图标 mIconManager.createIcons(entry); mRowInflaterTaskProvider.get().inflate(mContext, parent, entry, row -> { // 为视图设置控制器. 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); }); } }
上面无论走哪个分支,最后进入到inflateContentViews(entry, row, callback);这是一个回调:
NotificationRowBinderImpl#inflateContentViews()
// NotificationRowBinderImpl.java // 加载该行的基本内容视图 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 的一个内部接口;在 NotificationEntryManager 中被实现,所以将回调到 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) { // 省略部分代码...... // 添加一个notification会走到这里、 // 包括一开机就显示出来的那些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 的实现类是 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(); // 这里不仅仅更新了通知面版的通知视图,也更新了状态栏的通知图标 mNotificationPanel.updateNotificationViews(reason); }
我们这里看通知面板更新,即 mNotificationPanel.updateNotificationViews(reason) 方法。mNotificationPanel 为 NotificationPanelViewController 的对象。
NotificationPanelViewController#updateNotificationViews(reason)
// NotificationPanelViewController.java // 更新通知视图的部分和状态栏图标。每当显示的基础通知数据发生更改时, // 这由 NotificationPresenter 触发。 public void updateNotificationViews(String reason) { // 更新NotificationStackScrollLayout 这个视图类的各种信息 // updateSectionBoundaries() 这个方法还没弄明白,但我估计是添加/删除视图后布局重新定位,以及一个 mNotificationStackScrollLayoutController.updateSectionBoundaries(reason); // Footer 其实就是通知面板底部的两个按钮:“管理”、“全部清除”。 mNotificationStackScrollLayoutController.updateFooter(); // 更新状态栏的通知图标 mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList()); }
至此通知面板的视图完成添加、更新。
下面接着看下状态栏的通知图标更新:
NotificationIconAreaController#updateNotificationIcons()
// NotificationIconAreaController.java public void updateNotificationIcons(List<ListEntry> entries) { mNotificationEntries = entries; updateNotificationIcons(); } private void updateNotificationIcons() { Trace.beginSection("NotificationIconAreaController.updateNotificationIcons"); // 更新状态栏图标 updateStatusBarIcons(); updateShelfIcons(); // 更新 Aod 通知图标 updateAodNotificationIcons(); // 应用通知图标色调 applyNotificationIconsTint(); Trace.endSection(); }
下面都是调用 update XXX Icons() 这种类似的方法,接着调用 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保存即将显示的图标 ArrayList<StatusBarIconView> toShow = new ArrayList<>( mNotificationScrollLayout.getChildCount()); // 过滤通知,并保存需要显示的通知图标 for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) { // 获取一个通知视图 View view = mNotificationScrollLayout.getChildAt(i); if (view instanceof ExpandableNotificationRow) { NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry(); if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed, hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) { // 获取图标 StatusBarIconView iconView = function.apply(ent); if (iconView != null) { toShow.add(iconView); } } } } // ... // 把需要显示的图标添加到hostLayout中 final FrameLayout.LayoutParams params = generateIconLayoutParams(); for (int i = 0; i < toShow.size(); i++) { StatusBarIconView v = toShow.get(i); // 如果刚刚删除并再次添加,视图可能仍会暂时添加 hostLayout.removeTransientView(v); if (v.getParent() == null) { if (hideDismissed) { v.setOnDismissListener(mUpdateStatusBarIcons); } // 执行到最后是 NotificationIconContainer.addView 添加视图 // NotificationIconContainer本身没有addView、removeView方法, // 最终走的是其多层下去的父类ViewGroup的方法 hostLayout.addView(v, i, params); } } // ... }
到这里整个 Notification 流程分析完毕。