Merge "Provide mechanism for on demand loading/unloading of connected plugins" into udc-dev

This commit is contained in:
Hawkwood Glazier
2023-03-16 14:11:50 +00:00
committed by Android (Google) Code Review
10 changed files with 738 additions and 173 deletions

View File

@@ -29,14 +29,15 @@ import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProvider import com.android.systemui.plugins.ClockProvider
import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.ClockSettings import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginLifecycleManager
import com.android.systemui.plugins.PluginListener import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.Assert import com.android.systemui.util.Assert
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
private val TAG = ClockRegistry::class.simpleName!!
private const val DEBUG = true private const val DEBUG = true
private val KEY_TIMESTAMP = "appliedTimestamp" private val KEY_TIMESTAMP = "appliedTimestamp"
@@ -51,7 +52,10 @@ open class ClockRegistry(
val handleAllUsers: Boolean, val handleAllUsers: Boolean,
defaultClockProvider: ClockProvider, defaultClockProvider: ClockProvider,
val fallbackClockId: ClockId = DEFAULT_CLOCK_ID, val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
val keepAllLoaded: Boolean,
val subTag: String,
) { ) {
private val TAG = "${ClockRegistry::class.simpleName} ($subTag)"
interface ClockChangeListener { interface ClockChangeListener {
// Called when the active clock changes // Called when the active clock changes
fun onCurrentClockChanged() {} fun onCurrentClockChanged() {}
@@ -76,11 +80,85 @@ open class ClockRegistry(
private val pluginListener = private val pluginListener =
object : PluginListener<ClockProviderPlugin> { object : PluginListener<ClockProviderPlugin> {
override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) = override fun onPluginAttached(manager: PluginLifecycleManager<ClockProviderPlugin>) {
connectClocks(plugin) manager.loadPlugin()
}
override fun onPluginDisconnected(plugin: ClockProviderPlugin) = override fun onPluginLoaded(
disconnectClocks(plugin) 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 = private val userSwitchObserver =
@@ -96,7 +174,8 @@ open class ClockRegistry(
protected set(value) { protected set(value) {
if (field != value) { if (field != value) {
field = value field = value
scope.launch(mainDispatcher) { onClockChanged { it.onCurrentClockChanged() } } verifyLoadedProviders()
triggerOnCurrentClockChanged()
} }
} }
@@ -168,9 +247,36 @@ open class ClockRegistry(
Assert.isNotMainThread() Assert.isNotMainThread()
} }
private fun onClockChanged(func: (ClockChangeListener) -> Unit) { private var isClockChanged = AtomicBoolean(false)
assertMainThread() private fun triggerOnCurrentClockChanged() {
clockChangeListeners.forEach(func) 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) { public fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
@@ -190,7 +296,12 @@ open class ClockRegistry(
} }
init { 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)) { if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
throw IllegalArgumentException( throw IllegalArgumentException(
"$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
@@ -244,59 +355,87 @@ open class ClockRegistry(
} }
} }
private fun connectClocks(provider: ClockProvider) { private var isVerifying = AtomicBoolean(false)
var isAvailableChanged = false private fun verifyLoadedProviders() {
val currentId = currentClockId val shouldSchedule = isVerifying.compareAndSet(false, true)
for (clock in provider.getClocks()) { if (!shouldSchedule) {
val id = clock.clockId return
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() }
}
} }
if (isAvailableChanged) { scope.launch(bgDispatcher) {
onClockChanged { it.onAvailableClocksChanged() } 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) { private fun onConnected(clockId: ClockId) {
var isAvailableChanged = false if (DEBUG) {
val currentId = currentClockId Log.i(TAG, "Connected $clockId")
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() }
}
} }
if (isAvailableChanged) { if (currentClockId == clockId) {
onClockChanged { it.onAvailableClocksChanged() } 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( private data class ClockInfo(
val metadata: ClockMetadata, val metadata: ClockMetadata,
val provider: ClockProvider, var provider: ClockProvider?,
val manager: PluginLifecycleManager<ClockProviderPlugin>?,
) )
} }

View File

@@ -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();
}

View File

@@ -17,7 +17,32 @@ package com.android.systemui.plugins;
import android.content.Context; 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> { public interface PluginListener<T extends Plugin> {
/** /**
@@ -25,14 +50,69 @@ public interface PluginListener<T extends Plugin> {
* This may be called multiple times if multiple plugins are allowed. * This may be called multiple times if multiple plugins are allowed.
* It may also be called in the future if the plugin package changes * It may also be called in the future if the plugin package changes
* and needs to be reloaded. * 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 * Called when a plugin has been uninstalled/updated and should be removed
* from use. * from use.
*
* @deprecated Migrate to {@link #onPluginDetached} or {@link #onPluginUnloaded}
*/ */
@Deprecated
default void onPluginDisconnected(T plugin) { default void onPluginDisconnected(T plugin) {
// Optional. // 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);
}
}

View File

@@ -210,12 +210,12 @@ public class PluginActionManager<T extends Plugin> {
private void onPluginConnected(PluginInstance<T> pluginInstance) { private void onPluginConnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginConnected"); if (DEBUG) Log.d(TAG, "onPluginConnected");
PluginPrefs.setHasPlugins(mContext); PluginPrefs.setHasPlugins(mContext);
pluginInstance.onCreate(mContext, mListener); pluginInstance.onCreate();
} }
private void onPluginDisconnected(PluginInstance<T> pluginInstance) { private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginDisconnected"); if (DEBUG) Log.d(TAG, "onPluginDisconnected");
pluginInstance.onDestroy(mListener); pluginInstance.onDestroy();
} }
private void queryAll() { private void queryAll() {
@@ -312,7 +312,7 @@ public class PluginActionManager<T extends Plugin> {
try { try {
return mPluginInstanceFactory.create( return mPluginInstanceFactory.create(
mContext, appInfo, component, mContext, appInfo, component,
mPluginClass); mPluginClass, mListener);
} catch (InvalidVersionException e) { } catch (InvalidVersionException e) {
reportInvalidVersion(component, component.getClassName(), e); reportInvalidVersion(component, component.getClassName(), e);
} }

View File

@@ -21,13 +21,16 @@ import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment; import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import dalvik.system.PathClassLoader; import dalvik.system.PathClassLoader;
@@ -35,7 +38,7 @@ import dalvik.system.PathClassLoader;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.function.Supplier;
/** /**
* Contains a single instantiation of a Plugin. * Contains a single instantiation of a Plugin.
@@ -45,42 +48,102 @@ import java.util.Map;
* *
* @param <T> The type of plugin that this contains. * @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 String TAG = "PluginInstance";
private static final Map<String, ClassLoader> sClassLoaders = new ArrayMap<>();
private final Context mPluginContext; private final Context mAppContext;
private final VersionInfo mVersionInfo; private final PluginListener<T> mListener;
private final ComponentName mComponentName; 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, public PluginInstance(
VersionInfo versionInfo) { Context appContext,
PluginListener<T> listener,
ComponentName componentName,
PluginFactory<T> pluginFactory,
@Nullable T plugin) {
mAppContext = appContext;
mListener = listener;
mComponentName = componentName; mComponentName = componentName;
mPluginFactory = pluginFactory;
mPlugin = plugin; mPlugin = plugin;
mPluginContext = pluginContext;
mVersionInfo = versionInfo; if (mPlugin != null) {
mPluginContext = mPluginFactory.createPluginContext();
}
} }
/** Alerts listener and plugin that the plugin has been created. */ /** Alerts listener and plugin that the plugin has been created. */
public void onCreate(Context appContext, PluginListener<T> listener) { public void onCreate() {
if (!(mPlugin instanceof PluginFragment)) { mListener.onPluginAttached(this);
// Only call onCreate for plugins that aren't fragments, as fragments if (mPlugin == null) {
// will get the onCreate as part of the fragment lifecycle. loadPlugin();
mPlugin.onCreate(appContext, mPluginContext); } 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. */ /** Alerts listener and plugin that the plugin is being shutdown. */
public void onDestroy(PluginListener<T> listener) { public void onDestroy() {
listener.onPluginDisconnected(mPlugin); 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)) { if (!(mPlugin instanceof PluginFragment)) {
// Only call onDestroy for plugins that aren't fragments, as fragments // Only call onDestroy for plugins that aren't fragments, as fragments
// will get the onDestroy as part of the fragment lifecycle. // will get the onDestroy as part of the fragment lifecycle.
mPlugin.onDestroy(); mPlugin.onDestroy();
} }
mPlugin = null;
mPluginContext = null;
} }
/** /**
@@ -89,7 +152,7 @@ public class PluginInstance<T extends Plugin> {
* It does this by string comparison of the class names. * It does this by string comparison of the class names.
**/ **/
public boolean containsPluginClass(Class pluginClass) { public boolean containsPluginClass(Class pluginClass) {
return mPlugin.getClass().getName().equals(pluginClass.getName()); return mComponentName.getClassName().equals(pluginClass.getName());
} }
public ComponentName getComponentName() { public ComponentName getComponentName() {
@@ -101,7 +164,7 @@ public class PluginInstance<T extends Plugin> {
} }
public VersionInfo getVersionInfo() { public VersionInfo getVersionInfo() {
return mVersionInfo; return mPluginFactory.checkVersion(mPlugin);
} }
@VisibleForTesting @VisibleForTesting
@@ -134,21 +197,20 @@ public class PluginInstance<T extends Plugin> {
Context context, Context context,
ApplicationInfo appInfo, ApplicationInfo appInfo,
ComponentName componentName, ComponentName componentName,
Class<T> pluginClass) Class<T> pluginClass,
PluginListener<T> listener)
throws PackageManager.NameNotFoundException, ClassNotFoundException, throws PackageManager.NameNotFoundException, ClassNotFoundException,
InstantiationException, IllegalAccessException { InstantiationException, IllegalAccessException {
ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader); PluginFactory<T> pluginFactory = new PluginFactory<T>(
Context pluginContext = new PluginActionManager.PluginContextWrapper( context, mInstanceFactory, appInfo, componentName, mVersionChecker, pluginClass,
context.createApplicationContext(appInfo, 0), classLoader); () -> getClassLoader(appInfo, mBaseClassLoader));
Class<T> instanceClass = (Class<T>) Class.forName(
componentName.getClassName(), true, classLoader);
// TODO: Only create the plugin before version check if we need it for // TODO: Only create the plugin before version check if we need it for
// legacy version check. // legacy version check.
T instance = (T) mInstanceFactory.create(instanceClass); T instance = pluginFactory.createPlugin();
VersionInfo version = mVersionChecker.checkVersion( pluginFactory.checkVersion(instance);
instanceClass, pluginClass, instance); return new PluginInstance<T>(
return new PluginInstance<T>(componentName, instance, pluginContext, version); context, listener, componentName, pluginFactory, instance);
} }
private boolean isPluginPackagePrivileged(String packageName) { private boolean isPluginPackagePrivileged(String packageName) {
@@ -179,9 +241,6 @@ public class PluginInstance<T extends Plugin> {
+ appInfo.sourceDir + ", pkg: " + appInfo.packageName); + appInfo.sourceDir + ", pkg: " + appInfo.packageName);
return null; return null;
} }
if (sClassLoaders.containsKey(appInfo.packageName)) {
return sClassLoaders.get(appInfo.packageName);
}
List<String> zipPaths = new ArrayList<>(); List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>(); List<String> libPaths = new ArrayList<>();
@@ -190,13 +249,20 @@ public class PluginInstance<T extends Plugin> {
TextUtils.join(File.pathSeparator, zipPaths), TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths), TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader(baseClassLoader)); getParentClassLoader(baseClassLoader));
sClassLoaders.put(appInfo.packageName, classLoader);
return classLoader; return classLoader;
} }
} }
/** Class that compares a plugin class against an implementation for version matching. */ /** 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. */ /** Compares two plugin classes. */
public <T extends Plugin> VersionInfo checkVersion( public <T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) { Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
@@ -204,7 +270,7 @@ public class PluginInstance<T extends Plugin> {
VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass); VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
if (instanceVersion.hasVersionInfo()) { if (instanceVersion.hasVersionInfo()) {
pluginVersion.checkVersion(instanceVersion); pluginVersion.checkVersion(instanceVersion);
} else { } else if (plugin != null) {
int fallbackVersion = plugin.getVersion(); int fallbackVersion = plugin.getVersion();
if (fallbackVersion != pluginVersion.getDefaultVersion()) { if (fallbackVersion != pluginVersion.getDefaultVersion()) {
throw new VersionInfo.InvalidVersionException("Invalid legacy version", false); throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
@@ -225,4 +291,74 @@ public class PluginInstance<T extends Plugin> {
return (T) cls.newInstance(); 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);
}
}
} }

View File

@@ -60,7 +60,9 @@ public abstract class ClockRegistryModule {
featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
/* handleAllUsers= */ true, /* handleAllUsers= */ true,
new DefaultClockProvider(context, layoutInflater, resources), 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(); registry.registerListeners();
return registry; return registry;
} }

View File

@@ -73,7 +73,7 @@ public abstract class PluginsModule {
return new PluginInstance.Factory( return new PluginInstance.Factory(
PluginModule.class.getClassLoader(), PluginModule.class.getClassLoader(),
new PluginInstance.InstanceFactory<>(), new PluginInstance.InstanceFactory<>(),
new PluginInstance.VersionChecker(), new PluginInstance.VersionCheckerImpl(),
privilegedPlugins, privilegedPlugins,
isDebug); isDebug);
} }

