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

Commit c293d8fe authored by Lucas Dupin's avatar Lucas Dupin
Browse files

Allow whitelisted plugins on user builds

Plugins should only run on user builds if explicitly whitelisted.
It's also necessary to hold a signature permission:
  com.android.systemui.permission.PLUGIN

Test: atest PluginInstanceManagerTest
Test: atest PluginManagerTest
Test: manually try to install plugin on user build (whitelisted or not)
Bug: 111414690
Change-Id: If17b13f4caef677d641cba84b491b65c8135679b
parent 50df226b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -498,4 +498,7 @@

    <!-- Allow dragging the PIP to a location to close it -->
    <bool name="config_pipEnableDismissDragToEdge">true</bool>

    <!-- SystemUI Plugins that can be loaded on user builds. -->
    <string-array name="config_pluginWhitelist" translatable="false" />
</resources>
+10 −4
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;

@@ -41,7 +42,9 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.plugins.VersionInfo.InvalidVersionException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.android.systemui.R;

public class PluginInstanceManager<T extends Plugin> {

@@ -63,17 +66,19 @@ public class PluginInstanceManager<T extends Plugin> {
    private final boolean isDebuggable;
    private final PackageManager mPm;
    private final PluginManagerImpl mManager;
    private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>();

    PluginInstanceManager(Context context, String action, PluginListener<T> listener,
            boolean allowMultiple, Looper looper, VersionInfo version, PluginManagerImpl manager) {
        this(context, context.getPackageManager(), action, listener, allowMultiple, looper, version,
                manager, Build.IS_DEBUGGABLE);
                manager, Build.IS_DEBUGGABLE,
                context.getResources().getStringArray(R.array.config_pluginWhitelist));
    }

    @VisibleForTesting
    PluginInstanceManager(Context context, PackageManager pm, String action,
            PluginListener<T> listener, boolean allowMultiple, Looper looper, VersionInfo version,
            PluginManagerImpl manager, boolean debuggable) {
            PluginManagerImpl manager, boolean debuggable, String[] pluginWhitelist) {
        mMainHandler = new MainHandler(Looper.getMainLooper());
        mPluginHandler = new PluginHandler(looper);
        mManager = manager;
@@ -83,6 +88,7 @@ public class PluginInstanceManager<T extends Plugin> {
        mListener = listener;
        mAllowMultiple = allowMultiple;
        mVersion = version;
        mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist));
        isDebuggable = debuggable;
    }

