2/N Rename PluginInstanceManager to PluginActionManager.

Introduce PluginInstance.

PluginManager is in charge of coordinating the whole plugin system.

PluginActionManager is in charge of querying for the PackageManager
for plugins matching a specific action.

PluginActionManager creates PluginInstances for each package it finds,
and the PluginInstance is in charge of loading in the actual plugin
into the system.

This is another step down the path of being able to manually load in
plugins into the code before the PluginManager is ready.

Bug: 194781951
Test: atest SystemUITests && manual
Change-Id: Id525f27e362c29625168b56dc55f2899cd3f8810
This commit is contained in:
Dave Mankoff
2021-08-12 17:31:12 -04:00
parent c08fc30d0e
commit ecc3306fdc
14 changed files with 945 additions and 637 deletions

View File

@@ -0,0 +1,438 @@
/*
* Copyright (C) 2016 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.shared.plugins;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Coordinates all the available plugins for a given action.
*
* The available plugins are queried from the {@link PackageManager} via an an {@link Intent}
* action.
*
* @param <T> The type of plugin that this contains.
*/
public class PluginActionManager<T extends Plugin> {
private static final boolean DEBUG = false;
private static final String TAG = "PluginInstanceManager";
public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
private final Context mContext;
private final PluginListener<T> mListener;
private final String mAction;
private final boolean mAllowMultiple;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final PluginInstance.Factory mPluginInstanceFactory;
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
@VisibleForTesting
private final ArrayList<PluginInstance<T>> mPluginInstances = new ArrayList<>();
private final boolean mIsDebuggable;
private final PackageManager mPm;
private final Class<T> mPluginClass;
private final PluginInitializer mInitializer;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private PluginActionManager(
Context context,
PackageManager pm,
String action,
PluginListener<T> listener,
Class<T> pluginClass,
boolean allowMultiple,
Executor mainExecutor,
Executor bgExecutor,
boolean debuggable,
PluginInitializer initializer,
NotificationManager notificationManager,
PluginEnabler pluginEnabler,
List<String> privilegedPlugins,
PluginInstance.Factory pluginInstanceFactory) {
mPluginClass = pluginClass;
mInitializer = initializer;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mContext = context;
mPm = pm;
mAction = action;
mListener = listener;
mAllowMultiple = allowMultiple;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mPluginInstanceFactory = pluginInstanceFactory;
mPrivilegedPlugins.addAll(privilegedPlugins);
mIsDebuggable = debuggable;
}
/** Load all plugins matching this instance's action. */
public void loadAll() {
if (DEBUG) Log.d(TAG, "startListening");
mBgExecutor.execute(this::queryAll);
}
/** Unload all plugins managed by this instance. */
public void destroy() {
if (DEBUG) Log.d(TAG, "stopListening");
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> plugInstance : plugins) {
mMainExecutor.execute(() -> onPluginDisconnected(plugInstance));
}
}
/** Unload all matching plugins managed by this instance. */
public void onPackageRemoved(String pkg) {
mBgExecutor.execute(() -> removePkg(pkg));
}
/** Unload and then reload all matching plugins managed by this instance. */
public void reloadPackage(String pkg) {
mBgExecutor.execute(() -> {
removePkg(pkg);
queryPkg(pkg);
});
}
/** Disable a specific plugin managed by this instance. */
public boolean checkAndDisable(String className) {
boolean disableAny = false;
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> info : plugins) {
if (className.startsWith(info.getPackage())) {
disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
}
}
return disableAny;
}
/** Disable all plugins managed by this instance. */
public boolean disableAll() {
ArrayList<PluginInstance<T>> plugins = new ArrayList<>(mPluginInstances);
boolean disabledAny = false;
for (int i = 0; i < plugins.size(); i++) {
disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
}
return disabledAny;
}
boolean isPluginPrivileged(ComponentName pluginName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName == null) {
if (componentNameOrPackage.equals(pluginName.getPackageName())) {
return true;
}
} else {
if (componentName.equals(pluginName)) {
return true;
}
}
}
return false;
}
private boolean disable(
PluginInstance<T> pluginInstance, @PluginEnabler.DisableReason int reason) {
// Live by the sword, die by the sword.
// Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
ComponentName pluginComponent = pluginInstance.getComponentName();
// If a plugin is detected in the stack of a crash then this will be called for that
// plugin, if the plugin causing a crash cannot be identified, they are all disabled
// assuming one of them must be bad.
if (isPluginPrivileged(pluginComponent)) {
// Don't disable privileged plugins as they are a part of the OS.
return false;
}
Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
mPluginEnabler.setDisabled(pluginComponent, reason);
return true;
}
<C> boolean dependsOn(Plugin p, Class<C> cls) {
ArrayList<PluginInstance<T>> instances = new ArrayList<>(mPluginInstances);
for (PluginInstance<T> instance : instances) {
if (instance.containsPluginClass(p.getClass())) {
return instance.getVersionInfo() != null && instance.getVersionInfo().hasClass(cls);
}
}
return false;
}
@Override
public String toString() {
return String.format("%s@%s (action=%s)",
getClass().getSimpleName(), hashCode(), mAction);
}
private void onPluginConnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginConnected");
PluginPrefs.setHasPlugins(mContext);
mInitializer.handleWtfs();
pluginInstance.onCreate(mContext, mListener);
}
private void onPluginDisconnected(PluginInstance<T> pluginInstance) {
if (DEBUG) Log.d(TAG, "onPluginDisconnected");
pluginInstance.onDestroy(mListener);
}
private void queryAll() {
if (DEBUG) Log.d(TAG, "queryAll " + mAction);
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
PluginInstance<T> pluginInstance = mPluginInstances.get(i);
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
}
mPluginInstances.clear();
handleQueryPlugins(null);
}
private void removePkg(String pkg) {
for (int i = mPluginInstances.size() - 1; i >= 0; i--) {
final PluginInstance<T> pluginInstance = mPluginInstances.get(i);
if (pluginInstance.getPackage().equals(pkg)) {
mMainExecutor.execute(() -> onPluginDisconnected(pluginInstance));
mPluginInstances.remove(i);
}
}
}
private void queryPkg(String pkg) {
if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
if (mAllowMultiple || (mPluginInstances.size() == 0)) {
handleQueryPlugins(pkg);
} else {
if (DEBUG) Log.d(TAG, "Too many of " + mAction);
}
}
private void handleQueryPlugins(String pkgName) {
// This isn't actually a service and shouldn't ever be started, but is
// a convenient PM based way to manage our plugins.
Intent intent = new Intent(mAction);
if (pkgName != null) {
intent.setPackage(pkgName);
}
List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
if (result.size() > 1 && !mAllowMultiple) {
// TODO: Show warning.
Log.w(TAG, "Multiple plugins found for " + mAction);
if (DEBUG) {
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
Log.w(TAG, " " + name);
}
}
return;
}
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
PluginInstance<T> pluginInstance = loadPluginComponent(name);
if (pluginInstance != null) {
// add plugin before sending PLUGIN_CONNECTED message
mPluginInstances.add(pluginInstance);
mMainExecutor.execute(() -> onPluginConnected(pluginInstance));
}
}
}
private PluginInstance<T> loadPluginComponent(ComponentName component) {
// This was already checked, but do it again here to make extra extra sure, we don't
// use these on production builds.
if (!mIsDebuggable && !isPluginPrivileged(component)) {
// Never ever ever allow these on production builds, they are only for prototyping.
Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
return null;
}
if (!mPluginEnabler.isEnabled(component)) {
if (DEBUG) {
Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
}
return null;
}
String packageName = component.getPackageName();
try {
// TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
if (mPm.checkPermission(PLUGIN_PERMISSION, packageName)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Plugin doesn't have permission: " + packageName);
return null;
}
ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
if (DEBUG) {
Log.d(TAG, "createPlugin");
}
try {
return mPluginInstanceFactory.create(
mContext, appInfo, component,
mPluginClass);
} catch (InvalidVersionException e) {
reportInvalidVersion(component, component.getClassName(), e);
}
} catch (Throwable e) {
Log.w(TAG, "Couldn't load plugin: " + packageName, e);
return null;
}
return null;
}
private void reportInvalidVersion(
ComponentName component, String className, InvalidVersionException e) {
final int icon = Resources.getSystem().getIdentifier(
"stat_sys_warning", "drawable", "android");
final int color = Resources.getSystem().getIdentifier(
"system_notification_accent_color", "color", "android");
final Notification.Builder nb = new Notification.Builder(mContext,
PluginManager.NOTIFICATION_CHANNEL_ID)
.setStyle(new Notification.BigTextStyle())
.setSmallIcon(icon)
.setWhen(0)
.setShowWhen(false)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color));
String label = className;
try {
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
} catch (NameNotFoundException e2) {
// no-op
}
if (!e.isTooNew()) {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too old")
.setContentText("Contact plugin developer to get an updated"
+ " version.\n" + e.getMessage());
} else {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too new")
.setContentText("Check to see if an OTA is available.\n"
+ e.getMessage());
}
Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
Uri.parse("package://" + component.flattenToString()));
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
PendingIntent.FLAG_IMMUTABLE);
nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
// TODO: Warn user.
Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
+ ", expected " + e.getExpectedVersion());
}
/**
* Construct a {@link PluginActionManager}
*/
public static class Factory {
private final Context mContext;
private final PackageManager mPackageManager;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private final PluginInitializer mInitializer;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final List<String> mPrivilegedPlugins;
private final PluginInstance.Factory mPluginInstanceFactory;
public Factory(Context context, PackageManager packageManager,
Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
List<String> privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) {
mContext = context;
mPackageManager = packageManager;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mInitializer = initializer;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mPrivilegedPlugins = privilegedPlugins;
mPluginInstanceFactory = pluginInstanceFactory;
}
<T extends Plugin> PluginActionManager<T> create(
String action, PluginListener<T> listener, Class<T> pluginClass,
boolean allowMultiple, boolean debuggable) {
return new PluginActionManager<>(mContext, mPackageManager, action, listener,
pluginClass, allowMultiple, mMainExecutor, mBgExecutor,
debuggable, mInitializer, mNotificationManager, mPluginEnabler,
mPrivilegedPlugins, mPluginInstanceFactory);
}
}
/** */
public static class PluginContextWrapper extends ContextWrapper {
private final ClassLoader mClassLoader;
private LayoutInflater mInflater;
public PluginContextWrapper(Context base, ClassLoader classLoader) {
super(base);
mClassLoader = classLoader;
}
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
}
}