View File

@@ -27,13 +27,16 @@ import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.ClockSettings import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginLifecycleManager
import com.android.systemui.plugins.PluginManager import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.fail import junit.framework.Assert.fail
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@@ -49,6 +52,7 @@ import org.mockito.junit.MockitoJUnit
class ClockRegistryTest : SysuiTestCase() { class ClockRegistryTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule() @JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher private lateinit var dispatcher: CoroutineDispatcher
private lateinit var scope: TestScope private lateinit var scope: TestScope
@@ -58,37 +62,38 @@ class ClockRegistryTest : SysuiTestCase() {
@Mock private lateinit var mockDefaultClock: ClockController @Mock private lateinit var mockDefaultClock: ClockController
@Mock private lateinit var mockThumbnail: Drawable @Mock private lateinit var mockThumbnail: Drawable
@Mock private lateinit var mockContentResolver: ContentResolver @Mock private lateinit var mockContentResolver: ContentResolver
@Mock private lateinit var mockPluginLifecycle: PluginLifecycleManager<ClockProviderPlugin>
private lateinit var fakeDefaultProvider: FakeClockPlugin private lateinit var fakeDefaultProvider: FakeClockPlugin
private lateinit var pluginListener: PluginListener<ClockProviderPlugin> private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry private lateinit var registry: ClockRegistry
companion object { companion object {
private fun failFactory(): ClockController { private fun failFactory(clockId: ClockId): ClockController {
fail("Unexpected call to createClock") fail("Unexpected call to createClock: $clockId")
return null!! return null!!
} }
private fun failThumbnail(): Drawable? { private fun failThumbnail(clockId: ClockId): Drawable? {
fail("Unexpected call to getThumbnail") fail("Unexpected call to getThumbnail: $clockId")
return null return null
} }
} }
private class FakeClockPlugin : ClockProviderPlugin { private class FakeClockPlugin : ClockProviderPlugin {
private val metadata = mutableListOf<ClockMetadata>() private val metadata = mutableListOf<ClockMetadata>()
private val createCallbacks = mutableMapOf<ClockId, () -> ClockController>() private val createCallbacks = mutableMapOf<ClockId, (ClockId) -> ClockController>()
private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>() private val thumbnailCallbacks = mutableMapOf<ClockId, (ClockId) -> Drawable?>()
override fun getClocks() = metadata override fun getClocks() = metadata
override fun createClock(settings: ClockSettings): ClockController = override fun createClock(settings: ClockSettings): ClockController =
createCallbacks[settings.clockId!!]!!() createCallbacks[settings.clockId!!]!!(settings.clockId!!)
override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!() override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!(id)
fun addClock( fun addClock(
id: ClockId, id: ClockId,
name: String, name: String,
create: () -> ClockController = ::failFactory, create: (ClockId) -> ClockController = ::failFactory,
getThumbnail: () -> Drawable? = ::failThumbnail getThumbnail: (ClockId) -> Drawable? = ::failThumbnail
): FakeClockPlugin { ): FakeClockPlugin {
metadata.add(ClockMetadata(id, name)) metadata.add(ClockMetadata(id, name))
createCallbacks[id] = create createCallbacks[id] = create
@@ -99,7 +104,8 @@ class ClockRegistryTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
dispatcher = StandardTestDispatcher() scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
scope = TestScope(dispatcher) scope = TestScope(dispatcher)
fakeDefaultProvider = FakeClockPlugin() fakeDefaultProvider = FakeClockPlugin()
@@ -116,6 +122,8 @@ class ClockRegistryTest : SysuiTestCase() {
isEnabled = true, isEnabled = true,
handleAllUsers = true, handleAllUsers = true,
defaultClockProvider = fakeDefaultProvider, defaultClockProvider = fakeDefaultProvider,
keepAllLoaded = true,
subTag = "Test",
) { ) {
override fun querySettings() { } override fun querySettings() { }
override fun applySettings(value: ClockSettings?) { override fun applySettings(value: ClockSettings?) {
@@ -142,8 +150,8 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_3", "clock 3") .addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
val list = registry.getClocks() val list = registry.getClocks()
assertEquals( assertEquals(
list, list,
@@ -165,16 +173,18 @@ class ClockRegistryTest : SysuiTestCase() {
@Test @Test
fun clockIdConflict_ErrorWithoutCrash() { fun clockIdConflict_ErrorWithoutCrash() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail }) .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
.addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail }) .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2)
val list = registry.getClocks() val list = registry.getClocks()
assertEquals( assertEquals(
list, list,
@@ -202,8 +212,8 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null)) registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
val clock = registry.createCurrentClock() val clock = registry.createCurrentClock()
assertEquals(mockClock, clock) assertEquals(mockClock, clock)
@@ -220,9 +230,9 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null)) registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
pluginListener.onPluginDisconnected(plugin2) pluginListener.onPluginUnloaded(plugin2, mockPluginLifecycle)
val clock = registry.createCurrentClock() val clock = registry.createCurrentClock()
assertEquals(clock, mockDefaultClock) assertEquals(clock, mockDefaultClock)
@@ -230,15 +240,16 @@ class ClockRegistryTest : SysuiTestCase() {
@Test @Test
fun pluginRemoved_clockAndListChanged() { fun pluginRemoved_clockAndListChanged() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock }) .addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
var changeCallCount = 0 var changeCallCount = 0
var listChangeCallCount = 0 var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
@@ -247,23 +258,38 @@ class ClockRegistryTest : SysuiTestCase() {
}) })
registry.applySettings(ClockSettings("clock_3", null)) registry.applySettings(ClockSettings("clock_3", null))
assertEquals(0, changeCallCount) scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(0, listChangeCallCount) assertEquals(0, listChangeCallCount)
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1)
assertEquals(0, changeCallCount) scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(1, listChangeCallCount) assertEquals(1, listChangeCallCount)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2)
assertEquals(1, changeCallCount) scheduler.runCurrent()
assertEquals(2, changeCallCount)
assertEquals(2, listChangeCallCount) assertEquals(2, listChangeCallCount)
pluginListener.onPluginDisconnected(plugin1) pluginListener.onPluginUnloaded(plugin1, mockPluginLifecycle1)
assertEquals(1, changeCallCount) 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) assertEquals(3, listChangeCallCount)
pluginListener.onPluginDisconnected(plugin2) pluginListener.onPluginDetached(mockPluginLifecycle2)
assertEquals(2, changeCallCount) scheduler.runCurrent()
assertEquals(3, changeCallCount)
assertEquals(4, listChangeCallCount) assertEquals(4, listChangeCallCount)
} }

