前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SystemUI 开发之通知的实现逻辑(四)

SystemUI 开发之通知的实现逻辑(四)

作者头像
阳仔
发布2023-03-02 12:56:54
9030
发布2023-03-02 12:56:54
举报
文章被收录于专栏:终身开发者

0x00 介绍

今天我们来看一下 SystemUI中系统通知的实现流程,希望能解决一个问题:系统通知是如何完成监听然后显示在通知栏的? 在前面的《SystemUI开发之启动流程(一)》一文中,我们了解到 SystemUI 这个应用是由 SystemServer 启动起来的。它启动了 SystemUIService 这个 Android 服务,然后再由这个服务分别启动了 SystemUI定义的各种服务组件 例如 SystemBars, StatusBar, PowerUI, VolumeUI等等组件。本文将此出发看看系统通知是如何实现的。这里会涉及以下一些知识点:

  • SystemUI 应用是如何监听系统通知的
  • 通知的 UI是如何构建的

本文的代码是基于Android 10 来分析

0x01 监听系统通知的实现过程

StatusBar 的创建

在看系统通知的实现流程之前我们先来回顾一下 SystemUI组件的构建流程。注意这些服务组件就是前文《SystemUI 开发之服务组件概览(二)》提到在 config.xml中定义的,例如

代码语言:javascript
复制
<string name="config_statusBarComponent" translatable="false">com.android.systemui.statusbar.phone.StatusBar</string>

<string-array name="config_systemUIServiceComponents" translatable="false">
    ...省略一些代码
    <item>com.android.systemui.volume.VolumeUI</item>
    <item>com.android.systemui.stackdivider.Divider</item>
    <item>com.android.systemui.SystemBars</item>
    ...
</string-array>

需要注意的是这些服务组件是普通 Java类,跟 Android四大组件服务是不一样的概念,它们都是扩展自 SystemUI这个基类。

这里我们先关注 SystemBars这个组件的创建

代码语言:javascript
复制
/**
 * Ensure a single status bar service implementation is running at all times, using the in-process
 * implementation according to the product config.
 */
public class SystemBars extends SystemUI {
    //...省略一些代码

    @Override
    public void start() {
        if (DEBUG) Log.d(TAG, "start");
        createStatusBarFromConfig();
    }


    private void createStatusBarFromConfig() {
        if (DEBUG) Log.d(TAG, "createStatusBarFromConfig");
        final String clsName = mContext.getString(R.string.config_statusBarComponent);
        ...省略代码
        Class<?> cls = null;
        try {
            cls = mContext.getClassLoader().loadClass(clsName);
        } catch (Throwable t) {
            throw andLog("Error loading status bar component: " + clsName, t);
        }
        try {
            mStatusBar = (SystemUI) cls.newInstance();
        } catch (Throwable t) {
            throw andLog("Error creating status bar component: " + clsName, t);
        }
        ...省略代码
        // 这里执行SystemUI的start方法
        mStatusBar.start();
        ...省略代码
    }

    //...省略一些代码
}

可以看到这个类非常的简单,就是通过它创建了 StatusBar,这个就是手机里面的状态栏。在 start()方法里面执行了 createStatusBarFromConfig()方法,然后读取 xml文件中的类名配置,通过反射创建了 StatusBar类。

有些人可能会疑问为何不直接创建 StatusBar呢? 确实是可以的,但是这样做的好处是比较灵活,不同的设备可以配置不同的 StatusBar实现,比如平板,车机系统等等。

接下来我们看看 StatusBar 的实现。

如果你看源码,会发现这个类很大,有4000多行的代码,但这里只展示与本文相关的关键流程

代码语言:javascript
复制
public class StatusBar extends SystemUI ... {

    // ...省略一些代码
    // 这个就是监听系统通知的接口
    protected NotificationListener mNotificationListener;
    // 系统通知 Binder 接口
    protected IStatusBarService mBarService;

