前言
这部分我觉得三方应用使用的较多,分析的时候也是源码与三方应用结合分析的。
一. NotificationChannel 的创建
在源码中,我看到了一个很怪的类:NotificationChannels.java。这个类继承了 CoreStartable。
注:CoreStartable 就是 SystemUI,只是我这的源码的命名不一样,下面为了便于他人阅读,就以 SystemUI 来叫。
NotificationChannels.java 就百十行代码,很简单,一起看看这个类:
frameworks/base/packages/SystemUI/src/com/android/systemui/util/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() 中,具体如下:
frameworks/base/services/core/java/com/android/server/notification/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() 中完成的:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
private void setUpPresenter() {
// 省略部分代码......
// 这位置调用了NotificationsControllerImpl#initialize()的方法
mNotificationsController.initialize(
mPresenter,
mNotifListContainer,
mStackScrollerController.getNotifStackController(),
mNotificationActivityStarter,
mCentralSurfacesComponent.getBindRowCallback());
}
在 NotificationsControllerImpl#initialize() 中进行注册:
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
override fun initialize(
presenter: NotificationPresenter,
listContainer: NotificationListContainer,
stackController: NotifStackController,
notificationActivityStarter: NotificationActivityStarter,
bindRowCallback: NotificationRowBinderImpl.BindRowCallback
) {
// 注册回调
notificationListener.registerAsSystemService()
}
上述注册了之后,每当有通知来时就会回调到:NotificationListener#onNotificationPosted() 中,接着就会到 NotificationEntryManager中。
下面分析通知视图的加载,这里就直接从 NotificationEntryManager#onNotificationPosted() 开始。
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java
private final NotificationHandler mNotifListener = new NotificationHandler() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
maybeEmitBatch(sbn);
applyRanking(rankingMap);
final boolean shouldCoalesce = handleNotificationPosted(sbn, rankingMap);
// 通过key值进行判断,通知是否已存在
if (shouldCoalesce) {
mLogger.logEventCoalesced(sbn.getKey());
mHandler.onNotificationRankingUpdate(rankingMap);
} else {
mHandler.onNotificationPosted(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()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/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()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/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()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/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)
frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
// 更新通知视图的部分和状态栏图标。每当显示的基础通知数据发生更改时,
// 这由 NotificationPresenter 触发。
public void updateNotificationViews(String reason) {
// 更新NotificationStackScrollLayout 这个视图类的各种信息
// updateSectionBoundaries() 这个方法还没弄明白,但我估计是添加/删除视图后布局重新定位,以及一个
mNotificationStackScrollLayoutController.updateSectionBoundaries(reason);
// Footer 其实就是通知面板底部的两个按钮:“管理”、“全部清除”。
mNotificationStackScrollLayoutController.updateFooter();
// 更新状态栏的通知图标
mNotificationIconAreaController.updateNotificationIcons(createVisibleEntriesList());
}
至此通知面板的视图完成添加、更新。
下面接着看下状态栏的通知图标更新:
NotificationIconAreaController#updateNotificationIcons()
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/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():
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/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);
}
}
// ...
}