View File

@@ -79,11 +79,11 @@ public class PluginActionManagerTest extends SysuiTestCase {
private PluginInstance<TestPlugin> mPluginInstance; private PluginInstance<TestPlugin> mPluginInstance;
private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory( private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory(
this.getClass().getClassLoader(), this.getClass().getClassLoader(),
new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionChecker(), new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionCheckerImpl(),
Collections.emptyList(), false) { Collections.emptyList(), false) {
@Override @Override
public <T extends Plugin> PluginInstance<T> create(Context context, ApplicationInfo appInfo, 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; return (PluginInstance<T>) mPluginInstance;
} }
}; };
@@ -128,7 +128,7 @@ public class PluginActionManagerTest extends SysuiTestCase {
createPlugin(); createPlugin();
// Verify startup lifecycle // Verify startup lifecycle
verify(mPluginInstance).onCreate(mContext, mMockListener); verify(mPluginInstance).onCreate();
} }
@Test @Test
@@ -140,7 +140,7 @@ public class PluginActionManagerTest extends SysuiTestCase {
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Verify shutdown lifecycle // Verify shutdown lifecycle
verify(mPluginInstance).onDestroy(mMockListener); verify(mPluginInstance).onDestroy();
} }
@Test @Test
@@ -152,9 +152,9 @@ public class PluginActionManagerTest extends SysuiTestCase {
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Verify the old one was destroyed. // Verify the old one was destroyed.
verify(mPluginInstance).onDestroy(mMockListener); verify(mPluginInstance).onDestroy();
verify(mPluginInstance, Mockito.times(2)) verify(mPluginInstance, Mockito.times(2))
.onCreate(mContext, mMockListener); .onCreate();
} }
@Test @Test
@@ -188,7 +188,7 @@ public class PluginActionManagerTest extends SysuiTestCase {
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Verify startup lifecycle // Verify startup lifecycle
verify(mPluginInstance).onCreate(mContext, mMockListener); verify(mPluginInstance).onCreate();
} }
@Test @Test

