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
This commit is contained in:
Jason Monk
2016-08-16 13:17:56 -04:00
parent 72b817d1e6
commit 86bc331889
22 changed files with 1434 additions and 2 deletions

View File

@ -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 \

View File

@ -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" />

View File

@ -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)

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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" />

View File

@ -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");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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() {
}
}

View File

@ -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;
}
}
}

View File

@ -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.
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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 **;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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 \

View File

@ -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) {
}
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}