    @Override
    public void start() {
        // ...省略一些代码
        mNotificationListener =  Dependency.get(NotificationListener.class);
        mNotificationListener.registerAsSystemService();
        // ...省略一些代码

        // 这里获取系统的状态栏服务 StatusBarService
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));

        //...省略代码

        // Connect in to the status bar manager service
        mCommandQueue = getComponent(CommandQueue.class);
        mCommandQueue.addCallback(this);

        RegisterStatusBarResult result = null;
        try {
            result = mBarService.registerStatusBar(mCommandQueue);
        } catch (RemoteException ex) {
            ex.rethrowFromSystemServer();
        }

        // 将StatusBar在windows显示出来
        createAndAddWindows(result);

        // ...省略一些代码
    }

    // ...省略一些代码

    public void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
        // 创建StatusBar 的View
        makeStatusBarView(result);
        mNotificationShadeWindowController.attach();
        mStatusBarWindowController.attach();
    }
}

我们应该已经了解到所有 SystemUI的服务组件,执行方法都是从 start()方法开始的。 在这个方法里面通过 Dependency组件获取到 mNotificationListener实例,通过它进行注册系统通知的监听,这一步非常关键,这里涉及到 Binder的通信,这里建立了SystemUI应用和系统服务的通信通道。 此外还通过了 mBarServicemCommandQueue注册给了 mBarService,用于建立系统状态栏服务与 SystemUI的通信。不过这个状态栏不是本文的重点。

NotificationListener
代码语言:javascript
复制
/**
 * This class handles listening to notification updates and passing them along to
 * NotificationPresenter to be displayed to the user.
 */
@SuppressLint("OverrideAbstract")
public class NotificationListener extends NotificationListenerWithPlugins {

    //...省略代码
    // 用于处理通知添加、移除、更新的操作
    private final NotificationEntryManager mEntryManager =
            Dependency.get(NotificationEntryManager.class);
    @Override
    public void onListenerConnected() {
        //...省略代码
        NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
        onSilentStatusBarIconsVisibilityChanged(noMan.shouldHideSilentStatusBarIcons());
    }

    @Override
    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        // 通知到达,StatusBarNotification 是通知的数据
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            // 在主线程中处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                // ...省略代码
                // 更新或者添加通知,使用NotificationEntryManager来处理
                if (isUpdate) {
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    mEntryManager.addNotification(sbn, rankingMap);
                }
            });
        }
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
            int reason) {
        // 移除通知
        if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
            final String key = sbn.getKey();
            // 主线程操作,使用NotificationEntryManager来处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                mEntryManager.removeNotification(key, rankingMap, reason);
            });
        }
    }

    @Override
    public void onNotificationRankingUpdate(final RankingMap rankingMap) {
        // 更新通知内容
        if (rankingMap != null) {
            RankingMap r = onPluginRankingUpdate(rankingMap);
            // 主线程操作,使用NotificationEntryManager来处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                mEntryManager.updateNotificationRanking(r);
            });
        }
    }

}

代码比较简单,它是扩展于 NotificationListenerWithPlugins,用于监听系统通知的发送、移除、更新的回调,主要是通过 NotificationEntryManager接口执行对通知的各种操作。

NotificationListenerWithPlugins是继承于 NotificationListenerService

这个接口在

frameworks/base/core/java/android/service/notification/NotificationListenerService.java

它是 frameworks提供的,如果一个应用要监听系统通知,那么就必须继承它。我们接着看它的实现。

NotificationListenerService
代码语言:javascript
复制
public abstract class NotificationListenerService extends Service {

