From 37c423a4ad79f9f7e8b7a9db88a525167dfd7ce9 Mon Sep 17 00:00:00 2001 From: Hawkwood Glazier Date: Tue, 20 Dec 2022 23:29:43 +0000 Subject: [PATCH] Provide mechanism for on demand loading/unloading of connected plugins Bug: 266466757 Bug: 270860591 Test: Extended automated test to detect unfreed objects Test: Manually checked on device running memory Change-Id: I074601632890776a86a1117c3e305aca4d68503d --- .../systemui/shared/clocks/ClockRegistry.kt | 254 ++++++++++++++---- .../plugins/PluginLifecycleManager.java | 48 ++++ .../systemui/plugins/PluginListener.java | 86 +++++- .../shared/plugins/PluginActionManager.java | 6 +- .../shared/plugins/PluginInstance.java | 210 ++++++++++++--- .../keyguard/dagger/ClockRegistryModule.java | 4 +- .../systemui/plugins/PluginsModule.java | 2 +- .../shared/clocks/ClockRegistryTest.kt | 86 +++--- .../plugins/PluginActionManagerTest.java | 14 +- .../shared/plugins/PluginInstanceTest.java | 201 +++++++++++--- 10 files changed, 738 insertions(+), 173 deletions(-) create mode 100644 packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java 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 00c0a0b3e7b3f..e73afe74c03dc 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 @@ -29,14 +29,15 @@ import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockProvider import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.ClockSettings +import com.android.systemui.plugins.PluginLifecycleManager import com.android.systemui.plugins.PluginListener import com.android.systemui.plugins.PluginManager import com.android.systemui.util.Assert +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -private val TAG = ClockRegistry::class.simpleName!! private const val DEBUG = true private val KEY_TIMESTAMP = "appliedTimestamp" @@ -51,7 +52,10 @@ open class ClockRegistry( val handleAllUsers: Boolean, defaultClockProvider: ClockProvider, val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, + val keepAllLoaded: Boolean, + val subTag: String, ) { + private val TAG = "${ClockRegistry::class.simpleName} ($subTag)" interface ClockChangeListener { // Called when the active clock changes fun onCurrentClockChanged() {} @@ -76,11 +80,85 @@ open class ClockRegistry( private val pluginListener = object : PluginListener { - override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) = - connectClocks(plugin) + override fun onPluginAttached(manager: PluginLifecycleManager) { + manager.loadPlugin() + } - override fun onPluginDisconnected(plugin: ClockProviderPlugin) = - disconnectClocks(plugin) + override fun onPluginLoaded( + plugin: ClockProviderPlugin, + pluginContext: Context, + manager: PluginLifecycleManager + ) { + var isClockListChanged = false + for (clock in plugin.getClocks()) { + val id = clock.clockId + var isNew = false + val info = + availableClocks.getOrPut(id) { + isNew = true + ClockInfo(clock, plugin, manager) + } + + if (isNew) { + isClockListChanged = true + onConnected(id) + } + + if (manager != info.manager) { + Log.e( + TAG, + "Clock Id conflict on load: $id is registered to another provider" + ) + continue + } + + info.provider = plugin + onLoaded(id) + } + + if (isClockListChanged) { + triggerOnAvailableClocksChanged() + } + verifyLoadedProviders() + } + + override fun onPluginUnloaded( + plugin: ClockProviderPlugin, + manager: PluginLifecycleManager + ) { + for (clock in plugin.getClocks()) { + val id = clock.clockId + val info = availableClocks[id] + if (info?.manager != manager) { + Log.e( + TAG, + "Clock Id conflict on unload: $id is registered to another provider" + ) + continue + } + info.provider = null + onUnloaded(id) + } + + verifyLoadedProviders() + } + + override fun onPluginDetached(manager: PluginLifecycleManager) { + val removed = mutableListOf() + availableClocks.entries.removeAll { + if (it.value.manager != manager) { + return@removeAll false + } + + removed.add(it.key) + return@removeAll true + } + + removed.forEach(::onDisconnected) + if (removed.size > 0) { + triggerOnAvailableClocksChanged() + } + } } private val userSwitchObserver = @@ -96,7 +174,8 @@ open class ClockRegistry( protected set(value) { if (field != value) { field = value - scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } } + verifyLoadedProviders() + triggerOnCurrentClockChanged() } } @@ -168,9 +247,36 @@ open class ClockRegistry( Assert.isNotMainThread() } - private fun onClockChanged(func: (ClockChangeListener) -> Unit) { - assertMainThread() - clockChangeListeners.forEach(func) + private var isClockChanged = AtomicBoolean(false) + private fun triggerOnCurrentClockChanged() { + val shouldSchedule = isClockChanged.compareAndSet(false, true) + if (!shouldSchedule) { + return + } + + android.util.Log.e("HAWK", "triggerOnCurrentClockChanged") + scope.launch(mainDispatcher) { + assertMainThread() + android.util.Log.e("HAWK", "isClockChanged") + isClockChanged.set(false) + clockChangeListeners.forEach { it.onCurrentClockChanged() } + } + } + + private var isClockListChanged = AtomicBoolean(false) + private fun triggerOnAvailableClocksChanged() { + val shouldSchedule = isClockListChanged.compareAndSet(false, true) + if (!shouldSchedule) { + return + } + + android.util.Log.e("HAWK", "triggerOnAvailableClocksChanged") + scope.launch(mainDispatcher) { + assertMainThread() + android.util.Log.e("HAWK", "isClockListChanged") + isClockListChanged.set(false) + clockChangeListeners.forEach { it.onAvailableClocksChanged() } + } } public fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) { @@ -190,7 +296,12 @@ open class ClockRegistry( } init { - connectClocks(defaultClockProvider) + // Register default clock designs + for (clock in defaultClockProvider.getClocks()) { + availableClocks[clock.clockId] = ClockInfo(clock, defaultClockProvider, null) + } + + // Something has gone terribly wrong if the default clock isn't present if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { throw IllegalArgumentException( "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" @@ -244,59 +355,87 @@ open class ClockRegistry( } } - private fun connectClocks(provider: ClockProvider) { - var isAvailableChanged = false - val currentId = currentClockId - for (clock in provider.getClocks()) { - val id = clock.clockId - val current = availableClocks[id] - if (current != null) { - Log.e( - TAG, - "Clock Id conflict: $id is registered by both " + - "${provider::class.simpleName} and ${current.provider::class.simpleName}" - ) - continue - } - - availableClocks[id] = ClockInfo(clock, provider) - isAvailableChanged = true - if (DEBUG) { - Log.i(TAG, "Added ${clock.clockId}") - } - - if (currentId == id) { - if (DEBUG) { - Log.i(TAG, "Current clock ($currentId) was connected") - } - onClockChanged { it.onCurrentClockChanged() } - } + private var isVerifying = AtomicBoolean(false) + private fun verifyLoadedProviders() { + val shouldSchedule = isVerifying.compareAndSet(false, true) + if (!shouldSchedule) { + return } - if (isAvailableChanged) { - onClockChanged { it.onAvailableClocksChanged() } + scope.launch(bgDispatcher) { + if (keepAllLoaded) { + // Enforce that all plugins are loaded if requested + for ((_, info) in availableClocks) { + info.manager?.loadPlugin() + } + isVerifying.set(false) + return@launch + } + + val currentClock = availableClocks[currentClockId] + if (currentClock == null) { + // Current Clock missing, load no plugins and use default + for ((_, info) in availableClocks) { + info.manager?.unloadPlugin() + } + isVerifying.set(false) + return@launch + } + + val currentManager = currentClock.manager + currentManager?.loadPlugin() + + for ((_, info) in availableClocks) { + val manager = info.manager + if (manager != null && manager.isLoaded && currentManager != manager) { + manager.unloadPlugin() + } + } + isVerifying.set(false) } } - private fun disconnectClocks(provider: ClockProvider) { - var isAvailableChanged = false - val currentId = currentClockId - for (clock in provider.getClocks()) { - availableClocks.remove(clock.clockId) - isAvailableChanged = true - - if (DEBUG) { - Log.i(TAG, "Removed ${clock.clockId}") - } - - if (currentId == clock.clockId) { - Log.w(TAG, "Current clock ($currentId) was disconnected") - onClockChanged { it.onCurrentClockChanged() } - } + private fun onConnected(clockId: ClockId) { + if (DEBUG) { + Log.i(TAG, "Connected $clockId") } - if (isAvailableChanged) { - onClockChanged { it.onAvailableClocksChanged() } + if (currentClockId == clockId) { + if (DEBUG) { + Log.i(TAG, "Current clock ($clockId) was connected") + } + } + } + + private fun onLoaded(clockId: ClockId) { + if (DEBUG) { + Log.i(TAG, "Loaded $clockId") + } + + if (currentClockId == clockId) { + Log.i(TAG, "Current clock ($clockId) was loaded") + triggerOnCurrentClockChanged() + } + } + + private fun onUnloaded(clockId: ClockId) { + if (DEBUG) { + Log.i(TAG, "Unloaded $clockId") + } + + if (currentClockId == clockId) { + Log.w(TAG, "Current clock ($clockId) was unloaded") + triggerOnCurrentClockChanged() + } + } + + private fun onDisconnected(clockId: ClockId) { + if (DEBUG) { + Log.i(TAG, "Disconnected $clockId") + } + + if (currentClockId == clockId) { + Log.w(TAG, "Current clock ($clockId) was disconnected") } } @@ -345,6 +484,7 @@ open class ClockRegistry( private data class ClockInfo( val metadata: ClockMetadata, - val provider: ClockProvider, + var provider: ClockProvider?, + val manager: PluginLifecycleManager?, ) } 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 new file mode 100644 index 0000000000000..cc6a46fa7d6bc --- /dev/null +++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginLifecycleManager.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.plugins; + +/** + * Provides the ability for consumers to control plugin lifecycle. + * + * @param is the target plugin type + */ +public interface PluginLifecycleManager { + /** Returns the currently loaded plugin instance (if plugin is loaded) */ + T getPlugin(); + + /** returns true if the plugin is currently loaded */ + default boolean isLoaded() { + return getPlugin() != null; + } + + /** + * Loads and creates the plugin instance if it does not exist. + * + * This will trigger {@link PluginListener#onPluginLoaded} with the new instance if it did not + * already exist. + */ + void loadPlugin(); + + /** + * Unloads and destroys the plugin instance if it exists. + * + * This will trigger {@link PluginListener#onPluginUnloaded} if a concrete plugin instance + * existed when this call was made. + */ + void unloadPlugin(); +} 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 b488d2a84baad..c5f503216101f 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 @@ -17,7 +17,32 @@ package com.android.systemui.plugins; import android.content.Context; /** - * Interface for listening to plugins being connected. + * Interface for listening to plugins being connected and disconnected. + * + * The call order for a plugin is + * 1) {@link #onPluginAttached} + * Called when a new plugin is added to the device, or an existing plugin was replaced by + * the package manager. Will only be called once per package manager event. If multiple + * non-conflicting packages which have the same plugin interface are installed on the + * device, then this method can be called multiple times with different instances of + * {@link PluginLifecycleManager} (as long as `allowMultiple` was set to true when the + * listener was registered with {@link PluginManager#addPluginListener}). + * 2) {@link #onPluginLoaded} + * Called whenever a new instance of the plugin object is created and ready for use. Can be + * called multiple times per {@link PluginLifecycleManager}, but will always pass a newly + * created plugin object. {@link #onPluginUnloaded} with the previous plugin object will + * be called before another call to {@link #onPluginLoaded} is made. This method will be + * called once automatically after {@link #onPluginAttached}. Besides the initial call, + * {@link #onPluginLoaded} will occur due to {@link PluginLifecycleManager#loadPlugin}. + * 3) {@link #onPluginUnloaded} + * Called when a request to unload the plugin has been received. This can be triggered from + * a related call to {@link PluginLifecycleManager#unloadPlugin} or for any reason that + * {@link #onPluginDetached} would be triggered. + * 4) {@link #onPluginDetached} + * Called when the package is removed from the device, disabled, or replaced due to an + * external trigger. These are events from the android package manager. + * + * @param is the target plugin type */ public interface PluginListener { /** @@ -25,14 +50,69 @@ public interface PluginListener { * 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. + * + * @deprecated Migrate to {@link #onPluginLoaded} or {@link #onPluginAttached} */ - void onPluginConnected(T plugin, Context pluginContext); + @Deprecated + default void onPluginConnected(T plugin, Context pluginContext) { + // Optional + } + + /** + * 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}. + */ + default void onPluginAttached(PluginLifecycleManager manager) { + // Optional + } /** * Called when a plugin has been uninstalled/updated and should be removed * from use. + * + * @deprecated Migrate to {@link #onPluginDetached} or {@link #onPluginUnloaded} */ + @Deprecated default void onPluginDisconnected(T plugin) { // Optional. } -} + + /** + * Called when the plugin has been detached from the host application. Implementers should no + * longer attempt to reload it via this {@link PluginLifecycleManager}. If the package was + * updated and not removed, then {@link #onPluginAttached} will be called again when the updated + * package is available. + */ + default void onPluginDetached(PluginLifecycleManager manager) { + // Optional. + } + + /** + * Called when the plugin is loaded into the host's process and is available for use. This can + * happen several times if clients are using {@link PluginLifecycleManager} to manipulate a + * plugin's load state. Each call to {@link #onPluginLoaded} will have a matched call to + * {@link #onPluginUnloaded} when that plugin object should no longer be used. + */ + default void onPluginLoaded( + T plugin, + Context pluginContext, + PluginLifecycleManager manager + ) { + // Optional, default to deprecated version + onPluginConnected(plugin, pluginContext); + } + + /** + * Called when the plugin should no longer be used. Listeners should clean up all references to + * the relevant plugin so that it can be garbage collected. If the plugin object is required in + * the future a call can be made to {@link PluginLifecycleManager#loadPlugin} to create a new + * plugin object and trigger {@link #onPluginLoaded}. + */ + default void onPluginUnloaded(T plugin, PluginLifecycleManager manager) { + // Optional, default to deprecated version + onPluginDisconnected(plugin); + } +} \ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java index cc3d7a8931b03..3d05542116e04 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java @@ -210,12 +210,12 @@ public class PluginActionManager { private void onPluginConnected(PluginInstance pluginInstance) { if (DEBUG) Log.d(TAG, "onPluginConnected"); PluginPrefs.setHasPlugins(mContext); - pluginInstance.onCreate(mContext, mListener); + pluginInstance.onCreate(); } private void onPluginDisconnected(PluginInstance pluginInstance) { if (DEBUG) Log.d(TAG, "onPluginDisconnected"); - pluginInstance.onDestroy(mListener); + pluginInstance.onDestroy(); } private void queryAll() { @@ -312,7 +312,7 @@ public class PluginActionManager { try { return mPluginInstanceFactory.create( mContext, appInfo, component, - mPluginClass); + mPluginClass, mListener); } catch (InvalidVersionException e) { reportInvalidVersion(component, component.getClassName(), e); } 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 2f84602089e0e..016d573930e8c 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 @@ -21,13 +21,16 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.PluginFragment; +import com.android.systemui.plugins.PluginLifecycleManager; import com.android.systemui.plugins.PluginListener; import dalvik.system.PathClassLoader; @@ -35,7 +38,7 @@ import dalvik.system.PathClassLoader; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.function.Supplier; /** * Contains a single instantiation of a Plugin. @@ -45,42 +48,102 @@ import java.util.Map; * * @param The type of plugin that this contains. */ -public class PluginInstance { +public class PluginInstance implements PluginLifecycleManager { private static final String TAG = "PluginInstance"; - private static final Map sClassLoaders = new ArrayMap<>(); - private final Context mPluginContext; - private final VersionInfo mVersionInfo; + private final Context mAppContext; + private final PluginListener mListener; private final ComponentName mComponentName; - private final T mPlugin; + private final PluginFactory mPluginFactory; + + private Context mPluginContext; + private T mPlugin; /** */ - public PluginInstance(ComponentName componentName, T plugin, Context pluginContext, - VersionInfo versionInfo) { + public PluginInstance( + Context appContext, + PluginListener listener, + ComponentName componentName, + PluginFactory pluginFactory, + @Nullable T plugin) { + mAppContext = appContext; + mListener = listener; mComponentName = componentName; + mPluginFactory = pluginFactory; mPlugin = plugin; - mPluginContext = pluginContext; - mVersionInfo = versionInfo; + + if (mPlugin != null) { + mPluginContext = mPluginFactory.createPluginContext(); + } } /** Alerts listener and plugin that the plugin has been created. */ - public void onCreate(Context appContext, PluginListener listener) { - 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(appContext, mPluginContext); + public void onCreate() { + mListener.onPluginAttached(this); + 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); } - listener.onPluginConnected(mPlugin, mPluginContext); } /** Alerts listener and plugin that the plugin is being shutdown. */ - public void onDestroy(PluginListener listener) { - listener.onPluginDisconnected(mPlugin); + public void onDestroy() { + unloadPlugin(); + mListener.onPluginDetached(this); + } + + /** Returns the current plugin instance (if it is loaded). */ + @Nullable + public T getPlugin() { + return mPlugin; + } + + /** + * Loads and creates the plugin if it does not exist. + */ + public void loadPlugin() { + if (mPlugin != null) { + return; + } + + mPlugin = mPluginFactory.createPlugin(); + mPluginContext = mPluginFactory.createPluginContext(); + if (mPlugin == null || mPluginContext == null) { + return; + } + + 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); + } + + /** + * Unloads and destroys the current plugin instance if it exists. + * + * This will free the associated memory if there are not other references. + */ + public void unloadPlugin() { + if (mPlugin == null) { + return; + } + + mListener.onPluginUnloaded(mPlugin, this); if (!(mPlugin instanceof PluginFragment)) { // Only call onDestroy for plugins that aren't fragments, as fragments // will get the onDestroy as part of the fragment lifecycle. mPlugin.onDestroy(); } + mPlugin = null; + mPluginContext = null; } /** @@ -89,7 +152,7 @@ public class PluginInstance { * It does this by string comparison of the class names. **/ public boolean containsPluginClass(Class pluginClass) { - return mPlugin.getClass().getName().equals(pluginClass.getName()); + return mComponentName.getClassName().equals(pluginClass.getName()); } public ComponentName getComponentName() { @@ -101,7 +164,7 @@ public class PluginInstance { } public VersionInfo getVersionInfo() { - return mVersionInfo; + return mPluginFactory.checkVersion(mPlugin); } @VisibleForTesting @@ -134,21 +197,20 @@ public class PluginInstance { Context context, ApplicationInfo appInfo, ComponentName componentName, - Class pluginClass) + Class pluginClass, + PluginListener listener) throws PackageManager.NameNotFoundException, ClassNotFoundException, InstantiationException, IllegalAccessException { - ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader); - Context pluginContext = new PluginActionManager.PluginContextWrapper( - context.createApplicationContext(appInfo, 0), classLoader); - Class instanceClass = (Class) Class.forName( - componentName.getClassName(), true, classLoader); + 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 = (T) mInstanceFactory.create(instanceClass); - VersionInfo version = mVersionChecker.checkVersion( - instanceClass, pluginClass, instance); - return new PluginInstance(componentName, instance, pluginContext, version); + T instance = pluginFactory.createPlugin(); + pluginFactory.checkVersion(instance); + return new PluginInstance( + context, listener, componentName, pluginFactory, instance); } private boolean isPluginPackagePrivileged(String packageName) { @@ -179,9 +241,6 @@ public class PluginInstance { + appInfo.sourceDir + ", pkg: " + appInfo.packageName); return null; } - if (sClassLoaders.containsKey(appInfo.packageName)) { - return sClassLoaders.get(appInfo.packageName); - } List zipPaths = new ArrayList<>(); List libPaths = new ArrayList<>(); @@ -190,13 +249,20 @@ public class PluginInstance { TextUtils.join(File.pathSeparator, zipPaths), TextUtils.join(File.pathSeparator, libPaths), getParentClassLoader(baseClassLoader)); - sClassLoaders.put(appInfo.packageName, classLoader); return classLoader; } } /** Class that compares a plugin class against an implementation for version matching. */ - public static class VersionChecker { + public interface VersionChecker { + /** Compares two plugin classes. */ + VersionInfo checkVersion( + Class instanceClass, Class pluginClass, Plugin plugin); + } + + /** Class that compares a plugin class against an implementation for version matching. */ + public static class VersionCheckerImpl implements VersionChecker { + @Override /** Compares two plugin classes. */ public VersionInfo checkVersion( Class instanceClass, Class pluginClass, Plugin plugin) { @@ -204,7 +270,7 @@ public class PluginInstance { VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass); if (instanceVersion.hasVersionInfo()) { pluginVersion.checkVersion(instanceVersion); - } else { + } else if (plugin != null) { int fallbackVersion = plugin.getVersion(); if (fallbackVersion != pluginVersion.getDefaultVersion()) { throw new VersionInfo.InvalidVersionException("Invalid legacy version", false); @@ -225,4 +291,74 @@ public class PluginInstance { return (T) cls.newInstance(); } } + + /** + * Instanced wrapper of InstanceFactory + * + * @param is the type of the plugin object to be built + **/ + public static class PluginFactory { + private final Context mContext; + private final InstanceFactory mInstanceFactory; + private final ApplicationInfo mAppInfo; + private final ComponentName mComponentName; + private final VersionChecker mVersionChecker; + private final Class mPluginClass; + private final Supplier mClassLoaderFactory; + + public PluginFactory( + Context context, + InstanceFactory instanceFactory, + ApplicationInfo appInfo, + ComponentName componentName, + VersionChecker versionChecker, + Class pluginClass, + Supplier classLoaderFactory) { + mContext = context; + mInstanceFactory = instanceFactory; + mAppInfo = appInfo; + mComponentName = componentName; + mVersionChecker = versionChecker; + mPluginClass = pluginClass; + mClassLoaderFactory = classLoaderFactory; + } + + /** Creates the related plugin object from the factory */ + public T createPlugin() { + try { + ClassLoader loader = mClassLoaderFactory.get(); + Class instanceClass = (Class) Class.forName( + mComponentName.getClassName(), true, loader); + return (T) mInstanceFactory.create(instanceClass); + } catch (ClassNotFoundException ex) { + Log.e(TAG, "Failed to load plugin", ex); + } catch (IllegalAccessException ex) { + Log.e(TAG, "Failed to load plugin", ex); + } catch (InstantiationException ex) { + Log.e(TAG, "Failed to load plugin", ex); + } + return null; + } + + /** Creates a context wrapper for the plugin */ + public Context createPluginContext() { + try { + ClassLoader loader = mClassLoaderFactory.get(); + return new PluginActionManager.PluginContextWrapper( + mContext.createApplicationContext(mAppInfo, 0), loader); + } catch (NameNotFoundException ex) { + Log.e(TAG, "Failed to create plugin context", ex); + } + return null; + } + + /** Check Version and create VersionInfo for instance */ + public VersionInfo checkVersion(T instance) { + if (instance == null) { + instance = createPlugin(); + } + return mVersionChecker.checkVersion( + (Class) instance.getClass(), mPluginClass, instance); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index b1a83fbda7de6..6e98a1805d62b 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -60,7 +60,9 @@ public abstract class ClockRegistryModule { featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), /* handleAllUsers= */ true, new DefaultClockProvider(context, layoutInflater, resources), - context.getString(R.string.lockscreen_clock_id_fallback)); + context.getString(R.string.lockscreen_clock_id_fallback), + /* keepAllLoaded = */ false, + /* subTag = */ "System"); registry.registerListeners(); return registry; } diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java index 95f14190537fc..fbf1a0e46ce3c 100644 --- a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java @@ -73,7 +73,7 @@ public abstract class PluginsModule { return new PluginInstance.Factory( PluginModule.class.getClassLoader(), new PluginInstance.InstanceFactory<>(), - new PluginInstance.VersionChecker(), + new PluginInstance.VersionCheckerImpl(), privilegedPlugins, isDebug); } 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 1fdb3647fcb21..374aae1f29483 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 @@ -27,13 +27,16 @@ import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.ClockSettings import com.android.systemui.plugins.PluginListener +import com.android.systemui.plugins.PluginLifecycleManager import com.android.systemui.plugins.PluginManager import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals import junit.framework.Assert.fail import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Rule @@ -49,6 +52,7 @@ import org.mockito.junit.MockitoJUnit class ClockRegistryTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() + private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var scope: TestScope @@ -58,37 +62,38 @@ class ClockRegistryTest : SysuiTestCase() { @Mock private lateinit var mockDefaultClock: ClockController @Mock private lateinit var mockThumbnail: Drawable @Mock private lateinit var mockContentResolver: ContentResolver + @Mock private lateinit var mockPluginLifecycle: PluginLifecycleManager private lateinit var fakeDefaultProvider: FakeClockPlugin private lateinit var pluginListener: PluginListener private lateinit var registry: ClockRegistry companion object { - private fun failFactory(): ClockController { - fail("Unexpected call to createClock") + private fun failFactory(clockId: ClockId): ClockController { + fail("Unexpected call to createClock: $clockId") return null!! } - private fun failThumbnail(): Drawable? { - fail("Unexpected call to getThumbnail") + private fun failThumbnail(clockId: ClockId): Drawable? { + fail("Unexpected call to getThumbnail: $clockId") return null } } private class FakeClockPlugin : ClockProviderPlugin { private val metadata = mutableListOf() - private val createCallbacks = mutableMapOf ClockController>() - private val thumbnailCallbacks = mutableMapOf Drawable?>() + private val createCallbacks = mutableMapOf ClockController>() + private val thumbnailCallbacks = mutableMapOf Drawable?>() override fun getClocks() = metadata override fun createClock(settings: ClockSettings): ClockController = - createCallbacks[settings.clockId!!]!!() - override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!() + createCallbacks[settings.clockId!!]!!(settings.clockId!!) + override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!(id) fun addClock( id: ClockId, name: String, - create: () -> ClockController = ::failFactory, - getThumbnail: () -> Drawable? = ::failThumbnail + create: (ClockId) -> ClockController = ::failFactory, + getThumbnail: (ClockId) -> Drawable? = ::failThumbnail ): FakeClockPlugin { metadata.add(ClockMetadata(id, name)) createCallbacks[id] = create @@ -99,7 +104,8 @@ class ClockRegistryTest : SysuiTestCase() { @Before fun setUp() { - dispatcher = StandardTestDispatcher() + scheduler = TestCoroutineScheduler() + dispatcher = StandardTestDispatcher(scheduler) scope = TestScope(dispatcher) fakeDefaultProvider = FakeClockPlugin() @@ -116,6 +122,8 @@ class ClockRegistryTest : SysuiTestCase() { isEnabled = true, handleAllUsers = true, defaultClockProvider = fakeDefaultProvider, + keepAllLoaded = true, + subTag = "Test", ) { override fun querySettings() { } override fun applySettings(value: ClockSettings?) { @@ -142,8 +150,8 @@ class ClockRegistryTest : SysuiTestCase() { .addClock("clock_3", "clock 3") .addClock("clock_4", "clock 4") - pluginListener.onPluginConnected(plugin1, mockContext) - pluginListener.onPluginConnected(plugin2, mockContext) + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle) + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) val list = registry.getClocks() assertEquals( list, @@ -165,16 +173,18 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun clockIdConflict_ErrorWithoutCrash() { + val mockPluginLifecycle1 = mock>() val plugin1 = FakeClockPlugin() .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail }) .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail }) + val mockPluginLifecycle2 = mock>() val plugin2 = FakeClockPlugin() .addClock("clock_1", "clock 1") .addClock("clock_2", "clock 2") - pluginListener.onPluginConnected(plugin1, mockContext) - pluginListener.onPluginConnected(plugin2, mockContext) + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1) + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2) val list = registry.getClocks() assertEquals( list, @@ -202,8 +212,8 @@ class ClockRegistryTest : SysuiTestCase() { .addClock("clock_4", "clock 4") registry.applySettings(ClockSettings("clock_3", null)) - pluginListener.onPluginConnected(plugin1, mockContext) - pluginListener.onPluginConnected(plugin2, mockContext) + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle) + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) val clock = registry.createCurrentClock() assertEquals(mockClock, clock) @@ -220,9 +230,9 @@ class ClockRegistryTest : SysuiTestCase() { .addClock("clock_4", "clock 4") registry.applySettings(ClockSettings("clock_3", null)) - pluginListener.onPluginConnected(plugin1, mockContext) - pluginListener.onPluginConnected(plugin2, mockContext) - pluginListener.onPluginDisconnected(plugin2) + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle) + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) + pluginListener.onPluginUnloaded(plugin2, mockPluginLifecycle) val clock = registry.createCurrentClock() assertEquals(clock, mockDefaultClock) @@ -230,15 +240,16 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun pluginRemoved_clockAndListChanged() { + val mockPluginLifecycle1 = mock>() val plugin1 = FakeClockPlugin() .addClock("clock_1", "clock 1") .addClock("clock_2", "clock 2") + val mockPluginLifecycle2 = mock>() val plugin2 = FakeClockPlugin() .addClock("clock_3", "clock 3", { mockClock }) .addClock("clock_4", "clock 4") - var changeCallCount = 0 var listChangeCallCount = 0 registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { @@ -247,23 +258,38 @@ class ClockRegistryTest : SysuiTestCase() { }) registry.applySettings(ClockSettings("clock_3", null)) - assertEquals(0, changeCallCount) + scheduler.runCurrent() + assertEquals(1, changeCallCount) assertEquals(0, listChangeCallCount) - pluginListener.onPluginConnected(plugin1, mockContext) - assertEquals(0, changeCallCount) + pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1) + scheduler.runCurrent() + assertEquals(1, changeCallCount) assertEquals(1, listChangeCallCount) - pluginListener.onPluginConnected(plugin2, mockContext) - assertEquals(1, changeCallCount) + pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2) + scheduler.runCurrent() + assertEquals(2, changeCallCount) assertEquals(2, listChangeCallCount) - pluginListener.onPluginDisconnected(plugin1) - assertEquals(1, changeCallCount) + pluginListener.onPluginUnloaded(plugin1, mockPluginLifecycle1) + scheduler.runCurrent() + assertEquals(2, changeCallCount) + assertEquals(2, listChangeCallCount) + + pluginListener.onPluginUnloaded(plugin2, mockPluginLifecycle2) + scheduler.runCurrent() + assertEquals(3, changeCallCount) + assertEquals(2, listChangeCallCount) + + pluginListener.onPluginDetached(mockPluginLifecycle1) + scheduler.runCurrent() + assertEquals(3, changeCallCount) assertEquals(3, listChangeCallCount) - pluginListener.onPluginDisconnected(plugin2) - assertEquals(2, changeCallCount) + pluginListener.onPluginDetached(mockPluginLifecycle2) + scheduler.runCurrent() + assertEquals(3, changeCallCount) assertEquals(4, listChangeCallCount) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java index 05280fa826ed7..c39b29fb44353 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java @@ -79,11 +79,11 @@ public class PluginActionManagerTest extends SysuiTestCase { private PluginInstance mPluginInstance; private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory( this.getClass().getClassLoader(), - new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionChecker(), + new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionCheckerImpl(), Collections.emptyList(), false) { @Override public PluginInstance create(Context context, ApplicationInfo appInfo, - ComponentName componentName, Class pluginClass) { + ComponentName componentName, Class pluginClass, PluginListener listener) { return (PluginInstance) mPluginInstance; } }; @@ -128,7 +128,7 @@ public class PluginActionManagerTest extends SysuiTestCase { createPlugin(); // Verify startup lifecycle - verify(mPluginInstance).onCreate(mContext, mMockListener); + verify(mPluginInstance).onCreate(); } @Test @@ -140,7 +140,7 @@ public class PluginActionManagerTest extends SysuiTestCase { mFakeExecutor.runAllReady(); // Verify shutdown lifecycle - verify(mPluginInstance).onDestroy(mMockListener); + verify(mPluginInstance).onDestroy(); } @Test @@ -152,9 +152,9 @@ public class PluginActionManagerTest extends SysuiTestCase { mFakeExecutor.runAllReady(); // Verify the old one was destroyed. - verify(mPluginInstance).onDestroy(mMockListener); + verify(mPluginInstance).onDestroy(); verify(mPluginInstance, Mockito.times(2)) - .onCreate(mContext, mMockListener); + .onCreate(); } @Test @@ -188,7 +188,7 @@ public class PluginActionManagerTest extends SysuiTestCase { mFakeExecutor.runAllReady(); // Verify startup lifecycle - verify(mPluginInstance).onCreate(mContext, mMockListener); + verify(mPluginInstance).onCreate(); } @Test 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 bb9a1e971fd0f..d5e904c636d5a 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 @@ -16,11 +16,9 @@ package com.android.systemui.shared.plugins; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static junit.framework.Assert.assertNull; import android.content.ComponentName; import android.content.Context; @@ -31,6 +29,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginLifecycleManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.annotations.Requires; @@ -38,46 +37,64 @@ import com.android.systemui.plugins.annotations.Requires; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import java.lang.ref.WeakReference; import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; @SmallTest @RunWith(AndroidJUnit4.class) public class PluginInstanceTest extends SysuiTestCase { private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins"; - - @Mock - private TestPluginImpl mMockPlugin; - @Mock - private PluginListener mMockListener; - @Mock - private VersionInfo mVersionInfo; - ComponentName mTestPluginComponentName = + private static final ComponentName TEST_PLUGIN_COMPONENT_NAME = new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName()); + + private FakeListener mPluginListener; + private VersionInfo mVersionInfo; + private VersionInfo.InvalidVersionException mVersionException; + private PluginInstance.VersionChecker mVersionChecker; + + private RefCounter mCounter; private PluginInstance mPluginInstance; private PluginInstance.Factory mPluginInstanceFactory; - private ApplicationInfo mAppInfo; - private Context mPluginContext; - @Mock - private PluginInstance.VersionChecker mVersionChecker; + + // Because we're testing memory in this file, we must be careful not to assert the target + // objects, or capture them via mockito if we expect the garbage collector to later free them. + // Both JUnit and Mockito will save references and prevent these objects from being cleaned up. + private WeakReference mPlugin; + private WeakReference mPluginContext; @Before public void setup() throws Exception { - MockitoAnnotations.initMocks(this); + mCounter = new RefCounter(); mAppInfo = mContext.getApplicationInfo(); - mAppInfo.packageName = mTestPluginComponentName.getPackageName(); - when(mVersionChecker.checkVersion(any(), any(), any())).thenReturn(mVersionInfo); + mAppInfo.packageName = TEST_PLUGIN_COMPONENT_NAME.getPackageName(); + mPluginListener = new FakeListener(); + mVersionInfo = new VersionInfo(); + mVersionChecker = new PluginInstance.VersionChecker() { + @Override + public VersionInfo checkVersion( + Class instanceClass, + Class pluginClass, + Plugin plugin + ) { + if (mVersionException != null) { + throw mVersionException; + } + return mVersionInfo; + } + }; mPluginInstanceFactory = new PluginInstance.Factory( this.getClass().getClassLoader(), new PluginInstance.InstanceFactory() { @Override TestPlugin create(Class cls) { - return mMockPlugin; + TestPluginImpl plugin = new TestPluginImpl(mCounter); + mPlugin = new WeakReference<>(plugin); + return plugin; } }, mVersionChecker, @@ -85,8 +102,9 @@ public class PluginInstanceTest extends SysuiTestCase { false); mPluginInstance = mPluginInstanceFactory.create( - mContext, mAppInfo, mTestPluginComponentName, TestPlugin.class); - mPluginContext = mPluginInstance.getPluginContext(); + mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME, + TestPlugin.class, mPluginListener); + mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext()); } @Test @@ -96,29 +114,51 @@ public class PluginInstanceTest extends SysuiTestCase { @Test(expected = VersionInfo.InvalidVersionException.class) public void testIncorrectVersion() throws Exception { - ComponentName wrongVersionTestPluginComponentName = new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName()); - when(mVersionChecker.checkVersion(any(), any(), any())).thenThrow( - new VersionInfo.InvalidVersionException("test", true)); + mVersionException = new VersionInfo.InvalidVersionException("test", true); mPluginInstanceFactory.create( - mContext, mAppInfo, wrongVersionTestPluginComponentName, TestPlugin.class); + mContext, mAppInfo, wrongVersionTestPluginComponentName, + TestPlugin.class, mPluginListener); } @Test public void testOnCreate() { - mPluginInstance.onCreate(mContext, mMockListener); - verify(mMockPlugin).onCreate(mContext, mPluginContext); - verify(mMockListener).onPluginConnected(mMockPlugin, mPluginContext); + mPluginInstance.onCreate(); + assertEquals(1, mPluginListener.mAttachedCount); + assertEquals(1, mPluginListener.mLoadCount); + assertEquals(mPlugin.get(), mPluginInstance.getPlugin()); + assertInstances(1, 1); } @Test public void testOnDestroy() { - mPluginInstance.onDestroy(mMockListener); - verify(mMockListener).onPluginDisconnected(mMockPlugin); - verify(mMockPlugin).onDestroy(); + mPluginInstance.onDestroy(); + assertEquals(1, mPluginListener.mDetachedCount); + assertEquals(1, mPluginListener.mUnloadCount); + assertNull(mPluginInstance.getPlugin()); + assertInstances(0, -1); // Destroyed but never created + } + + @Test + public void testOnRepeatedlyLoadUnload_PluginFreed() { + mPluginInstance.onCreate(); + mPluginInstance.loadPlugin(); + assertInstances(1, 1); + + mPluginInstance.unloadPlugin(); + assertNull(mPluginInstance.getPlugin()); + assertInstances(0, 0); + + mPluginInstance.loadPlugin(); + assertInstances(1, 1); + + mPluginInstance.unloadPlugin(); + mPluginInstance.onDestroy(); + assertNull(mPluginInstance.getPlugin()); + assertInstances(0, 0); } // This target class doesn't matter, it just needs to have a Requires to hit the flow where @@ -129,10 +169,103 @@ public class PluginInstanceTest extends SysuiTestCase { String ACTION = "testAction"; } + public void assertInstances(Integer allocated, Integer created) { + // Run the garbage collector to finalize and deallocate outstanding + // instances. Since the GC doesn't always appear to want to run + // completely when we ask, we ask it 10 times in a short loop. + for (int i = 0; i < 10; i++) { + System.runFinalization(); + System.gc(); + } + + mCounter.assertInstances(allocated, created); + } + + public static class RefCounter { + public final AtomicInteger mAllocatedInstances = new AtomicInteger(); + public final AtomicInteger mCreatedInstances = new AtomicInteger(); + + public void assertInstances(Integer allocated, Integer created) { + if (allocated != null) { + assertEquals(allocated.intValue(), mAllocatedInstances.get()); + } + if (created != null) { + assertEquals(created.intValue(), mCreatedInstances.get()); + } + } + } + @Requires(target = TestPlugin.class, version = TestPlugin.VERSION) public static class TestPluginImpl implements TestPlugin { + public final RefCounter mCounter; + public TestPluginImpl(RefCounter counter) { + mCounter = counter; + mCounter.mAllocatedInstances.getAndIncrement(); + } + + @Override + public void finalize() { + mCounter.mAllocatedInstances.getAndDecrement(); + } + @Override public void onCreate(Context sysuiContext, Context pluginContext) { + mCounter.mCreatedInstances.getAndIncrement(); + } + + @Override + public void onDestroy() { + mCounter.mCreatedInstances.getAndDecrement(); + } + } + + public class FakeListener implements PluginListener { + public int mAttachedCount = 0; + public int mDetachedCount = 0; + public int mLoadCount = 0; + public int mUnloadCount = 0; + + @Override + public void onPluginAttached(PluginLifecycleManager manager) { + mAttachedCount++; + assertEquals(PluginInstanceTest.this.mPluginInstance, manager); + } + + @Override + public void onPluginDetached(PluginLifecycleManager manager) { + mDetachedCount++; + assertEquals(PluginInstanceTest.this.mPluginInstance, manager); + } + + @Override + public void onPluginLoaded( + TestPlugin plugin, + Context pluginContext, + PluginLifecycleManager manager + ) { + mLoadCount++; + TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get(); + if (expectedPlugin != null) { + assertEquals(expectedPlugin, plugin); + } + Context expectedContext = PluginInstanceTest.this.mPluginContext.get(); + if (expectedContext != null) { + assertEquals(expectedContext, pluginContext); + } + assertEquals(PluginInstanceTest.this.mPluginInstance, manager); + } + + @Override + public void onPluginUnloaded( + TestPlugin plugin, + PluginLifecycleManager manager + ) { + mUnloadCount++; + TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get(); + if (expectedPlugin != null) { + assertEquals(expectedPlugin, plugin); + } + assertEquals(PluginInstanceTest.this.mPluginInstance, manager); } } }