SystemUI 开发之通知 Notification 的关键 API(三)

2021-12-08 09:15:30 浏览数 (1)

0x00 介绍

前文已经了解了 SystemUI 中各个组件的用途,如果没有记错的话,大概有20多个组件。今天我们来看一下其中我认为最为重要的通知组件都有哪些关键 API。了解它们是我们进一步理解 Notification 实现逻辑的入口。

回顾一下,之前我们介绍组件的用途时跟通知有关的组件有

代码语言:javascript复制
com.android.systemui.util.NotificationChannels
用来处理通知的逻辑

com.android.systemui.status.phone.StatusBar
状态栏,也包含了通知栏和其它重要的 UI 交互,例如键盘锁等。这里也会监听通知

当然还有其它一些组件例如 PowerUI 也会发送通知,但我们更关注接收通知并处理通知相关的逻辑。

接下来我们会详细介绍这两个类中是如何处理通知的

本文是基于 Android 10 源码

0x01 NotificationChannels

NotificationChannels 类还是比较简单的

代码语言:javascript复制
public class NotificationChannels extends SystemUI {
    // ...
    // 省略代码

    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 设备的话还会创建画中画通知通道。

那什么是 NotificationChannel 呢?在 Android 8.0 使用通知时必须指定 NotificationChannel,这样其实是为了避免过分地打扰用户,用户有能力可以对一些指定的通知进行关闭,而不影响其它用户关心的通知。例如一个应用里面会提示很多类型通知,但是用户只关心其中某个通知,那么用户就可以通过设置进行配置。

关于更多的使用信息可以参考官方文档

0x02 StatusBar

在用户界面上 StatusBar 多数情况下是会一直显示在屏幕顶部(全屏应用会隐藏),它是 SystemUI 中一个非常核心的功能,有将近 5000 行的代码也可以从另一个方面知晓它的重要程度。首先看一下构造函数,竟然有很多参数,我几乎看不过来。所以先放弃构造函数的入口。然后我们知道它也是继承自 SystemUI 类,所以我们可以关注它的 start 方法,看它做了哪些初始化的工作。

不过 start 方法也不简单,有将近190行的代码。但我们这里暂时只关心与 Notification 相关的逻辑,精简之后的代码是这样的:

代码语言:javascript复制
public void start() {
        // ...
        // 省略代码
        // 创建windows
        createAndAddWindows(result);

        // Make sure we always have the most current wallpaper info.
        IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
        mContext.registerReceiverAsUser(mWallpaperChangedReceiver, UserHandle.ALL,
                wallpaperChangedFilter, null /* broadcastPermission */, null /* scheduler */);
        mWallpaperChangedReceiver.onReceive(mContext, null);

        // 这里似乎跟通知有关
        // Set up the initial notification state. This needs to happen before CommandQueue.disable()
        setUpPresenter();

        // 设置 systemui 可见性(navigationbar 和statusbar)
        setSystemUiVisibility(mDisplayId, result.mSystemUiVisibility,
                result.mFullscreenStackSysUiVisibility, result.mDockedStackSysUiVisibility,
                0xffffffff, result.mFullscreenStackBounds, result.mDockedStackBounds,
                result.mNavbarColorManagedByIme);
        // StatusBarManagerService has a back up of IME token and it's restored here.
        setImeWindowStatus(mDisplayId, result.mImeToken, result.mImeWindowVis,
                result.mImeBackDisposition, result.mShowImeSwitcher);
        // ...
        // 省略代码
    }

跟进到 setUpPresenter 方法

代码语言:javascript复制
private void setUpPresenter() {
        // Set up the initial notification state.
        mActivityLaunchAnimator = new ActivityLaunchAnimator(
                mStatusBarWindow, this, mNotificationPanel,
                (NotificationListContainer) mStackScroller);

        final NotificationRowBinderImpl rowBinder =
                new NotificationRowBinderImpl(
                        mContext,
                        SystemUIFactory.getInstance().provideAllowNotificationLongPress());
        // mNotificationPanel 是通知面板
        // mStackScroller 是 NotificationStackScrollLayout 的实例,它是通知列表
        // mStatusBarWindow 整合StatusBar 和 NotificationPanel的window
        mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanel,
                mHeadsUpManager, mStatusBarWindow, mStackScroller, mDozeScrimController,
                mScrimController, mActivityLaunchAnimator, mStatusBarKeyguardViewManager,
                mNotificationAlertingManager, rowBinder);

        // 通知列表的Controller,它内部持有 NotificationListContainer 引用(其实就是NotificationStackScrollLayout)
        mNotificationListController =
                new NotificationListController(
                        mEntryManager,
                        (NotificationListContainer) mStackScroller,
                        mForegroundServiceController,
                        mDeviceProvisionedController);

        mAppOpsController.addCallback(APP_OPS, this);
        mNotificationShelf.setOnActivatedListener(mPresenter);
        mRemoteInputManager.getController().addCallback(mStatusBarWindowController);

        final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback =
                (StatusBarRemoteInputCallback) Dependency.get(
                        NotificationRemoteInputManager.Callback.class);
        mShadeController = Dependency.get(ShadeController.class);
        final ActivityStarter activityStarter = Dependency.get(ActivityStarter.class);

        // 用于处理通知相关的各种交互,例如点击通知后跳转个某个应用等交互
        mNotificationActivityStarter = new StatusBarNotificationActivityStarter(mContext,
                mCommandQueue, mAssistManager, mNotificationPanel, mPresenter, mEntryManager,
                mHeadsUpManager, activityStarter, mActivityLaunchAnimator,
                mBarService, mStatusBarStateController, mKeyguardManager, mDreamManager,
                mRemoteInputManager, mStatusBarRemoteInputCallback, mGroupManager,
                mLockscreenUserManager, mShadeController, mKeyguardMonitor,
                mNotificationInterruptionStateProvider, mMetricsLogger,
                new LockPatternUtils(mContext), Dependency.get(MAIN_HANDLER),
                Dependency.get(BG_HANDLER), mActivityIntentHelper, mBubbleController);

        mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);

        mEntryManager.setRowBinder(rowBinder);
        rowBinder.setNotificationClicker(new NotificationClicker(
                this, Dependency.get(BubbleController.class), mNotificationActivityStarter));

        mGroupAlertTransferHelper.bind(mEntryManager, mGroupManager);
        mNotificationListController.bind();
    }

setUpPresenter 方法中会创建很多跟通知相关的对象,以及对相关对象之间建立关系。其中就包括了 NotificationListContainerNotificationListControllerNotificationShelfNotificationPanelStatusBarNotificationActivityStarter 等,这些都是处理通知逻辑的关键 API。 如果刚开始接触其实对这些类的印象是比较模糊的,不知道从何入手,当我们了解了这些类的用途以及它们在操作系统中的用户界面,我们就会有一个比较直观的认识,接下来会重点探索这些类的内在逻辑。

0x03 引用

•关于通知通道官方文档 https://developer.android.com/training/notify-user/channels?hl=zh-cn•在线源码阅读 https://cs.android.com/

0 人点赞