    /**
     * Directly register this service with the Notification Manager.
     *
     * <p>Only system services may use this call. It will fail for non-system callers.
     * Apps should ask the user to add their listener in Settings.
     *
     * @param context Context required for accessing resources. Since this service isn't
     *    launched as a real Service when using this method, a context has to be passed in.
     * @param componentName the component that will consume the notification information
     * @param currentUser the user to use as the stream filter
     * @hide
     * @removed
     */
    @SystemApi
    public void registerAsSystemService(Context context, ComponentName componentName,
            int currentUser) throws RemoteException {
        if (mWrapper == null) {
            mWrapper = new NotificationListenerWrapper();
        }
        mSystemContext = context;
        INotificationManager noMan = getNotificationInterface();
        mHandler = new MyHandler(context.getMainLooper());
        mCurrentUser = currentUser;
        noMan.registerListener(mWrapper, componentName, currentUser);
    }

    /**
     * Directly unregister this service from the Notification Manager.
     *
     * <p>This method will fail for listeners that were not registered
     * with (@link registerAsService).
     * @hide
     * @removed
     */
    @SystemApi
    public void unregisterAsSystemService() throws RemoteException {
        if (mWrapper != null) {
            INotificationManager noMan = getNotificationInterface();
            noMan.unregisterListener(mWrapper, mCurrentUser);
        }
    }
}

看源码这个代码量也很大,不过我们知道它是一个 Android四大组件的 Service,有 registerAsSystemService()方法用于注册系统服务。这个方法是通过 INotificationManager传递 NotificationListenerWrapper来实现注册的。 我们会发现 NotificationListenerWrapper继承自 INotificationListener.Stub,它也是通过 Binder来进行通信的。

到这里小结一下:在构建 StatusBar过程中,通过 NotificationListener完成了系统通知的监听的注册。

现在我们回来看看通知的控件是如何创建的

0x02 通知控件的创建

当收到系统通知时,在 NotificationListeneronNotificationPosted()方法会执行,这时候就会在主线程中执行添加或更新的操作,这里我们关注添加的操作。

代码语言:javascript
复制
@Override
    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        // 通知到达,StatusBarNotification 是通知的数据
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            // 在主线程中处理
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                // ...省略代码
                // 更新或者添加通知,使用NotificationEntryManager来处理
                if (isUpdate) {
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    mEntryManager.addNotification(sbn, rankingMap);
                }
            });
        }
    }

收到通知到达,就通过 NotificationEntryManager来进行操作,现在来看看 NotificationEntryManager中的实现

代码语言:javascript
复制
public class NotificationEntryManager implements ...{

    // ...省略代码

    // 添加通知
    public void addNotification(StatusBarNotification notification, RankingMap ranking) {
        try {
            addNotificationInternal(notification, ranking);
        } catch (InflationException e) {
            handleInflationException(notification, e);
        }
    }

    private void addNotificationInternal(StatusBarNotification notification,
            NotificationListenerService.RankingMap rankingMap) throws InflationException {
        String key = notification.getKey();
        // ...省略代码
        // 更新数据
        mNotificationData.updateRanking(rankingMap);
        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
        rankingMap.getRanking(key, ranking);

        // ...省略代码
        // Construct the expanded view.
        // 异步创建控件
        requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
                REASON_CANCEL));

        // ...省略代码
    }
    private void bindRow(NotificationEntry entry, ExpandableNotificationRow row) {
        mListContainer.bindRow(row);
        mNotificationRemoteInputManager.bindRow(row);
        row.setOnActivatedListener(mPresenter);
        entry.setRow(row);
        mNotifBindPipeline.manageRow(entry, row);
        mBindRowCallback.onBindRow(row);
    }

    // ...省略代码
}

这里的逻辑也比较清楚, addNotification()中直接使用 addNotificationInternal(),此方法又通过 NotificationRowBinder进行异步创建控件

这是一个接口,具体实现是在

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java