View File

@@ -28,7 +28,7 @@ public interface PluginInitializer {
/**
* Called from {@link PluginInstanceManager}.
* Called from {@link PluginActionManager}.
*/
void handleWtfs();
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright (C) 2021 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.shared.plugins;
import android.app.LoadedApk;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginListener;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Contains a single instantiation of a Plugin.
*
* This class and its related Factory are in charge of actually instantiating a plugin and
* managing any state related to it.
*
* @param <T> The type of plugin that this contains.
*/
public class PluginInstance<T extends Plugin> {
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 ComponentName mComponentName;
private final T mPlugin;
/** */
public PluginInstance(ComponentName componentName, T plugin, Context pluginContext,
VersionInfo versionInfo) {
mComponentName = componentName;
mPlugin = plugin;
mPluginContext = pluginContext;
mVersionInfo = versionInfo;
}
/** 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);
}
listener.onPluginConnected(mPlugin, mPluginContext);
}
/** Alerts listener and plugin that the plugin is being shutdown. */
public void onDestroy(PluginListener<T> listener) {
listener.onPluginDisconnected(mPlugin);
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();
}
}
/**
* Returns if the contained plugin matches the passed in class name.
*
* It does this by string comparison of the class names.
**/
public boolean containsPluginClass(Class pluginClass) {
return mPlugin.getClass().getName().equals(pluginClass.getName());
}
public ComponentName getComponentName() {
return mComponentName;
}
public String getPackage() {
return mComponentName.getPackageName();
}
public VersionInfo getVersionInfo() {
return mVersionInfo;
}
@VisibleForTesting
Context getPluginContext() {
return mPluginContext;
}
/** Used to create new {@link PluginInstance}s. */
public static class Factory {
private final ClassLoader mBaseClassLoader;
private final InstanceFactory<?> mInstanceFactory;
private final VersionChecker mVersionChecker;
private final boolean mIsDebug;
private final List<String> mPrivilegedPlugins;
/** Factory used to construct {@link PluginInstance}s. */
public Factory(ClassLoader classLoader, InstanceFactory<?> instanceFactory,
VersionChecker versionChecker,
List<String> privilegedPlugins,
boolean isDebug) {
mPrivilegedPlugins = privilegedPlugins;
mBaseClassLoader = classLoader;
mInstanceFactory = instanceFactory;
mVersionChecker = versionChecker;
mIsDebug = isDebug;
}
/** Construct a new PluginInstance. */
public <T extends Plugin> PluginInstance<T> create(
Context context,
ApplicationInfo appInfo,
ComponentName componentName,
Class<T> pluginClass)
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);
// 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);
}
private boolean isPluginPackagePrivileged(String packageName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(
componentNameOrPackage);
if (componentName != null) {
if (componentName.getPackageName().equals(packageName)) {
return true;
}
} else if (componentNameOrPackage.equals(packageName)) {
return true;
}
}
return false;
}
private ClassLoader getParentClassLoader(ClassLoader baseClassLoader) {
return new PluginManagerImpl.ClassLoaderFilter(
baseClassLoader, "com.android.systemui.plugin");
}
/** Returns class loader specific for the given plugin. */
private ClassLoader getClassLoader(ApplicationInfo appInfo,
ClassLoader baseClassLoader) {
if (!mIsDebug && !isPluginPackagePrivileged(appInfo.packageName)) {
Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
+ 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<>();
LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
ClassLoader classLoader = new PathClassLoader(
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 {
/** Compares two plugin classes. */
public <T extends Plugin> VersionInfo checkVersion(
Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
if (instanceVersion.hasVersionInfo()) {
pluginVersion.checkVersion(instanceVersion);
} else {
int fallbackVersion = plugin.getVersion();
if (fallbackVersion != pluginVersion.getDefaultVersion()) {
throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
}
return null;
}
return instanceVersion;
}
}
/**
* Simple class to create a new instance. Useful for testing.
*
* @param <T> The type of plugin this create.
**/
public static class InstanceFactory<T extends Plugin> {
T create(Class cls) throws IllegalAccessException, InstantiationException {
return (T) cls.newInstance();
}
}
}

View File

@@ -1,513 +0,0 @@
/*
* Copyright (C) 2016 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.shared.plugins;
import android.app.LoadedApk;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
public class PluginInstanceManager<T extends Plugin> {
private static final boolean DEBUG = false;
private static final String TAG = "PluginInstanceManager";
public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
private final Context mContext;
private final PluginListener<T> mListener;
private final String mAction;
private final boolean mAllowMultiple;
private final VersionInfo mVersion;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final InstanceFactory<T> mInstanceFactory;
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
@VisibleForTesting
private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
private final boolean mIsDebuggable;
private final PackageManager mPm;
private final PluginInitializer mInitializer;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private PluginManagerImpl.ClassLoaderFilter mParentClassLoader;
private PluginInstanceManager(Context context, PackageManager pm, String action,
PluginListener<T> listener, boolean allowMultiple, Executor mainExecutor,
Executor bgExecutor, VersionInfo version, boolean debuggable,
PluginInitializer initializer, NotificationManager notificationManager,
PluginEnabler pluginEnabler, List<String> privilegedPlugins,
InstanceFactory<T> instanceFactory) {
mInitializer = initializer;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mContext = context;
mPm = pm;
mAction = action;
mListener = listener;
mAllowMultiple = allowMultiple;
mVersion = version;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mInstanceFactory = instanceFactory;
mPrivilegedPlugins.addAll(privilegedPlugins);
mIsDebuggable = debuggable;
}
public void loadAll() {
if (DEBUG) Log.d(TAG, "startListening");
mBgExecutor.execute(this::queryAll);
}
public void destroy() {
if (DEBUG) Log.d(TAG, "stopListening");
ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
for (PluginInfo<T> pluginInfo : plugins) {
mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
}
}
public void onPackageRemoved(String pkg) {
mBgExecutor.execute(() -> removePkg(pkg));
}
public void onPackageChange(String pkg) {
mBgExecutor.execute(() -> removePkg(pkg));
mBgExecutor.execute(() -> queryPkg(pkg));
}
public boolean checkAndDisable(String className) {
boolean disableAny = false;
ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
for (PluginInfo<T> info : plugins) {
if (className.startsWith(info.mPackage)) {
disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
}
}
return disableAny;
}
public boolean disableAll() {
ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
boolean disabledAny = false;
for (int i = 0; i < plugins.size(); i++) {
disabledAny |= disable(plugins.get(i), PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
}
return disabledAny;
}
private boolean isPluginPackagePrivileged(String packageName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName != null) {
if (componentName.getPackageName().equals(packageName)) {
return true;
}
} else if (componentNameOrPackage.equals(packageName)) {
return true;
}
}
return false;
}
private boolean isPluginPrivileged(ComponentName pluginName) {
for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName == null) {
if (componentNameOrPackage.equals(pluginName.getPackageName())) {
return true;
}
} else {
if (componentName.equals(pluginName)) {
return true;
}
}
}
return false;
}
private boolean disable(PluginInfo<T> info, @PluginEnabler.DisableReason int reason) {
// Live by the sword, die by the sword.
// Misbehaving plugins get disabled and won't come back until uninstall/reinstall.
ComponentName pluginComponent = new ComponentName(info.mPackage, info.mClass);
// If a plugin is detected in the stack of a crash then this will be called for that
// plugin, if the plugin causing a crash cannot be identified, they are all disabled
// assuming one of them must be bad.
if (isPluginPrivileged(pluginComponent)) {
// Don't disable whitelisted plugins as they are a part of the OS.
return false;
}
Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
mPluginEnabler.setDisabled(pluginComponent, reason);
return true;
}
<C> boolean dependsOn(Plugin p, Class<C> cls) {
ArrayList<PluginInfo<T>> plugins = new ArrayList<>(mPlugins);
for (PluginInfo<T> info : plugins) {
if (info.mPlugin.getClass().getName().equals(p.getClass().getName())) {
return info.mVersion != null && info.mVersion.hasClass(cls);
}
}
return false;
}
@Override
public String toString() {
return String.format("%s@%s (action=%s)",
getClass().getSimpleName(), hashCode(), mAction);
}
private void onPluginConnected(PluginInfo<T> pluginInfo) {
if (DEBUG) Log.d(TAG, "onPluginConnected");
PluginPrefs.setHasPlugins(mContext);
mInitializer.handleWtfs();
if (!(pluginInfo.mPlugin instanceof PluginFragment)) {
// Only call onCreate for plugins that aren't fragments, as fragments
// will get the onCreate as part of the fragment lifecycle.
pluginInfo.mPlugin.onCreate(mContext, pluginInfo.mPluginContext);
}
mListener.onPluginConnected(pluginInfo.mPlugin, pluginInfo.mPluginContext);
}
private void onPluginDisconnected(T plugin) {
if (DEBUG) Log.d(TAG, "onPluginDisconnected");
mListener.onPluginDisconnected(plugin);
if (!(plugin instanceof PluginFragment)) {
// Only call onDestroy for plugins that aren't fragments, as fragments
// will get the onDestroy as part of the fragment lifecycle.
plugin.onDestroy();
}
}
private void queryAll() {
if (DEBUG) Log.d(TAG, "queryAll " + mAction);
for (int i = mPlugins.size() - 1; i >= 0; i--) {
PluginInfo<T> pluginInfo = mPlugins.get(i);
mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
}
mPlugins.clear();
handleQueryPlugins(null);
}
private void removePkg(String pkg) {
for (int i = mPlugins.size() - 1; i >= 0; i--) {
final PluginInfo<T> pluginInfo = mPlugins.get(i);
if (pluginInfo.mPackage.equals(pkg)) {
mMainExecutor.execute(() -> onPluginDisconnected(pluginInfo.mPlugin));
mPlugins.remove(i);
}
}
}
private void queryPkg(String pkg) {
if (DEBUG) Log.d(TAG, "queryPkg " + pkg + " " + mAction);
if (mAllowMultiple || (mPlugins.size() == 0)) {
handleQueryPlugins(pkg);
} else {
if (DEBUG) Log.d(TAG, "Too many of " + mAction);
}
}
private void handleQueryPlugins(String pkgName) {
// This isn't actually a service and shouldn't ever be started, but is
// a convenient PM based way to manage our plugins.
Intent intent = new Intent(mAction);
if (pkgName != null) {
intent.setPackage(pkgName);
}
List<ResolveInfo> result = mPm.queryIntentServices(intent, 0);
if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins");
if (result.size() > 1 && !mAllowMultiple) {
// TODO: Show warning.
Log.w(TAG, "Multiple plugins found for " + mAction);
if (DEBUG) {
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
Log.w(TAG, " " + name);
}
}
return;
}
for (ResolveInfo info : result) {
ComponentName name = new ComponentName(info.serviceInfo.packageName,
info.serviceInfo.name);
PluginInfo<T> pluginInfo = handleLoadPlugin(name);
if (pluginInfo == null) continue;
// add plugin before sending PLUGIN_CONNECTED message
mPlugins.add(pluginInfo);
mMainExecutor.execute(() -> onPluginConnected(pluginInfo));
}
}
protected PluginInfo<T> handleLoadPlugin(ComponentName component) {
// This was already checked, but do it again here to make extra extra sure, we don't
// use these on production builds.
if (!mIsDebuggable && !isPluginPrivileged(component)) {
// Never ever ever allow these on production builds, they are only for prototyping.
Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
return null;
}
if (!mPluginEnabler.isEnabled(component)) {
if (DEBUG) Log.d(TAG, "Plugin is not enabled, aborting load: " + component);
return null;
}
String pkg = component.getPackageName();
String cls = component.getClassName();
try {
ApplicationInfo info = mPm.getApplicationInfo(pkg, 0);
// TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on
if (mPm.checkPermission(PLUGIN_PERMISSION, pkg)
!= PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Plugin doesn't have permission: " + pkg);
return null;
}
// Create our own ClassLoader so we can use our own code as the parent.
ClassLoader classLoader = getClassLoader(info);
Context pluginContext = new PluginContextWrapper(
mContext.createApplicationContext(info, 0), classLoader);
Class<?> pluginClass = Class.forName(cls, true, classLoader);
// TODO: Only create the plugin before version check if we need it for
// legacy version check.
T plugin = mInstanceFactory.create(pluginClass);
try {
VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
if (DEBUG) Log.d(TAG, "createPlugin");
return new PluginInfo<>(pkg, cls, plugin, pluginContext, version);
} catch (InvalidVersionException e) {
final int icon = Resources.getSystem().getIdentifier(
"stat_sys_warning", "drawable", "android");
final int color = Resources.getSystem().getIdentifier(
"system_notification_accent_color", "color", "android");
final Notification.Builder nb = new Notification.Builder(mContext,
PluginManager.NOTIFICATION_CHANNEL_ID)
.setStyle(new Notification.BigTextStyle())
.setSmallIcon(icon)
.setWhen(0)
.setShowWhen(false)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color));
String label = cls;
try {
label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
} catch (NameNotFoundException e2) {
}
if (!e.isTooNew()) {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too old")
.setContentText("Contact plugin developer to get an updated"
+ " version.\n" + e.getMessage());
} else {
// Localization not required as this will never ever appear in a user build.
nb.setContentTitle("Plugin \"" + label + "\" is too new")
.setContentText("Check to see if an OTA is available.\n"
+ e.getMessage());
}
Intent i = new Intent(PluginManagerImpl.DISABLE_PLUGIN).setData(
Uri.parse("package://" + component.flattenToString()));
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i,
PendingIntent.FLAG_IMMUTABLE);
nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
// TODO: Warn user.
Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion()
+ ", expected " + mVersion);
return null;
}
} catch (Throwable e) {
Log.w(TAG, "Couldn't load plugin: " + pkg, e);
return null;
}
}
private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version)
throws InvalidVersionException {
VersionInfo pv = new VersionInfo().addClass(pluginClass);
if (pv.hasVersionInfo()) {
version.checkVersion(pv);
} else {
int fallbackVersion = plugin.getVersion();
if (fallbackVersion != version.getDefaultVersion()) {
throw new InvalidVersionException("Invalid legacy version", false);
}
return null;
}
return pv;
}
/** Returns class loader specific for the given plugin. */
public ClassLoader getClassLoader(ApplicationInfo appInfo) {
if (!mIsDebuggable && !isPluginPackagePrivileged(appInfo.packageName)) {
Log.w(TAG, "Cannot get class loader for non-privileged plugin. Src:"
+ appInfo.sourceDir + ", pkg: " + appInfo.packageName);
return null;
}
if (mClassLoaders.containsKey(appInfo.packageName)) {
return mClassLoaders.get(appInfo.packageName);
}
List<String> zipPaths = new ArrayList<>();
List<String> libPaths = new ArrayList<>();
LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
ClassLoader classLoader = new PathClassLoader(
TextUtils.join(File.pathSeparator, zipPaths),
TextUtils.join(File.pathSeparator, libPaths),
getParentClassLoader());
mClassLoaders.put(appInfo.packageName, classLoader);
return classLoader;
}
private ClassLoader getParentClassLoader() {
if (mParentClassLoader == null) {
// Lazily load this so it doesn't have any effect on devices without plugins.
mParentClassLoader = new PluginManagerImpl.ClassLoaderFilter(
getClass().getClassLoader(), "com.android.systemui.plugin");
}
return mParentClassLoader;
}
/**
* Construct a {@link PluginInstanceManager}
*/
public static class Factory {
private final Context mContext;
private final PackageManager mPackageManager;
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private final PluginInitializer mInitializer;
private final NotificationManager mNotificationManager;
private final PluginEnabler mPluginEnabler;
private final List<String> mPrivilegedPlugins;
private InstanceFactory<?> mInstanceFactory;
public Factory(Context context, PackageManager packageManager,
Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
List<String> privilegedPlugins) {
mContext = context;
mPackageManager = packageManager;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
mInitializer = initializer;
mNotificationManager = notificationManager;
mPluginEnabler = pluginEnabler;
mPrivilegedPlugins = privilegedPlugins;
mInstanceFactory = new InstanceFactory<>();
}
@VisibleForTesting
<T extends Plugin> Factory setInstanceFactory(InstanceFactory<T> instanceFactory) {
mInstanceFactory = instanceFactory;
return this;
}
<T extends Plugin> PluginInstanceManager<T> create(
String action, PluginListener<T> listener, boolean allowMultiple,
VersionInfo version, boolean debuggable) {
return new PluginInstanceManager<T>(mContext, mPackageManager, action, listener,
allowMultiple, mMainExecutor, mBgExecutor, version, debuggable,
mInitializer, mNotificationManager, mPluginEnabler,
mPrivilegedPlugins, (InstanceFactory<T>) mInstanceFactory);
}
}
public static class PluginContextWrapper extends ContextWrapper {
private final ClassLoader mClassLoader;
private LayoutInflater mInflater;
public PluginContextWrapper(Context base, ClassLoader classLoader) {
super(base);
mClassLoader = classLoader;
}
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
}
static class PluginInfo<T extends Plugin> {
private final Context mPluginContext;
private final VersionInfo mVersion;
private final String mClass;
T mPlugin;
String mPackage;
public PluginInfo(String pkg, String cls, T plugin, Context pluginContext,
VersionInfo info) {
mPlugin = plugin;
mClass = cls;
mPackage = pkg;
mPluginContext = pluginContext;
mVersion = info;
}
}
static class InstanceFactory<T extends Plugin> {
T create(Class cls) throws IllegalAccessException, InstantiationException {
return (T) cls.newInstance();
}
}
}

View File

@@ -30,13 +30,15 @@ public interface PluginManager {
/** Returns plugins that don't get disabled when an exceptoin occurs. */
String[] getPrivilegedPlugins();
<T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls);
<T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
/** */
<T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls);
/** */
<T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
boolean allowMultiple);
<T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls);
Class<T> cls);
<T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls, boolean allowMultiple);
Class<T> cls, boolean allowMultiple);
void removePluginListener(PluginListener<?> listener);

