diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 24dcd3e3b70d3..2fbf42fc0f39a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -508,4 +508,7 @@ true + + + diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java index d5541e9be17ef..7bc7e5f0095e3 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.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 { @@ -63,17 +66,19 @@ public class PluginInstanceManager { private final boolean isDebuggable; private final PackageManager mPm; private final PluginManagerImpl mManager; + private final ArraySet mWhitelistedPlugins = new ArraySet<>(); PluginInstanceManager(Context context, String action, PluginListener 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 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 { mListener = listener; mAllowMultiple = allowMultiple; mVersion = version; + mWhitelistedPlugins.addAll(Arrays.asList(pluginWhitelist)); isDebuggable = debuggable; } @@ -294,9 +300,9 @@ public class PluginInstanceManager { protected PluginInfo 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 2a17e35f00dd8..1cbf1fe0f2c47 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.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 mClassLoaders = new ArrayMap<>(); private final ArraySet mOneShotPackages = new ArraySet<>(); + private final ArraySet mWhitelistedPlugins = new ArraySet<>(); private final Context mContext; private final PluginInstanceManagerFactory mFactory; private final boolean isDebuggable; @@ -79,30 +80,30 @@ 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. - 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 getOneShotPlugin(Class cls) { @@ -117,10 +118,6 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage } public 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 void addPluginListener(String action, PluginListener 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); } 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 04441abee92e0..19974f8fc7106 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 @@ 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. 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 94dbc2ad7147e..438f9e49699ba 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.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