代码语言:javascript
复制
/**
     * Inflates the views for the given entry (possibly asynchronously).
     */
    @Override
    public void inflateViews(
            NotificationEntry entry,
            NotificationRowContentBinder.InflationCallback callback)
            throws InflationException {
        ViewGroup parent = mListContainer.getViewParentForNotification(entry);
        // 如果通知本身就存在,执行更新操作
        if (entry.rowExists()) {
            mIconManager.updateIcons(entry);
            ExpandableNotificationRow row = entry.getRow();
            row.reset();
            updateRow(entry, row);
            inflateContentViews(entry, row, callback);
        } else {
            // 首次添加通知
            mIconManager.createIcons(entry);
            // 通过RowInflaterTask来异步创建控件,这个控件的名称ExpandableNotificationRow
            new RowInflaterTask().inflate(mContext, parent, entry,
                    row -> {
                        // Setup the controller for the view.
                        // 将通知数据与控件进行绑定并更新
                        bindRow(entry, row);
                        updateRow(entry, row);
                    });
        }
    }

inflateViews()方法中会根据当前的通知是否存在进行更新或者添加,首次添加通知时会使用一个异步构建控件的接口来创建 ExpandableNotificationRow实例,这个就是在通知栏中显示的具体控件。

看看 RowInflaterTask.inflate的实现

代码语言:javascript
复制
/**
     * Inflates a new notificationView. This should not be called twice on this object
     */
    public void inflate(Context context, ViewGroup parent, NotificationEntry entry,
            RowInflationFinishedListener listener) {
        if (TRACE_ORIGIN) {
            mInflateOrigin = new Throwable("inflate requested here");
        }
        mListener = listener;
        AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);
        mEntry = entry;
        entry.setInflationTask(this);
        inflater.inflate(R.layout.status_bar_notification_row, parent, this);
    }

这里使用了 AsyncLayoutInflater这个异步创建布局的接口来实现的

这个接口是 androidx里面的标准接口,对一些复杂的控件可以使用此接口提高创建的效率

https://developer.android.com/reference/androidx/asynclayoutinflater/view/AsyncLayoutInflater

另外我们看到一条通知的静态布局文件 status_bar_notification_row.xml

代码语言:javascript
复制
<!-- extends FrameLayout -->
<com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:clickable="true"
    >

    <!-- Menu displayed behind notification added here programmatically -->

    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@+id/backgroundNormal"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.android.systemui.statusbar.notification.row.NotificationBackgroundView
        android:id="@+id/backgroundDimmed"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.android.systemui.statusbar.notification.row.NotificationContentView
        android:id="@+id/expanded"
       android:layout_width="match_parent"
       android:layout_height="wrap_content" />

    <com.android.systemui.statusbar.notification.row.NotificationContentView
        android:id="@+id/expandedPublic"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    ...省略代码

</com.android.systemui.statusbar.notification.row.ExpandableNotificationRow>

它的布局结构中顶层是扩展自 FrameLayoutExpandableNotificationRow,然后是两个自定义的背景图 NotificationBackgroundView,还有两个跟通知内容相关的 NotificationContentView。如果需要自定义这里的布局或修改一些样式,可以关注这些类的实现。

0x03 总结

最后通过一张简单的流程来总结一下

SystemServer启动了 SystemUIService,然后通过反射创建在 xml中配置的 SystemUI服务组件列表,这其中就包括了 StatusBar组件,这是系统状态栏。 这个状态栏会与系统的状态栏服务 StatusBarService建立 Binder通信。同时还会使用 NotificatioinListener注册系统通知的监听。 当收到通知后相应的回调接口 onNotificationPosted会被执行,然后使用 NotificationEntryManager对相应的通知进行处理。

0x04 引用

  • https://cs.android.com/android/platform/superproject/+/android-10.0.0_r10:frameworks/base/packages/SystemUI/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 愤怒的代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x00 介绍
  • 0x01 监听系统通知的实现过程
    • StatusBar 的创建
      • NotificationListener
        • NotificationListenerService
        • 0x02 通知控件的创建
        • 0x03 总结
        • 0x04 引用
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档