@@ -294,9 +300,9 @@ public class PluginInstanceManager<T extends Plugin> {
        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) {
            if (!isDebuggable && !mWhitelistedPlugins.contains(component.getPackageName())) {
                // Never ever ever allow these on production builds, they are only for prototyping.
                Log.d(TAG, "Somehow hit second debuggable check");
                Log.w(TAG, "Plugin cannot be loaded on production build: " + component);
                return null;
            }
            String pkg = component.getPackageName();
+20 −26
Original line number Diff line number Diff line
@@ -37,13 +37,12 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Log.TerribleFailure;
import android.util.Log.TerribleFailureHandler;
import android.widget.Toast;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.annotations.ProvidesInterface;
@@ -53,13 +52,14 @@ import dalvik.system.PathClassLoader;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Arrays;
import java.util.Map;

/**
 * @see Plugin
 */
public class PluginManagerImpl extends BroadcastReceiver implements PluginManager {

    private static final String TAG = PluginManagerImpl.class.getSimpleName();
    static final String DISABLE_PLUGIN = "com.android.systemui.action.DISABLE_PLUGIN";

    private static PluginManager sInstance;
@@ -68,6 +68,7 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
            = new ArrayMap<>();
    private final Map<String, ClassLoader> mClassLoaders = new ArrayMap<>();
    private final ArraySet<String> mOneShotPackages = new ArraySet<>();
    private final ArraySet<String> mWhitelistedPlugins = new ArraySet<>();
    private final Context mContext;
    private final PluginInstanceManagerFactory mFactory;
    private final boolean isDebuggable;
@@ -79,23 +80,24 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
    private boolean mWtfsSet;

    public PluginManagerImpl(Context context) {
        this(context, new PluginInstanceManagerFactory(),
                Build.IS_DEBUGGABLE, Thread.getUncaughtExceptionPreHandler());
        this(context, new PluginInstanceManagerFactory(), Build.IS_DEBUGGABLE,
                context.getResources().getStringArray(R.array.config_pluginWhitelist),
                Thread.getUncaughtExceptionPreHandler());
    }

    @VisibleForTesting
    PluginManagerImpl(Context context, PluginInstanceManagerFactory factory, boolean debuggable,
            UncaughtExceptionHandler defaultHandler) {
            String[] whitelistedPlugins, UncaughtExceptionHandler defaultHandler) {
        mContext = context;
        mFactory = factory;
        mLooper = Dependency.get(Dependency.BG_LOOPER);
        isDebuggable = debuggable;
        mWhitelistedPlugins.addAll(Arrays.asList(whitelistedPlugins));
        mPluginPrefs = new PluginPrefs(mContext);

        PluginExceptionHandler uncaughtExceptionHandler = new PluginExceptionHandler(
                defaultHandler);
        Thread.setUncaughtExceptionPreHandler(uncaughtExceptionHandler);
        if (isDebuggable) {
        new Handler(mLooper).post(() -> {
            // Plugin dependencies that don't have another good home can go here, but
            // dependencies that have better places to init can happen elsewhere.
@@ -103,7 +105,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
                    .allowPluginDependency(ActivityStarter.class);
        });
    }
    }

    public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
        ProvidesInterface info = cls.getDeclaredAnnotation(ProvidesInterface.class);
@@ -117,10 +118,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
    }

    public <T extends Plugin> T getOneShotPlugin(String action, Class<?> cls) {
        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");
        }
@@ -153,10 +150,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage

    public <T extends Plugin> void addPluginListener(String action, PluginListener<T> listener,
            Class cls, boolean allowMultiple) {
        if (!isDebuggable) {
            // Never ever ever allow these on production builds, they are only for prototyping.
            return;
        }
        mPluginPrefs.addAction(action);
        PluginInstanceManager p = mFactory.createPluginInstanceManager(mContext, action, listener,
                allowMultiple, mLooper, cls, this);
@@ -166,10 +159,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
    }

    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).destroy();
        if (mPluginMap.size() == 0) {
@@ -261,6 +250,11 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage
    }

    public ClassLoader getClassLoader(String sourceDir, String pkg) {
        if (!isDebuggable && !mWhitelistedPlugins.contains(pkg)) {
            Log.w(TAG, "Cannot get class loader for non-whitelisted plugin. Src:" + sourceDir +
                    ", pkg: " + pkg);
            return null;
        }
        if (mClassLoaders.containsKey(pkg)) {
            return mClassLoaders.get(pkg);
        }
+22 −2
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ import java.util.List;
@RunWith(AndroidJUnit4.class)
public class PluginInstanceManagerTest extends SysuiTestCase {

    private static final String WHITELISTED_PACKAGE = "com.android.systemui";
    // Static since the plugin needs to be generated by the PluginInstanceManager using newInstance.
    private static Plugin sMockPlugin;

@@ -88,7 +89,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
        mMockVersionInfo = mock(VersionInfo.class);
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
                mMockManager, true);
                mMockManager, true, new String[0]);
        sMockPlugin = mock(Plugin.class);
        when(sMockPlugin.getVersion()).thenReturn(1);
    }
@@ -186,7 +187,7 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
        // Create a version that thinks the build is not debuggable.
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
                mMockManager, false);
                mMockManager, false, new String[0]);
        setupFakePmQuery();

        mPluginInstanceManager.loadAll();