View File

@@ -16,11 +16,9 @@
package com.android.systemui.shared.plugins; package com.android.systemui.shared.plugins;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -31,6 +29,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.annotations.Requires; 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.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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.Collections;
import java.util.concurrent.atomic.AtomicInteger;
@SmallTest @SmallTest
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class PluginInstanceTest extends SysuiTestCase { public class PluginInstanceTest extends SysuiTestCase {
private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins"; private static final String PRIVILEGED_PACKAGE = "com.android.systemui.plugins";
private static final ComponentName TEST_PLUGIN_COMPONENT_NAME =
@Mock
private TestPluginImpl mMockPlugin;
@Mock
private PluginListener<TestPlugin> mMockListener;
@Mock
private VersionInfo mVersionInfo;
ComponentName mTestPluginComponentName =
new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName()); 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<TestPlugin> mPluginInstance;
private PluginInstance.Factory mPluginInstanceFactory; private PluginInstance.Factory mPluginInstanceFactory;
private ApplicationInfo mAppInfo; private ApplicationInfo mAppInfo;
private Context mPluginContext;
@Mock // Because we're testing memory in this file, we must be careful not to assert the target
private PluginInstance.VersionChecker mVersionChecker; // 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 @Before
public void setup() throws Exception { public void setup() throws Exception {
MockitoAnnotations.initMocks(this); mCounter = new RefCounter();
mAppInfo = mContext.getApplicationInfo(); mAppInfo = mContext.getApplicationInfo();
mAppInfo.packageName = mTestPluginComponentName.getPackageName(); mAppInfo.packageName = TEST_PLUGIN_COMPONENT_NAME.getPackageName();
when(mVersionChecker.checkVersion(any(), any(), any())).thenReturn(mVersionInfo); 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( mPluginInstanceFactory = new PluginInstance.Factory(
this.getClass().getClassLoader(), this.getClass().getClassLoader(),
new PluginInstance.InstanceFactory<TestPlugin>() { new PluginInstance.InstanceFactory<TestPlugin>() {
@Override @Override
TestPlugin create(Class cls) { TestPlugin create(Class cls) {
return mMockPlugin; TestPluginImpl plugin = new TestPluginImpl(mCounter);
mPlugin = new WeakReference<>(plugin);
return plugin;
} }
}, },
mVersionChecker, mVersionChecker,
@@ -85,8 +102,9 @@ public class PluginInstanceTest extends SysuiTestCase {
false); false);
mPluginInstance = mPluginInstanceFactory.create( mPluginInstance = mPluginInstanceFactory.create(
mContext, mAppInfo, mTestPluginComponentName, TestPlugin.class); mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME,
mPluginContext = mPluginInstance.getPluginContext(); TestPlugin.class, mPluginListener);
mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext());
} }
@Test @Test
@@ -96,29 +114,51 @@ public class PluginInstanceTest extends SysuiTestCase {
@Test(expected = VersionInfo.InvalidVersionException.class) @Test(expected = VersionInfo.InvalidVersionException.class)
public void testIncorrectVersion() throws Exception { public void testIncorrectVersion() throws Exception {
ComponentName wrongVersionTestPluginComponentName = ComponentName wrongVersionTestPluginComponentName =
new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName()); new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
when(mVersionChecker.checkVersion(any(), any(), any())).thenThrow( mVersionException = new VersionInfo.InvalidVersionException("test", true);
new VersionInfo.InvalidVersionException("test", true));
mPluginInstanceFactory.create( mPluginInstanceFactory.create(
mContext, mAppInfo, wrongVersionTestPluginComponentName, TestPlugin.class); mContext, mAppInfo, wrongVersionTestPluginComponentName,
TestPlugin.class, mPluginListener);
} }
@Test @Test
public void testOnCreate() { public void testOnCreate() {
mPluginInstance.onCreate(mContext, mMockListener); mPluginInstance.onCreate();
verify(mMockPlugin).onCreate(mContext, mPluginContext); assertEquals(1, mPluginListener.mAttachedCount);
verify(mMockListener).onPluginConnected(mMockPlugin, mPluginContext); assertEquals(1, mPluginListener.mLoadCount);
assertEquals(mPlugin.get(), mPluginInstance.getPlugin());
assertInstances(1, 1);
} }
@Test @Test
public void testOnDestroy() { public void testOnDestroy() {
mPluginInstance.onDestroy(mMockListener); mPluginInstance.onDestroy();
verify(mMockListener).onPluginDisconnected(mMockPlugin); assertEquals(1, mPluginListener.mDetachedCount);
verify(mMockPlugin).onDestroy(); 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 // 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"; 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) @Requires(target = TestPlugin.class, version = TestPlugin.VERSION)
public static class TestPluginImpl implements TestPlugin { 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 @Override
public void onCreate(Context sysuiContext, Context pluginContext) { 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);
} }
} }
} }