Merge "Plugins for sysui"
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
29
packages/SystemUI/plugin/Android.mk
Normal file
29
packages/SystemUI/plugin/Android.mk
Normal 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)
|
||||
24
packages/SystemUI/plugin/AndroidManifest.xml
Normal file
24
packages/SystemUI/plugin/AndroidManifest.xml
Normal 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>
|
||||
15
packages/SystemUI/plugin/ExamplePlugin/Android.mk
Normal file
15
packages/SystemUI/plugin/ExamplePlugin/Android.mk
Normal 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)
|
||||
31
packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml
Normal file
31
packages/SystemUI/plugin/ExamplePlugin/AndroidManifest.xml
Normal 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>
|
||||
@@ -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" />
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 **;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,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;
|
||||
@@ -183,6 +187,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user