Use the correct user context in CustomTile
CustomTile was using the SystemUI context (in user 0) to retrieve service information and resources. This failed in 3rd party tiles that could be installed just for a particular user but were not installed in user 0. Additionally, having observed cases where the sysui_qs_tiles setting has "work,work,work,....", this CL cleans that up and prevents it happening in the future by not allowing repeats in the specs list. Fixes some leaks in CustomTile: * destroy CustomTile that changes user * remove Change listerner from TileLifecycleManager Test: atest QSTileHostTest CustomTileTest Test: manual Test: hprof Fixes: 141904434 Change-Id: I92b26294dec63477943d28e2000fe3e0c16f8aa3
This commit is contained in:
@@ -28,6 +28,7 @@ import android.provider.Settings;
|
||||
import android.provider.Settings.Secure;
|
||||
import android.service.quicksettings.Tile;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.systemui.DumpController;
|
||||
@@ -61,6 +62,7 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -91,6 +93,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
|
||||
private int mCurrentUser;
|
||||
private final Optional<StatusBar> mStatusBarOptional;
|
||||
private Context mUserContext;
|
||||
|
||||
@Inject
|
||||
public QSTileHost(Context context,
|
||||
@@ -107,6 +110,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
QSLogger qsLogger) {
|
||||
mIconController = iconController;
|
||||
mContext = context;
|
||||
mUserContext = context;
|
||||
mTunerService = tunerService;
|
||||
mPluginManager = pluginManager;
|
||||
mDumpController = dumpController;
|
||||
@@ -207,6 +211,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public Context getUserContext() {
|
||||
return mUserContext;
|
||||
}
|
||||
|
||||
public TileServices getTileServices() {
|
||||
return mServices;
|
||||
@@ -227,6 +234,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
}
|
||||
final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
|
||||
int currentUser = ActivityManager.getCurrentUser();
|
||||
if (currentUser != mCurrentUser) {
|
||||
mUserContext = mContext.createContextAsUser(UserHandle.of(currentUser), 0);
|
||||
}
|
||||
if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
|
||||
mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
|
||||
tile -> {
|
||||
@@ -253,6 +263,13 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
|
||||
}
|
||||
} else {
|
||||
// This means that the tile is a CustomTile AND the user is different, so let's
|
||||
// destroy it
|
||||
if (tile != null) {
|
||||
tile.destroy();
|
||||
Log.d(TAG, "Destroying tile for wrong user: " + tileSpec);
|
||||
mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user");
|
||||
}
|
||||
Log.d(TAG, "Creating tile: " + tileSpec);
|
||||
try {
|
||||
tile = createTile(tileSpec);
|
||||
@@ -273,7 +290,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
}
|
||||
}
|
||||
mCurrentUser = currentUser;
|
||||
List<String> currentSpecs = new ArrayList(mTileSpecs);
|
||||
List<String> currentSpecs = new ArrayList<>(mTileSpecs);
|
||||
mTileSpecs.clear();
|
||||
mTileSpecs.addAll(tileSpecs);
|
||||
mTiles.clear();
|
||||
@@ -300,7 +317,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
}
|
||||
|
||||
public void addTile(String spec) {
|
||||
changeTileSpecs(tileSpecs-> tileSpecs.add(spec));
|
||||
changeTileSpecs(tileSpecs-> !tileSpecs.contains(spec) && tileSpecs.add(spec));
|
||||
}
|
||||
|
||||
private void changeTileSpecs(Predicate<List<String>> changeFunction) {
|
||||
@@ -314,9 +331,12 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
}
|
||||
|
||||
public void addTile(ComponentName tile) {
|
||||
List<String> newSpecs = new ArrayList<>(mTileSpecs);
|
||||
newSpecs.add(0, CustomTile.toSpec(tile));
|
||||
changeTiles(mTileSpecs, newSpecs);
|
||||
String spec = CustomTile.toSpec(tile);
|
||||
if (!mTileSpecs.contains(spec)) {
|
||||
List<String> newSpecs = new ArrayList<>(mTileSpecs);
|
||||
newSpecs.add(0, spec);
|
||||
changeTiles(mTileSpecs, newSpecs);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeTile(ComponentName tile) {
|
||||
@@ -380,16 +400,26 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
|
||||
}
|
||||
final ArrayList<String> tiles = new ArrayList<String>();
|
||||
boolean addedDefault = false;
|
||||
Set<String> addedSpecs = new ArraySet<>();
|
||||
for (String tile : tileList.split(",")) {
|
||||
tile = tile.trim();
|
||||
if (tile.isEmpty()) continue;
|
||||
if (tile.equals("default")) {
|
||||
if (!addedDefault) {
|
||||
tiles.addAll(getDefaultSpecs(context));
|
||||
List<String> defaultSpecs = getDefaultSpecs(context);
|
||||
for (String spec : defaultSpecs) {
|
||||
if (!addedSpecs.contains(spec)) {
|
||||
tiles.add(spec);
|
||||
addedSpecs.add(spec);
|
||||
}
|
||||
}
|
||||
addedDefault = true;
|
||||
}
|
||||
} else {
|
||||
tiles.add(tile);
|
||||
if (!addedSpecs.contains(tile)) {
|
||||
tiles.add(tile);
|
||||
addedSpecs.add(tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
@@ -72,15 +73,19 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
private android.graphics.drawable.Icon mDefaultIcon;
|
||||
private CharSequence mDefaultLabel;
|
||||
|
||||
private final Context mUserContext;
|
||||
|
||||
private boolean mListening;
|
||||
private boolean mIsTokenGranted;
|
||||
private boolean mIsShowingDialog;
|
||||
|
||||
private CustomTile(QSTileHost host, String action) {
|
||||
private CustomTile(QSTileHost host, String action, Context userContext) {
|
||||
super(host);
|
||||
mWindowManager = WindowManagerGlobal.getWindowManagerService();
|
||||
mComponent = ComponentName.unflattenFromString(action);
|
||||
mTile = new Tile();
|
||||
mUserContext = userContext;
|
||||
mUser = mUserContext.getUserId();
|
||||
updateDefaultTileAndIcon();
|
||||
mServiceManager = host.getTileServices().getTileWrapper(this);
|
||||
if (mServiceManager.isToggleableTile()) {
|
||||
@@ -90,7 +95,6 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
|
||||
mService = mServiceManager.getTileService();
|
||||
mServiceManager.setTileChangeListener(this);
|
||||
mUser = ActivityManager.getCurrentUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,7 +104,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
|
||||
private void updateDefaultTileAndIcon() {
|
||||
try {
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
PackageManager pm = mUserContext.getPackageManager();
|
||||
int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE;
|
||||
if (isSystemApp(pm)) {
|
||||
flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
|
||||
@@ -318,11 +322,11 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
state.state = tileState;
|
||||
Drawable drawable;
|
||||
try {
|
||||
drawable = mTile.getIcon().loadDrawable(mContext);
|
||||
drawable = mTile.getIcon().loadDrawable(mUserContext);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Invalid icon, forcing into unavailable state");
|
||||
state.state = Tile.STATE_UNAVAILABLE;
|
||||
drawable = mDefaultIcon.loadDrawable(mContext);
|
||||
drawable = mDefaultIcon.loadDrawable(mUserContext);
|
||||
}
|
||||
|
||||
final Drawable drawableF = drawable;
|
||||
@@ -387,7 +391,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
return ComponentName.unflattenFromString(action);
|
||||
}
|
||||
|
||||
public static CustomTile create(QSTileHost host, String spec) {
|
||||
public static CustomTile create(QSTileHost host, String spec, Context userContext) {
|
||||
if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
|
||||
throw new IllegalArgumentException("Bad custom tile spec: " + spec);
|
||||
}
|
||||
@@ -395,6 +399,6 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
|
||||
if (action.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty custom tile spec action");
|
||||
}
|
||||
return new CustomTile(host, action);
|
||||
return new CustomTile(host, action, userContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements
|
||||
if (mPackageReceiverRegistered.get() || mUserReceiverRegistered.get()) {
|
||||
stopPackageListening();
|
||||
}
|
||||
mChangeListener = null;
|
||||
}
|
||||
|
||||
private void handleDeath() {
|
||||
|
||||
@@ -178,7 +178,9 @@ public class QSFactoryImpl implements QSFactory {
|
||||
}
|
||||
|
||||
// Custom tiles
|
||||
if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
|
||||
if (tileSpec.startsWith(CustomTile.PREFIX)) {
|
||||
return CustomTile.create(mHost, tileSpec, mHost.getUserContext());
|
||||
}
|
||||
|
||||
// Debug tiles.
|
||||
if (Build.IS_DEBUGGABLE) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
@@ -76,7 +77,9 @@ import javax.inject.Provider;
|
||||
public class QSTileHostTest extends SysuiTestCase {
|
||||
|
||||
private static String MOCK_STATE_STRING = "MockState";
|
||||
private static final String CUSTOM_TILE_SPEC = "custom(TEST_PKG/.TEST_CLS)";
|
||||
private static ComponentName CUSTOM_TILE =
|
||||
ComponentName.unflattenFromString("TEST_PKG/.TEST_CLS");
|
||||
private static final String CUSTOM_TILE_SPEC = CustomTile.toSpec(CUSTOM_TILE);
|
||||
|
||||
@Mock
|
||||
private StatusBarIconController mIconController;
|
||||
@@ -114,24 +117,30 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpController,
|
||||
mBroadcastDispatcher, mStatusBar, mQSLogger);
|
||||
setUpTileFactory();
|
||||
|
||||
// Override this config so there are no unexpected tiles
|
||||
mContext.getOrCreateTestableResources().addOverride(
|
||||
com.android.internal.R.string.config_defaultExtraQuickSettingsTiles,
|
||||
"");
|
||||
|
||||
Settings.Secure.putStringForUser(mContext.getContentResolver(), QSTileHost.TILES_SETTING,
|
||||
"", ActivityManager.getCurrentUser());
|
||||
}
|
||||
|
||||
private void setUpTileFactory() {
|
||||
when(mMockState.toString()).thenReturn(MOCK_STATE_STRING);
|
||||
// Only create this kind of tiles
|
||||
when(mDefaultFactory.createTile(anyString())).thenAnswer(
|
||||
invocation -> {
|
||||
String spec = invocation.getArgument(0);
|
||||
switch (spec) {
|
||||
case "spec1":
|
||||
return new TestTile1(mQSTileHost);
|
||||
case "spec2":
|
||||
return new TestTile2(mQSTileHost);
|
||||
case CUSTOM_TILE_SPEC:
|
||||
return mCustomTile;
|
||||
default:
|
||||
return null;
|
||||
if ("spec1".equals(spec)) {
|
||||
return new TestTile1(mQSTileHost);
|
||||
} else if ("spec2".equals(spec)) {
|
||||
return new TestTile2(mQSTileHost);
|
||||
} else if (CUSTOM_TILE_SPEC.equals(spec)) {
|
||||
return mCustomTile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
when(mCustomTile.isAvailable()).thenReturn(true);
|
||||
@@ -222,6 +231,58 @@ public class QSTileHostTest extends SysuiTestCase {
|
||||
verify(mQSLogger).logTileAdded(CUSTOM_TILE_SPEC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoRepeatedSpecs_addTile() {
|
||||
mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1,spec2");
|
||||
|
||||
mQSTileHost.addTile("spec1");
|
||||
|
||||
assertEquals(2, mQSTileHost.mTileSpecs.size());
|
||||
assertEquals("spec1", mQSTileHost.mTileSpecs.get(0));
|
||||
assertEquals("spec2", mQSTileHost.mTileSpecs.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoRepeatedSpecs_customTile() {
|
||||
mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, CUSTOM_TILE_SPEC);
|
||||
|
||||
mQSTileHost.addTile(CUSTOM_TILE);
|
||||
|
||||
assertEquals(1, mQSTileHost.mTileSpecs.size());
|
||||
assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadTileSpec_repeated() {
|
||||
List<String> specs = QSTileHost.loadTileSpecs(mContext, "spec1,spec1,spec2");
|
||||
|
||||
assertEquals(2, specs.size());
|
||||
assertEquals("spec1", specs.get(0));
|
||||
assertEquals("spec2", specs.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadTileSpec_repeatedInDefault() {
|
||||
mContext.getOrCreateTestableResources()
|
||||
.addOverride(R.string.quick_settings_tiles_default, "spec1,spec1");
|
||||
List<String> specs = QSTileHost.loadTileSpecs(mContext, "default");
|
||||
|
||||
// Remove spurious tiles, like dbg:mem
|
||||
specs.removeIf(spec -> !"spec1".equals(spec));
|
||||
assertEquals(1, specs.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadTileSpec_repeatedDefaultAndSetting() {
|
||||
mContext.getOrCreateTestableResources()
|
||||
.addOverride(R.string.quick_settings_tiles_default, "spec1");
|
||||
List<String> specs = QSTileHost.loadTileSpecs(mContext, "default,spec1");
|
||||
|
||||
// Remove spurious tiles, like dbg:mem
|
||||
specs.removeIf(spec -> !"spec1".equals(spec));
|
||||
assertEquals(1, specs.size());
|
||||
}
|
||||
|
||||
private static class TestQSTileHost extends QSTileHost {
|
||||
TestQSTileHost(Context context, StatusBarIconController iconController,
|
||||
QSFactoryImpl defaultFactory, Handler mainHandler, Looper bgLooper,
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.android.systemui.plugins.qs.QSTile
|
||||
import com.android.systemui.qs.QSTileHost
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -84,13 +85,26 @@ class CustomTileTest : SysuiTestCase() {
|
||||
.thenReturn(mServiceInfo)
|
||||
mServiceInfo.applicationInfo = mApplicationInfo
|
||||
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC)
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC, mContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCorrectUser() {
|
||||
assertEquals(0, customTile.user)
|
||||
|
||||
val userContext = mock(Context::class.java)
|
||||
`when`(userContext.packageManager).thenReturn(mPackageManager)
|
||||
`when`(userContext.userId).thenReturn(10)
|
||||
|
||||
val tile = CustomTile.create(mTileHost, TILE_SPEC, userContext)
|
||||
|
||||
assertEquals(10, tile.user)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testToggleableTileHasBooleanState() {
|
||||
`when`(mTileServiceManager.isToggleableTile).thenReturn(true)
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC)
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC, mContext)
|
||||
|
||||
assertTrue(customTile.state is QSTile.BooleanState)
|
||||
assertTrue(customTile.newTileState() is QSTile.BooleanState)
|
||||
@@ -105,7 +119,7 @@ class CustomTileTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun testValueUpdatedInBooleanTile() {
|
||||
`when`(mTileServiceManager.isToggleableTile).thenReturn(true)
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC)
|
||||
customTile = CustomTile.create(mTileHost, TILE_SPEC, mContext)
|
||||
customTile.qsTile.icon = mock(Icon::class.java)
|
||||
`when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java)))
|
||||
.thenReturn(mock(Drawable::class.java))
|
||||
|
||||
Reference in New Issue
Block a user