Merge "Add one-shot plugin support"

This commit is contained in:
TreeHugger Robot
2017-01-11 19:35:19 +00:00
committed by Android (Google) Code Review
9 changed files with 170 additions and 45 deletions

View File

@@ -221,6 +221,15 @@
</intent-filter>
</receiver>
<receiver android:name=".SysuiRestartReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.android.systemui.action.RESTART" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<service android:name=".ImageWallpaper"
android:permission="android.permission.BIND_WALLPAPER"
android:exported="true" />

View File

@@ -87,6 +87,21 @@ public class PluginInstanceManager<T extends Plugin> {
isDebuggable = debuggable;
}
public PluginInfo<T> getPlugin() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new RuntimeException("Must be called from UI thread");
}
mPluginHandler.handleQueryPlugins(null /* All packages */);
if (mPluginHandler.mPlugins.size() > 0) {
mMainHandler.removeMessages(MainHandler.PLUGIN_CONNECTED);
PluginInfo<T> info = mPluginHandler.mPlugins.get(0);
PluginPrefs.setHasPlugins(mContext);
info.mPlugin.onCreate(mContext, info.mPluginContext);
return info;
}
return null;
}
public void loadAll() {
if (DEBUG) Log.d(TAG, "startListening");
mPluginHandler.sendEmptyMessage(PluginHandler.QUERY_ALL);
@@ -366,11 +381,11 @@ public class PluginInstanceManager<T extends Plugin> {
}
}
private static class PluginInfo<T> {
static class PluginInfo<T> {
private final Context mPluginContext;
private T mPlugin;
private String mClass;
private String mPackage;
T mPlugin;
String mPackage;
public PluginInfo(String pkg, String cls, T plugin, Context pluginContext) {
mPlugin = plugin;

View File

@@ -14,7 +14,10 @@
package com.android.systemui.plugins;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -23,16 +26,20 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import dalvik.system.PathClassLoader;
@@ -54,11 +61,14 @@ public class PluginManager extends BroadcastReceiver {
private final ArrayMap<PluginListener<?>, PluginInstanceManager> mPluginMap
= new ArrayMap<>();
private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
private final ArraySet<String> mOneShotPackages = new ArraySet<>();
private final Context mContext;
private final PluginInstanceManagerFactory mFactory;
private final boolean isDebuggable;
private final PluginPrefs mPluginPrefs;
private ClassLoaderFilter mParentClassLoader;
private boolean mListening;
private boolean mHasOneShot;
private PluginManager(Context context) {
this(context, new PluginInstanceManagerFactory(),
@@ -80,6 +90,27 @@ public class PluginManager extends BroadcastReceiver {
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
public <T extends Plugin> T getOneShotPlugin(String action, int version) {
if (!isDebuggable) {
// Never ever ever allow these on production builds, they are only for prototyping.
return null;
}
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new RuntimeException("Must be called from UI thread");
}
PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null,
false, mBackgroundThread.getLooper(), version, this);
mPluginPrefs.addAction(action);
PluginInfo<T> info = p.getPlugin();
if (info != null) {
mOneShotPackages.add(info.mPackage);
mHasOneShot = true;
startListening();
return info.mPlugin;
}
return null;
}
public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
int version) {
addPluginListener(action, listener, version, false);
@@ -96,9 +127,7 @@ public class PluginManager extends BroadcastReceiver {
allowMultiple, mBackgroundThread.getLooper(), version, this);
p.loadAll();
mPluginMap.put(listener, p);
if (mPluginMap.size() == 1) {
startListening();
}
startListening();
}
public void removePluginListener(PluginListener<?> listener) {
@@ -108,12 +137,12 @@ public class PluginManager extends BroadcastReceiver {
}
if (!mPluginMap.containsKey(listener)) return;
mPluginMap.remove(listener).destroy();
if (mPluginMap.size() == 0) {
stopListening();
}
stopListening();
}
private void startListening() {
if (mListening) return;
mListening = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -126,6 +155,9 @@ public class PluginManager extends BroadcastReceiver {
}
private void stopListening() {
// Never stop listening if a one-shot is present.
if (!mListening || mHasOneShot) return;
mListening = false;
mContext.unregisterReceiver(this);
}
@@ -147,6 +179,34 @@ public class PluginManager extends BroadcastReceiver {
} else {
Uri data = intent.getData();
String pkg = data.getEncodedSchemeSpecificPart();
if (mOneShotPackages.contains(pkg)) {
int icon = mContext.getResources().getIdentifier("tuner", "drawable",
mContext.getPackageName());
int color = Resources.getSystem().getIdentifier(
"system_notification_accent_color", "color", "android");
String label = pkg;
try {
PackageManager pm = mContext.getPackageManager();
label = pm.getApplicationInfo(pkg, 0).loadLabel(pm).toString();
} catch (NameNotFoundException e) {
}
// Localization not required as this will never ever appear in a user build.
final Notification.Builder nb = new Notification.Builder(mContext)
.setSmallIcon(icon)
.setWhen(0)
.setShowWhen(false)
.setPriority(Notification.PRIORITY_MAX)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setColor(mContext.getColor(color))
.setContentTitle("Plugin \"" + label + "\" has updated")
.setContentText("Restart SysUI for changes to take effect.");
Intent i = new Intent("com.android.systemui.action.RESTART").setData(
Uri.parse("package://" + pkg));
PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0);
nb.addAction(new Action.Builder(null, "Restart SysUI", pi).build());
mContext.getSystemService(NotificationManager.class).notifyAsUser(pkg,
SystemMessage.NOTE_PLUGIN, nb.build(), UserHandle.ALL);
}
clearClassLoader(pkg);
if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
for (PluginInstanceManager manager : mPluginMap.values()) {

View File

@@ -80,7 +80,6 @@ public class SystemUIApplication extends Application {
ShortcutKeyDispatcher.class,
VendorServices.class,
LatencyTester.class,
DozeFactory.Initializer.class,
};
/**

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2017 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;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Process;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
public class SysuiRestartReceiver extends BroadcastReceiver {
public static String ACTION = "com.android.systemui.action.RESTART";
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION.equals(intent.getAction())) {
String pkg = intent.getData().toString().substring(10);
NotificationManager.from(context).cancel(pkg, SystemMessage.NOTE_PLUGIN);
Process.killProcess(Process.myPid());
}
}
}

View File

@@ -34,31 +34,10 @@ import com.android.systemui.statusbar.phone.DozeParameters;
public class DozeFactory {
private static DozeFactory sInstance;
private final DozeProvider mDozePlugin;
private DozeProvider mDozePlugin;
/** Returns the singleton instance. */
public static DozeFactory getInstance(Context context) {
if (sInstance == null) {
sInstance = new DozeFactory();
PluginManager.getInstance(context).addPluginListener(DozeProvider.ACTION,
new PluginListener<DozeProvider>() {
@Override
public void onPluginConnected(DozeProvider plugin) {
sInstance.mDozePlugin = plugin;
}
@Override
public void onPluginDisconnected(DozeProvider plugin) {
if (sInstance.mDozePlugin == plugin) {
sInstance.mDozePlugin = null;
}
}
},
DozeProvider.VERSION, false /* Only one */);
}
return sInstance;
public DozeFactory(DozeProvider plugin) {
mDozePlugin = plugin;
}
/** Creates a DozeMachine with its parts for {@code dozeService}. */
@@ -202,13 +181,4 @@ public class DozeFactory {
return mInner.wrap(runnable);
}
}
/** Hack: We need to initialize the plugin listener before doze actually starts.
* This will be unnecessary once we have proper one-shot support */
public static class Initializer extends SystemUI {
@Override
public void start() {
getInstance(mContext);
}
}
}

View File

@@ -19,6 +19,10 @@ package com.android.systemui.doze;
import android.service.dreams.DreamService;
import android.util.Log;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.doze.DozeProvider;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -38,7 +42,9 @@ public class DozeService extends DreamService implements DozeMachine.Service {
setWindowless(true);
mDozeMachine = DozeFactory.getInstance(getApplication()).assembleMachine(this);
DozeProvider provider = PluginManager.getInstance(this)
.getOneShotPlugin(DozeProvider.ACTION, DozeProvider.VERSION);
mDozeMachine = new DozeFactory(provider).assembleMachine(this);
}
@Override

View File

@@ -15,6 +15,7 @@
package com.android.systemui.plugins;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Matchers.any;
@@ -39,11 +40,13 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.HandlerThread;
import android.os.UserHandle;
import android.support.test.annotation.UiThreadTest;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import org.junit.After;
import org.junit.Before;
@@ -92,6 +95,15 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
sMockPlugin = null;
}
@UiThreadTest
@Test
public void testGetPlugin() throws Exception {
setupFakePmQuery();
PluginInfo p = mPluginInstanceManager.getPlugin();
assertNotNull(p.mPlugin);
verify(sMockPlugin).onCreate(any(), any());
}
@Test
public void testNoPlugins() throws Exception {
when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn(

View File

@@ -13,6 +13,8 @@
*/
package com.android.systemui.plugins;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -24,11 +26,13 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.support.test.annotation.UiThreadTest;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.PluginManager.PluginInstanceManagerFactory;
import org.junit.Before;
@@ -66,6 +70,16 @@ public class PluginManagerTest extends SysuiTestCase {
mMockListener = mock(PluginListener.class);
}
@UiThreadTest
@Test
public void testOneShot() {
Plugin mockPlugin = mock(Plugin.class);
when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
null));
Plugin result = mPluginManager.getOneShotPlugin("myAction", 1);
assertTrue(result == mockPlugin);
}
@Test
public void testAddListener() {
mPluginManager.addPluginListener("myAction", mMockListener, 1);
@@ -86,9 +100,12 @@ public class PluginManagerTest extends SysuiTestCase {
mPluginManager = new PluginManager(getContext(), mMockFactory, false,
mMockExceptionHandler);
resetExceptionHandler();
mPluginManager.addPluginListener("myAction", mMockListener, 1);
mPluginManager.addPluginListener("myAction", mMockListener, 1);
verify(mMockPluginInstance, Mockito.never()).loadAll();
assertNull(mPluginManager.getOneShotPlugin("myPlugin", 1));
verify(mMockPluginInstance, Mockito.never()).getPlugin();
}
@Test