车载Android应用开发与分析 - 初试 SystemUI Plugin

在前面的视频、文章中我们介绍完了整个车载Android应用开发所需要的基础知识:

  1. 【视频文稿】车载Android应用开发与分析 - 走进车载操作系统 - 掘金
  2. 【视频文稿】车载Android应用开发与分析 - AOSP的下载与编译 - 掘金
  3. 【视频文稿】车载Android应用开发与分析 - 开发系统应用 - 掘金
  4. 【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上) - 掘金
  5. 【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(下) - 掘金

本期内容,我们介绍原生Android Automotive中车载应用的实现方式和它的原理。首先要介绍的就是车载应用开发中非常重要的一个系统应用,Android系统的UI - SystemUI

由于原生Android系统的SystemUI代码量很大、内容也非常庞杂,这里我会挑选出对车载SystemUI开发具有参考意义的模块进行介绍,大约会有4-5期的内容,主要分为以下几个模块:

  1. 车载Android应用开发与分析 - SystemUI 「功能」与「源码结构」分析 - 掘金
  2. 车载Android应用开发与分析 - 初试 SystemUI Plugin

SystemUI的源代码可能是所有Android原生应用中最复杂的一个,当我们需要定制SystemUI时,庞大的源码量会对的定制化开发带来巨大的潜在风险。所以目前车载SystemUI常见的做法就是,从原生SystemUI中移植少量必须的源码,然后从头定制一个源码、功能完全可控的SystemUI。

重新开发一个SystemUI就是唯一的选项吗?当然不是!Google官方早就注意到了这个问题,所以SystemUI中提供插件化的开发方式 - SystemUI Plugin。

本文源码地址:/frameworks/base/+/refs/heads/main/packages/SystemUI/plugin/ExamplePlugin/

本文源码环境基于Android 13

SystemUI Plugin

SystemUI plugin机制是一种让SystemUI的功能可以被动态替换或修改的方法,它可以让开发者快速创建和迭代SystemUI的原型,而尽可能少的修改SystemUI的主框架。

注意:使用Plugin并不能保证我们完全不需要修改SystemUI的主框架,毕竟需求永远是多变的。

Plugin Hooks

Plugin hooks是一些预定义的插件接口,它们可以让应用实现一些特定的功能,并通过Intent和注解来注册和声明插件的类型和版本。Plugin hooks有多种类型,例如OverlayPlugin, QSFactory, VolumeDialog等,每种类型都有一个对应的action和expected interface,用于标识插件的功能和要求。

Android 13中Plugin hooks预定义接口主要有以下几种:

  • BcSmartspaceDataPlugin:这个plugin可以让应用提供自定义的数据给锁屏界面上的智能空间(BcSmartspace),例如天气、日历、新闻等。
  • ClockProviderPlugin:这个plugin可以让应用提供自定义的时钟样式给锁屏界面和始终应用。
  • DozeServicePlugin:这个plugin可以让应用自定义Doze模式的行为,例如控制屏幕亮度、显示内容、传感器等。
  • FalsingPlugin:这个plugin可以让应用自定义对误触(Falsing)事件的检测和处理,例如判断用户是否真的想滑动通知栏或解锁屏幕等。
  • GlobalActions:这个plugin可以让应用自定义全局操作(GlobalActions)对话框的外观和行为,例如添加新的操作按钮或改变对话框样式。
  • GlobalActionsPanelPlugin:这个plugin可以让应用在全局操作对话框中添加一个可展开的面板,用于显示更多的操作选项或信息。
  • IntentButtonProvider:这个plugin可以让应用在锁屏界面上添加一个自定义的按钮,用于启动一个指定的Intent。
  • NavigationEdgeBackPlugin:这个plugin可以让应用自定义导航栏边缘返回(NavigationEdgeBack)手势的行为,例如改变触发区域或动画效果。
  • NotificationListenerController:这个plugin可以让应用控制通知监听器(NotificationListener)服务的连接和断开,以及获取通知事件和数据。
  • NotificationMenuRowPlugin:这个plugin可以让应用自定义通知菜单栏(NotificationMenuRow)的外观和行为,例如添加新的菜单项或改变菜单样式。
  • NotificationPersonExtractorPlugin:这个plugin可以让应用自定义从通知中提取人物信息(NotificationPersonExtractor)的逻辑,例如识别通知中包含的联系人或头像等。
  • OverlayPlugin:这个plugin可以让应用自定义覆盖在通知栏上方的视图(OverlayView),用于显示一些额外的内容或功能。
  • PluginFragment:这个plugin可以让应用在SystemUI中嵌入一个Fragment,用于显示一些自定义的界面或功能。
  • QSFactory:这个plugin可以让应用提供自定义的快速设置工厂(QSFactory),用于创建快速设置图块或面板。
  • SensorManagerPlugin:这个plugin可以让应用使用SensorManager服务来注册和取消注册传感器监听器,以及获取传感器事件和数据。
  • ToastPlugin:这个plugin可以让应用自定义Toast消息(Toast)的外观和行为,例如改变Toast位置或持续时间等。
  • ViewProvider:这个plugin可以让应用提供一个自定义的视图(View),用于替换SystemUI中某些组件或功能。
  • VolumeDialog:这个plugin可以让应用自定义音量调节对话框(VolumeDialog)的外观和行为,例如添加新的音量控制选项或改变音量条的样式。

