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

Commit 4ba3f9aa authored by Fabian Kozynski's avatar Fabian Kozynski
Browse files

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
Merged-In: I92b26294dec63477943d28e2000fe3e0c16f8aa3
(cherry picked from commit a6862914)
parent e7e664d1
Loading
Loading
Loading
Loading
+37 −7
Original line number Diff line number Diff line
@@ -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.Dumpable;
@@ -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;
        mDumpManager = dumpManager;
@@ -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,10 +331,13 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
    }

    public void addTile(ComponentName tile) {
        String spec = CustomTile.toSpec(tile);
        if (!mTileSpecs.contains(spec)) {
            List<String> newSpecs = new ArrayList<>(mTileSpecs);
        newSpecs.add(0, CustomTile.toSpec(tile));
            newSpecs.add(0, spec);
            changeTiles(mTileSpecs, newSpecs);
        }
    }

    public void removeTile(ComponentName tile) {
        List<String> newSpecs = new ArrayList<>(mTileSpecs);
@@ -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 {
                if (!addedSpecs.contains(tile)) {
                    tiles.add(tile);
                    addedSpecs.add(tile);
                }
            }
        }
        return tiles;
+11 −7
Original line number Diff line number Diff line
@@ -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);
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -279,6 +279,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements
        if (mPackageReceiverRegistered.get() || mUserReceiverRegistered.get()) {
            stopPackageListening();
        }
        mChangeListener = null;
    }

    private void handleDeath() {
+3 −1
Original line number Diff line number Diff line
@@ -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) {
+71 −10
Original line number Diff line number Diff line
@@ -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,23 +117,29 @@ public class QSTileHostTest extends SysuiTestCase {
                mLooper.getLooper(), mPluginManager, mTunerService, mAutoTiles, mDumpManager,
                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":
                    if ("spec1".equals(spec)) {
                        return new TestTile1(mQSTileHost);
                        case "spec2":
                    } else if ("spec2".equals(spec)) {
                        return new TestTile2(mQSTileHost);
                        case CUSTOM_TILE_SPEC:
                    } else if (CUSTOM_TILE_SPEC.equals(spec)) {
                        return mCustomTile;
                        default:
                    } else {
                        return null;
                    }
                });
@@ -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,
Loading