Android 14 NotificationChannels与Notification的加载流程

前言

这部分我觉得三方应用使用的较多,分析的时候也是源码与三方应用结合分析的。

一. 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);
        }
    }
    // ...
}

相关推荐

  1. Android14 StatusBar顶部图标流程

    2024-04-09 13:06:02       26 阅读
  2. JVM流程

    2024-04-09 13:06:02       33 阅读
  3. android ClassLoaderdex

    2024-04-09 13:06:02       44 阅读
  4. android开发-20】androidnotification用法讲解

    2024-04-09 13:06:02       48 阅读
  5. android audio 相机按键音:(二)修改

    2024-04-09 13:06:02       28 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-09 13:06:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-09 13:06:02       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-09 13:06:02       82 阅读
  4. Python语言-面向对象

    2024-04-09 13:06:02       91 阅读

热门阅读

  1. 十分钟学会WebSocket

    2024-04-09 13:06:02       37 阅读
  2. redis集群搭建教程

    2024-04-09 13:06:02       38 阅读
  3. 数据大屏:现代数据分析与可视化的重要工具

    2024-04-09 13:06:02       34 阅读
  4. Docker中运行ASP.NET Core应用

    2024-04-09 13:06:02       36 阅读
  5. 大家问的最多的问题统一回复

    2024-04-09 13:06:02       37 阅读
  6. 网桥设置介绍

    2024-04-09 13:06:02       39 阅读
  7. 文心一言和GPT-4横向对比

    2024-04-09 13:06:02       33 阅读
  8. docker 的使用与说明

    2024-04-09 13:06:02       36 阅读