From ecc3306fdc7a988962984dd2cf09eabed673eaa2 Mon Sep 17 00:00:00 2001 From: Dave Mankoff Date: Thu, 12 Aug 2021 17:31:12 -0400 Subject: [PATCH] 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 --- .../shared/plugins/PluginActionManager.java | 438 +++++++++++++++ .../shared/plugins/PluginInitializer.java | 2 +- .../shared/plugins/PluginInstance.java | 228 ++++++++ .../shared/plugins/PluginInstanceManager.java | 513 ------------------ .../shared/plugins/PluginManager.java | 10 +- .../shared/plugins/PluginManagerImpl.java | 40 +- .../systemui/shared/plugins/VersionInfo.java | 12 + .../systemui/PluginInflateContainer.java | 4 +- .../systemui/plugins/PluginsModule.java | 28 +- .../systemui/tuner/PluginFragment.java | 4 +- ...Test.java => PluginActionManagerTest.java} | 136 ++--- .../shared/plugins/PluginInstanceTest.java | 138 +++++ .../shared/plugins/PluginManagerTest.java | 21 +- .../utils/leaks/FakePluginManager.java | 8 +- 14 files changed, 945 insertions(+), 637 deletions(-) create mode 100644 packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java create mode 100644 packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java delete mode 100644 packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java rename packages/SystemUI/tests/src/com/android/systemui/shared/plugins/{PluginInstanceManagerTest.java => PluginActionManagerTest.java} (65%) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java new file mode 100644 index 0000000000000..af541f07fb68a --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java @@ -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 The type of plugin that this contains. + */ +public class PluginActionManager { + + 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 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 mPrivilegedPlugins = new ArraySet<>(); + + @VisibleForTesting + private final ArrayList> mPluginInstances = new ArrayList<>(); + private final boolean mIsDebuggable; + private final PackageManager mPm; + private final Class mPluginClass; + private final PluginInitializer mInitializer; + private final Executor mMainExecutor; + private final Executor mBgExecutor; + + private PluginActionManager( + Context context, + PackageManager pm, + String action, + PluginListener listener, + Class pluginClass, + boolean allowMultiple, + Executor mainExecutor, + Executor bgExecutor, + boolean debuggable, + PluginInitializer initializer, + NotificationManager notificationManager, + PluginEnabler pluginEnabler, + List 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> plugins = new ArrayList<>(mPluginInstances); + for (PluginInstance 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> plugins = new ArrayList<>(mPluginInstances); + for (PluginInstance 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> 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 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; + } + + boolean dependsOn(Plugin p, Class cls) { + ArrayList> instances = new ArrayList<>(mPluginInstances); + for (PluginInstance 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 pluginInstance) { + if (DEBUG) Log.d(TAG, "onPluginConnected"); + PluginPrefs.setHasPlugins(mContext); + mInitializer.handleWtfs(); + pluginInstance.onCreate(mContext, mListener); + } + + private void onPluginDisconnected(PluginInstance 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 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 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 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 pluginInstance = loadPluginComponent(name); + if (pluginInstance != null) { + // add plugin before sending PLUGIN_CONNECTED message + mPluginInstances.add(pluginInstance); + mMainExecutor.execute(() -> onPluginConnected(pluginInstance)); + } + } + } + + private PluginInstance 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 mPrivilegedPlugins; + private final PluginInstance.Factory mPluginInstanceFactory; + + public Factory(Context context, PackageManager packageManager, + Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer, + NotificationManager notificationManager, PluginEnabler pluginEnabler, + List privilegedPlugins, PluginInstance.Factory pluginInstanceFactory) { + mContext = context; + mPackageManager = packageManager; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; + mInitializer = initializer; + mNotificationManager = notificationManager; + mPluginEnabler = pluginEnabler; + mPrivilegedPlugins = privilegedPlugins; + mPluginInstanceFactory = pluginInstanceFactory; + } + + PluginActionManager create( + String action, PluginListener listener, Class 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); + } + } + +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java index 895b6cd96d6fe..f0e0320e165f1 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInitializer.java @@ -28,7 +28,7 @@ public interface PluginInitializer { /** - * Called from {@link PluginInstanceManager}. + * Called from {@link PluginActionManager}. */ void handleWtfs(); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java new file mode 100644 index 0000000000000..2f84602089e0e --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java @@ -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 The type of plugin that this contains. + */ +public class PluginInstance { + private static final String TAG = "PluginInstance"; + private static final Map 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 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 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 mPrivilegedPlugins; + + /** Factory used to construct {@link PluginInstance}s. */ + public Factory(ClassLoader classLoader, InstanceFactory instanceFactory, + VersionChecker versionChecker, + List privilegedPlugins, + boolean isDebug) { + mPrivilegedPlugins = privilegedPlugins; + mBaseClassLoader = classLoader; + mInstanceFactory = instanceFactory; + mVersionChecker = versionChecker; + mIsDebug = isDebug; + } + + /** Construct a new PluginInstance. */ + public PluginInstance create( + Context context, + ApplicationInfo appInfo, + ComponentName componentName, + Class pluginClass) + throws PackageManager.NameNotFoundException, ClassNotFoundException, + InstantiationException, IllegalAccessException { + + ClassLoader classLoader = getClassLoader(appInfo, mBaseClassLoader); + Context pluginContext = new PluginActionManager.PluginContextWrapper( + context.createApplicationContext(appInfo, 0), classLoader); + Class instanceClass = (Class) Class.forName( + componentName.getClassName(), true, classLoader); + // TODO: Only create the plugin before version check if we need it for + // legacy version check. + T instance = (T) mInstanceFactory.create(instanceClass); + VersionInfo version = mVersionChecker.checkVersion( + instanceClass, pluginClass, instance); + return new PluginInstance(componentName, instance, pluginContext, version); + } + + 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 zipPaths = new ArrayList<>(); + List 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 VersionInfo checkVersion( + Class instanceClass, Class 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 The type of plugin this create. + **/ + public static class InstanceFactory { + T create(Class cls) throws IllegalAccessException, InstantiationException { + return (T) cls.newInstance(); + } + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java deleted file mode 100644 index dcd3b3eb5dd41..0000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstanceManager.java +++ /dev/null @@ -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 { - - 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 mListener; - private final String mAction; - private final boolean mAllowMultiple; - private final VersionInfo mVersion; - private final NotificationManager mNotificationManager; - private final PluginEnabler mPluginEnabler; - private final InstanceFactory mInstanceFactory; - private final ArraySet mPrivilegedPlugins = new ArraySet<>(); - private final Map mClassLoaders = new ArrayMap<>(); - - @VisibleForTesting - private final ArrayList> 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 listener, boolean allowMultiple, Executor mainExecutor, - Executor bgExecutor, VersionInfo version, boolean debuggable, - PluginInitializer initializer, NotificationManager notificationManager, - PluginEnabler pluginEnabler, List privilegedPlugins, - InstanceFactory 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> plugins = new ArrayList<>(mPlugins); - for (PluginInfo 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> plugins = new ArrayList<>(mPlugins); - for (PluginInfo info : plugins) { - if (className.startsWith(info.mPackage)) { - disableAny |= disable(info, PluginEnabler.DISABLED_FROM_EXPLICIT_CRASH); - } - } - return disableAny; - } - - public boolean disableAll() { - ArrayList> 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 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; - } - - boolean dependsOn(Plugin p, Class cls) { - ArrayList> plugins = new ArrayList<>(mPlugins); - for (PluginInfo 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 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 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 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 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 pluginInfo = handleLoadPlugin(name); - if (pluginInfo == null) continue; - - // add plugin before sending PLUGIN_CONNECTED message - mPlugins.add(pluginInfo); - mMainExecutor.execute(() -> onPluginConnected(pluginInfo)); - } - } - - protected PluginInfo 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 zipPaths = new ArrayList<>(); - List 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 mPrivilegedPlugins; - private InstanceFactory mInstanceFactory; - - public Factory(Context context, PackageManager packageManager, - Executor mainExecutor, Executor bgExecutor, PluginInitializer initializer, - NotificationManager notificationManager, PluginEnabler pluginEnabler, - List privilegedPlugins) { - mContext = context; - mPackageManager = packageManager; - mMainExecutor = mainExecutor; - mBgExecutor = bgExecutor; - mInitializer = initializer; - mNotificationManager = notificationManager; - mPluginEnabler = pluginEnabler; - mPrivilegedPlugins = privilegedPlugins; - - mInstanceFactory = new InstanceFactory<>(); - } - - @VisibleForTesting - Factory setInstanceFactory(InstanceFactory instanceFactory) { - mInstanceFactory = instanceFactory; - return this; - } - - PluginInstanceManager create( - String action, PluginListener listener, boolean allowMultiple, - VersionInfo version, boolean debuggable) { - return new PluginInstanceManager(mContext, mPackageManager, action, listener, - allowMultiple, mMainExecutor, mBgExecutor, version, debuggable, - mInitializer, mNotificationManager, mPluginEnabler, - mPrivilegedPlugins, (InstanceFactory) 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 { - 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 create(Class cls) throws IllegalAccessException, InstantiationException { - return (T) cls.newInstance(); - } - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java index d264bf2fae52c..c89be869115b5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManager.java @@ -30,13 +30,15 @@ public interface PluginManager { /** Returns plugins that don't get disabled when an exceptoin occurs. */ String[] getPrivilegedPlugins(); - void addPluginListener(PluginListener listener, Class cls); - void addPluginListener(PluginListener listener, Class cls, + /** */ + void addPluginListener(PluginListener listener, Class cls); + /** */ + void addPluginListener(PluginListener listener, Class cls, boolean allowMultiple); void addPluginListener(String action, PluginListener listener, - Class cls); + Class cls); void addPluginListener(String action, PluginListener listener, - Class cls, boolean allowMultiple); + Class cls, boolean allowMultiple); void removePluginListener(PluginListener listener); diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java index ea7b0c34136a9..7539f995dab47 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginManagerImpl.java @@ -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, PluginInstanceManager> mPluginMap + private final ArrayMap, PluginActionManager> mPluginMap = new ArrayMap<>(); private final Map mClassLoaders = new ArrayMap<>(); private final ArraySet 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 defaultHandlerOptional, PluginEnabler pluginEnabler, PluginPrefs pluginPrefs, List 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 void addPluginListener(PluginListener listener, Class cls) { + /** */ + public void addPluginListener(PluginListener listener, Class cls) { addPluginListener(listener, cls, false); } - public void addPluginListener(PluginListener listener, Class cls, + /** */ + public void addPluginListener(PluginListener listener, Class cls, boolean allowMultiple) { addPluginListener(PluginManager.Helper.getAction(cls), listener, cls, allowMultiple); } public void addPluginListener(String action, PluginListener listener, - Class cls) { + Class cls) { addPluginListener(action, listener, cls, false); } public void addPluginListener(String action, PluginListener listener, - Class cls, boolean allowMultiple) { + Class cls, boolean allowMultiple) { mPluginPrefs.addAction(action); - PluginInstanceManager p = mInstanceManagerFactory.create(action, listener, allowMultiple, - new VersionInfo().addClass(cls), isDebuggable()); + PluginActionManager 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()); } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java index bb845cd879238..6be3243879d6d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/VersionInfo.java @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java index f9617cad848e8..c7f1006a40423 100644 --- a/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java +++ b/packages/SystemUI/src/com/android/systemui/PluginInflateContainer.java @@ -53,7 +53,7 @@ public class PluginInflateContainer extends AutoReinflateContainer private static final String TAG = "PluginInflateContainer"; - private Class mClass; + private Class 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) Class.forName(viewType); } catch (Exception e) { Log.d(TAG, "Problem getting class info " + viewType, e); mClass = null; diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java index 1ea9b3c08b1ec..6a93fb7e4861a 100644 --- a/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginsModule.java @@ -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 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 privilegedPlugins) { - return new PluginInstanceManager.Factory( + @Named(PLUGIN_PRIVILEGED) List 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 uncaughtExceptionHandlerOptional, diff --git a/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java index 20857eaba7d48..fe183fc9e872a 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/PluginFragment.java @@ -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 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; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java similarity index 65% rename from packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java rename to packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java index 790b4dd11825b..ce366657b7b24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginActionManagerTest.java @@ -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 mMockListener; - private PluginInstanceManager mPluginInstanceManager; + private PluginListener mMockListener; + private PluginActionManager 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 mPluginInstanceFactory = - new PluginInstanceManager.InstanceFactory() { + private PluginInstance 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 PluginInstance create(Context context, ApplicationInfo appInfo, + ComponentName componentName, Class pluginClass) { + return (PluginInstance) 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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java new file mode 100644 index 0000000000000..bb9a1e971fd0f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java @@ -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 mMockListener; + @Mock + private VersionInfo mVersionInfo; + ComponentName mTestPluginComponentName = + new ComponentName(PRIVILEGED_PACKAGE, TestPluginImpl.class.getName()); + private PluginInstance 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() { + @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) { + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java index 4590dd8295506..1eadd522352e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginManagerTest.java @@ -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 mMockPluginInstance; + private PluginActionManager.Factory mMockFactory; + private PluginActionManager mMockPluginInstance; private PluginManagerImpl mPluginManager; - private PluginListener mMockListener; + private PluginListener 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(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java index 8e1c0f7301e1e..d245c727dcf81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java @@ -30,24 +30,24 @@ public class FakePluginManager implements PluginManager { @Override public void addPluginListener(String action, PluginListener listener, - Class cls, boolean allowMultiple) { + Class cls, boolean allowMultiple) { mLeakChecker.addCallback(listener); } @Override - public void addPluginListener(PluginListener listener, Class cls) { + public void addPluginListener(PluginListener listener, Class cls) { mLeakChecker.addCallback(listener); } @Override - public void addPluginListener(PluginListener listener, Class cls, + public void addPluginListener(PluginListener listener, Class cls, boolean allowMultiple) { mLeakChecker.addCallback(listener); } @Override public void addPluginListener(String action, PluginListener listener, - Class cls) { + Class cls) { mLeakChecker.addCallback(listener); }