diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java index 62d3ce43474fd..eab47223bee16 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -14,18 +14,27 @@ 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; 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.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.UserHandle; import android.util.Log; import android.view.LayoutInflater; @@ -260,10 +269,9 @@ public class PluginInstanceManager { String pkg = component.getPackageName(); String cls = component.getClassName(); try { - PackageManager pm = mPm; - ApplicationInfo info = pm.getApplicationInfo(pkg, 0); + ApplicationInfo info = mPm.getApplicationInfo(pkg, 0); // TODO: This probably isn't needed given that we don't have IGNORE_SECURITY on - if (pm.checkPermission(PLUGIN_PERMISSION, pkg) + if (mPm.checkPermission(PLUGIN_PERMISSION, pkg) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Plugin doesn't have permission: " + pkg); return null; @@ -275,6 +283,44 @@ public class PluginInstanceManager { Class pluginClass = Class.forName(cls, true, classLoader); T plugin = (T) pluginClass.newInstance(); if (plugin.getVersion() != mVersion) { + final int id = mContext.getResources().getIdentifier("notification_plugin", + "id", mContext.getPackageName()); + final int icon = mContext.getResources().getIdentifier("tuner", "drawable", + mContext.getPackageName()); + final int color = Resources.getSystem().getIdentifier( + "system_notification_accent_color", "color", "android"); + final Notification.Builder nb = new Notification.Builder(mContext) + .setStyle(new Notification.BigTextStyle()) + .setSmallIcon(icon) + .setWhen(0) + .setShowWhen(false) + .setPriority(Notification.PRIORITY_MAX) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setColor(mContext.getColor(color)); + String label = cls; + try { + label = mPm.getServiceInfo(component, 0).loadLabel(mPm).toString(); + } catch (NameNotFoundException e) { + } + if (plugin.getVersion() < mVersion) { + // Localization not required as this will never ever appear in a user build. + nb.setContentTitle("Plugin \"" + label + "\" is too old") + .setContentText("Contact plugin developer to get an updated" + + " version.\nPlugin version: " + plugin.getVersion() + + "\nSystem version: " + mVersion); + } else { + // Localization not required as this will never ever appear in a user build. + nb.setContentTitle("Plugin \"" + label + "\" is too new") + .setContentText("Check to see if an OTA is available.\n" + + "Plugin version: " + plugin.getVersion() + + "\nSystem version: " + mVersion); + } + Intent i = new Intent(PluginManager.DISABLE_PLUGIN).setData( + Uri.parse("package://" + component.flattenToString())); + PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, i, 0); + nb.addAction(new Action.Builder(null, "Disable plugin", pi).build()); + mContext.getSystemService(NotificationManager.class) + .notifyAsUser(cls, id, nb.build(), UserHandle.ALL); // TODO: Warn user. Log.w(TAG, "Plugin has invalid interface version " + plugin.getVersion() + ", expected " + mVersion); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java index 60cf3122966a9..c3de09250da44 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginManager.java @@ -14,11 +14,14 @@ package com.android.systemui.plugins; +import android.app.NotificationManager; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; 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.net.Uri; import android.os.Build; @@ -42,6 +45,8 @@ public class PluginManager extends BroadcastReceiver { public static final String PLUGIN_CHANGED = "com.android.systemui.action.PLUGIN_CHANGED"; + static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN"; + private static PluginManager sInstance; private final HandlerThread mBackgroundThread; @@ -112,6 +117,7 @@ public class PluginManager extends BroadcastReceiver { filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(PLUGIN_CHANGED); + filter.addAction(DISABLE_PLUGIN); filter.addDataScheme("package"); mContext.registerReceiver(this, filter); filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); @@ -128,6 +134,17 @@ public class PluginManager extends BroadcastReceiver { for (PluginInstanceManager manager : mPluginMap.values()) { manager.loadAll(); } + } else if (DISABLE_PLUGIN.equals(intent.getAction())) { + Uri uri = intent.getData(); + ComponentName component = ComponentName.unflattenFromString( + uri.toString().substring(10)); + mContext.getPackageManager().setComponentEnabledSetting(component, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + int id = mContext.getResources().getIdentifier("notification_plugin", "id", + mContext.getPackageName()); + mContext.getSystemService(NotificationManager.class).cancel(component.getClassName(), + id); } else { Uri data = intent.getData(); String pkg = data.getEncodedSchemeSpecificPart(); diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 7ef2abdf9b2a4..56cd8c7c97a35 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -54,6 +54,7 @@ + diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java index 9050b83f4ce03..66e617bd8f758 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java @@ -17,11 +17,15 @@ package com.android.systemui.plugins; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; +import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -34,6 +38,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.HandlerThread; +import android.os.UserHandle; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; @@ -72,7 +77,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { mMockPm = mock(PackageManager.class); mMockListener = mock(PluginListener.class); mMockManager = mock(PluginManager.class); - when(mMockManager.getClassLoader(Mockito.any(), Mockito.any())) + when(mMockManager.getClassLoader(any(), any())) .thenReturn(getClass().getClassLoader()); mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, true); @@ -87,8 +92,8 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testNoPlugins() { - when(mMockPm.queryIntentServices(Mockito.any(), Mockito.anyInt())).thenReturn( + public void testNoPlugins() throws Exception { + when(mMockPm.queryIntentServices(any(), anyInt())).thenReturn( Collections.emptyList()); mPluginInstanceManager.loadAll(); @@ -100,7 +105,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testPluginCreate() { + public void testPluginCreate() throws Exception { createPlugin(); // Verify startup lifecycle @@ -110,7 +115,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testPluginDestroy() { + public void testPluginDestroy() throws Exception { createPlugin(); // Get into valid created state. mPluginInstanceManager.destroy(); @@ -124,7 +129,9 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testIncorrectVersion() { + public void testIncorrectVersion() throws Exception { + NotificationManager nm = mock(NotificationManager.class); + mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm); setupFakePmQuery(); when(sMockPlugin.getVersion()).thenReturn(2); @@ -136,10 +143,12 @@ public class PluginInstanceManagerTest extends SysuiTestCase { // Plugin shouldn't be connected because it is the wrong version. verify(mMockListener, Mockito.never()).onPluginConnected( ArgumentCaptor.forClass(Plugin.class).capture()); + verify(nm).notifyAsUser(eq(TestPlugin.class.getName()), eq(R.id.notification_plugin), any(), + eq(UserHandle.ALL)); } @Test - public void testReloadOnChange() { + public void testReloadOnChange() throws Exception { createPlugin(); // Get into valid created state. mPluginInstanceManager.onPackageChange("com.android.systemui"); @@ -159,7 +168,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testNonDebuggable() { + public void testNonDebuggable() throws Exception { // Create a version that thinks the build is not debuggable. mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction", mMockListener, true, mHandlerThread.getLooper(), 1, mMockManager, false); @@ -176,7 +185,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testCheckAndDisable() { + public void testCheckAndDisable() throws Exception { createPlugin(); // Get into valid created state. // Start with an unrelated class. @@ -197,7 +206,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase { } @Test - public void testDisableAll() { + public void testDisableAll() throws Exception { createPlugin(); // Get into valid created state. mPluginInstanceManager.disableAll(); @@ -208,29 +217,26 @@ public class PluginInstanceManagerTest extends SysuiTestCase { ArgumentCaptor.forClass(int.class).capture()); } - private void setupFakePmQuery() { + private void setupFakePmQuery() throws Exception { List list = new ArrayList<>(); ResolveInfo info = new ResolveInfo(); - info.serviceInfo = new ServiceInfo(); + info.serviceInfo = mock(ServiceInfo.class); info.serviceInfo.packageName = "com.android.systemui"; info.serviceInfo.name = TestPlugin.class.getName(); + when(info.serviceInfo.loadLabel(any())).thenReturn("Test Plugin"); list.add(info); - when(mMockPm.queryIntentServices(Mockito.any(), Mockito.anyInt())).thenReturn(list); + when(mMockPm.queryIntentServices(any(), Mockito.anyInt())).thenReturn(list); + when(mMockPm.getServiceInfo(any(), anyInt())).thenReturn(info.serviceInfo); 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); - } + ApplicationInfo appInfo = getContext().getApplicationInfo(); + when(mMockPm.getApplicationInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn( + appInfo); } - private void createPlugin() { + private void createPlugin() throws Exception { setupFakePmQuery(); mPluginInstanceManager.loadAll(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java index 4b1827d51a34c..63b1817a1f78d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java @@ -13,10 +13,17 @@ */ package com.android.systemui.plugins; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.NotificationManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; @@ -113,6 +120,24 @@ public class PluginManagerTest extends SysuiTestCase { ArgumentCaptor.forClass(Throwable.class).capture()); } + @Test + public void testDisableIntent() { + NotificationManager nm = mock(NotificationManager.class); + PackageManager pm = mock(PackageManager.class); + mContext.addMockSystemService(Context.NOTIFICATION_SERVICE, nm); + mContext.setMockPackageManager(pm); + + ComponentName testComponent = new ComponentName(getContext().getPackageName(), + PluginManagerTest.class.getName()); + Intent intent = new Intent(PluginManager.DISABLE_PLUGIN); + intent.setData(Uri.parse("package://" + testComponent.flattenToString())); + mPluginManager.onReceive(mContext, intent); + verify(nm).cancel(eq(testComponent.getClassName()), eq(R.id.notification_plugin)); + verify(pm).setComponentEnabledSetting(eq(testComponent), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP)); + } + private void resetExceptionHandler() { mPluginExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); // Set back the real exception handler so the test can crash if it wants to. diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java index a95280641aa15..710f88af46bc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java @@ -23,6 +23,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Handler; import android.os.IBinder; @@ -42,6 +43,7 @@ public class TestableContext extends ContextWrapper { private ArrayMap mMockServices; private ArrayMap mActiveServices; + private PackageManager mMockPackageManager; private Tracker mReceiver; private Tracker mService; private Tracker mComponent; @@ -59,6 +61,18 @@ public class TestableContext extends ContextWrapper { mComponent = test.getTracker("component"); } + public void setMockPackageManager(PackageManager mock) { + mMockPackageManager = mock; + } + + @Override + public PackageManager getPackageManager() { + if (mMockPackageManager != null) { + return mMockPackageManager; + } + return super.getPackageManager(); + } + @Override public Resources getResources() { return super.getResources();