Plugins for sysui
Why this is safe:
- To never ever be used in production code, simply for rapid
prototyping (multiple checks in place)
- Guarded by signature level permission checks, so only matching
signed code will be used
- Any crashing plugins are auto-disabled and sysui is allowed
to continue in peace
Now on to what it actually does. 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:
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.
-------
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:
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 */);
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.
<uses-permission android:name="com.android.systemui.permission.PLUGIN" />
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:
<service android:name=".TestOverlayPlugin">
<intent-filter>
<action android:name="com.android.systemui.action.PLUGIN_COMPONENT" />
</intent-filter>
</service>
Change-Id: I42c573a94907ca7a2eaacbb0a44614d49b8fc26f
diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk
index 258c82e..e5897e3 100644
--- a/packages/SystemUI/Android.mk
+++ b/packages/SystemUI/Android.mk
@@ -23,6 +23,7 @@
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 3cc16de..8ed1be5 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 0000000..86527db
--- /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 0000000..7c057dc
--- /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 0000000..4c82c75
--- /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 0000000..bd2c71c
--- /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 0000000..b2910cb
--- /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 0000000..5fdbbf9
--- /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 0000000..a2f84dc
--- /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 0000000..91a2604
--- /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 0000000..b31b199
--- /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 0000000..2a7139c
--- /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 0000000..b2f92d6
--- /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 0000000..aa0b3c5
--- /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 0000000..af49d43
--- /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 9182f7e..364885a 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 52b5a54..1da88ef 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.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 @@
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 222e8df..9e5b881 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.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 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 876a33e..9cd6680 100644
--- a/packages/SystemUI/tests/Android.mk
+++ b/packages/SystemUI/tests/Android.mk
@@ -34,6 +34,7 @@
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 869805e..d943eb6 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 @@
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 @@
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 0000000..ab7de39
--- /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 0000000..56e742a
--- /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);
+ }
+}