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
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 11bd392..6e7c720 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -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>
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
index d5541e9..7bc7e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java
@@ -33,6 +33,7 @@
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.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 @@
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 @@
mListener = listener;
mAllowMultiple = allowMultiple;
mVersion = version;
+ mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist));
isDebuggable = debuggable;
}
@@ -294,9 +300,9 @@
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();
diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java
index 2a17e35..1cbf1fe 100644
--- a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java
@@ -37,13 +37,12 @@
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 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 @@
= 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,30 +80,30 @@
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.
- Dependency.get(PluginDependencyProvider.class)
- .allowPluginDependency(ActivityStarter.class);
- });
- }
+ 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.
+ Dependency.get(PluginDependencyProvider.class)
+ .allowPluginDependency(ActivityStarter.class);
+ });
}
public <T extends Plugin> T getOneShotPlugin(Class<T> cls) {
@@ -117,10 +118,6 @@
}
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 <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 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 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);
}
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 04441ab..19974f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginInstanceManagerTest.java
@@ -64,6 +64,7 @@
@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 @@
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 @@
// 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();
@@ -199,6 +200,25 @@
}
@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.
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 94dbc2a..438f9e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java
@@ -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.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 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 @@
@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 @@
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 @@
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 @@
}
@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