View File

@@ -47,26 +47,26 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
private static final String TAG = PluginManagerImpl.class.getSimpleName();
static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";
private final ArrayMap<PluginListener<?>, PluginInstanceManager<?>> mPluginMap
private final ArrayMap<PluginListener<?>, PluginActionManager<?>> mPluginMap
= new ArrayMap<>();
private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
private final Context mContext;
private final PluginInstanceManager.Factory mInstanceManagerFactory;
private final PluginActionManager.Factory mActionManagerFactory;
private final boolean mIsDebuggable;
private final PluginPrefs mPluginPrefs;
private final PluginEnabler mPluginEnabler;
private boolean mListening;
public PluginManagerImpl(Context context,
PluginInstanceManager.Factory instanceManagerFactory,
PluginActionManager.Factory actionManagerFactory,
boolean debuggable,
Optional<UncaughtExceptionHandler> defaultHandlerOptional,
PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs,
List<String> privilegedPlugins) {
mContext = context;
mInstanceManagerFactory = instanceManagerFactory;
mActionManagerFactory = actionManagerFactory;
mIsDebuggable = debuggable;
mPrivilegedPlugins.addAll(privilegedPlugins);
mPluginPrefs = pluginPrefs;
@@ -85,25 +85,27 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
return mPrivilegedPlugins.toArray(new String[0]);
}
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
/** */
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls) {
addPluginListener(listener, cls, false);
}
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
/** */
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
boolean allowMultiple) {
addPluginListener(PluginManager.Helper.getAction(cls), listener, cls, allowMultiple);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls) {
Class<T> cls) {
addPluginListener(action, listener, cls, false);
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls, boolean allowMultiple) {
Class<T> cls, boolean allowMultiple) {
mPluginPrefs.addAction(action);
PluginInstanceManager<T> p = mInstanceManagerFactory.create(action, listener, allowMultiple,
new VersionInfo().addClass(cls), isDebuggable());
PluginActionManager<T> p = mActionManagerFactory.create(action, listener, cls,
allowMultiple, isDebuggable());
p.loadAll();
synchronized (this) {
mPluginMap.put(listener, p);
@@ -135,7 +137,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
filter.addAction(PLUGIN_CHANGED);
filter.addAction(DISABLE_PLUGIN);
filter.addDataScheme("package");
mContext.registerReceiver(this, filter, PluginInstanceManager.PLUGIN_PERMISSION, null);
mContext.registerReceiver(this, filter, PluginActionManager.PLUGIN_PERMISSION, null);
filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
mContext.registerReceiver(this, filter);
}
@@ -150,7 +152,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
synchronized (this) {
for (PluginInstanceManager<?> manager : mPluginMap.values()) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.loadAll();
}
}
@@ -189,12 +191,14 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
}
}
synchronized (this) {
if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
for (PluginInstanceManager<?> manager : mPluginMap.values()) {
manager.onPackageChange(pkg);
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
|| Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())
|| Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
for (PluginActionManager<?> actionManager : mPluginMap.values()) {
actionManager.reloadPackage(pkg);
}
} else {
for (PluginInstanceManager<?> manager : mPluginMap.values()) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
manager.onPackageRemoved(pkg);
}
}
@@ -284,7 +288,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
// disable all the plugins, so we can be sure that SysUI is running as
// best as possible.
synchronized (this) {
for (PluginInstanceManager<?> manager : mPluginMap.values()) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
disabledAny |= manager.disableAll();
}
}
@@ -304,7 +308,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
boolean disabledAny = false;
synchronized (this) {
for (StackTraceElement element : throwable.getStackTrace()) {
for (PluginInstanceManager<?> manager : mPluginMap.values()) {
for (PluginActionManager<?> manager : mPluginMap.values()) {
disabledAny |= manager.checkAndDisable(element.getClassName());
}
}

View File

@@ -119,6 +119,8 @@ public class VersionInfo {
public static class InvalidVersionException extends RuntimeException {
private final boolean mTooNew;
private int mExpected;
private int mActual;
public InvalidVersionException(String str, boolean tooNew) {
super(str);
@@ -128,11 +130,21 @@ public class VersionInfo {
public InvalidVersionException(Class<?> cls, boolean tooNew, int expected, int actual) {
super(cls.getSimpleName() + " expected version " + expected + " but had " + actual);
mTooNew = tooNew;
mExpected = expected;
mActual = actual;
}
public boolean isTooNew() {
return mTooNew;
}
public int getExpectedVersion() {
return mExpected;
}
public int getActualVersion() {
return mActual;
}
}
private static class Version {

View File

@@ -53,7 +53,7 @@ public class PluginInflateContainer extends AutoReinflateContainer
private static final String TAG = "PluginInflateContainer";
private Class<?> mClass;
private Class<ViewProvider> mClass;
private View mPluginView;
public PluginInflateContainer(Context context, @Nullable AttributeSet attrs) {
@@ -61,7 +61,7 @@ public class PluginInflateContainer extends AutoReinflateContainer
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PluginInflateContainer);
String viewType = a.getString(R.styleable.PluginInflateContainer_viewType);
try {
mClass = Class.forName(viewType);
mClass = (Class<ViewProvider>) Class.forName(viewType);
} catch (Exception e) {
Log.d(TAG, "Problem getting class info " + viewType, e);
mClass = null;

View File

@@ -23,10 +23,12 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import com.android.systemui.dagger.PluginModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.shared.plugins.PluginActionManager;
import com.android.systemui.shared.plugins.PluginEnabler;
import com.android.systemui.shared.plugins.PluginInitializer;
import com.android.systemui.shared.plugins.PluginInstanceManager;
import com.android.systemui.shared.plugins.PluginInstance;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.shared.plugins.PluginManagerImpl;
import com.android.systemui.shared.plugins.PluginPrefs;
@@ -71,14 +73,28 @@ public abstract class PluginsModule {
@Provides
@Singleton
static PluginInstanceManager.Factory providePluginInstanceManagerFactory(Context context,
static PluginInstance.Factory providesPluginInstanceFactory(
@Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins,
@Named(PLUGIN_DEBUG) boolean isDebug) {
return new PluginInstance.Factory(
PluginModule.class.getClassLoader(),
new PluginInstance.InstanceFactory<>(),
new PluginInstance.VersionChecker(),
privilegedPlugins,
isDebug);
}
@Provides
@Singleton
static PluginActionManager.Factory providePluginInstanceManagerFactory(Context context,
PackageManager packageManager, @Main Executor mainExecutor,
@Named(PLUGIN_THREAD) Executor pluginExecutor, PluginInitializer initializer,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
@Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) {
return new PluginInstanceManager.Factory(
@Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins,
PluginInstance.Factory pluginInstanceFactory) {
return new PluginActionManager.Factory(
context, packageManager, mainExecutor, pluginExecutor, initializer,
notificationManager, pluginEnabler, privilegedPlugins);
notificationManager, pluginEnabler, privilegedPlugins, pluginInstanceFactory);
}
@Provides
@@ -91,7 +107,7 @@ public abstract class PluginsModule {
@Provides
static PluginManager providesPluginManager(
Context context,
PluginInstanceManager.Factory instanceManagerFactory,
PluginActionManager.Factory instanceManagerFactory,
@Named(PLUGIN_DEBUG) boolean debug,
@Named(PRE_HANDLER)
Optional<Thread.UncaughtExceptionHandler> uncaughtExceptionHandlerOptional,

View File

@@ -38,8 +38,8 @@ import com.android.internal.util.ArrayUtils;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.PluginEnablerImpl;
import com.android.systemui.shared.plugins.PluginActionManager;
import com.android.systemui.shared.plugins.PluginEnabler;
import com.android.systemui.shared.plugins.PluginInstanceManager;
import com.android.systemui.shared.plugins.PluginManager;
import com.android.systemui.shared.plugins.PluginPrefs;
@@ -102,7 +102,7 @@ public class PluginFragment extends PreferenceFragment {
}
List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{
PluginInstanceManager.PLUGIN_PERMISSION},
PluginActionManager.PLUGIN_PERMISSION},
PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
apps.forEach(app -> {
if (!plugins.containsKey(app.packageName)) return;

View File

@@ -1,15 +1,17 @@
/*
* Copyright (C) 2016 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
* 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.
* 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.shared.plugins;
@@ -19,8 +21,6 @@ import static junit.framework.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -41,13 +41,11 @@ import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.Requires;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -64,14 +62,14 @@ import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class PluginInstanceManagerTest extends SysuiTestCase {
public class PluginActionManagerTest extends SysuiTestCase {
private static final String PRIVILEGED_PACKAGE = "com.android.systemui.shared.plugins";
private TestPlugin mMockPlugin;
private PackageManager mMockPm;
private PluginListener<Plugin> mMockListener;
private PluginInstanceManager<Plugin> mPluginInstanceManager;
private PluginListener<TestPlugin> mMockListener;
private PluginActionManager<TestPlugin> mPluginActionManager;
private VersionInfo mMockVersionInfo;
private PluginEnabler mMockEnabler;
ComponentName mTestPluginComponentName =
@@ -79,15 +77,20 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
private PluginInitializer mInitializer;
private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
NotificationManager mNotificationManager;
private PluginInstanceManager.Factory mInstanceManagerFactory;
private final PluginInstanceManager.InstanceFactory<Plugin> mPluginInstanceFactory =
new PluginInstanceManager.InstanceFactory<Plugin>() {
private PluginInstance<TestPlugin> mPluginInstance;
private PluginInstance.Factory mPluginInstanceFactory = new PluginInstance.Factory(
this.getClass().getClassLoader(),
new PluginInstance.InstanceFactory<>(), new PluginInstance.VersionChecker(),
Collections.emptyList(), false) {
@Override
Plugin create(Class cls) {
return mMockPlugin;
public <T extends Plugin> PluginInstance<T> create(Context context, ApplicationInfo appInfo,
ComponentName componentName, Class<T> pluginClass) {
return (PluginInstance<T>) mPluginInstance;
}
};
private PluginActionManager.Factory mActionManagerFactory;
@Before
public void setup() throws Exception {
mContext = new MyContextWrapper(mContext);
@@ -98,13 +101,15 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
mInitializer = mock(PluginInitializer.class);
mNotificationManager = mock(NotificationManager.class);
mMockPlugin = mock(TestPlugin.class);
mInstanceManagerFactory = new PluginInstanceManager.Factory(getContext(), mMockPm,
mPluginInstance = mock(PluginInstance.class);
when(mPluginInstance.getComponentName()).thenReturn(mTestPluginComponentName);
when(mPluginInstance.getPackage()).thenReturn(mTestPluginComponentName.getPackageName());
mActionManagerFactory = new PluginActionManager.Factory(getContext(), mMockPm,
mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager, mMockEnabler,
new ArrayList<>())
.setInstanceFactory(mPluginInstanceFactory);
new ArrayList<>(), mPluginInstanceFactory);
mPluginInstanceManager = mInstanceManagerFactory.create("myAction", mMockListener,
true, mMockVersionInfo, true);
mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
TestPlugin.class, true, true);
when(mMockPlugin.getVersion()).thenReturn(1);
}
@@ -112,7 +117,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
public void testNoPlugins() {
when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(
Collections.emptyList());
mPluginInstanceManager.loadAll();
mPluginActionManager.loadAll();
mFakeExecutor.runAllReady();
@@ -121,68 +126,47 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
@Test
public void testPluginCreate() throws Exception {
//Debug.waitForDebugger();
createPlugin();
// Verify startup lifecycle
verify(mMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener).onPluginConnected(any(), any());
verify(mPluginInstance).onCreate(mContext, mMockListener);
}
@Test
public void testPluginDestroy() throws Exception {
createPlugin(); // Get into valid created state.
mPluginInstanceManager.destroy();
mPluginActionManager.destroy();
mFakeExecutor.runAllReady();
// Verify shutdown lifecycle
verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
verify(mMockPlugin).onDestroy();
}
@Test
public void testIncorrectVersion() throws Exception {
setupFakePmQuery();
doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
mPluginInstanceManager.loadAll();
mFakeExecutor.runAllReady();
// Plugin shouldn't be connected because it is the wrong version.
verify(mMockListener, never()).onPluginConnected(any(), any());
verify(mNotificationManager).notify(eq(SystemMessage.NOTE_PLUGIN), any());
verify(mPluginInstance).onDestroy(mMockListener);
}
@Test
public void testReloadOnChange() throws Exception {
createPlugin(); // Get into valid created state.
mPluginInstanceManager.onPackageChange(PRIVILEGED_PACKAGE);
mPluginActionManager.reloadPackage(PRIVILEGED_PACKAGE);
mFakeExecutor.runAllReady();
// Verify the old one was destroyed.
verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
verify(mMockPlugin).onDestroy();
// Also verify we got a second onCreate.
verify(mMockPlugin, Mockito.times(2)).onCreate(
ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any());
verify(mPluginInstance).onDestroy(mMockListener);
verify(mPluginInstance, Mockito.times(2))
.onCreate(mContext, mMockListener);
}
@Test
public void testNonDebuggable() throws Exception {
// Create a version that thinks the build is not debuggable.
mPluginInstanceManager = mInstanceManagerFactory.create("myAction", mMockListener,
true, mMockVersionInfo, false);
mPluginActionManager = mActionManagerFactory.create("myAction", mMockListener,
TestPlugin.class, true, false);
setupFakePmQuery();
mPluginInstanceManager.loadAll();
mPluginActionManager.loadAll();
mFakeExecutor.runAllReady();
@@ -193,22 +177,20 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
@Test
public void testNonDebuggable_privileged() throws Exception {
// Create a version that thinks the build is not debuggable.
PluginInstanceManager.Factory factory = new PluginInstanceManager.Factory(getContext(),
PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
mMockPm, mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager,
mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE));
factory.setInstanceFactory(mPluginInstanceFactory);
mPluginInstanceManager = factory.create("myAction", mMockListener,
true, mMockVersionInfo, false);
mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
mPluginInstanceFactory);
mPluginActionManager = factory.create("myAction", mMockListener,
TestPlugin.class, true, false);
setupFakePmQuery();
mPluginInstanceManager.loadAll();
mPluginActionManager.loadAll();
mFakeExecutor.runAllReady();
// Verify startup lifecycle
verify(mMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener).onPluginConnected(any(), any());
verify(mPluginInstance).onCreate(mContext, mMockListener);
}
@Test
@@ -216,12 +198,12 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
createPlugin(); // Get into valid created state.
// Start with an unrelated class.
boolean result = mPluginInstanceManager.checkAndDisable(Activity.class.getName());
boolean result = mPluginActionManager.checkAndDisable(Activity.class.getName());
assertFalse(result);
verify(mMockEnabler, never()).setDisabled(any(ComponentName.class), anyInt());
// Now hand it a real class and make sure it disables the plugin.
result = mPluginInstanceManager.checkAndDisable(TestPlugin.class.getName());
result = mPluginActionManager.checkAndDisable(TestPlugin.class.getName());
assertTrue(result);
verify(mMockEnabler).setDisabled(
mTestPluginComponentName, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH);
@@ -231,24 +213,24 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
public void testDisableAll() throws Exception {
createPlugin(); // Get into valid created state.
mPluginInstanceManager.disableAll();
mPluginActionManager.disableAll();
verify(mMockEnabler).setDisabled(
mTestPluginComponentName, PluginEnabler.DISABLED_FROM_SYSTEM_CRASH);
}
@Test
public void testDisableWhitelisted() throws Exception {
PluginInstanceManager.Factory factory = new PluginInstanceManager.Factory(getContext(),
public void testDisablePrivileged() throws Exception {
PluginActionManager.Factory factory = new PluginActionManager.Factory(getContext(),
mMockPm, mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager,
mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE));
factory.setInstanceFactory(mPluginInstanceFactory);
mPluginInstanceManager = factory.create("myAction", mMockListener,
true, mMockVersionInfo, false);
mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE),
mPluginInstanceFactory);
mPluginActionManager = factory.create("myAction", mMockListener,
TestPlugin.class, true, false);
createPlugin(); // Get into valid created state.
mPluginInstanceManager.disableAll();
mPluginActionManager.disableAll();
verify(mMockPm, never()).setComponentEnabledSetting(
ArgumentCaptor.forClass(ComponentName.class).capture(),
@@ -282,14 +264,14 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
private void createPlugin() throws Exception {
setupFakePmQuery();
mPluginInstanceManager.loadAll();
mPluginActionManager.loadAll();
mFakeExecutor.runAllReady();
}
// Real context with no registering/unregistering of receivers.
private static class MyContextWrapper extends SysuiTestableContext {
public MyContextWrapper(Context base) {
MyContextWrapper(Context base) {
super(base);
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2021 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.shared.plugins;
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 android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.annotations.Requires;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
@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 =
new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName());
private PluginInstance<TestPlugin> mPluginInstance;
private PluginInstance.Factory mPluginInstanceFactory;
private ApplicationInfo mAppInfo;
private Context mPluginContext;
@Mock
private PluginInstance.VersionChecker mVersionChecker;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mAppInfo = mContext.getApplicationInfo();
mAppInfo.packageName = mTestPluginComponentName.getPackageName();
when(mVersionChecker.checkVersion(any(), any(), any())).thenReturn(mVersionInfo);
mPluginInstanceFactory = new PluginInstance.Factory(
this.getClass().getClassLoader(),
new PluginInstance.InstanceFactory<TestPlugin>() {
@Override
TestPlugin create(Class cls) {
return mMockPlugin;
}
},
mVersionChecker,
Collections.singletonList(PRIVILEGED_PACKAGE),
false);
mPluginInstance = mPluginInstanceFactory.create(
mContext, mAppInfo, mTestPluginComponentName, TestPlugin.class);
mPluginContext = mPluginInstance.getPluginContext();
}
@Test
public void testCorrectVersion() {
assertNotNull(mPluginInstance);
}
@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));
mPluginInstanceFactory.create(
mContext, mAppInfo, wrongVersionTestPluginComponentName, TestPlugin.class);
}
@Test
public void testOnCreate() {
mPluginInstance.onCreate(mContext, mMockListener);
verify(mMockPlugin).onCreate(mContext, mPluginContext);
verify(mMockListener).onPluginConnected(mMockPlugin, mPluginContext);
}
@Test
public void testOnDestroy() {
mPluginInstance.onDestroy(mMockListener);
verify(mMockListener).onPluginDisconnected(mMockPlugin);
verify(mMockPlugin).onDestroy();
}
// This target class doesn't matter, it just needs to have a Requires to hit the flow where
// the mock version info is called.
@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
public interface TestPlugin extends Plugin {
int VERSION = 1;
String ACTION = "testAction";
}
@Requires(target = TestPlugin.class, version = TestPlugin.VERSION)
public static class TestPluginImpl implements TestPlugin {
@Override
public void onCreate(Context sysuiContext, Context pluginContext) {
}
}
}

View File

@@ -13,6 +13,7 @@
*/
package com.android.systemui.shared.plugins;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -54,10 +55,10 @@ public class PluginManagerTest extends SysuiTestCase {
private static final String PRIVILEGED_PACKAGE = "com.android.systemui";
private PluginInstanceManager.Factory mMockFactory;
private PluginInstanceManager<Plugin> mMockPluginInstance;
private PluginActionManager.Factory mMockFactory;
private PluginActionManager<TestPlugin> mMockPluginInstance;
private PluginManagerImpl mPluginManager;
private PluginListener<?> mMockListener;
private PluginListener<TestPlugin> mMockListener;
private PackageManager mMockPackageManager;
private PluginEnabler mPluginEnabler;
private PluginPrefs mPluginPrefs;
@@ -70,11 +71,11 @@ public class PluginManagerTest extends SysuiTestCase {
public void setup() throws Exception {
mRealExceptionHandler = Thread.getUncaughtExceptionPreHandler();
mMockExceptionHandler = mock(UncaughtExceptionHandler.class);
mMockFactory = mock(PluginInstanceManager.Factory.class);
mMockPluginInstance = mock(PluginInstanceManager.class);
mMockFactory = mock(PluginActionManager.Factory.class);
mMockPluginInstance = mock(PluginActionManager.class);
mPluginEnabler = mock(PluginEnabler.class);
mPluginPrefs = mock(PluginPrefs.class);
when(mMockFactory.create(any(), any(), Mockito.anyBoolean(), any(), Mockito.anyBoolean()))
when(mMockFactory.create(any(), any(), eq(TestPlugin.class), anyBoolean(), anyBoolean()))
.thenReturn(mMockPluginInstance);
mMockPackageManager = mock(PackageManager.class);
@@ -116,8 +117,8 @@ public class PluginManagerTest extends SysuiTestCase {
applicationInfo.sourceDir = sourceDir;
applicationInfo.packageName = PRIVILEGED_PACKAGE;
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(false),
any(VersionInfo.class), eq(false));
verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(TestPlugin.class),
eq(false), eq(false));
verify(mMockPluginInstance).loadAll();
}
@@ -138,8 +139,8 @@ public class PluginManagerTest extends SysuiTestCase {
invalidApplicationInfo.sourceDir = sourceDir;
invalidApplicationInfo.packageName = "com.android.invalidpackage";
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(false),
any(VersionInfo.class), eq(false));
verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(TestPlugin.class),
eq(false), eq(false));
verify(mMockPluginInstance).loadAll();
}

View File

@@ -30,24 +30,24 @@ public class FakePluginManager implements PluginManager {
@Override
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls, boolean allowMultiple) {
Class<T> cls, boolean allowMultiple) {
mLeakChecker.addCallback(listener);
}
@Override
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls) {
mLeakChecker.addCallback(listener);
}
@Override
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls,
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<T> cls,
boolean allowMultiple) {
mLeakChecker.addCallback(listener);
}
@Override
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
Class<?> cls) {
Class<T> cls) {
mLeakChecker.addCallback(listener);
}