Decoupling an Android Launcher by Transplanting AOSP’s Hidden Plugin Framework
theme: cyanosis highlight: agate
As intelligent cockpit features grow richer, services like multimedia, vehicle settings, and weather have begun integrating into the Launcher as cards, gradually turning the Launcher into an aggregation hub for all kinds of cockpit business. When the number of cards was small, the Launcher team could develop them directly, and simple scenarios were handled via Widgets. As cards multiplied, we had to hand them off to individual business owners for development, then integrate them into the Launcher as AARs.
But an AAR is still compile-time integration; a strong version and release coupling remains between the business modules and the Launcher. Once the card scale expands further, debugging and maintaining the Launcher becomes extremely difficult. A more natural question emerges:
Can business cards stop participating in the Launcher compilation as part of it, and instead be provided as independent APKs — like "plugins" — that the Launcher dynamically discovers, loads, and uses at runtime?
This leads to a very interesting architectural design in Android — the Plugin mechanism. Through Plugins, we can attempt to transform the Launcher from "the bearer of all business code" into "a host and scheduler for plugins": the Launcher only defines interfaces, lifecycles, and display containers, while specific card capabilities are implemented by different business plugins, further reducing coupling between modules.
Friends who worked on early internet Android apps are probably familiar with the concept of "pluginization." However, the Plugin introduced here is not the common application-layer plugin framework, but rather a plugin mechanism that already exists within the native Android system. For example, the blogger's earlier piece Automotive Android App Development & Analysis - First Try with SystemUI Plugin is a fairly typical application of it.
This article will attempt to transplant the SystemUI Plugin Framework and implement dynamic loading of third-party Widget plugin APKs in a custom Launcher.
This transplant has three main goals:
- Verify the feasibility of host APK dynamically loading plugin APKs in an in-vehicle environment;
- Verify the architectural scheme where Launcher cards are provided by independent APKs and dynamically loaded;
- Strip the SystemUI Plugin Framework out of the SystemUI project and package it as an SDK that can be independently maintained and reused.
1. Overall Architecture and Effect Demonstration
The project contains four Gradle Modules:
| Module | Type | Role |
|---|---|---|
| plugin-api | Library | Plugin interfaces, Listeners, lifecycle interfaces, and version annotations |
| host | Application | Plugin host, responsible for discovery, loading, and lifecycle management |
| media-plugin | Application | Multimedia card plugin |
| weather-plugin | Application | Weather card plugin |
1.1 plugin-api: The SDK Between Host and Plugins
plugin-api contains no concrete implementation; it only defines the interfaces that both sides must adhere to.
It mainly includes:
| Interface / Annotation | Role |
|---|---|
| Plugin | Base interface for all plugins |
| PluginListener | Plugin lifecycle callbacks |
| PluginLifecycleManager | Controls loading and unloading of a single plugin instance |
| WidgetViewPlugin | Launcher card plugin interface |
| LauncherOverlayPlugin | Launcher Overlay plugin interface |
| @Requires | Declares dependency interface version |
| @ProvidesInterface | Declares Plugin API version |
For the Launcher, it doesn't need to know how a "weather card" or "music card" is specifically implemented; it only needs to recognize: WidgetViewPlugin, and then call something like:
View createView(Context pluginContext);
to obtain the View provided by the plugin. This is the core idea of the entire Plugin architecture:
The Host depends on interfaces, not on concrete business implementations.
2. How Plugins Are Discovered
AOSP's design is very interesting. The plugin implementation class is declared in the Manifest as a <service>:
<service
android:name=".widget.MediaWidgetViewPlugin"
android:exported="false">
<intent-filter>
<action android:name="com.android.systemui.action.PLUGIN_WIDGET_VIEW" />
</intent-filter>
</service>
But this Service is never actually started. It merely borrows PackageManager's existing Intent query mechanism, treating the Service as a "plugin registry."
The comment in the AOSP source code puts it very directly:
This isn't actually a service and shouldn't ever be started, but is a convenient PM based way to manage our plugins.
The Host only needs:
Intent intent = new Intent(action);
List<ResolveInfo> result =
packageManager.queryIntentServices(intent, 0);
to find all plugin components that declared the corresponding Action.
The entire process can be summarized as:
Note here: PackageManager queries are responsible for "discovering candidate plugins"; whether loading is actually allowed is subsequently verified by PluginActionManager.
For example, AOSP further checks:
mPm.checkPermission(PLUGIN_PERMISSION, packageName)
An APK without the Plugin permission, even if discovered by the query, will not enter the subsequent loading process.
3. How Plugins Are Loaded
SystemUI does not directly use the host ClassLoader to load plugins; instead, it creates an independent PathClassLoader for the plugin.
The core structure is roughly as follows:
3.1 ClassLoader Isolation
The core code comes from PluginInstance:
List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>();
LoadedApk.makePaths(
null,
true,
appInfo,
zipPaths,
libPaths);
ClassLoader classLoader = new PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader(baseClassLoader));
Where:
LoadedApk.makePaths(...)
is responsible for obtaining loading paths for the plugin APK, Native Libraries, etc. Then it creates: PathClassLoader to load the plugin code.
3.2 ClassLoaderFilter
The plugin's Parent ClassLoader does not directly expose the entire Host; instead, it wraps a layer: ClassLoaderFilter. In my transplanted version, only a few necessary packages are opened:
androidx.constraintlayout.widget
com.android.systemui.common
com.android.systemui.log
com.android.systemui.plugin
Its purpose is not to create a true "security sandbox," but to restrict the Host implementation classes that the plugin can directly access.
More precisely, it provides:
Class visibility isolation at the ClassLoader level.
The plugin still runs in the Host process, so it should not be understood as process-level security isolation.
3.3 Creating Plugin via Reflection
After finding the plugin class, reflection is performed through the plugin ClassLoader:
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(),
true,
loader);
T result = (T) mInstanceFactory.create(instanceClass);
At this point:
Plugin APK
↓
PathClassLoader
↓
Class.forName()
↓
Plugin Instance
The plugin code officially enters the Host process for execution.
3.4 Loading Resources
Loading Java classes alone is not enough; plugins usually also need to access their own:
layout
drawable
string
color
style
Therefore, the Host also needs to create an independent Application Context for the plugin:
Context context =
mContext.createApplicationContext(appInfo, 0);
Then wrap another layer:
PluginContextWrapper
The key point is overriding:
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
Also, for LayoutInflater, do:
cloneInContext(this)
This way, when the plugin does:
LayoutInflater.inflate(...)
on its own XML, custom Views can also find the corresponding class through the plugin's own ClassLoader.
Finally:
PluginContext
├── Resources → Plugin APK
└── ClassLoader → Plugin PathClassLoader
The plugin's code and resources are truly connected together.
4. Plugin Lifecycle
A plugin is not simply:
load → exist permanently
Instead, it has a complete lifecycle:
onPluginAttached
↓
onPluginLoaded
↓
onPluginUnloaded
↓
onPluginDetached
The corresponding relationship is roughly:
Where:
4.1 onPluginAttached
The Host has discovered the Plugin, but the plugin instance may not be loaded immediately.
The Listener can return:
false
to implement lazy loading.
4.2 onPluginLoaded
The plugin has completed:
ClassLoader
→ Instance
→ PluginContext
→ Version Check
At this point, the Host can officially use the plugin.
4.3 onPluginUnloaded
The plugin instance is released.
Here you should:
- Remove View
- Cancel Animator
- Remove Handler Callback
- Release Listener
4.4 onPluginDetached
The PluginInstance lifecycle ends completely.
5. What Happens After a Plugin APK Update
PluginManagerImpl listens for:
PACKAGE_ADDED
PACKAGE_CHANGED
PACKAGE_REPLACED
PACKAGE_REMOVED
USER_UNLOCKED
For example, when a plugin executes:
adb install -r plugin.apk
The system sends:
PACKAGE_REPLACED
After the Host receives it, it will:
PACKAGE_REPLACED
↓
clear ClassLoader
↓
reloadPackage()
↓
removePkg()
↓
queryPkg()
↓
Create new PluginInstance
↓
Load new plugin
Corresponding sequence:
In theory, this chain can achieve:
Plugin APK updates without reinstalling the Host APK.
If the framework is fully implemented, the Host itself should not depend on force-stop to see new code.
If during actual debugging you still must:
adb shell am force-stop com.android.launcher3
to update, it usually indicates there are still issues like:
- Old ClassLoader not cleaned up;
- Plugin View still held;
- PluginInstance lifecycle not fully exited;
- Static objects holding old plugin;
- References from Context / Listener / Animator;
Therefore, force-stop is more suitable as a debugging fallback and should not become part of the formal Plugin hot-update mechanism.
6. Security and Stability
The biggest advantage of Plugin is flexibility, but the biggest risk also comes from here:
Plugin code ultimately runs inside the Host process.
If the Host is:
android.uid.system
Then after plugin code enters the Host process, it actually executes within the permission boundary of this process.
Therefore, this mechanism is not suitable for loading truly "untrusted third-party APKs."
It is more suitable for:
OEM internal business plugins
System pre-installed plugins
Platform-signed plugins
Controlled software ecosystems
AOSP itself also implements multiple layers of protection.
6.1 Signature Permission
Host defines:
<permission
android:name="com.android.systemui.permission.PLUGIN"
android:protectionLevel="signature" />
Plugin requests:
<uses-permission
android:name="com.android.systemui.permission.PLUGIN" />
Then the Host checks again before loading the plugin:
mPm.checkPermission(PLUGIN_PERMISSION,packageName)
So the process is actually:
PackageManager discovers candidate plugins
↓
PLUGIN permission check
↓
Plugin Enabled check
↓
Version check
↓
Load
In this PoC, the Host and Plugin use the same set of platform signatures, thereby restricting the range of APKs that can connect to the Plugin Framework.
6.2 Plugin API Version Check
SystemUI Plugin does not simply judge:
versionCode
Instead, it uses:
@ProvidesInterface
@Requires
to describe the dependency relationships between Plugin APIs.
Subsequently, matching is performed by VersionChecker. This allows the Host to determine:
- Plugin API is too old
- Plugin API is too new
- Dependency interface version mismatch
rather than encountering NoSuchMethodError or LinkageError only when actually calling methods. For a long-evolving Launcher Plugin API, this is very important.
6.3 Crash Circuit Breaker
Plugins and Host run in the same process; a single plugin crash can directly bring down the entire Launcher.
Therefore, AOSP designed a Plugin Disable mechanism. If a specific Plugin can be located from the stack trace, that Plugin is disabled. If it's impossible to determine who caused the crash, then:
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.disableAll();
}
That is:
When the offending plugin cannot be found, disable all of them.
For core system processes like SystemUI or Launcher, this design is reasonable.
The core goal is not to guarantee that plugins always run, but:
Prioritize ensuring the host process can recover.
6.4 Production Build Restrictions
The AOSP Plugin Framework itself is very cautious about production builds.
The core logic is similar to:
if (!mIsDebuggable && !isPluginPrivileged(component)) {
return null;
}
That is:
userdebug / eng
→ Can be used for normal Plugin debugging
user
→ Only allows privileged Plugin
Therefore, this framework is not designed for "arbitrary third-party APK dynamic code execution" in the first place.
It is closer to:
A system module dynamic extension mechanism within a controlled environment.
7. How Multiple Plugins Coexist
There is another noteworthy issue here. PluginActionManager defaults to:
allowMultiple = false
When multiple Plugins are found for the same Action, it considers it a conflict.
Therefore, initially:
example-plugin
weather-plugin
both declared: PLUGIN_WIDGET_VIEW, and could not be loaded simultaneously. This project uses different Actions to distinguish different card types:
PLUGIN_WIDGET_VIEW
PLUGIN_WIDGET_VIEW_WEATHER
The Host registers separately:
Listener A
Listener B
Forming:
And through:
Map<String, View> mPluginViews;
maintains the View lifecycle corresponding to each Plugin.
Actual logs:
PluginInstance: Created plugin:
com.example.plugin.widget.ExampleWidgetViewPlugin
ExampleWidgetViewPlugin: onCreate
ExampleWidgetViewPlugin: createView
PluginInstance: Created plugin:
com.example.plugin.weather.WeatherWidgetViewPlugin
proving that two Plugins have simultaneously entered the same Host process.
8. Summary
After truly extracting SystemUI Plugin from AOSP, you'll find its core is actually not complex. The entire mechanism can be condensed into a few steps:
Plugin API
↓
Manifest Service + Action
↓
PackageManager Discovery
↓
Permission / Version Check
↓
PathClassLoader
↓
PluginContext
↓
Reflection
↓
Plugin Lifecycle
But what's truly valuable is not "dynamically loading APKs" itself, but the architectural problem it solves:
Traditional Launcher
Launcher APK
├── Media Card
├── Weather Card
├── Vehicle Card
├── Navigation Card
└── ...
↓
Pluginized Launcher
Launcher Host
├── Plugin API
├── Plugin Manager
└── Card Container
Media Plugin APK
Weather Plugin APK
Vehicle Plugin APK
Navigation Plugin APK
The Launcher transforms from:
The bearer of all business code
gradually into:
The host, scheduler, and display container for Plugins.
Business cards can then have independent code, resources, versions, and lifecycles. For traditional internet Apps, this Plugin scheme that depends on hidden APIs and runs within the host process is clearly unsuitable.
But for intelligent cockpit platforms that can control the Framework, system signatures, and the overall software ecosystem, this Plugin mechanism is instead very suitable as a means of module decoupling. After understanding and mastering its implementation principles, its application scenarios need not be limited to Launcher and SystemUI; it can also be extended to other system modules that require dynamic expansion and independent delivery.
This article was written with assistance from ChatGPT-5.6 Sol, code generated by Deepseek V4-Flash, and secondarily reviewed by KiMi-K3.
Source code: https://github.com/linxu-link/LauncherPluginHost
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
For example, the minus-one screen.