@@ -198,6 +199,25 @@ public class PluginInstanceManagerTest extends SysuiTestCase {
        verify(mMockListener, Mockito.never()).onPluginConnected(any(), any());
    }

    @Test
    public void testNonDebuggable_whitelist() throws Exception {
        // Create a version that thinks the build is not debuggable.
        mPluginInstanceManager = new PluginInstanceManager(mContextWrapper, mMockPm, "myAction",
                mMockListener, true, mHandlerThread.getLooper(), mMockVersionInfo,
                mMockManager, false, new String[] {WHITELISTED_PACKAGE});
        setupFakePmQuery();

        mPluginInstanceManager.loadAll();

        waitForIdleSync(mPluginInstanceManager.mPluginHandler);
        waitForIdleSync(mPluginInstanceManager.mMainHandler);

        // Verify startup lifecycle
        verify(sMockPlugin).onCreate(ArgumentCaptor.forClass(Context.class).capture(),
                ArgumentCaptor.forClass(Context.class).capture());
        verify(mMockListener).onPluginConnected(any(), any());
    }

    @Test
    public void testCheckAndDisable() throws Exception {
        createPlugin(); // Get into valid created state.
+23 −12
Original line number Diff line number Diff line
@@ -13,8 +13,9 @@
 */
package com.android.systemui.plugins;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -26,8 +27,6 @@ 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 android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -36,11 +35,10 @@ import android.testing.TestableLooper.RunWithLooper;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.Dependency;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.PluginInstanceManager.PluginInfo;
import com.android.systemui.plugins.PluginManagerImpl.PluginInstanceManagerFactory;
import com.android.systemui.plugins.annotations.ProvidesInterface;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -54,6 +52,8 @@ import java.lang.Thread.UncaughtExceptionHandler;
@RunWithLooper
public class PluginManagerTest extends SysuiTestCase {

    private static final String WHITELISTED_PACKAGE = "com.android.systemui";

    private PluginInstanceManagerFactory mMockFactory;
    private PluginInstanceManager mMockPluginInstance;
    private PluginManagerImpl mPluginManager;
@@ -74,7 +74,7 @@ public class PluginManagerTest extends SysuiTestCase {
        when(mMockFactory.createPluginInstanceManager(Mockito.any(), Mockito.any(), Mockito.any(),
                Mockito.anyBoolean(), Mockito.any(), Mockito.any(), Mockito.any()))
                .thenReturn(mMockPluginInstance);
        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, true,
        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, true, new String[0],
                mMockExceptionHandler);
        resetExceptionHandler();
        mMockListener = mock(PluginListener.class);
@@ -87,7 +87,7 @@ public class PluginManagerTest extends SysuiTestCase {
        when(mMockPluginInstance.getPlugin()).thenReturn(new PluginInfo(null, null, mockPlugin,
                null, null));
        Plugin result = mPluginManager.getOneShotPlugin("myAction", TestPlugin.class);
        assertTrue(result == mockPlugin);
        assertSame(mockPlugin, result);
    }

    @Test
@@ -106,16 +106,27 @@ public class PluginManagerTest extends SysuiTestCase {
    }

    @Test
    public void testNonDebuggable() {
    @RunWithLooper(setAsMainLooper = true)
    public void testNonDebuggable_noWhitelist() {
        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, false,
                mMockExceptionHandler);
                new String[0], mMockExceptionHandler);
        resetExceptionHandler();

        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
        verify(mMockPluginInstance, Mockito.never()).loadAll();

        assertNull(mPluginManager.getOneShotPlugin("myPlugin", TestPlugin.class));
        verify(mMockPluginInstance, Mockito.never()).getPlugin();
        assertNull(mPluginManager.getClassLoader("myPlugin", WHITELISTED_PACKAGE));
    }

    @Test
    @RunWithLooper(setAsMainLooper = true)
    public void testNonDebuggable_whitelistedPkg() {
        mPluginManager = new PluginManagerImpl(getContext(), mMockFactory, false,
                new String[] {WHITELISTED_PACKAGE}, mMockExceptionHandler);
        resetExceptionHandler();

        mPluginManager.addPluginListener("myAction", mMockListener, TestPlugin.class);
        assertNotNull(mPluginManager.getClassLoader("myPlugin", WHITELISTED_PACKAGE));
        assertNull(mPluginManager.getClassLoader("myPlugin", "com.android.invalidpackage"));
    }

    @Test