Android 13 NotificationChannels与Notification的加载流程

简介: Android 13 NotificationChannels与Notification的加载流程
一、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 中。这里我作了一个流程图:


image.png

从 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 的显示过程


这里代码有点多,我制作了一个通知传递的方法调用流程图:


image.png


上述流程图中,我们可能更比较关注 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组件思维导图:



image.png


SystemUI 关键布局图


image.png



根布局: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 流程分析完毕。

相关文章
|
15天前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
66 6
|
13天前
|
Android开发 UED
Android 中加载 Gif 动画
【10月更文挑战第20天】加载 Gif 动画是 Android 开发中的一项重要技能。通过使用第三方库或自定义实现,可以方便地在应用中展示生动的 Gif 动画。在实际应用中,需要根据具体情况进行合理选择和优化,以确保用户体验和性能的平衡。可以通过不断的实践和探索,进一步掌握在 Android 中加载 Gif 动画的技巧和方法,为开发高质量的 Android 应用提供支持。
|
14天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
16天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
22 3
|
17天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
19 2
|
30天前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
33 3
|
16天前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
13 0
|
2月前
|
Android开发 开发者
Android面试之Activity启动流程简述
每个Android开发者都熟悉的Activity,但你是否了解它的启动流程呢?本文将带你深入了解。启动流程涉及四个关键角色:Launcher进程、SystemServer的AMS、应用程序的ActivityThread及Zygote进程。核心在于AMS与ActivityThread间的通信。文章详细解析了从Launcher启动Activity的过程,包括通过AIDL获取AMS、Zygote进程启动以及ActivityThread与AMS的通信机制。接着介绍了如何创建Application及Activity的具体步骤。整体流程清晰明了,帮助你更深入理解Activity的工作原理。
46 0
|
API Android开发
【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | Hook 点分析 )(一)
【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | Hook 点分析 )(一)
171 0
【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | Hook 点分析 )(一)
|
Android开发
【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 )(四)
【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 主线程创建 Activity 实例之前使用插件 Activity 类替换占位的组件 )(四)
170 0