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

Commit bac16b93 authored by Mady Mellor's avatar Mady Mellor
Browse files

Sysui bubbles multiuser fixes: persistence / overflow

* Persist bubbles per-user - rather than one list the
  XML now has a list per-user. The entries in these lists
  still include userId for workprofile since bubbles are
  mixed in the stack / overflow for workprofile.
* When loading bubbles, only the ones for the current user
  are loaded / hit bubbleController code
* When user changes, overflow data should be re-loaded
* Allow the bubble window to be visible for all users

Test: atest BubbleXmlHelperTest BubbleVolatileRepositoryTest
BubblePersistentRepositoryTest BubblesTest
Bug: 173408780

Change-Id: I88cb7cc7ee676d8e0756328a95a54fdaf018a013
parent b5a783d6
Loading
Loading
Loading
Loading
+43 −8
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.PointF;
@@ -64,6 +65,7 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseSetArray;
import android.view.View;
import android.view.ViewGroup;
@@ -144,6 +146,8 @@ public class BubbleController {

    // Tracks the id of the current (foreground) user.
    private int mCurrentUserId;
    // Current profiles of the user (e.g. user with a workprofile)
    private SparseArray<UserInfo> mCurrentProfiles;
    // Saves notification keys of active bubbles when users are switched.
    private final SparseSetArray<String> mSavedBubbleKeysPerUser;

@@ -153,8 +157,8 @@ public class BubbleController {
    // Callback that updates BubbleOverflowActivity on data change.
    @Nullable private BubbleData.Listener mOverflowListener = null;

    // Only load overflow data from disk once
    private boolean mOverflowDataLoaded = false;
    // Typically only load once & after user switches
    private boolean mOverflowDataLoadNeeded = true;

    /**
     * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
@@ -468,14 +472,31 @@ public class BubbleController {
        updateStack();
    }

    private void onUserChanged(int newUserId) {
    /** Called when the current user changes. */
    @VisibleForTesting
    public void onUserChanged(int newUserId) {
        saveBubbles(mCurrentUserId);
        mCurrentUserId = newUserId;

        mBubbleData.dismissAll(DISMISS_USER_CHANGED);
        mBubbleData.clearOverflow();
        mOverflowDataLoadNeeded = true;

        restoreBubbles(newUserId);
        mCurrentUserId = newUserId;
        mBubbleData.setCurrentUserId(newUserId);
    }

    /** Called when the profiles for the current user change. **/
    public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
        mCurrentProfiles = currentProfiles;
    }

    /** Whether this userId belongs to the current user. */
    private boolean isCurrentProfile(int userId) {
        return userId == UserHandle.USER_ALL
                || (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null);
    }

    /**
     * Sets whether to perform inflation on the same thread as the caller. This method should only
     * be used in tests, not in production.
@@ -556,6 +577,7 @@ public class BubbleController {
        mWmLayoutParams.setTitle("Bubbles!");
        mWmLayoutParams.packageName = mContext.getPackageName();
        mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;

        try {
            mAddedToWindowManager = true;
@@ -639,7 +661,7 @@ public class BubbleController {
            });
        });
        // Finally, remove the entries for this user now that bubbles are restored.
        mSavedBubbleKeysPerUser.remove(mCurrentUserId);
        mSavedBubbleKeysPerUser.remove(userId);
    }

    private void updateForThemeChanges() {
@@ -804,12 +826,12 @@ public class BubbleController {
     * Fills the overflow bubbles by loading them from disk.
     */
    void loadOverflowBubblesFromDisk() {
        if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) {
        if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) {
            // we don't need to load overflow bubbles from disk if it is already in memory
            return;
        }
        mOverflowDataLoaded = true;
        mDataRepository.loadBubbles((bubbles) -> {
        mOverflowDataLoadNeeded = false;
        mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
            bubbles.forEach(bubble -> {
                if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
                    // if the bubble is already active, there's no need to push it to overflow
@@ -911,6 +933,12 @@ public class BubbleController {
            Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
            BubbleEntry entry = entryData.first;
            boolean shouldBubbleUp = entryData.second;

            if (entry != null && !isCurrentProfile(
                    entry.getStatusBarNotification().getUser().getIdentifier())) {
                return;
            }

            rankingMap.getRanking(key, mTmpRanking);
            boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
            if (isActiveBubble && !mTmpRanking.canBubble()) {
@@ -1427,6 +1455,13 @@ public class BubbleController {
            });
        }

        @Override
        public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
            mMainExecutor.execute(() -> {
                BubbleController.this.onCurrentProfilesChanged(currentProfiles);
            });
        }

        @Override
        public void onConfigChanged(Configuration newConfig) {
            mMainExecutor.execute(() -> {
+12 −1
Original line number Diff line number Diff line
@@ -510,7 +510,8 @@ public class BubbleData {
                        || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
                        || reason == Bubbles.DISMISS_BLOCKED
                        || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
                        || reason == Bubbles.DISMISS_PACKAGE_REMOVED)) {
                        || reason == Bubbles.DISMISS_PACKAGE_REMOVED
                        || reason == Bubbles.DISMISS_USER_CHANGED)) {

                Bubble b = getOverflowBubbleWithKey(key);
                if (DEBUG_BUBBLE_DATA) {
@@ -642,6 +643,16 @@ public class BubbleData {
        }
    }

    /**
     * Removes all bubbles from the overflow, called when the user changes.
     */
    public void clearOverflow() {
        while (!mOverflowBubbles.isEmpty()) {
            doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED);
        }
        dispatchPendingChanges();
    }

    private void dispatchPendingChanges() {
        if (mListener != null && mStateChange.anythingChanged()) {
            mListener.applyUpdate(mStateChange);
+29 −22
Original line number Diff line number Diff line
@@ -58,7 +58,8 @@ internal class BubbleDataRepository(
     */
    fun addBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
        if (DEBUG) Log.d(TAG, "adding ${bubbles.size} bubbles")
        val entities = transform(userId, bubbles).also(volatileRepository::addBubbles)
        val entities = transform(bubbles).also {
            b -> volatileRepository.addBubbles(userId, b) }
        if (entities.isNotEmpty()) persistToDisk()
    }

@@ -67,14 +68,15 @@ internal class BubbleDataRepository(
     */
    fun removeBubbles(@UserIdInt userId: Int, bubbles: List<Bubble>) {
        if (DEBUG) Log.d(TAG, "removing ${bubbles.size} bubbles")
        val entities = transform(userId, bubbles).also(volatileRepository::removeBubbles)
        val entities = transform(bubbles).also {
            b -> volatileRepository.removeBubbles(userId, b) }
        if (entities.isNotEmpty()) persistToDisk()
    }

    private fun transform(userId: Int, bubbles: List<Bubble>): List<BubbleEntity> {
    private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
        return bubbles.mapNotNull { b ->
            BubbleEntity(
                    userId,
                    b.user.identifier,
                    b.packageName,
                    b.metadataShortcutId ?: return@mapNotNull null,
                    b.key,
@@ -116,10 +118,11 @@ internal class BubbleDataRepository(
    /**
     * Load bubbles from disk.
     * @param cb The callback to be run after the bubbles are loaded.  This callback is always made
     *           on the main thread of the hosting process.
     *           on the main thread of the hosting process. The callback is only run if there are
     *           bubbles.
     */
    @SuppressLint("WrongConstant")
    fun loadBubbles(cb: (List<Bubble>) -> Unit) = ioScope.launch {
    fun loadBubbles(userId: Int, cb: (List<Bubble>) -> Unit) = ioScope.launch {
        /**
         * Load BubbleEntity from disk.
         * e.g.
@@ -129,8 +132,9 @@ internal class BubbleDataRepository(
         *     BubbleEntity(0, "com.example.messenger", "id-1")
         * ]
         */
        val entities = persistentRepository.readFromDisk()
        volatileRepository.addBubbles(entities)
        val entitiesByUser = persistentRepository.readFromDisk()
        val entities = entitiesByUser.get(userId) ?: return@launch
        volatileRepository.addBubbles(userId, entities)
        /**
         * Extract userId/packageName from these entities.
         * e.g.
@@ -139,9 +143,10 @@ internal class BubbleDataRepository(
         * ]
         */
        val shortcutKeys = entities.map { ShortcutKey(it.userId, it.packageName) }.toSet()

        /**
         * Retrieve shortcuts with given userId/packageName combination, then construct a mapping
         * from the userId/packageName pair to a list of associated ShortcutInfo.
         * Retrieve shortcuts with given userId/packageName combination, then construct a
         * mapping from the userId/packageName pair to a list of associated ShortcutInfo.
         * e.g.
         * {
         *     ShortcutKey(0, "com.example.messenger") -> [
@@ -161,12 +166,13 @@ internal class BubbleDataRepository(
                            .setQueryFlags(SHORTCUT_QUERY_FLAG), UserHandle.of(key.userId))
                    ?: emptyList()
        }.groupBy { ShortcutKey(it.userId, it.`package`) }
        // For each entity loaded from xml, find the corresponding ShortcutInfo then convert them
        // into Bubble.
        // For each entity loaded from xml, find the corresponding ShortcutInfo then convert
        // them into Bubble.
        val bubbles = entities.mapNotNull { entity ->
            shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
                    ?.firstOrNull { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
                    ?.let { shortcutInfo -> Bubble(
                    ?.let { shortcutInfo ->
                        Bubble(
                                entity.key,
                                shortcutInfo,
                                entity.desiredHeight,
@@ -175,7 +181,8 @@ internal class BubbleDataRepository(
                                entity.taskId,
                                entity.locus,
                                mainExecutor
                    ) }
                        )
                    }
        }
        mainExecutor.execute { cb(bubbles) }
    }
+10 −4
Original line number Diff line number Diff line
@@ -217,10 +217,16 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
    static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) {
        Objects.requireNonNull(context);
        if (icon == null) return null;
        if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
        try {
            if (icon.getType() == Icon.TYPE_URI
                    || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) {
                context.grantUriPermission(context.getPackageName(),
                        icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            return icon.loadDrawable(context);
        } catch (Exception e) {
            Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage());
            return null;
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -21,12 +21,14 @@ import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Looper;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.ArraySet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;

import androidx.annotation.IntDef;
@@ -213,6 +215,13 @@ public interface Bubbles {
     */
    void onUserChanged(int newUserId);

    /**
     * Called when the current user profiles change.
     *
     * @param currentProfiles the user infos for the current profile.
     */
    void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles);

    /**
     * Called when config changed.
     *
Loading