Merge "Allow whitelisted plugins on user builds"

This commit is contained in:
TreeHugger Robot
2018-09-22 01:12:30 +00:00
committed by Android (Google) Code Review
5 changed files with 78 additions and 44 deletions

View File

@@ -508,4 +508,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>

View File

@@ -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();

View File

@@ -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,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 extends Plugin> T getOneShotPlugin(Class<T> cls) {
@@ -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);
}

View File

@@ -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.

View File

@@ -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