Merge "1/N Move plugin loading into PluginInstanceManager." into sc-v2-dev

This commit is contained in:
Dave Mankoff
2021-08-17 13:07:49 +00:00
committed by Android (Google) Code Review
5 changed files with 319 additions and 321 deletions

View File

@@ -14,6 +14,7 @@
package com.android.systemui.shared.plugins; package com.android.systemui.shared.plugins;
import android.app.LoadedApk;
import android.app.Notification; import android.app.Notification;
import android.app.Notification.Action; import android.app.Notification.Action;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -28,6 +29,8 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -39,9 +42,12 @@ import com.android.systemui.plugins.PluginFragment;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException; import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
public class PluginInstanceManager<T extends Plugin> { public class PluginInstanceManager<T extends Plugin> {
@@ -56,33 +62,42 @@ public class PluginInstanceManager<T extends Plugin> {
private final String mAction; private final String mAction;
private final boolean mAllowMultiple; private final boolean mAllowMultiple;
private final VersionInfo mVersion; 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 @VisibleForTesting
private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>(); private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>();
private final boolean isDebuggable; private final boolean mIsDebuggable;
private final PackageManager mPm; private final PackageManager mPm;
private final PluginManagerImpl mManager;
private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>();
private final PluginInitializer mInitializer; private final PluginInitializer mInitializer;
private final Executor mMainExecutor; private final Executor mMainExecutor;
private final Executor mBgExecutor; private final Executor mBgExecutor;
PluginInstanceManager(Context context, PackageManager pm, String action, private PluginManagerImpl.ClassLoaderFilter mParentClassLoader;
private PluginInstanceManager(Context context, PackageManager pm, String action,
PluginListener<T> listener, boolean allowMultiple, Executor mainExecutor, PluginListener<T> listener, boolean allowMultiple, Executor mainExecutor,
Executor bgExecutor, VersionInfo version, PluginManagerImpl manager, boolean debuggable, Executor bgExecutor, VersionInfo version, boolean debuggable,
String[] pluginWhitelist, PluginInitializer initializer) { PluginInitializer initializer, NotificationManager notificationManager,
PluginEnabler pluginEnabler, List<String> privilegedPlugins,
InstanceFactory<T> instanceFactory) {
mInitializer = initializer; mInitializer = initializer;
mMainExecutor = mainExecutor; mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor; mBgExecutor = bgExecutor;
mManager = manager;
mContext = context; mContext = context;
mPm = pm; mPm = pm;
mAction = action; mAction = action;
mListener = listener; mListener = listener;
mAllowMultiple = allowMultiple; mAllowMultiple = allowMultiple;
mVersion = version; mVersion = version;
mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist)); mNotificationManager = notificationManager;
isDebuggable = debuggable; mPluginEnabler = pluginEnabler;
mInstanceFactory = instanceFactory;
mPrivilegedPlugins.addAll(privilegedPlugins);
mIsDebuggable = debuggable;
} }
public void loadAll() { public void loadAll() {
@@ -127,8 +142,22 @@ public class PluginInstanceManager<T extends Plugin> {
return disabledAny; return disabledAny;
} }
private boolean isPluginWhitelisted(ComponentName pluginName) { private boolean isPluginPackagePrivileged(String packageName) {
for (String componentNameOrPackage : mWhitelistedPlugins) { 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); ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
if (componentName == null) { if (componentName == null) {
if (componentNameOrPackage.equals(pluginName.getPackageName())) { if (componentNameOrPackage.equals(pluginName.getPackageName())) {
@@ -151,12 +180,12 @@ public class PluginInstanceManager<T extends Plugin> {
// If a plugin is detected in the stack of a crash then this will be called for that // 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 // plugin, if the plugin causing a crash cannot be identified, they are all disabled
// assuming one of them must be bad. // assuming one of them must be bad.
if (isPluginWhitelisted(pluginComponent)) { if (isPluginPrivileged(pluginComponent)) {
// Don't disable whitelisted plugins as they are a part of the OS. // Don't disable whitelisted plugins as they are a part of the OS.
return false; return false;
} }
Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString()); Log.w(TAG, "Disabling plugin " + pluginComponent.flattenToShortString());
mManager.getPluginEnabler().setDisabled(pluginComponent, reason); mPluginEnabler.setDisabled(pluginComponent, reason);
return true; return true;
} }
@@ -228,134 +257,164 @@ public class PluginInstanceManager<T extends Plugin> {
} }
} }
private void handleQueryPlugins(String pkgName) { private void handleQueryPlugins(String pkgName) {
// This isn't actually a service and shouldn't ever be started, but is // This isn't actually a service and shouldn't ever be started, but is
// a convenient PM based way to manage our plugins. // a convenient PM based way to manage our plugins.
Intent intent = new Intent(mAction); Intent intent = new Intent(mAction);
if (pkgName != null) { if (pkgName != null) {
intent.setPackage(pkgName); 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));
}
} }
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;
protected PluginInfo<T> handleLoadPlugin(ComponentName component) { // add plugin before sending PLUGIN_CONNECTED message
// This was already checked, but do it again here to make extra extra sure, we don't mPlugins.add(pluginInfo);
// use these on production builds. mMainExecutor.execute(() -> onPluginConnected(pluginInfo));
if (!isDebuggable && !isPluginWhitelisted(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);
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; return null;
} }
if (!mManager.getPluginEnabler().isEnabled(component)) { // Create our own ClassLoader so we can use our own code as the parent.
if (DEBUG) Log.d(TAG, "Plugin is not enabled, aborting load: " + component); ClassLoader classLoader = getClassLoader(info);
return null; Context pluginContext = new PluginContextWrapper(
} mContext.createApplicationContext(info, 0), classLoader);
String pkg = component.getPackageName(); Class<?> pluginClass = Class.forName(cls, true, classLoader);
String cls = component.getClassName(); // TODO: Only create the plugin before version check if we need it for
// legacy version check.
T plugin = mInstanceFactory.create(pluginClass);
try { try {
ApplicationInfo info = mPm.getApplicationInfo(pkg, 0); VersionInfo version = checkVersion(pluginClass, plugin, mVersion);
// TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on if (DEBUG) Log.d(TAG, "createPlugin");
if (mPm.checkPermission(PLUGIN_PERMISSION, pkg) return new PluginInfo<>(pkg, cls, plugin, pluginContext, version);
!= PackageManager.PERMISSION_GRANTED) { } catch (InvalidVersionException e) {
Log.d(TAG, "Plugin doesn't have permission: " + pkg); final int icon = Resources.getSystem().getIdentifier(
return null; "stat_sys_warning", "drawable", "android");
} final int color = Resources.getSystem().getIdentifier(
// Create our own ClassLoader so we can use our own code as the parent. "system_notification_accent_color", "color", "android");
ClassLoader classLoader = mManager.getClassLoader(info); final Notification.Builder nb = new Notification.Builder(mContext,
Context pluginContext = new PluginContextWrapper( PluginManager.NOTIFICATION_CHANNEL_ID)
mContext.createApplicationContext(info, 0), classLoader); .setStyle(new Notification.BigTextStyle())
Class<?> pluginClass = Class.forName(cls, true, classLoader); .setSmallIcon(icon)
// TODO: Only create the plugin before version check if we need it for .setWhen(0)
// legacy version check. .setShowWhen(false)
T plugin = (T) pluginClass.newInstance(); .setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color));
String label = cls;
try { try {
VersionInfo version = checkVersion(pluginClass, plugin, mVersion); label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString();
if (DEBUG) Log.d(TAG, "createPlugin"); } catch (NameNotFoundException e2) {
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());
mContext.getSystemService(NotificationManager.class)
.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) { if (!e.isTooNew()) {
Log.w(TAG, "Couldn't load plugin: " + pkg, e); // 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; 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);
} }
private VersionInfo checkVersion(Class<?> pluginClass, T plugin, VersionInfo version) List<String> zipPaths = new ArrayList<>();
throws InvalidVersionException { List<String> libPaths = new ArrayList<>();
VersionInfo pv = new VersionInfo().addClass(pluginClass); LoadedApk.makePaths(null, true, appInfo, zipPaths, libPaths);
if (pv.hasVersionInfo()) { ClassLoader classLoader = new PathClassLoader(
version.checkVersion(pv); TextUtils.join(File.pathSeparator, zipPaths),
} else { TextUtils.join(File.pathSeparator, libPaths),
int fallbackVersion = plugin.getVersion(); getParentClassLoader());
if (fallbackVersion != version.getDefaultVersion()) { mClassLoaders.put(appInfo.packageName, classLoader);
throw new InvalidVersionException("Invalid legacy version", false); return classLoader;
} }
return null;
} private ClassLoader getParentClassLoader() {
return pv; 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} * Construct a {@link PluginInstanceManager}
@@ -366,23 +425,40 @@ public class PluginInstanceManager<T extends Plugin> {
private final Executor mMainExecutor; private final Executor mMainExecutor;
private final Executor mBgExecutor; private final Executor mBgExecutor;
private final PluginInitializer mInitializer; 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, public Factory(Context context, PackageManager packageManager,
Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer) { Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
List<String> privilegedPlugins) {
mContext = context; mContext = context;
mPackageManager = packageManager; mPackageManager = packageManager;
mMainExecutor = mainExecutor; mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor; mBgExecutor = bgExecutor;
mInitializer = initializer; 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( <T extends Plugin> PluginInstanceManager<T> create(
String action, String action, PluginListener<T> listener, boolean allowMultiple,
PluginListener<T> listener, boolean allowMultiple, VersionInfo version, VersionInfo version, boolean debuggable) {
PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist) { return new PluginInstanceManager<T>(mContext, mPackageManager, action, listener,
return new PluginInstanceManager<>(mContext, mPackageManager, action, listener, allowMultiple, mMainExecutor, mBgExecutor, version, debuggable,
allowMultiple, mMainExecutor, mBgExecutor, version, manager, debuggable, mInitializer, mNotificationManager, mPluginEnabler,
pluginWhitelist, mInitializer); mPrivilegedPlugins, (InstanceFactory<T>) mInstanceFactory);
} }
} }
@@ -428,4 +504,10 @@ public class PluginInstanceManager<T extends Plugin> {
mVersion = info; mVersion = info;
} }
} }
static class InstanceFactory<T extends Plugin> {
T create(Class cls) throws IllegalAccessException, InstantiationException {
return (T) cls.newInstance();
}
}
} }

