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...

相关推荐
lichong9515 小时前
【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之video 的各种状态和生命周期调用说明
android·vue.js·macos
app出海创收老李6 小时前
海外独立创收日记(1)-我是如何从0到1在Google Play获得睡后被动收入的?
android·程序员
lang9998886 小时前
kodi在Android4.0.4安装播放歌曲显示歌词
android·kodi·歌词插件
yzx9910136 小时前
构建未来:深度学习、嵌入式与安卓开发的融合创新之路
android·人工智能·深度学习
前行的小黑炭6 小时前
Android :如何快速让布局适配手机和平板?
android·java·kotlin
Yang-Never10 小时前
Kotlin协程 -> Job.join() 完整流程图与核心源码分析
android·开发语言·kotlin·android studio
一笑的小酒馆16 小时前
Android性能优化之截屏时黑屏卡顿问题
android
懒人村杂货铺18 小时前
Android BLE 扫描完整实战
android
TeleostNaCl21 小时前
如何安装 Google 通用的驱动以便使用 ADB 和 Fastboot 调试(Bootloader)设备
android·经验分享·adb·android studio·android-studio·android runtime