Plugin 上手

创建一个AndroidStudio的SystemUI plugin项目,可以参考以下的步骤:

1)编译SystemUIPluginLib.jar

使用Plugin之前我们需要编译出SystemUIPluginLib.jar,在AOSP源码根目录执行下面的指令。

make SystemUIPluginLib

然后就可以在下面的目录中得到SystemUIPluginLib.jar

out/target/product/emulator_x86/obj/JAVA_LIBRARIES/SystemUIPluginLib_intermediates/javalib.jar

在AOSP的文档中建议使用 frameworks/base/packages/SystemUI/plugin/update_plugin_lib.sh 脚本编译 SystemUIPluginLib.jar,不过我编译时出现了环境配置问题。

2)配置系统签名

在build.gradle中配置系统签名。

android {
    ...
    signingConfigs {
        sign {
            storeFile file('system.keystore')
            storePassword '123456'
            keyAlias 'cardemo'
            keyPassword '123456'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.sign
        }
        debug {
            minifyEnabled false
            signingConfig signingConfigs.sign
        }
    }
}

关于如何制作系统签名,请参考:车载Android应用开发与分析 - 开发系统应用 - 掘金

3)创建一个Plugin

在plugin项目中定义一个类,实现自Plugin中已经提供的各种插件,并使用Requires注解声明target和version字段,这些字段用于标识插件的类型和版本。

@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {
    private static final String TAG = "SampleOverlayPlugin";
    private Context mPluginContext;

    private View mStatusBarView;
    private View mNavBarView;

    @Override
    public void onCreate(Context sysuiContext, Context pluginContext) {
        Log.d(TAG, "onCreate");
        mPluginContext = pluginContext;
    }

    @Override
    public void onDestroy() {
        if (mInputSetup) {
            mStatusBarView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
                    onComputeInternalInsetsListener);
        }
        Log.d(TAG, "onDestroy");
        if (mStatusBarView != null) {
            mStatusBarView.post(() -> ((ViewGroup) mStatusBarView.getParent()).removeView(mStatusBarView));
        }
        if (mNavBarView != null) {
            mNavBarView.post(() -> ((ViewGroup) mNavBarView.getParent()).removeView(mNavBarView));
        }
    }

@Override
public void setup(View notificationShadeWindowView, View navBar) {
    Log.d(TAG, "Setup");
    if (notificationShadeWindowView instanceof ViewGroup) {
        mStatusBarView = LayoutInflater.from(mPluginContext)
                .inflate(R.layout.colored_overlay, (ViewGroup) notificationShadeWindowView, false);
        ((ViewGroup) notificationShadeWindowView).removeAllViews();
        ((ViewGroup) notificationShadeWindowView).addView(mStatusBarView);
    }
    if (navBar instanceof ViewGroup) {
        mNavBarView = LayoutInflater.from(mPluginContext)
                .inflate(R.layout.colored_overlay, (ViewGroup) navBar, false);
        ((ViewGroup) navBar).removeAllViews();
        ((ViewGroup) navBar).addView(mNavBarView);
    }
}
}

注意:Android不同版本中SystemUI的代码存在不小的差异,例如:Android13中setup(View statusBar, View navBar)中返回的statusBar实际上是NotificationShadeWindowView。

4)注册Plugin

在plugin项目的AndroidManifest中注册一个service,使用action和permission属性指定插件的接口和权限,这样SystemUI就可以通过Intent找到插件。

<uses-permission android:name="com.android.systemui.permission.PLUGIN" />

<application>

    <service
        android:name=".SampleOverlayPlugin"
        android:exported="false"
        android:label="@string/plugin_label"
        tools:ignore="Instantiatable">
        <intent-filter>
            <action android:name="com.android.systemui.action.PLUGIN_OVERLAY" />
        </intent-filter>
    </service>

</application>

的name可以在我们实现的plugin接口中找到。

SystemUI为了保证系统安全,对于plugin的加载,构筑了两道防线:

第一道防线是Build.IS_DEBUGGABLE检查。SysUI 在扫描或加载设备上的任何插件之前,会检查Build.IS_DEBUGGABLE,以确保构建是可调试的。

第二道防线是就是签名权限。所有插件都必须被系统签名且持有com.android.systemui.permission.PLUGIN权限才能加载其任何代码,否则将记录违规行为,并忽略插件。

5)运行Plugin

将plugin.apk push 到Android 13 模拟器的/system/priv-app/ 目录下,重启。可以看到如下的效果:

  1. NavBar的所有子View被移除,并添加了一个红色的View;
  2. NotificationShadeWindowView的所有子View被移除,并添加了一个红色的View。

总结

本文初试了SystemUI插件机制,在编写本文时发现Plugin相关的资料少的可怜,即使是官方资料有的也过时了。所以就像标题那样,本文只是简单尝试了Plugin,如何使用Plugin来详细定制一个完全符合我们需求的SystemUI呢?这个我们放到以后再写,因为接下来需要先来分析SystemUI Plugin的原理,在资料如此稀少的情况下,不了解原理几乎无法写出符合需求的Plugin。在分析的原理的过程中,我们会逐步补完、理解一些Plugin的概念。

以上就是本文的所有内容,感谢你的阅读,希望对你所有帮助。

参考资料

Sysui plugin

SystemUI Plugin 简介及使用

/SystemUI/docs/plugins.md