View File

@@ -14,24 +14,15 @@
package com.android.systemui.shared.plugins; 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.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.SystemProperties; import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log; import android.util.Log;
@@ -41,14 +32,9 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler; import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -64,16 +50,13 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
private final ArrayMap<PluginListener<?>, PluginInstanceManager<?>> mPluginMap private final ArrayMap<PluginListener<?>, PluginInstanceManager<?>> mPluginMap
= new ArrayMap<>(); = new ArrayMap<>();
private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>(); private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
private final ArraySet<String> mOneShotPackages = new ArraySet<>();
private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>(); private final ArraySet<String> mPrivilegedPlugins = new ArraySet<>();
private final Context mContext; private final Context mContext;
private final PluginInstanceManager.Factory mInstanceManagerFactory; private final PluginInstanceManager.Factory mInstanceManagerFactory;
private final boolean mIsDebuggable; private final boolean mIsDebuggable;
private final PluginPrefs mPluginPrefs; private final PluginPrefs mPluginPrefs;
private final PluginEnabler mPluginEnabler; private final PluginEnabler mPluginEnabler;
private ClassLoaderFilter mParentClassLoader;
private boolean mListening; private boolean mListening;
private boolean mHasOneShot;
public PluginManagerImpl(Context context, public PluginManagerImpl(Context context,
PluginInstanceManager.Factory instanceManagerFactory, PluginInstanceManager.Factory instanceManagerFactory,
@@ -81,11 +64,11 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
Optional<UncaughtExceptionHandler> defaultHandlerOptional, Optional<UncaughtExceptionHandler> defaultHandlerOptional,
PluginEnabler pluginEnabler, PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs, PluginPrefs pluginPrefs,
String[] privilegedPlugins) { List<String> privilegedPlugins) {
mContext = context; mContext = context;
mInstanceManagerFactory = instanceManagerFactory; mInstanceManagerFactory = instanceManagerFactory;
mIsDebuggable = debuggable; mIsDebuggable = debuggable;
mPrivilegedPlugins.addAll(Arrays.asList(privilegedPlugins)); mPrivilegedPlugins.addAll(privilegedPlugins);
mPluginPrefs = pluginPrefs; mPluginPrefs = pluginPrefs;
mPluginEnabler = pluginEnabler; mPluginEnabler = pluginEnabler;
@@ -102,10 +85,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
return mPrivilegedPlugins.toArray(new String[0]); return mPrivilegedPlugins.toArray(new String[0]);
} }
public PluginEnabler getPluginEnabler() {
return mPluginEnabler;
}
public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) { public <T extends Plugin> void addPluginListener(PluginListener<T> listener, Class<?> cls) {
addPluginListener(listener, cls, false); addPluginListener(listener, cls, false);
} }
@@ -124,8 +103,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
Class<?> cls, boolean allowMultiple) { Class<?> cls, boolean allowMultiple) {
mPluginPrefs.addAction(action); mPluginPrefs.addAction(action);
PluginInstanceManager<T> p = mInstanceManagerFactory.create(action, listener, allowMultiple, PluginInstanceManager<T> p = mInstanceManagerFactory.create(action, listener, allowMultiple,
new VersionInfo().addClass(cls), this, isDebuggable(), new VersionInfo().addClass(cls), isDebuggable());
getPrivilegedPlugins());
p.loadAll(); p.loadAll();
synchronized (this) { synchronized (this) {
mPluginMap.put(listener, p); mPluginMap.put(listener, p);
@@ -163,8 +141,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
} }
private void stopListening() { private void stopListening() {
// Never stop listening if a one-shot is present. if (!mListening) return;
if (!mListening || mHasOneShot) return;
mListening = false; mListening = false;
mContext.unregisterReceiver(this); mContext.unregisterReceiver(this);
} }
@@ -185,42 +162,13 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
// Don't disable privileged plugins as they are a part of the OS. // Don't disable privileged plugins as they are a part of the OS.
return; return;
} }
getPluginEnabler().setDisabled(component, PluginEnabler.DISABLED_INVALID_VERSION); mPluginEnabler.setDisabled(component, PluginEnabler.DISABLED_INVALID_VERSION);
mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(), mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(),
SystemMessage.NOTE_PLUGIN); SystemMessage.NOTE_PLUGIN);
} else { } else {
Uri data = intent.getData(); Uri data = intent.getData();
String pkg = data.getEncodedSchemeSpecificPart(); String pkg = data.getEncodedSchemeSpecificPart();
ComponentName componentName = ComponentName.unflattenFromString(pkg); ComponentName componentName = ComponentName.unflattenFromString(pkg);
if (mOneShotPackages.contains(pkg)) {
int icon = Resources.getSystem().getIdentifier(
"stat_sys_warning", "drawable", "android");
int color = Resources.getSystem().getIdentifier(
"system_notification_accent_color", "color", "android");
String label = pkg;
try {
PackageManager pm = mContext.getPackageManager();
label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
} catch (NameNotFoundException e) {
}
// Localization not required as this will never ever appear in a user build.
final Notification.Builder nb =
new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(icon)
.setWhen(0)
.setShowWhen(false)
.setPriority(Notification.PRIORITY_MAX)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color))
.setContentTitle("Plugin \"" + label + "\" has updated")
.setContentText("Restart SysUI for changes to take effect.");
Intent i = new Intent("com.android.systemui.action.RESTART").setData(
Uri.parse("package://" + pkg));
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, PendingIntent.FLAG_MUTABLE_UNAUDITED);
nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
mContext.getSystemService(NotificationManager.class)
.notify(SystemMessage.NOTE_PLUGIN, nb.build());
}
if (clearClassLoader(pkg)) { if (clearClassLoader(pkg)) {
if (Build.IS_ENG) { if (Build.IS_ENG) {
Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show(); Toast.makeText(mContext, "Reloading " + pkg, Toast.LENGTH_LONG).show();
@@ -231,13 +179,13 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction()) if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())
&& componentName != null) { && componentName != null) {
@PluginEnabler.DisableReason int disableReason = @PluginEnabler.DisableReason int disableReason =
getPluginEnabler().getDisableReason(componentName); mPluginEnabler.getDisableReason(componentName);
if (disableReason == PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH if (disableReason == PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH
|| disableReason == PluginEnabler.DISABLED_FROM_SYSTEM_CRASH || disableReason == PluginEnabler.DISABLED_FROM_SYSTEM_CRASH
|| disableReason == PluginEnabler.DISABLED_INVALID_VERSION) { || disableReason == PluginEnabler.DISABLED_INVALID_VERSION) {
Log.i(TAG, "Re-enabling previously disabled plugin that has been " Log.i(TAG, "Re-enabling previously disabled plugin that has been "
+ "updated: " + componentName.flattenToShortString()); + "updated: " + componentName.flattenToShortString());
getPluginEnabler().setEnabled(componentName); mPluginEnabler.setEnabled(componentName);
} }
} }
synchronized (this) { synchronized (this) {
@@ -254,41 +202,10 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
} }
} }
/** 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 boolean clearClassLoader(String pkg) { private boolean clearClassLoader(String pkg) {
return mClassLoaders.remove(pkg) != null; return mClassLoaders.remove(pkg) != null;
} }
ClassLoader getParentClassLoader() {
if (mParentClassLoader == null) {
// Lazily load this so it doesn't have any effect on devices without plugins.
mParentClassLoader = new ClassLoaderFilter(getClass().getClassLoader(),
"com.android.systemui.plugin");
}
return mParentClassLoader;
}
public <T> boolean dependsOn(Plugin p, Class<T> cls) { public <T> boolean dependsOn(Plugin p, Class<T> cls) {
synchronized (this) { synchronized (this) {
for (int i = 0; i < mPluginMap.size(); i++) { for (int i = 0; i < mPluginMap.size(); i++) {
@@ -310,20 +227,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
} }
} }
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) { private boolean isPluginPrivileged(ComponentName pluginName) {
for (String componentNameOrPackage : mPrivilegedPlugins) { for (String componentNameOrPackage : mPrivilegedPlugins) {
ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage); ComponentName componentName = ComponentName.unflattenFromString(componentNameOrPackage);
@@ -340,7 +243,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
// This allows plugins to include any libraries or copied code they want by only including // This allows plugins to include any libraries or copied code they want by only including
// classes from the plugin library. // classes from the plugin library.
private static class ClassLoaderFilter extends ClassLoader { static class ClassLoaderFilter extends ClassLoader {
private final String mPackage; private final String mPackage;
private final ClassLoader mBase; private final ClassLoader mBase;

View File

@@ -18,6 +18,7 @@ package com.android.systemui.plugins;
import static com.android.systemui.util.concurrency.GlobalConcurrencyModule.PRE_HANDLER; import static com.android.systemui.util.concurrency.GlobalConcurrencyModule.PRE_HANDLER;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
@@ -32,6 +33,8 @@ import com.android.systemui.shared.plugins.PluginPrefs;
import com.android.systemui.util.concurrency.GlobalConcurrencyModule; import com.android.systemui.util.concurrency.GlobalConcurrencyModule;
import com.android.systemui.util.concurrency.ThreadFactory; import com.android.systemui.util.concurrency.ThreadFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -70,9 +73,12 @@ public abstract class PluginsModule {
@Singleton @Singleton
static PluginInstanceManager.Factory providePluginInstanceManagerFactory(Context context, static PluginInstanceManager.Factory providePluginInstanceManagerFactory(Context context,
PackageManager packageManager, @Main Executor mainExecutor, PackageManager packageManager, @Main Executor mainExecutor,
@Named(PLUGIN_THREAD) Executor pluginExecutor, PluginInitializer initializer) { @Named(PLUGIN_THREAD) Executor pluginExecutor, PluginInitializer initializer,
NotificationManager notificationManager, PluginEnabler pluginEnabler,
@Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) {
return new PluginInstanceManager.Factory( return new PluginInstanceManager.Factory(
context, packageManager, mainExecutor, pluginExecutor, initializer); context, packageManager, mainExecutor, pluginExecutor, initializer,
notificationManager, pluginEnabler, privilegedPlugins);
} }
@Provides @Provides
@@ -91,7 +97,7 @@ public abstract class PluginsModule {
Optional<Thread.UncaughtExceptionHandler> uncaughtExceptionHandlerOptional, Optional<Thread.UncaughtExceptionHandler> uncaughtExceptionHandlerOptional,
PluginEnabler pluginEnabler, PluginEnabler pluginEnabler,
PluginPrefs pluginPrefs, PluginPrefs pluginPrefs,
@Named(PLUGIN_PRIVILEGED) String[] privilegedPlugins) { @Named(PLUGIN_PRIVILEGED) List<String> privilegedPlugins) {
return new PluginManagerImpl(context, instanceManagerFactory, debug, return new PluginManagerImpl(context, instanceManagerFactory, debug,
uncaughtExceptionHandlerOptional, pluginEnabler, pluginPrefs, uncaughtExceptionHandlerOptional, pluginEnabler, pluginPrefs,
privilegedPlugins); privilegedPlugins);
@@ -104,7 +110,7 @@ public abstract class PluginsModule {
@Provides @Provides
@Named(PLUGIN_PRIVILEGED) @Named(PLUGIN_PRIVILEGED)
static String[] providesPrivilegedPlugins(PluginInitializer initializer, Context context) { static List<String> providesPrivilegedPlugins(PluginInitializer initializer, Context context) {
return initializer.getPrivilegedPlugins(context); return Arrays.asList(initializer.getPrivilegedPlugins(context));
} }
} }

View File

@@ -31,7 +31,6 @@ import android.app.NotificationManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
@@ -44,6 +43,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.plugins.Plugin; import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.annotations.Requires; import com.android.systemui.plugins.annotations.Requires;
@@ -51,12 +51,12 @@ import com.android.systemui.shared.plugins.VersionInfo.InvalidVersionException;
import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.time.FakeSystemClock;
import org.junit.After;
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.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -66,43 +66,46 @@ import java.util.List;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class PluginInstanceManagerTest extends SysuiTestCase { public class PluginInstanceManagerTest extends SysuiTestCase {
private static final String WHITELISTED_PACKAGE = "com.android.systemui"; private static final String PRIVILEGED_PACKAGE = "com.android.systemui.shared.plugins";
// Static since the plugin needs to be generated by the PluginInstanceManager using newInstance. private TestPlugin mMockPlugin;
private static Plugin sMockPlugin;
private Context mContextWrapper;
private PackageManager mMockPm; private PackageManager mMockPm;
private PluginListener mMockListener; private PluginListener<Plugin> mMockListener;
private PluginInstanceManager mPluginInstanceManager; private PluginInstanceManager<Plugin> mPluginInstanceManager;
private PluginManagerImpl mMockManager;
private VersionInfo mMockVersionInfo; private VersionInfo mMockVersionInfo;
private PluginEnabler mMockEnabler; private PluginEnabler mMockEnabler;
ComponentName mTestPluginComponentName = ComponentName mTestPluginComponentName =
new ComponentName(WHITELISTED_PACKAGE, TestPlugin.class.getName()); new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
private PluginInitializer mInitializer; private PluginInitializer mInitializer;
private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
NotificationManager mNotificationManager;
private PluginInstanceManager.Factory mInstanceManagerFactory;
private final PluginInstanceManager.InstanceFactory<Plugin> mPluginInstanceFactory =
new PluginInstanceManager.InstanceFactory<Plugin>() {
@Override
Plugin create(Class cls) {
return mMockPlugin;
}
};
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
mContextWrapper = new MyContextWrapper(getContext()); mContext = new MyContextWrapper(mContext);
mMockPm = mock(PackageManager.class); mMockPm = mock(PackageManager.class);
mMockListener = mock(PluginListener.class); mMockListener = mock(PluginListener.class);
mMockManager = mock(PluginManagerImpl.class);
when(mMockManager.getClassLoader(any())).thenReturn(getClass().getClassLoader());
mMockEnabler = mock(PluginEnabler.class); mMockEnabler = mock(PluginEnabler.class);
when(mMockManager.getPluginEnabler()).thenReturn(mMockEnabler);
mMockVersionInfo = mock(VersionInfo.class); mMockVersionInfo = mock(VersionInfo.class);
mInitializer = mock(PluginInitializer.class); mInitializer = mock(PluginInitializer.class);
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", mNotificationManager = mock(NotificationManager.class);
mMockListener, true, mFakeExecutor, mFakeExecutor, mMockPlugin = mock(TestPlugin.class);
mMockVersionInfo, mMockManager, true, new String[0], mInitializer); mInstanceManagerFactory = new PluginInstanceManager.Factory(getContext(), mMockPm,
sMockPlugin = mock(Plugin.class); mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager, mMockEnabler,
when(sMockPlugin.getVersion()).thenReturn(1); new ArrayList<>())
} .setInstanceFactory(mPluginInstanceFactory);
@After mPluginInstanceManager = mInstanceManagerFactory.create("myAction", mMockListener,
public void tearDown() { true, mMockVersionInfo, true);
sMockPlugin = null; when(mMockPlugin.getVersion()).thenReturn(1);
} }
@Test @Test
@@ -121,7 +124,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
createPlugin(); createPlugin();
// Verify startup lifecycle // Verify startup lifecycle
verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(), verify(mMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture()); ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener).onPluginConnected(any(), any()); verify(mMockListener).onPluginConnected(any(), any());
} }
@@ -137,13 +140,11 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
// Verify shutdown lifecycle // Verify shutdown lifecycle
verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture()); verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
verify(sMockPlugin).onDestroy(); verify(mMockPlugin).onDestroy();
} }
@Test @Test
public void testIncorrectVersion() throws Exception { public void testIncorrectVersion() throws Exception {
NotificationManager nm = mock(NotificationManager.class);
mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm);
setupFakePmQuery(); setupFakePmQuery();
doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any()); doThrow(new InvalidVersionException("", false)).when(mMockVersionInfo).checkVersion(any());
@@ -151,25 +152,24 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Plugin shouldn't be connected because it is the wrong version. // Plugin shouldn't be connected because it is the wrong version.
verify(mMockListener, never()).onPluginConnected(any(), any()); verify(mMockListener, never()).onPluginConnected(any(), any());
verify(nm).notify(eq(SystemMessage.NOTE_PLUGIN), any()); verify(mNotificationManager).notify(eq(SystemMessage.NOTE_PLUGIN), any());
} }
@Test @Test
public void testReloadOnChange() throws Exception { public void testReloadOnChange() throws Exception {
createPlugin(); // Get into valid created state. createPlugin(); // Get into valid created state.
mPluginInstanceManager.onPackageChange("com.android.systemui"); mPluginInstanceManager.onPackageChange(PRIVILEGED_PACKAGE);
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Verify the old one was destroyed. // Verify the old one was destroyed.
verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture()); verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture());
verify(sMockPlugin).onDestroy(); verify(mMockPlugin).onDestroy();
// Also verify we got a second onCreate. // Also verify we got a second onCreate.
verify(sMockPlugin, Mockito.times(2)).onCreate( verify(mMockPlugin, Mockito.times(2)).onCreate(
ArgumentCaptor.forClass(Context.class).capture(), ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture()); ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any()); verify(mMockListener, Mockito.times(2)).onPluginConnected(any(), any());
@@ -178,9 +178,8 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
@Test @Test
public void testNonDebuggable() throws Exception { public void testNonDebuggable() throws Exception {
// Create a version that thinks the build is not debuggable. // Create a version that thinks the build is not debuggable.
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", mPluginInstanceManager = mInstanceManagerFactory.create("myAction", mMockListener,
mMockListener, true, mFakeExecutor, mFakeExecutor, true, mMockVersionInfo, false);
mMockVersionInfo, mMockManager, false, new String[0], mInitializer);
setupFakePmQuery(); setupFakePmQuery();
mPluginInstanceManager.loadAll(); mPluginInstanceManager.loadAll();
@@ -192,12 +191,14 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
} }
@Test @Test
public void testNonDebuggable_whitelist() throws Exception { public void testNonDebuggable_privileged() throws Exception {
// Create a version that thinks the build is not debuggable. // Create a version that thinks the build is not debuggable.
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", PluginInstanceManager.Factory factory = new PluginInstanceManager.Factory(getContext(),
mMockListener, true, mFakeExecutor, mFakeExecutor, mMockPm, mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager,
mMockVersionInfo, mMockManager, false, mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE));
new String[] {WHITELISTED_PACKAGE}, mInitializer); factory.setInstanceFactory(mPluginInstanceFactory);
mPluginInstanceManager = factory.create("myAction", mMockListener,
true, mMockVersionInfo, false);
setupFakePmQuery(); setupFakePmQuery();
mPluginInstanceManager.loadAll(); mPluginInstanceManager.loadAll();
@@ -205,7 +206,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
mFakeExecutor.runAllReady(); mFakeExecutor.runAllReady();
// Verify startup lifecycle // Verify startup lifecycle
verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(), verify(mMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
ArgumentCaptor.forClass(Context.class).capture()); ArgumentCaptor.forClass(Context.class).capture());
verify(mMockListener).onPluginConnected(any(), any()); verify(mMockListener).onPluginConnected(any(), any());
} }
@@ -238,10 +239,13 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
@Test @Test
public void testDisableWhitelisted() throws Exception { public void testDisableWhitelisted() throws Exception {
mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", PluginInstanceManager.Factory factory = new PluginInstanceManager.Factory(getContext(),
mMockListener, true, mFakeExecutor, mFakeExecutor, mMockPm, mFakeExecutor, mFakeExecutor, mInitializer, mNotificationManager,
mMockVersionInfo, mMockManager, false, new String[] {WHITELISTED_PACKAGE}, mMockEnabler, Collections.singletonList(PRIVILEGED_PACKAGE));
mInitializer); factory.setInstanceFactory(mPluginInstanceFactory);
mPluginInstanceManager = factory.create("myAction", mMockListener,
true, mMockVersionInfo, false);
createPlugin(); // Get into valid created state. createPlugin(); // Get into valid created state.
mPluginInstanceManager.disableAll(); mPluginInstanceManager.disableAll();
@@ -266,9 +270,12 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn( when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn(
PackageManager.PERMISSION_GRANTED); PackageManager.PERMISSION_GRANTED);
ApplicationInfo appInfo = getContext().getApplicationInfo(); when(mMockPm.getApplicationInfo(Mockito.anyString(), anyInt())).thenAnswer(
when(mMockPm.getApplicationInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn( (Answer<ApplicationInfo>) invocation -> {
appInfo); ApplicationInfo appInfo = getContext().getApplicationInfo();
appInfo.packageName = invocation.getArgument(0);
return appInfo;
});
when(mMockEnabler.isEnabled(mTestPluginComponentName)).thenReturn(true); when(mMockEnabler.isEnabled(mTestPluginComponentName)).thenReturn(true);
} }
@@ -281,7 +288,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
} }
// Real context with no registering/unregistering of receivers. // Real context with no registering/unregistering of receivers.
private static class MyContextWrapper extends ContextWrapper { private static class MyContextWrapper extends SysuiTestableContext {
public MyContextWrapper(Context base) { public MyContextWrapper(Context base) {
super(base); super(base);
} }
@@ -307,17 +314,15 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
public static class TestPlugin implements Plugin { public static class TestPlugin implements Plugin {
@Override @Override
public int getVersion() { public int getVersion() {
return sMockPlugin.getVersion(); return 1;
} }
@Override @Override
public void onCreate(Context sysuiContext, Context pluginContext) { public void onCreate(Context sysuiContext, Context pluginContext) {
sMockPlugin.onCreate(sysuiContext, pluginContext);
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
sMockPlugin.onDestroy();
} }
} }
} }

View File

@@ -13,9 +13,8 @@
*/ */
package com.android.systemui.shared.plugins; package com.android.systemui.shared.plugins;
import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.any;
import static org.junit.Assert.assertNull; import static org.mockito.Mockito.eq;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -44,6 +43,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.lang.Thread.UncaughtExceptionHandler; import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional; import java.util.Optional;
@SmallTest @SmallTest
@@ -51,10 +52,10 @@ import java.util.Optional;
@RunWithLooper @RunWithLooper
public class PluginManagerTest extends SysuiTestCase { public class PluginManagerTest extends SysuiTestCase {
private static final String WHITELISTED_PACKAGE = "com.android.systemui"; private static final String PRIVILEGED_PACKAGE = "com.android.systemui";
private PluginInstanceManager.Factory mMockFactory; private PluginInstanceManager.Factory mMockFactory;
private PluginInstanceManager mMockPluginInstance; private PluginInstanceManager<Plugin> mMockPluginInstance;
private PluginManagerImpl mPluginManager; private PluginManagerImpl mPluginManager;
private PluginListener<?> mMockListener; private PluginListener<?> mMockListener;
private PackageManager mMockPackageManager; private PackageManager mMockPackageManager;
@@ -73,16 +74,14 @@ public class PluginManagerTest extends SysuiTestCase {
mMockPluginInstance = mock(PluginInstanceManager.class); mMockPluginInstance = mock(PluginInstanceManager.class);
mPluginEnabler = mock(PluginEnabler.class); mPluginEnabler = mock(PluginEnabler.class);
mPluginPrefs = mock(PluginPrefs.class); mPluginPrefs = mock(PluginPrefs.class);
when(mMockFactory.create(Mockito.any(), Mockito.any(), when(mMockFactory.create(any(), any(), Mockito.anyBoolean(), any(), Mockito.anyBoolean()))
Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.anyBoolean(),
Mockito.any()))
.thenReturn(mMockPluginInstance); .thenReturn(mMockPluginInstance);
mMockPackageManager = mock(PackageManager.class); mMockPackageManager = mock(PackageManager.class);
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, true, getContext(), mMockFactory, true,
Optional.of(mMockExceptionHandler), mPluginEnabler, Optional.of(mMockExceptionHandler), mPluginEnabler,
mPluginPrefs, new String[0]); mPluginPrefs, new ArrayList<>());
resetExceptionHandler(); resetExceptionHandler();
mMockListener = mock(PluginListener.class); mMockListener = mock(PluginListener.class);
@@ -105,46 +104,49 @@ public class PluginManagerTest extends SysuiTestCase {
@Test @Test
@RunWithLooper(setAsMainLooper = true) @RunWithLooper(setAsMainLooper = true)
public void testNonDebuggable_noWhitelist() { public void testNonDebuggable_nonPrivileged() {
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, false, getContext(), mMockFactory, false,
Optional.of(mMockExceptionHandler), mPluginEnabler, Optional.of(mMockExceptionHandler), mPluginEnabler,
mPluginPrefs, new String[0]); mPluginPrefs, new ArrayList<>());
resetExceptionHandler(); resetExceptionHandler();
String sourceDir = "myPlugin"; String sourceDir = "myPlugin";
ApplicationInfo applicationInfo = new ApplicationInfo(); ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.sourceDir = sourceDir; applicationInfo.sourceDir = sourceDir;
applicationInfo.packageName = WHITELISTED_PACKAGE; applicationInfo.packageName = PRIVILEGED_PACKAGE;
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class); mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
assertNull(mPluginManager.getClassLoader(applicationInfo)); verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(false),
any(VersionInfo.class), eq(false));
verify(mMockPluginInstance).loadAll();
} }
@Test @Test
@RunWithLooper(setAsMainLooper = true) @RunWithLooper(setAsMainLooper = true)
public void testNonDebuggable_whitelistedPkg() { public void testNonDebuggable_privilegedPackage() {
mPluginManager = new PluginManagerImpl( mPluginManager = new PluginManagerImpl(
getContext(), mMockFactory, false, getContext(), mMockFactory, false,
Optional.of(mMockExceptionHandler), mPluginEnabler, Optional.of(mMockExceptionHandler), mPluginEnabler,
mPluginPrefs, new String[] {WHITELISTED_PACKAGE}); mPluginPrefs, Collections.singletonList(PRIVILEGED_PACKAGE));
resetExceptionHandler(); resetExceptionHandler();
String sourceDir = "myPlugin"; String sourceDir = "myPlugin";
ApplicationInfo whiteListedApplicationInfo = new ApplicationInfo(); ApplicationInfo privilegedApplicationInfo = new ApplicationInfo();
whiteListedApplicationInfo.sourceDir = sourceDir; privilegedApplicationInfo.sourceDir = sourceDir;
whiteListedApplicationInfo.packageName = WHITELISTED_PACKAGE; privilegedApplicationInfo.packageName = PRIVILEGED_PACKAGE;
ApplicationInfo invalidApplicationInfo = new ApplicationInfo(); ApplicationInfo invalidApplicationInfo = new ApplicationInfo();
invalidApplicationInfo.sourceDir = sourceDir; invalidApplicationInfo.sourceDir = sourceDir;
invalidApplicationInfo.packageName = "com.android.invalidpackage"; invalidApplicationInfo.packageName = "com.android.invalidpackage";
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class); mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
assertNotNull(mPluginManager.getClassLoader(whiteListedApplicationInfo)); verify(mMockFactory).create(eq("myAction"), eq(mMockListener), eq(false),
assertNull(mPluginManager.getClassLoader(invalidApplicationInfo)); any(VersionInfo.class), eq(false));
verify(mMockPluginInstance).loadAll();
} }
@Test @Test
public void testExceptionHandler_foundPlugin() { public void testExceptionHandler_foundPlugin() {
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class); mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true); when(mMockPluginInstance.checkAndDisable(any())).thenReturn(true);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable()); mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());
@@ -159,7 +161,7 @@ public class PluginManagerTest extends SysuiTestCase {
@Test @Test
public void testExceptionHandler_noFoundPlugin() { public void testExceptionHandler_noFoundPlugin() {
mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class); mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false); when(mMockPluginInstance.checkAndDisable(any())).thenReturn(false);
mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable()); mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable());