summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/Android.mk1
-rw-r--r--packages/SystemUI/AndroidManifest.xml3
-rw-r--r--packages/SystemUI/plugin/Android.mk29
-rw-r--r--packages/SystemUI/plugin/AndroidManifest.xml24
-rw-r--r--packages/SystemUI/plugin/ExamplePlugin/Android.mk15
-rw-r--r--packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml31
-rw-r--r--packages/SystemUI/plugin/ExamplePlugin/res/layout/colored_overlay.xml22
-rw-r--r--packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/CustomView.java46
-rw-r--r--packages/SystemUI/plugin/ExamplePlugin/src/com/android/systemui/plugin/testoverlayplugin/SampleOverlayPlugin.java71
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java24
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/Plugin.java132
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java342
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginListener.java36
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java138
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginUtils.java27
-rw-r--r--packages/SystemUI/proguard.flags3
-rw-r--r--packages/SystemUI/src/com/android/systemui/SystemUIApplication.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java3
-rw-r--r--packages/SystemUI/tests/Android.mk1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java67
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java284
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java121
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);
+ }
+}