SystemUI plugin 开发

SystemUI Plugins

SystemUI插件提供了一种快速替换SystemUI原有组件的方法,可以在运行时更改SystemUI的行为。

现在已有插件: here

一、使用方法

1. Plugin 接口

在包com.android.systemui.plugins中定义SystemUI 和 Plugin 中共同使用的接口:

java 复制代码
@ProvidesInterface(action = MyPlugin.ACTION, version = MyPlugin.VERSION)
@DependsOn(target = OtherInterface.class)
public interface MyPlugin extends Plugin {
    String ACTION = "com.android.systemui.action.PLUGIN_MY_PLUGIN";
    int VERSION = 1;
    ...
}
  • VERSION: 解决兼容性问题,每次增加或者修改接口,VERSION需要增大, 以避免Plugin接口没有默认实现
  • ACTION: 用于关联Plugin apk 中声明的组件

2. Plugin Listener

可以通过 PluginManager注册监听器PluginListener, 可以实现在相应插件安装时调用方法

java 复制代码
public interface PluginListener<T extends Plugin> {
    /**
     * Called when the plugin has been loaded and is ready to be used.
     * This may be called multiple times if multiple plugins are allowed.
     * It may also be called in the future if the plugin package changes
     * and needs to be reloaded.
     */
    void onPluginConnected(T plugin);

    /**
     * Called when a plugin has been uninstalled/updated and should be removed
     * from use.
     */
    default void onPluginDisconnected(T plugin) {
        // Optional.
    }
}

3. 使用实例

  1. 在SystemUI编译PluginLib.aar, 让Plugin apk 依赖这个lib

  2. AndroidManifest.xml

xml 复制代码
       <service android:name=".SampleOverlayPlugin"
            android:label="@string/plugin_label">
            <intent-filter>
                <action android:name="com.android.systemui.action.PLUGIN_OVERLAY" />
            </intent-filter>
        </service>
  • 可以通过PackageManagerService 去查找组件

  • 可以方便的通过PMS 结果禁用/启用组件

    permission

xml 复制代码
 <uses-permission android:name="com.android.systemui.permission.PLUGIN" />
  1. 实现接口
java 复制代码
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {
    ...
    public void onCreate(Context sysuiContext, Context pluginContext) {

    }

    public void onDestroy() {

    }
}

二、设计原理

1. crash处理

PluginManagerImpl中定义了一个UncaughtExceptionHandler, 在发生crash 时检查是否为插件引起,如果是,并且插件不在白名单中,则禁用插件

白名单配置: res/values/config.xml

xml 复制代码
    <!-- SystemUI Plugins that can be loaded on user builds. -->
    <string-array name="config_pluginWhitelist" translatable="false">
        <item>com.android.systemui</item>
        <item>com.systemui.plugin</item>
    </string-array>

白名单中插件在每次SystemUI 进程启动时加载

2. 插件加载流程

插件加载时机: SystemUI 进程启动 或者 插件apk 安装(监听 Intent.ACTION_PACKAGE_ADDED 广播)

rust 复制代码
SystemUIService.onCreate

 -> VolumeUI.start()

-> new VolumeDialogComponent() 

-> ExtensionBuilder.withPlugin

-> PluginManagerImpl.addPluginListener

->PluginInstanceManagerFactory.createPluginInstanceManager

一个插件组件对应一个PluginInstanceManager

com/android/systemui/plugins/PluginInstanceManager.java

java 复制代码
        protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
            ... ...
            try {
                ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
                // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
                if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
                        != PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG, "Plugin doesn't have permission: " + pkg);
                    return null;
                }
                // Create our own ClassLoader so we can use our own code as the parent.
                //1. 创建plugin classloader
                ClassLoader classLoader = mManager.getClassLoader(info);
                 //2. 创建plugin Context
                Context pluginContext = new PluginContextWrapper(
                        mContext.createApplicationContext(info, 0), classLoader);
                Class<?> pluginClass = Class.forName(cls, true, classLoader);
                // TODO: Only create the plugin before version check if we need it for
                // legacy version check.
                T plugin = (T) pluginClass.newInstance();
                try {
                    //3. 检查版本
                    VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
                    if (DEBUG) Log.d(TAG, "createPlugin");
                    return new PluginInfo(pkg, cls, plugin, pluginContext, version);
                } catch (InvalidVersionException e) {
                    // 处理版本不一致情况 ... 
                  ... ...
            } catch (Throwable e) {
                Log.w(TAG, "Couldn't load plugin: " + pkg, e);
                return null;
            }
        }

