diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 0e20444347fd2..8cba2ab0b70ba 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -48,6 +48,18 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext private val KEY_TIMESTAMP = "appliedTimestamp" +private val KNOWN_PLUGINS = + mapOf>( + "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 ConcurrentHashMap.concurrentGetOrPut( key: TKey, @@ -127,8 +139,61 @@ open class ClockRegistry( private val pluginListener = object : PluginListener { - override fun onPluginAttached(manager: PluginLifecycleManager) { - manager.loadPlugin() + override fun onPluginAttached( + manager: PluginLifecycleManager + ): 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( diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 3ae328e67e3d9..537b7a41a898c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -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( diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java index cc6a46fa7d6bc..56c3f93d49b42 100644 --- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java +++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java @@ -16,12 +16,20 @@ package com.android.systemui.plugins; +import android.content.ComponentName; + /** * Provides the ability for consumers to control plugin lifecycle. * * @param is the target plugin type */ public interface PluginLifecycleManager { + /** 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(); diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginListener.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginListener.java index c5f503216101f..bd0bd8942d5ef 100644 --- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginListener.java +++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginListener.java @@ -60,13 +60,18 @@ public interface PluginListener { /** * 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 manager) { + default boolean onPluginAttached(PluginLifecycleManager manager) { // Optional + return true; } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java index 9a9a2426507f8..6b67c092890de 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java @@ -79,17 +79,26 @@ public class PluginInstance 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 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 implements PluginLifecycleManager PluginFactory pluginFactory = new PluginFactory( 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( - context, listener, componentName, pluginFactory, instance); + context, listener, componentName, pluginFactory, null); } private boolean isPluginPackagePrivileged(String packageName) { @@ -332,7 +338,9 @@ public class PluginInstance implements PluginLifecycleManager ClassLoader loader = mClassLoaderFactory.get(); Class instanceClass = (Class) 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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index eef4470c48cd7..04c93cb71e422 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -297,6 +297,54 @@ class ClockRegistryTest : SysuiTestCase() { assertEquals(4, listChangeCallCount) } + @Test + fun unknownPluginAttached_clockAndListUnchanged_loadRequested() { + val mockPluginLifecycle = mock>() + 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>() + whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.falcon.one") + val mockPluginLifecycle2 = mock>() + 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>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java index d5e904c636d5a..88d853e244e03 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java @@ -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 { + 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 manager) { + public boolean onPluginAttached(PluginLifecycleManager manager) { mAttachedCount++; assertEquals(PluginInstanceTest.this.mPluginInstance, manager); + return mAttachReturn; } @Override