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
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 00c0a0b..e73afe7 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.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 @@
     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 @@
 
     private val pluginListener =
         object : PluginListener<ClockProviderPlugin> {
-            override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
-                connectClocks(plugin)
+            override fun onPluginAttached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
+                manager.loadPlugin()
+            }
 
-            override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
-                disconnectClocks(plugin)
+            override fun onPluginLoaded(
+                plugin: ClockProviderPlugin,
+                pluginContext: Context,
+                manager: PluginLifecycleManager<ClockProviderPlugin>
+            ) {
+                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<ClockProviderPlugin>
+            ) {
+                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<ClockProviderPlugin>) {
+                val removed = mutableListOf<ClockId>()
+                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 @@
         protected set(value) {
             if (field != value) {
                 field = value
-                scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } }
+                verifyLoadedProviders()
+                triggerOnCurrentClockChanged()
             }
         }
 
@@ -168,9 +247,36 @@
         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 @@
         }
 
     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 @@
         }
     }
 
-    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 @@
 
     private data class ClockInfo(
         val metadata: ClockMetadata,
-        val provider: ClockProvider,
+        var provider: ClockProvider?,
+        val manager: PluginLifecycleManager<ClockProviderPlugin>?,
     )
 }
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 0000000..cc6a46f
--- /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 <T> is the target plugin type
+ */
+public interface PluginLifecycleManager<T extends Plugin> {
+    /** 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 b488d2a..c5f5032 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 @@
 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 <T> is the target plugin type
  */
 public interface PluginListener<T extends Plugin> {
     /**
@@ -25,14 +50,69 @@
      * 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<T> 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<T> 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<T> 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<T> 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 cc3d7a8..3d05542 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 @@
     private void onPluginConnected(PluginInstance<T> pluginInstance) {
         if (DEBUG) Log.d(TAG, "onPluginConnected");
         PluginPrefs.setHasPlugins(mContext);
-        pluginInstance.onCreate(mContext, mListener);
+        pluginInstance.onCreate();
     }
 
     private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
         if (DEBUG) Log.d(TAG, "onPluginDisconnected");
-        pluginInstance.onDestroy(mListener);
+        pluginInstance.onDestroy();
     }
 
     private void queryAll() {
@@ -312,7 +312,7 @@
             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 2f84602..016d573 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.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 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 @@
  *
  * @param <T> The type of plugin that this contains.
  */
-public class PluginInstance<T extends Plugin> {
+public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
     private static final String TAG = "PluginInstance";
-    private static final Map<String, ClassLoader> sClassLoaders = new ArrayMap<>();
 
-    private final Context mPluginContext;
-    private final VersionInfo mVersionInfo;
+    private final Context mAppContext;
+    private final PluginListener<T> mListener;
     private final ComponentName mComponentName;
-    private final T mPlugin;
+    private final PluginFactory<T> mPluginFactory;
+
+    private Context mPluginContext;
+    private T mPlugin;
 
     /** */
-    public PluginInstance(ComponentName componentName, T plugin, Context pluginContext,
-            VersionInfo versionInfo) {
+    public PluginInstance(
+            Context appContext,
+            PluginListener<T> listener,
+            ComponentName componentName,
+            PluginFactory<T> 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<T> 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<T> 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 @@
      * 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 VersionInfo getVersionInfo() {
-        return mVersionInfo;
+        return mPluginFactory.checkVersion(mPlugin);
     }
 
     @VisibleForTesting
@@ -134,21 +197,20 @@
                 Context context,
                 ApplicationInfo appInfo,
                 ComponentName componentName,
-                Class<T> pluginClass)
+                Class<T> pluginClass,
+                PluginListener<T> listener)
                 throws PackageManager.NameNotFoundException, ClassNotFoundException,
                 InstantiationException, IllegalAccessException {
 
-            ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader);
-            Context pluginContext = new PluginActionManager.PluginContextWrapper(
-                    context.createApplicationContext(appInfo, 0), classLoader);
-            Class<T> instanceClass = (Class<T>) Class.forName(
-                    componentName.getClassName(), true, classLoader);
+            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 = (T) mInstanceFactory.create(instanceClass);
-            VersionInfo version = mVersionChecker.checkVersion(
-                    instanceClass, pluginClass, instance);
-            return new PluginInstance<T>(componentName, instance, pluginContext, version);
+            T instance = pluginFactory.createPlugin();
+            pluginFactory.checkVersion(instance);
+            return new PluginInstance<T>(
+                    context, listener, componentName, pluginFactory, instance);
         }
 
         private boolean isPluginPackagePrivileged(String packageName) {
@@ -179,9 +241,6 @@
                         + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
                 return null;
             }
-            if (sClassLoaders.containsKey(appInfo.packageName)) {
-                return sClassLoaders.get(appInfo.packageName);
-            }
 
             List<String> zipPaths = new ArrayList<>();
             List<String> libPaths = new ArrayList<>();
@@ -190,13 +249,20 @@
                     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. */
+        <T extends Plugin> VersionInfo checkVersion(
+                Class<T> instanceClass, Class<T> 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 <T extends Plugin> VersionInfo checkVersion(
                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
@@ -204,7 +270,7 @@
             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 @@
             return (T) cls.newInstance();
         }
     }
+
+    /**
+     * Instanced wrapper of InstanceFactory
+     *
+     * @param <T> is the type of the plugin object to be built
+     **/
+    public static class PluginFactory<T extends Plugin> {
+        private final Context mContext;
+        private final InstanceFactory<?> mInstanceFactory;
+        private final ApplicationInfo mAppInfo;
+        private final ComponentName mComponentName;
+        private final VersionChecker mVersionChecker;
+        private final Class<T> mPluginClass;
+        private final Supplier<ClassLoader> mClassLoaderFactory;
+
+        public PluginFactory(
+                Context context,
+                InstanceFactory<?> instanceFactory,
+                ApplicationInfo appInfo,
+                ComponentName componentName,
+                VersionChecker versionChecker,
+                Class<T> pluginClass,
+                Supplier<ClassLoader> 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<T> instanceClass = (Class<T>) 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<T>) 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 b1a83fb..6e98a18 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 @@
                 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 95f1419..fbf1a0e 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 @@
         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 1fdb364..374aae1 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.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 @@
 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 @@
     @Mock private lateinit var mockDefaultClock: ClockController
     @Mock private lateinit var mockThumbnail: Drawable
     @Mock private lateinit var mockContentResolver: ContentResolver
+    @Mock private lateinit var mockPluginLifecycle: PluginLifecycleManager<ClockProviderPlugin>
     private lateinit var fakeDefaultProvider: FakeClockPlugin
     private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
     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<ClockMetadata>()
-        private val createCallbacks = mutableMapOf<ClockId, () -> ClockController>()
-        private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>()
+        private val createCallbacks = mutableMapOf<ClockId, (ClockId) -> ClockController>()
+        private val thumbnailCallbacks = mutableMapOf<ClockId, (ClockId) -> 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 @@
 
     @Before
     fun setUp() {
-        dispatcher = StandardTestDispatcher()
+        scheduler = TestCoroutineScheduler()
+        dispatcher = StandardTestDispatcher(scheduler)
         scope = TestScope(dispatcher)
 
         fakeDefaultProvider = FakeClockPlugin()
@@ -116,6 +122,8 @@
             isEnabled = true,
             handleAllUsers = true,
             defaultClockProvider = fakeDefaultProvider,
+            keepAllLoaded = true,
+            subTag = "Test",
         ) {
             override fun querySettings() { }
             override fun applySettings(value: ClockSettings?) {
@@ -142,8 +150,8 @@
             .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 @@
 
     @Test
     fun clockIdConflict_ErrorWithoutCrash() {
+        val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
         val plugin1 = FakeClockPlugin()
             .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
             .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
 
+        val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
         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 @@
             .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 @@
             .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 @@
 
     @Test
     fun pluginRemoved_clockAndListChanged() {
+        val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
         val plugin1 = FakeClockPlugin()
             .addClock("clock_1", "clock 1")
             .addClock("clock_2", "clock 2")
 
+        val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
         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 @@
         })
 
         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 05280fa..c39b29f 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 @@
     private PluginInstance<TestPlugin> 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 <T extends Plugin> PluginInstance<T> create(Context context, ApplicationInfo appInfo,
-                ComponentName componentName, Class<T> pluginClass) {
+                ComponentName componentName, Class<T> pluginClass, PluginListener<T> listener) {
             return (PluginInstance<T>) mPluginInstance;
         }
     };
@@ -128,7 +128,7 @@
         createPlugin();
 
         // Verify startup lifecycle
-        verify(mPluginInstance).onCreate(mContext, mMockListener);
+        verify(mPluginInstance).onCreate();
     }
 
     @Test
@@ -140,7 +140,7 @@
         mFakeExecutor.runAllReady();
 
         // Verify shutdown lifecycle
-        verify(mPluginInstance).onDestroy(mMockListener);
+        verify(mPluginInstance).onDestroy();
     }
 
     @Test
@@ -152,9 +152,9 @@
         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 @@
         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 bb9a1e9..d5e904c 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 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 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<TestPlugin> 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<TestPlugin> 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<TestPluginImpl> mPlugin;
+    private WeakReference<Context> 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 <T extends Plugin> VersionInfo checkVersion(
+                    Class<T> instanceClass,
+                    Class<T> pluginClass,
+                    Plugin plugin
+            ) {
+                if (mVersionException != null) {
+                    throw mVersionException;
+                }
+                return mVersionInfo;
+            }
+        };
 
         mPluginInstanceFactory = new PluginInstance.Factory(
                 this.getClass().getClassLoader(),
                 new PluginInstance.InstanceFactory<TestPlugin>() {
                     @Override
                     TestPlugin create(Class cls) {
-                        return mMockPlugin;
+                        TestPluginImpl plugin = new TestPluginImpl(mCounter);
+                        mPlugin = new WeakReference<>(plugin);
+                        return plugin;
                     }
                 },
                 mVersionChecker,
@@ -85,8 +102,9 @@
                 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 @@
 
     @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 @@
         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<TestPlugin> {
+        public int mAttachedCount = 0;
+        public int mDetachedCount = 0;
+        public int mLoadCount = 0;
+        public int mUnloadCount = 0;
+
+        @Override
+        public void onPluginAttached(PluginLifecycleManager<TestPlugin> manager) {
+            mAttachedCount++;
+            assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
+        }
+
+        @Override
+        public void onPluginDetached(PluginLifecycleManager<TestPlugin> manager) {
+            mDetachedCount++;
+            assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
+        }
+
+        @Override
+        public void onPluginLoaded(
+                TestPlugin plugin,
+                Context pluginContext,
+                PluginLifecycleManager<TestPlugin> 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<TestPlugin> manager
+        ) {
+            mUnloadCount++;
+            TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get();
+            if (expectedPlugin != null) {
+                assertEquals(expectedPlugin, plugin);
+            }
+            assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
         }
     }
 }