这个方法中做了以下工作:

  • 创建plugin classloader

  • 创建plugin Context

  • 检查version :

    版本不一致会弹出通知 : "Plugin xxx is too old..." 或者 "Plugin xxx is too new...", 也不会继续加再这个插件了

3. ClassLoader

只有"com.android.systemui.plugin" 包名为这个时,使用SystemUI 类加载器, 即PluginLib 中类

SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java

java 复制代码
    private static class ClassLoaderFilter extends ClassLoader {
        private final String mPackage;
        private final ClassLoader mBase;

        public ClassLoaderFilter(ClassLoader base, String pkg) {
            super(ClassLoader.getSystemClassLoader());
            mBase = base;
            mPackage = pkg;
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (!name.startsWith(mPackage)) super.loadClass(name, resolve);
            return mBase.loadClass(name);
        }
    }

4. Plugin Context

Context 创建

java 复制代码
                Context pluginContext = new PluginContextWrapper(
                        mContext.createApplicationContext(info, 0), classLoader);

注意事项

1.PluginDependency

SystemUI中 通过 Dependency.get(XXX.class)获取依赖项

Plugin 中可以通过PluginDependency 获取的SystemUI 组件

  • VolumeDialogController - Mostly just API for the volume plugin

  • ActivityStarter - Allows starting of intents while co-operating with keyguard unlocks.

如何添加:

  1. 将接口放到com.android.systemui.plugins包下面
  2. 调用PluginDependencyProvider.allowPluginDependency

src/com/android/systemui/volume/VolumeDialogComponent.java

java 复制代码
    @Inject
    public VolumeDialogComponent(Context context, KeyguardViewMediator keyguardViewMediator,
            VolumeDialogControllerImpl volumeDialogController) {
                ...
                 // Allow plugins to reference the VolumeDialogController.
                Dependency.get(PluginDependencyProvider.class)
                        .allowPluginDependency(VolumeDialogController.class);
            }

2.注解

Requires , DependsOn

java 复制代码
@Requires(target = VolumeDialog.class, version = VolumeDialog.VERSION)
@DependsOn(target = Callback.class)
@Requires(target = VolumeDialogController.class, version = VolumeDialogController.VERSION)
@Requires(target = PluginDependency.class, version = PluginDependency.VERSION)
@Dependencies({@DependsOn(
        target = VolumeDialogController.StreamState.class
), @DependsOn(
        target = VolumeDialogController.State.class
), @DependsOn(
        target = VolumeDialogController.Callbacks.class
)})
public class VolumeDialogPlugin implements VolumeDialog {

    private  VolumeDialogController mController;
    @Override
    public void init(int i, Callback callback) {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void onCreate(Context sysuiContext, Context pluginContext) {
    }

    @Override
    public void onDestroy() {
    }

    private final VolumeDialog.Callback mVolumeDialogCallback = new VolumeDialog.Callback() {
        @Override
        public void onZenSettingsClicked() {
        }

        @Override
        public void onZenPrioritySettingsClicked() {
        }
    };
}

3. ClassNotFoundException

plugin context 中Resources 对象 ClassLoader对象 为null,在加载资源时候,如果没用Context ClassLoader ,如 DrawableInflater ,会抛找不到类异常,用下面方法可以解决

java 复制代码
    setResourceClassLoader(context.getResources(), context.getClassLoader());
    

    public void setResourceClassLoader(Resources resources, ClassLoader classLoader) {
        try {
            Field field = Resources.class.getDeclaredField("mClassLoader");
            field.setAccessible(true);
            Object object = field.get(resources);
            field.set(resources, classLoader);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

参考文档

android.googlesource.com/platform/fr...

相关推荐
三少爷的鞋1 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)5 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码7 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓12 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer