Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26bc8996 authored by Jason Monk's avatar Jason Monk
Browse files

Add notifications for incorrect plugin versions

Test: runtest systemui
Change-Id: Ic59a583202a8a20fbfc6fb504e6ab60ecc71ce78
parent 46708586
Loading
Loading
Loading
Loading
+49 −3
Original line number Diff line number Diff line
@@ -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<T extends Plugin> {
            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<T extends Plugin> {
                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);
+17 −0
Original line number Diff line number Diff line
@@ -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();
+1 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@
    <item type="id" name="notification_hidden"/>
    <item type="id" name="notification_volumeui"/>
    <item type="id" name="notification_temperature"/>
    <item type="id" name="notification_plugin"/>
    <item type="id" name="transformation_start_x_tag"/>
    <item type="id" name="transformation_start_y_tag"/>
    <item type="id" name="transformation_start_scale_x_tag"/>
+28 −22
Original line number Diff line number Diff line
@@ -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<ResolveInfo> 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);
        }
    }

    private void createPlugin() {
    private void createPlugin() throws Exception {
        setupFakePmQuery();

        mPluginInstanceManager.loadAll();
+25 −0
Original line number Diff line number Diff line
@@ -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.
Loading