Skip dynamic load of known clock plugins

Bug: 279933902
Test: Checked load/unload cycles on device
Change-Id: I80b403ba8544c6aded5c766fb7dba6322a12a37a
This commit is contained in:
Hawkwood Glazier
2023-06-01 19:31:57 +00:00
parent a6550a836b
commit a4649ba3bf
7 changed files with 174 additions and 24 deletions

View File

@@ -48,6 +48,18 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private val KEY_TIMESTAMP = "appliedTimestamp"
private val KNOWN_PLUGINS =
mapOf<String, List<ClockMetadata>>(
"com.android.systemui.falcon.one" to listOf(ClockMetadata("ANALOG_CLOCK_BIGNUM")),
"com.android.systemui.falcon.two" to listOf(ClockMetadata("DIGITAL_CLOCK_CALLIGRAPHY")),
"com.android.systemui.falcon.three" to listOf(ClockMetadata("DIGITAL_CLOCK_FLEX")),
"com.android.systemui.falcon.four" to listOf(ClockMetadata("DIGITAL_CLOCK_GROWTH")),
"com.android.systemui.falcon.five" to listOf(ClockMetadata("DIGITAL_CLOCK_HANDWRITTEN")),
"com.android.systemui.falcon.six" to listOf(ClockMetadata("DIGITAL_CLOCK_INFLATE")),
"com.android.systemui.falcon.seven" to listOf(ClockMetadata("DIGITAL_CLOCK_METRO")),
"com.android.systemui.falcon.eight" to listOf(ClockMetadata("DIGITAL_CLOCK_NUMBEROVERLAP")),
"com.android.systemui.falcon.nine" to listOf(ClockMetadata("DIGITAL_CLOCK_WEATHER")),
)
private fun <TKey, TVal> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
key: TKey,
@@ -127,8 +139,61 @@ open class ClockRegistry(
private val pluginListener =
object : PluginListener<ClockProviderPlugin> {
override fun onPluginAttached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
manager.loadPlugin()
override fun onPluginAttached(
manager: PluginLifecycleManager<ClockProviderPlugin>
): Boolean {
if (keepAllLoaded) {
// Always load new plugins if requested
return true
}
val knownClocks = KNOWN_PLUGINS.get(manager.getPackage())
if (knownClocks == null) {
logBuffer.tryLog(
TAG,
LogLevel.WARNING,
{ str1 = manager.getPackage() },
{ "Loading unrecognized clock package: $str1" }
)
return true
}
logBuffer.tryLog(
TAG,
LogLevel.INFO,
{ str1 = manager.getPackage() },
{ "Skipping initial load of known clock package package: $str1" }
)
var isClockListChanged = false
for (metadata in knownClocks) {
val id = metadata.clockId
val info =
availableClocks.concurrentGetOrPut(id, ClockInfo(metadata, null, manager)) {
isClockListChanged = true
onConnected(id)
}
if (manager != info.manager) {
logBuffer.tryLog(
TAG,
LogLevel.ERROR,
{ str1 = id },
{ "Clock Id conflict on known attach: $str1 is double registered" }
)
continue
}
info.provider = null
}
if (isClockListChanged) {
triggerOnAvailableClocksChanged()
}
verifyLoadedProviders()
// Load executed via verifyLoadedProviders
return false
}
override fun onPluginLoaded(

View File

@@ -190,7 +190,9 @@ enum class ClockTickRate(val value: Int) {
data class ClockMetadata(
val clockId: ClockId,
val name: String,
)
) {
constructor(clockId: ClockId) : this(clockId, clockId) {}
}
/** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
data class ClockConfig(

View File

@@ -16,12 +16,20 @@
package com.android.systemui.plugins;
import android.content.ComponentName;
/**
* Provides the ability for consumers to control plugin lifecycle.
*
* @param <T> is the target plugin type
*/
public interface PluginLifecycleManager<T extends Plugin> {
/** Returns the ComponentName of the target plugin. Maybe be called when not loaded. */
ComponentName getComponentName();
/** Returns the package name of the target plugin. May be called when not loaded. */
String getPackage();
/** Returns the currently loaded plugin instance (if plugin is loaded) */
T getPlugin();

View File

@@ -60,13 +60,18 @@ public interface PluginListener<T extends Plugin> {
/**
* Called when the plugin is first attached to the host application. {@link #onPluginLoaded}
* will be automatically called as well when first attached. 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. Each call to {@link #onPluginAttached} will provide a new
* or different {@link PluginLifecycleManager}.
* will be automatically called as well when first attached if true is returned. 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. Each call to
* {@link #onPluginAttached} will provide a new or different {@link PluginLifecycleManager}.
*
* @return returning true will immediately load the plugin and call onPluginLoaded with the
* created object. false will skip loading, but the listener can load it at any time using the
* provided PluginLifecycleManager. Loading plugins immediately is the default behavior.
*/
default void onPluginAttached(PluginLifecycleManager<T> manager) {
default boolean onPluginAttached(PluginLifecycleManager<T> manager) {
// Optional
return true;
}
/**

View File

@@ -79,17 +79,26 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager
/** Alerts listener and plugin that the plugin has been created. */
public void onCreate() {
mListener.onPluginAttached(this);
boolean loadPlugin = mListener.onPluginAttached(this);
if (!loadPlugin) {
if (mPlugin != null) {
unloadPlugin();
}
return;
}
if (mPlugin == null) {
loadPlugin();
} else {
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
return;
}
mPluginFactory.checkVersion(mPlugin);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
mPlugin.onCreate(mAppContext, mPluginContext);
}
mListener.onPluginLoaded(mPlugin, mPluginContext, this);
}
/** Alerts listener and plugin that the plugin is being shutdown. */
@@ -118,6 +127,7 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager
return;
}
mPluginFactory.checkVersion(mPlugin);
if (!(mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
@@ -205,12 +215,8 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager
PluginFactory<T> pluginFactory = new PluginFactory<T>(
context, mInstanceFactory, appInfo, componentName, mVersionChecker, pluginClass,
() -> getClassLoader(appInfo, mBaseClassLoader));
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
T instance = pluginFactory.createPlugin();
pluginFactory.checkVersion(instance);
return new PluginInstance<T>(
context, listener, componentName, pluginFactory, instance);
context, listener, componentName, pluginFactory, null);
}
private boolean isPluginPackagePrivileged(String packageName) {
@@ -332,7 +338,9 @@ public class PluginInstance<T extends Plugin> implements PluginLifecycleManager
ClassLoader loader = mClassLoaderFactory.get();
Class<T> instanceClass = (Class<T>) Class.forName(
mComponentName.getClassName(), true, loader);
return (T) mInstanceFactory.create(instanceClass);
T result = (T) mInstanceFactory.create(instanceClass);
Log.v(TAG, "Created plugin: " + result);
return result;
} catch (ClassNotFoundException ex) {
Log.e(TAG, "Failed to load plugin", ex);
} catch (IllegalAccessException ex) {

View File

@@ -297,6 +297,54 @@ class ClockRegistryTest : SysuiTestCase() {
assertEquals(4, listChangeCallCount)
}
@Test
fun unknownPluginAttached_clockAndListUnchanged_loadRequested() {
val mockPluginLifecycle = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle.getPackage()).thenReturn("some.other.package")
var changeCallCount = 0
var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() { changeCallCount++ }
override fun onAvailableClocksChanged() { listChangeCallCount++ }
})
assertEquals(true, pluginListener.onPluginAttached(mockPluginLifecycle))
scheduler.runCurrent()
assertEquals(0, changeCallCount)
assertEquals(0, listChangeCallCount)
}
@Test
fun knownPluginAttached_clockAndListChanged_notLoaded() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.falcon.one")
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.falcon.two")
var changeCallCount = 0
var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() { changeCallCount++ }
override fun onAvailableClocksChanged() { listChangeCallCount++ }
})
registry.applySettings(ClockSettings("DIGITAL_CLOCK_CALLIGRAPHY", null))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(0, listChangeCallCount)
assertEquals(false, pluginListener.onPluginAttached(mockPluginLifecycle1))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(1, listChangeCallCount)
assertEquals(false, pluginListener.onPluginAttached(mockPluginLifecycle2))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(2, listChangeCallCount)
}
@Test
fun pluginAddRemove_concurrentModification() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()

