diff options
22 files changed, 1434 insertions, 2 deletions
diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk index 258c82ee227e..e5897e3e5350 100644 --- a/packages/SystemUI/Android.mk +++ b/packages/SystemUI/Android.mk @@ -23,6 +23,7 @@ LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src) LOCAL_STATIC_ANDROID_LIBRARIES := \ + SystemUIPluginLib \ Keyguard \ android-support-v7-recyclerview \ android-support-v7-preference \ diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 3cc16de415a3..8ed1be592ea4 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -138,6 +138,9 @@ android:protectionLevel="signature" /> <uses-permission android:name="com.android.systemui.permission.SELF" /> + <permission android:name="com.android.systemui.permission.PLUGIN" + android:protectionLevel="signature" /> + <!-- Adding Quick Settings tiles --> <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" /> diff --git a/packages/SystemUI/plugin/Android.mk b/packages/SystemUI/plugin/Android.mk new file mode 100644 index 000000000000..86527db878c6 --- /dev/null +++ b/packages/SystemUI/plugin/Android.mk @@ -0,0 +1,29 @@ +# 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. + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_USE_AAPT2 := true + +LOCAL_MODULE_TAGS := optional + +LOCAL_MODULE := SystemUIPluginLib + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res +LOCAL_JAR_EXCLUDE_FILES := none + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/packages/SystemUI/plugin/AndroidManifest.xml b/packages/SystemUI/plugin/AndroidManifest.xml new file mode 100644 index 000000000000..7c057dc78ac7 --- /dev/null +++ b/packages/SystemUI/plugin/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.plugins"> + + <uses-sdk + android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SystemUI/plugin/ExamplePlugin/Android.mk b/packages/SystemUI/plugin/ExamplePlugin/Android.mk new file mode 100644 index 000000000000..4c82c7505ad3 --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/Android.mk @@ -0,0 +1,15 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_USE_AAPT2 := true + +LOCAL_PACKAGE_NAME := ExamplePlugin + +LOCAL_JAVA_LIBRARIES := SystemUIPluginLib + +LOCAL_CERTIFICATE := platform +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +include $(BUILD_PACKAGE) diff --git a/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml b/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml new file mode 100644 index 000000000000..bd2c71c38f5a --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.plugin.testoverlayplugin"> + + <uses-permission android:name="com.android.systemui.permission.PLUGIN" /> + + <application> + <service android:name=".SampleOverlayPlugin"> + <intent-filter> + <action android:name="com.android.systemui.action.PLUGIN_OVERLAY" /> + </intent-filter> + </service> + </application> + +</manifest> diff --git a/packages/SystemUI/plugin/ExamplePlugin/res/layout/colored_overlay.xml b/packages/SystemUI/plugin/ExamplePlugin/res/layout/colored_overlay.xml new file mode 100644 index 000000000000..b2910cb19b7c --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/res/layout/colored_overlay.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** Copyright 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. +--> + +<com.android.systemui.plugin.testoverlayplugin.CustomView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#80ff0000" /> diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java new file mode 100644 index 000000000000..5fdbbf989b2b --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java @@ -0,0 +1,46 @@ +/* + * 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.plugin.testoverlayplugin; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +/** + * View with some logging to show that its being run. + */ +public class CustomView extends View { + + private static final String TAG = "CustomView"; + + public CustomView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + Log.d(TAG, "new instance"); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + Log.d(TAG, "onAttachedToWindow"); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + Log.d(TAG, "onDetachedFromWindow"); + } +} diff --git a/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java new file mode 100644 index 000000000000..a2f84dc7af2a --- /dev/null +++ b/packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java @@ -0,0 +1,71 @@ +/* + * 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.plugin.testoverlayplugin; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.OverlayPlugin; + +public class SampleOverlayPlugin implements OverlayPlugin { + private static final String TAG = "SampleOverlayPlugin"; + private Context mPluginContext; + + private View mStatusBarView; + private View mNavBarView; + + @Override + public int getVersion() { + Log.d(TAG, "getVersion " + VERSION); + return VERSION; + } + + @Override + public void onCreate(Context sysuiContext, Context pluginContext) { + Log.d(TAG, "onCreate"); + mPluginContext = pluginContext; + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + if (mStatusBarView != null) { + mStatusBarView.post( + () -> ((ViewGroup) mStatusBarView.getParent()).removeView(mStatusBarView)); + } + if (mNavBarView != null) { + mNavBarView.post(() -> ((ViewGroup) mNavBarView.getParent()).removeView(mNavBarView)); + } + } + + @Override + public void setup(View statusBar, View navBar) { + Log.d(TAG, "Setup"); + + if (statusBar instanceof ViewGroup) { + mStatusBarView = LayoutInflater.from(mPluginContext) + .inflate(R.layout.colored_overlay, (ViewGroup) statusBar, false); + ((ViewGroup) statusBar).addView(mStatusBarView); + } + if (navBar instanceof ViewGroup) { + mNavBarView = LayoutInflater.from(mPluginContext) + .inflate(R.layout.colored_overlay, (ViewGroup) navBar, false); + ((ViewGroup) navBar).addView(mNavBarView); + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java new file mode 100644 index 000000000000..91a260435eaa --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java @@ -0,0 +1,24 @@ +/* + * 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.plugins; + +import android.view.View; + +public interface OverlayPlugin extends Plugin { + + String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY"; + int VERSION = 1; + + void setup(View statusBar, View navBar); +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java new file mode 100644 index 000000000000..b31b199376de --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java @@ -0,0 +1,132 @@ +/* + * 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.plugins; + +import android.content.Context; + +/** + * Plugins are separate APKs that + * are expected to implement interfaces provided by SystemUI. Their + * code is dynamically loaded into the SysUI process which can allow + * for multiple prototypes to be created and run on a single android + * build. + * + * PluginLifecycle: + * <pre class="prettyprint"> + * + * plugin.onCreate(Context sysuiContext, Context pluginContext); + * --- This is always called before any other calls + * + * pluginListener.onPluginConnected(Plugin p); + * --- This lets the plugin hook know that a plugin is now connected. + * + * ** Any other calls back and forth between sysui/plugin ** + * + * pluginListener.onPluginDisconnected(Plugin p); + * --- Lets the plugin hook know that it should stop interacting with + * this plugin and drop all references to it. + * + * plugin.onDestroy(); + * --- Finally the plugin can perform any cleanup to ensure that its not + * leaking into the SysUI process. + * + * Any time a plugin APK is updated the plugin is destroyed and recreated + * to load the new code/resources. + * + * </pre> + * + * Creating plugin hooks: + * + * To create a plugin hook, first create an interface in + * frameworks/base/packages/SystemUI/plugin that extends Plugin. + * Include in it any hooks you want to be able to call into from + * sysui and create callback interfaces for anything you need to + * pass through into the plugin. + * + * Then to attach to any plugins simply add a plugin listener and + * onPluginConnected will get called whenever new plugins are installed, + * updated, or enabled. Like this example from SystemUIApplication: + * + * <pre class="prettyprint"> + * {@literal + * PluginManager.getInstance(this).addPluginListener(OverlayPlugin.COMPONENT, + * new PluginListener<OverlayPlugin>() { + * @Override + * public void onPluginConnected(OverlayPlugin plugin) { + * PhoneStatusBar phoneStatusBar = getComponent(PhoneStatusBar.class); + * if (phoneStatusBar != null) { + * plugin.setup(phoneStatusBar.getStatusBarWindow(), + * phoneStatusBar.getNavigationBarView()); + * } + * } + * }, OverlayPlugin.VERSION, true /* Allow multiple plugins *\/); + * } + * </pre> + * Note the VERSION included here. Any time incompatible changes in the + * interface are made, this version should be changed to ensure old plugins + * aren't accidentally loaded. Since the plugin library is provided by + * SystemUI, default implementations can be added for new methods to avoid + * version changes when possible. + * + * Implementing a Plugin: + * + * See the ExamplePlugin for an example Android.mk on how to compile + * a plugin. Note that SystemUILib is not static for plugins, its classes + * are provided by SystemUI. + * + * Plugin security is based around a signature permission, so plugins must + * hold the following permission in their manifest. + * + * <pre class="prettyprint"> + * {@literal + * <uses-permission android:name="com.android.systemui.permission.PLUGIN" /> + * } + * </pre> + * + * A plugin is found through a querying for services, so to let SysUI know + * about it, create a service with a name that points at your implementation + * of the plugin interface with the action accompanying it: + * + * <pre class="prettyprint"> + * {@literal + * <service android:name=".TestOverlayPlugin"> + * <intent-filter> + * <action android:name="com.android.systemui.action.PLUGIN_COMPONENT" /> + * </intent-filter> + * </service> + * } + * </pre> + */ +public interface Plugin { + + /** + * Should be implemented as the following directly referencing the version constant + * from the plugin interface being implemented, this will allow recompiles to automatically + * pick up the current version. + * <pre class="prettyprint"> + * {@literal + * public int getVersion() { + * return VERSION; + * } + * } + * @return + */ + int getVersion(); + + default void onCreate(Context sysuiContext, Context pluginContext) { + } + + default void onDestroy() { + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java new file mode 100644 index 000000000000..2a7139c3a74c --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -0,0 +1,342 @@ +/* + * 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.plugins; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.LayoutInflater; + +import com.android.internal.annotations.VisibleForTesting; + +import dalvik.system.PathClassLoader; + +import java.util.ArrayList; +import java.util.List; + +public class PluginInstanceManager<T extends Plugin> extends BroadcastReceiver { + + private static final boolean DEBUG = false; + + private static final String TAG = "PluginInstanceManager"; + private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; + + private final Context mContext; + private final PluginListener<T> mListener; + private final String mAction; + private final boolean mAllowMultiple; + private final int mVersion; + + @VisibleForTesting + final MainHandler mMainHandler; + @VisibleForTesting + final PluginHandler mPluginHandler; + private final boolean isDebuggable; + private final PackageManager mPm; + private final ClassLoaderFactory mClassLoaderFactory; + + PluginInstanceManager(Context context, String action, PluginListener<T> listener, + boolean allowMultiple, Looper looper, int version) { + this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version, + Build.IS_DEBUGGABLE, new ClassLoaderFactory()); + } + + @VisibleForTesting + PluginInstanceManager(Context context, PackageManager pm, String action, + PluginListener<T> listener, boolean allowMultiple, Looper looper, int version, + boolean debuggable, ClassLoaderFactory classLoaderFactory) { + mMainHandler = new MainHandler(Looper.getMainLooper()); + mPluginHandler = new PluginHandler(looper); + mContext = context; + mPm = pm; + mAction = action; + mListener = listener; + mAllowMultiple = allowMultiple; + mVersion = version; + isDebuggable = debuggable; + mClassLoaderFactory = classLoaderFactory; + } + + public void startListening() { + if (DEBUG) Log.d(TAG, "startListening"); + mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL); + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiver(this, filter); + filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); + mContext.registerReceiver(this, filter); + } + + public void stopListening() { + if (DEBUG) Log.d(TAG, "stopListening"); + ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins); + for (PluginInfo plugin : plugins) { + mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED, + plugin.mPlugin).sendToTarget(); + } + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) Log.d(TAG, "onReceive " + intent); + if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) { + mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL); + } else { + Uri data = intent.getData(); + String pkgName = data.getEncodedSchemeSpecificPart(); + mPluginHandler.obtainMessage(PluginHandler.REMOVE_PKG, pkgName).sendToTarget(); + if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { + mPluginHandler.obtainMessage(PluginHandler.QUERY_PKG, pkgName).sendToTarget(); + } + } + } + + public boolean checkAndDisable(String className) { + boolean disableAny = false; + ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins); + for (PluginInfo info : plugins) { + if (className.startsWith(info.mPackage)) { + disable(info); + disableAny = true; + } + } + return disableAny; + } + + public void disableAll() { + ArrayList<PluginInfo> plugins = new ArrayList<>(mPluginHandler.mPlugins); + plugins.forEach(this::disable); + } + + private void disable(PluginInfo info) { + // Live by the sword, die by the sword. + // Misbehaving plugins get disabled and won't come back until uninstall/reinstall. + + // 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. + Log.w(TAG, "Disabling plugin " + info.mPackage + "/" + info.mClass); + mPm.setComponentEnabledSetting( + new ComponentName(info.mPackage, info.mClass), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } + + private class MainHandler extends Handler { + private static final int PLUGIN_CONNECTED = 1; + private static final int PLUGIN_DISCONNECTED = 2; + + public MainHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PLUGIN_CONNECTED: + if (DEBUG) Log.d(TAG, "onPluginConnected"); + PluginInfo<T> info = (PluginInfo<T>) msg.obj; + info.mPlugin.onCreate(mContext, info.mPluginContext); + mListener.onPluginConnected(info.mPlugin); + break; + case PLUGIN_DISCONNECTED: + if (DEBUG) Log.d(TAG, "onPluginDisconnected"); + mListener.onPluginDisconnected((T) msg.obj); + ((T) msg.obj).onDestroy(); + break; + default: + super.handleMessage(msg); + break; + } + } + } + + static class ClassLoaderFactory { + public ClassLoader createClassLoader(String path, ClassLoader base) { + return new PathClassLoader(path, base); + } + } + + private class PluginHandler extends Handler { + private static final int QUERY_ALL = 1; + private static final int QUERY_PKG = 2; + private static final int REMOVE_PKG = 3; + + private final ArrayList<PluginInfo<T>> mPlugins = new ArrayList<>(); + + public PluginHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case QUERY_ALL: + if (DEBUG) Log.d(TAG, "queryAll " + mAction); + for (int i = mPlugins.size() - 1; i >= 0; i--) { + PluginInfo<T> plugin = mPlugins.get(i); + mListener.onPluginDisconnected(plugin.mPlugin); + plugin.mPlugin.onDestroy(); + } + mPlugins.clear(); + handleQueryPlugins(null); + break; + case REMOVE_PKG: + String pkg = (String) msg.obj; + for (int i = mPlugins.size() - 1; i >= 0; i--) { + final PluginInfo<T> plugin = mPlugins.get(i); + if (plugin.mPackage.equals(pkg)) { + mMainHandler.obtainMessage(MainHandler.PLUGIN_DISCONNECTED, + plugin.mPlugin).sendToTarget(); + mPlugins.remove(i); + } + } + break; + case QUERY_PKG: + String p = (String) msg.obj; + if (DEBUG) Log.d(TAG, "queryPkg " + p + " " + mAction); + if (mAllowMultiple || (mPlugins.size() == 0)) { + handleQueryPlugins(p); + } else { + if (DEBUG) Log.d(TAG, "Too many of " + mAction); + } + break; + default: + super.handleMessage(msg); + } + } + + private void handleQueryPlugins(String pkgName) { + // This isn't actually a service and shouldn't ever be started, but is + // a convenient PM based way to manage our plugins. + Intent intent = new Intent(mAction); + if (pkgName != null) { + intent.setPackage(pkgName); + } + List<ResolveInfo> result = + mPm.queryIntentServices(intent, 0); + if (DEBUG) Log.d(TAG, "Found " + result.size() + " plugins"); + if (result.size() > 1 && !mAllowMultiple) { + // TODO: Show warning. + Log.w(TAG, "Multiple plugins found for " + mAction); + return; + } + for (ResolveInfo info : result) { + ComponentName name = new ComponentName(info.serviceInfo.packageName, + info.serviceInfo.name); + PluginInfo<T> t = handleLoadPlugin(name); + if (t == null) continue; + mMainHandler.obtainMessage(mMainHandler.PLUGIN_CONNECTED, t).sendToTarget(); + mPlugins.add(t); + } + } + + 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 (!isDebuggable) { + // Never ever ever allow these on production builds, they are only for prototyping. + Log.d(TAG, "Somehow hit second debuggable check"); + return null; + } + String pkg = component.getPackageName(); + String cls = component.getClassName(); + try { + PackageManager pm = mPm; + ApplicationInfo info = pm.getApplicationInfo(pkg, 0); + // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on + if (pm.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 = mClassLoaderFactory.createClassLoader(info.sourceDir, + getClass().getClassLoader()); + Context pluginContext = new PluginContextWrapper( + mContext.createApplicationContext(info, 0), classLoader); + Class<?> pluginClass = Class.forName(cls, true, classLoader); + T plugin = (T) pluginClass.newInstance(); + if (plugin.getVersion() != mVersion) { + // TODO: Warn user. + Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion() + + ", expected " + mVersion); + return null; + } + if (DEBUG) Log.d(TAG, "createPlugin"); + return new PluginInfo(pkg, cls, plugin, pluginContext); + } catch (Exception e) { + Log.w(TAG, "Couldn't load plugin: " + pkg, e); + return null; + } + } + } + + 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); + } + } + + private static class PluginInfo<T> { + private final Context mPluginContext; + private T mPlugin; + private String mClass; + private String mPackage; + + public PluginInfo(String pkg, String cls, T plugin, Context pluginContext) { + mPlugin = plugin; + mClass = cls; + mPackage = pkg; + mPluginContext = pluginContext; + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java new file mode 100644 index 000000000000..b2f92d6c017e --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java @@ -0,0 +1,36 @@ +/* + * 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.plugins; + +/** + * Interface for listening to plugins being connected. + */ +public interface PluginListener<T extends Plugin> { + /** + * Called when the plugin has been loaded and is ready to be used. + * This may be called multiple times if multiple plugins are allowed. + * It may also be called in the future if the plugin package changes + * and needs to be reloaded. + */ + void onPluginConnected(T plugin); + + /** + * Called when a plugin has been uninstalled/updated and should be removed + * from use. + */ + default void onPluginDisconnected(T plugin) { + // Optional. + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java new file mode 100644 index 000000000000..aa0b3c586747 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java @@ -0,0 +1,138 @@ +/* + * 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.plugins; + +import android.content.Context; +import android.os.Build; +import android.os.HandlerThread; +import android.os.Looper; +import android.util.ArrayMap; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.Thread.UncaughtExceptionHandler; + +/** + * @see Plugin + */ +public class PluginManager { + + private static PluginManager sInstance; + + private final HandlerThread mBackgroundThread; + private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap + = new ArrayMap<>(); + private final Context mContext; + private final PluginInstanceManagerFactory mFactory; + private final boolean isDebuggable; + + private PluginManager(Context context) { + this(context, new PluginInstanceManagerFactory(), Build.IS_DEBUGGABLE, + Thread.getDefaultUncaughtExceptionHandler()); + } + + @VisibleForTesting + PluginManager(Context context, PluginInstanceManagerFactory factory, boolean debuggable, + UncaughtExceptionHandler defaultHandler) { + mContext = context; + mFactory = factory; + mBackgroundThread = new HandlerThread("Plugins"); + mBackgroundThread.start(); + isDebuggable = debuggable; + + PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler( + defaultHandler); + Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); + } + + public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener, + int version) { + addPluginListener(action, listener, version, false); + } + + public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener, + int version, boolean allowMultiple) { + if (!isDebuggable) { + // Never ever ever allow these on production builds, they are only for prototyping. + return; + } + PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener, + allowMultiple, mBackgroundThread.getLooper(), version); + p.startListening(); + mPluginMap.put(listener, p); + } + + public void removePluginListener(PluginListener<?> listener) { + if (!isDebuggable) { + // Never ever ever allow these on production builds, they are only for prototyping. + return; + } + if (!mPluginMap.containsKey(listener)) return; + mPluginMap.remove(listener).stopListening(); + } + + public static PluginManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new PluginManager(context.getApplicationContext()); + } + return sInstance; + } + + @VisibleForTesting + public static class PluginInstanceManagerFactory { + public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context, + String action, PluginListener<T> listener, boolean allowMultiple, Looper looper, + int version) { + return new PluginInstanceManager(context, action, listener, allowMultiple, looper, + version); + } + } + + private class PluginExceptionHandler implements UncaughtExceptionHandler { + private final UncaughtExceptionHandler mHandler; + + private PluginExceptionHandler(UncaughtExceptionHandler handler) { + mHandler = handler; + } + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + // Search for and disable plugins that may have been involved in this crash. + boolean disabledAny = checkStack(throwable); + if (!disabledAny) { + // We couldn't find any plugins involved in this crash, just to be safe + // disable all the plugins, so we can be sure that SysUI is running as + // best as possible. + for (PluginInstanceManager manager : mPluginMap.values()) { + manager.disableAll(); + } + } + + // Run the normal exception handler so we can crash and cleanup our state. + mHandler.uncaughtException(thread, throwable); + } + + private boolean checkStack(Throwable throwable) { + if (throwable == null) return false; + boolean disabledAny = false; + for (StackTraceElement element : throwable.getStackTrace()) { + for (PluginInstanceManager manager : mPluginMap.values()) { + disabledAny |= manager.checkAndDisable(element.getClassName()); + } + } + return disabledAny | checkStack(throwable.getCause()); + } + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginUtils.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginUtils.java new file mode 100644 index 000000000000..af49d43c97e1 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginUtils.java @@ -0,0 +1,27 @@ +/* + * 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.plugins; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; + +public class PluginUtils { + + public static void setId(Context sysuiContext, View view, String id) { + int i = sysuiContext.getResources().getIdentifier(id, "id", sysuiContext.getPackageName()); + view.setId(i); + } +} diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index 9182f7ef0126..364885a2d6eb 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -37,3 +37,6 @@ -keep class ** extends android.support.v14.preference.PreferenceFragment -keep class com.android.systemui.tuner.* +-keep class com.android.systemui.plugins.** { + public protected **; +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 52b5a54a7621..1da88ef37a7f 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -27,7 +27,11 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.util.Log; +import com.android.systemui.plugins.OverlayPlugin; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.PluginManager; import com.android.systemui.stackdivider.Divider; +import com.android.systemui.statusbar.phone.PhoneStatusBar; import java.util.HashMap; import java.util.Map; @@ -174,6 +178,18 @@ public class SystemUIApplication extends Application { mServices[i].onBootCompleted(); } } + PluginManager.getInstance(this).addPluginListener(OverlayPlugin.ACTION, + new PluginListener<OverlayPlugin>() { + @Override + public void onPluginConnected(OverlayPlugin plugin) { + PhoneStatusBar phoneStatusBar = getComponent(PhoneStatusBar.class); + if (phoneStatusBar != null) { + plugin.setup(phoneStatusBar.getStatusBarWindow(), + phoneStatusBar.getNavigationBarView()); + } + } + }, OverlayPlugin.VERSION, true /* Allow multiple plugins */); + mServicesStarted = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 222e8dfa2c82..9e5b881f8a59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; import android.widget.LinearLayout; import com.android.systemui.R; import com.android.systemui.RecentsComponent; @@ -52,7 +53,7 @@ import com.android.systemui.statusbar.policy.DeadZone; import java.io.FileDescriptor; import java.io.PrintWriter; -public class NavigationBarView extends LinearLayout { +public class NavigationBarView extends FrameLayout { final static boolean DEBUG = false; final static String TAG = "StatusBar/NavBarView"; diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk index 876a33eb9517..9cd668045f8e 100644 --- a/packages/SystemUI/tests/Android.mk +++ b/packages/SystemUI/tests/Android.mk @@ -34,6 +34,7 @@ LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \ frameworks/base/packages/SystemUI/res \ LOCAL_STATIC_ANDROID_LIBRARIES := \ + SystemUIPluginLib \ Keyguard \ android-support-v7-recyclerview \ android-support-v7-preference \ diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java index 869805edd360..d943eb6a1a60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -17,13 +17,17 @@ package com.android.systemui; import android.content.Context; import android.support.test.InstrumentationRegistry; -import android.test.AndroidTestCase; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; import org.junit.Before; /** * Base class that does System UI specific setup. */ public class SysuiTestCase { + + private Handler mHandler; protected Context mContext; @Before @@ -34,4 +38,65 @@ public class SysuiTestCase { protected Context getContext() { return mContext; } + + protected void waitForIdleSync() { + if (mHandler == null) { + mHandler = new Handler(Looper.getMainLooper()); + } + waitForIdleSync(mHandler); + } + + protected void waitForIdleSync(Handler h) { + validateThread(h.getLooper()); + Idler idler = new Idler(null); + h.getLooper().getQueue().addIdleHandler(idler); + // Ensure we are non-idle, so the idle handler can run. + h.post(new EmptyRunnable()); + idler.waitForIdle(); + } + + private static final void validateThread(Looper l) { + if (Looper.myLooper() == l) { + throw new RuntimeException( + "This method can not be called from the looper being synced"); + } + } + + public static final class EmptyRunnable implements Runnable { + public void run() { + } + } + + public static final class Idler implements MessageQueue.IdleHandler { + private final Runnable mCallback; + private boolean mIdle; + + public Idler(Runnable callback) { + mCallback = callback; + mIdle = false; + } + + @Override + public boolean queueIdle() { + if (mCallback != null) { + mCallback.run(); + } + synchronized (this) { + mIdle = true; + notifyAll(); + } + return false; + } + + public void waitForIdle() { + synchronized (this) { + while (!mIdle) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java new file mode 100644 index 000000000000..ab7de39a49b6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java @@ -0,0 +1,284 @@ +/* + * 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.plugins; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.net.Uri; +import android.os.HandlerThread; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.PluginInstanceManager.ClassLoaderFactory; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PluginInstanceManagerTest extends SysuiTestCase { + + // Static since the plugin needs to be generated by the PluginInstanceManager using newInstance. + private static Plugin sMockPlugin; + + private HandlerThread mHandlerThread; + private Context mContextWrapper; + private PackageManager mMockPm; + private PluginListener mMockListener; + private PluginInstanceManager mPluginInstanceManager; + + @Before + public void setup() throws Exception { + mHandlerThread = new HandlerThread("test_thread"); + mHandlerThread.start(); + mContextWrapper = new MyContextWrapper(getContext()); + mMockPm = mock(PackageManager.class); + mMockListener = mock(PluginListener.class); + mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", + mMockListener, true, mHandlerThread.getLooper(), 1, true, + new TestClassLoaderFactory()); + sMockPlugin = mock(Plugin.class); + when(sMockPlugin.getVersion()).thenReturn(1); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + sMockPlugin = null; + } + + @Test + public void testNoPlugins() { + when(mMockPm.queryIntentServices(Mockito.any(), Mockito.anyInt())).thenReturn( + Collections.emptyList()); + mPluginInstanceManager.startListening(); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler); + + verify(mMockListener, Mockito.never()).onPluginConnected( + ArgumentCaptor.forClass(Plugin.class).capture()); + } + + @Test + public void testPluginCreate() { + createPlugin(); + + // Verify startup lifecycle + verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(), + ArgumentCaptor.forClass(Context.class).capture()); + verify(mMockListener).onPluginConnected(ArgumentCaptor.forClass(Plugin.class).capture()); + } + + @Test + public void testPluginDestroy() { + createPlugin(); // Get into valid created state. + + mPluginInstanceManager.stopListening(); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler); + + // Verify shutdown lifecycle + verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture()); + verify(sMockPlugin).onDestroy(); + } + + @Test + public void testIncorrectVersion() { + setupFakePmQuery(); + when(sMockPlugin.getVersion()).thenReturn(2); + + mPluginInstanceManager.startListening(); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler); + + // Plugin shouldn't be connected because it is the wrong version. + verify(mMockListener, Mockito.never()).onPluginConnected( + ArgumentCaptor.forClass(Plugin.class).capture()); + } + + @Test + public void testReloadOnChange() { + createPlugin(); // Get into valid created state. + + // Send a package changed broadcast. + Intent i = new Intent(Intent.ACTION_PACKAGE_CHANGED, + Uri.fromParts("package", "com.android.systemui", null)); + mPluginInstanceManager.onReceive(mContextWrapper, i); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler); + + // Verify the old one was destroyed. + verify(mMockListener).onPluginDisconnected(ArgumentCaptor.forClass(Plugin.class).capture()); + verify(sMockPlugin).onDestroy(); + // Also verify we got a second onCreate. + verify(sMockPlugin, Mockito.times(2)).onCreate( + ArgumentCaptor.forClass(Context.class).capture(), + ArgumentCaptor.forClass(Context.class).capture()); + verify(mMockListener, Mockito.times(2)).onPluginConnected( + ArgumentCaptor.forClass(Plugin.class).capture()); + } + + @Test + public void testNonDebuggable() { + // Create a version that thinks the build is not debuggable. + mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", + mMockListener, true, mHandlerThread.getLooper(), 1, false, + new TestClassLoaderFactory()); + setupFakePmQuery(); + + mPluginInstanceManager.startListening(); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler);; + + // Non-debuggable build should receive no plugins. + verify(mMockListener, Mockito.never()).onPluginConnected( + ArgumentCaptor.forClass(Plugin.class).capture()); + } + + @Test + public void testCheckAndDisable() { + createPlugin(); // Get into valid created state. + + // Start with an unrelated class. + boolean result = mPluginInstanceManager.checkAndDisable(Activity.class.getName()); + assertFalse(result); + verify(mMockPm, Mockito.never()).setComponentEnabledSetting( + ArgumentCaptor.forClass(ComponentName.class).capture(), + ArgumentCaptor.forClass(int.class).capture(), + ArgumentCaptor.forClass(int.class).capture()); + + // Now hand it a real class and make sure it disables the plugin. + result = mPluginInstanceManager.checkAndDisable(TestPlugin.class.getName()); + assertTrue(result); + verify(mMockPm).setComponentEnabledSetting( + ArgumentCaptor.forClass(ComponentName.class).capture(), + ArgumentCaptor.forClass(int.class).capture(), + ArgumentCaptor.forClass(int.class).capture()); + } + + @Test + public void testDisableAll() { + createPlugin(); // Get into valid created state. + + mPluginInstanceManager.disableAll(); + + verify(mMockPm).setComponentEnabledSetting( + ArgumentCaptor.forClass(ComponentName.class).capture(), + ArgumentCaptor.forClass(int.class).capture(), + ArgumentCaptor.forClass(int.class).capture()); + } + + private void setupFakePmQuery() { + List<ResolveInfo> list = new ArrayList<>(); + ResolveInfo info = new ResolveInfo(); + info.serviceInfo = new ServiceInfo(); + info.serviceInfo.packageName = "com.android.systemui"; + info.serviceInfo.name = TestPlugin.class.getName(); + list.add(info); + when(mMockPm.queryIntentServices(Mockito.any(), Mockito.anyInt())).thenReturn(list); + + when(mMockPm.checkPermission(Mockito.anyString(), Mockito.anyString())).thenReturn( + PackageManager.PERMISSION_GRANTED); + + try { + ApplicationInfo appInfo = getContext().getApplicationInfo(); + when(mMockPm.getApplicationInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn( + appInfo); + } catch (NameNotFoundException e) { + // Shouldn't be possible, but if it is, we want to fail. + throw new RuntimeException(e); + } + } + + private void createPlugin() { + setupFakePmQuery(); + + mPluginInstanceManager.startListening(); + + waitForIdleSync(mPluginInstanceManager.mPluginHandler); + waitForIdleSync(mPluginInstanceManager.mMainHandler); + } + + private static class TestClassLoaderFactory extends ClassLoaderFactory { + @Override + public ClassLoader createClassLoader(String path, ClassLoader base) { + return base; + } + } + + // Real context with no registering/unregistering of receivers. + private static class MyContextWrapper extends ContextWrapper { + public MyContextWrapper(Context base) { + super(base); + } + + @Override + public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + return null; + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + } + } + + public static class TestPlugin implements Plugin { + @Override + public int getVersion() { + return sMockPlugin.getVersion(); + } + + @Override + public void onCreate(Context sysuiContext, Context pluginContext) { + sMockPlugin.onCreate(sysuiContext, pluginContext); + } + + @Override + public void onDestroy() { + sMockPlugin.onDestroy(); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java new file mode 100644 index 000000000000..56e742aa0663 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java @@ -0,0 +1,121 @@ +/* + * 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.plugins; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.Thread.UncaughtExceptionHandler; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PluginManagerTest extends SysuiTestCase { + + private PluginInstanceManagerFactory mMockFactory; + private PluginInstanceManager mMockPluginInstance; + private PluginManager mPluginManager; + private PluginListener mMockListener; + + private UncaughtExceptionHandler mRealExceptionHandler; + private UncaughtExceptionHandler mMockExceptionHandler; + private UncaughtExceptionHandler mPluginExceptionHandler; + + @Before + public void setup() throws Exception { + mRealExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + mMockExceptionHandler = mock(UncaughtExceptionHandler.class); + mMockFactory = mock(PluginInstanceManagerFactory.class); + mMockPluginInstance = mock(PluginInstanceManager.class); + when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(), + Mockito.anyBoolean(), Mockito.any(), Mockito.anyInt())) + .thenReturn(mMockPluginInstance); + mPluginManager = new PluginManager(getContext(), mMockFactory, true, mMockExceptionHandler); + resetExceptionHandler(); + mMockListener = mock(PluginListener.class); + } + + @Test + public void testAddListener() { + mPluginManager.addPluginListener("myAction", mMockListener, 1); + + verify(mMockPluginInstance).startListening(); + } + + @Test + public void testRemoveListener() { + mPluginManager.addPluginListener("myAction", mMockListener, 1); + + mPluginManager.removePluginListener(mMockListener); + verify(mMockPluginInstance).stopListening(); + } + + @Test + public void testNonDebuggable() { + mPluginManager = new PluginManager(getContext(), mMockFactory, false, + mMockExceptionHandler); + resetExceptionHandler(); + mPluginManager.addPluginListener("myAction", mMockListener, 1); + + verify(mMockPluginInstance, Mockito.never()).startListening(); + } + + @Test + public void testExceptionHandler_foundPlugin() { + mPluginManager.addPluginListener("myAction", mMockListener, 1); + when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(true); + + mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable()); + + verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable( + ArgumentCaptor.forClass(String.class).capture()); + verify(mMockPluginInstance, Mockito.never()).disableAll(); + verify(mMockExceptionHandler).uncaughtException( + ArgumentCaptor.forClass(Thread.class).capture(), + ArgumentCaptor.forClass(Throwable.class).capture()); + } + + @Test + public void testExceptionHandler_noFoundPlugin() { + mPluginManager.addPluginListener("myAction", mMockListener, 1); + when(mMockPluginInstance.checkAndDisable(Mockito.any())).thenReturn(false); + + mPluginExceptionHandler.uncaughtException(Thread.currentThread(), new Throwable()); + + verify(mMockPluginInstance, Mockito.atLeastOnce()).checkAndDisable( + ArgumentCaptor.forClass(String.class).capture()); + verify(mMockPluginInstance).disableAll(); + verify(mMockExceptionHandler).uncaughtException( + ArgumentCaptor.forClass(Thread.class).capture(), + ArgumentCaptor.forClass(Throwable.class).capture()); + } + + private void resetExceptionHandler() { + mPluginExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + // Set back the real exception handler so the test can crash if it wants to. + Thread.setDefaultUncaughtExceptionHandler(mRealExceptionHandler); + } +} |