View File

@@ -122,6 +122,7 @@ public class PluginInstanceTest extends SysuiTestCase {
mPluginInstanceFactory.create(
mContext, mAppInfo, wrongVersionTestPluginComponentName,
TestPlugin.class, mPluginListener);
mPluginInstance.onCreate();
}
@Test
@@ -135,11 +136,12 @@ public class PluginInstanceTest extends SysuiTestCase {
@Test
public void testOnDestroy() {
mPluginInstance.onCreate();
mPluginInstance.onDestroy();
assertEquals(1, mPluginListener.mDetachedCount);
assertEquals(1, mPluginListener.mUnloadCount);
assertNull(mPluginInstance.getPlugin());
assertInstances(0, -1); // Destroyed but never created
assertInstances(0, 0); // Destroyed but never created
}
@Test
@@ -161,6 +163,16 @@ public class PluginInstanceTest extends SysuiTestCase {
assertInstances(0, 0);
}
@Test
public void testOnAttach_SkipLoad() {
mPluginListener.mAttachReturn = false;
mPluginInstance.onCreate();
assertEquals(1, mPluginListener.mAttachedCount);
assertEquals(0, mPluginListener.mLoadCount);
assertEquals(null, mPluginInstance.getPlugin());
assertInstances(0, 0);
}
// This target class doesn't matter, it just needs to have a Requires to hit the flow where
// the mock version info is called.
@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
@@ -220,15 +232,17 @@ public class PluginInstanceTest extends SysuiTestCase {
}
public class FakeListener implements PluginListener<TestPlugin> {
public boolean mAttachReturn = true;
public int mAttachedCount = 0;
public int mDetachedCount = 0;
public int mLoadCount = 0;
public int mUnloadCount = 0;
@Override
public void onPluginAttached(PluginLifecycleManager<TestPlugin> manager) {
public boolean onPluginAttached(PluginLifecycleManager<TestPlugin> manager) {
mAttachedCount++;
assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
return mAttachReturn;
}
@Override