diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d7f1292cb717e..1c2f0d8ca7b2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -70,12 +70,10 @@ import android.os.UserHandle; import android.os.UserManager; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -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; import android.view.WindowInsets; @@ -109,8 +107,10 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -177,8 +177,8 @@ public class BubbleController implements ConfigurationChangeListener { private int mCurrentUserId; // Current profiles of the user (e.g. user with a workprofile) private SparseArray mCurrentProfiles; - // Saves notification keys of active bubbles when users are switched. - private final SparseSetArray mSavedBubbleKeysPerUser; + // Saves data about active bubbles when users are switched. + private final SparseArray mSavedUserBubbleData; // Used when ranking updates occur and we check if things should bubble / unbubble private NotificationListenerService.Ranking mTmpRanking; @@ -271,7 +271,7 @@ public class BubbleController implements ConfigurationChangeListener { mCurrentUserId = ActivityManager.getCurrentUser(); mBubblePositioner = positioner; mBubbleData = data; - mSavedBubbleKeysPerUser = new SparseSetArray<>(); + mSavedUserBubbleData = new SparseArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context); mDisplayController = displayController; @@ -420,6 +420,13 @@ public class BubbleController implements ConfigurationChangeListener { List users = mUserManager.getAliveUsers(); mDataRepository.sanitizeBubbles(users); + // Init profiles + SparseArray userProfiles = new SparseArray<>(); + for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { + userProfiles.put(user.id, user); + } + mCurrentProfiles = userProfiles; + mShellController.addConfigurationChangeListener(this); } @@ -774,11 +781,13 @@ public class BubbleController implements ConfigurationChangeListener { */ private void saveBubbles(@UserIdInt int userId) { // First clear any existing keys that might be stored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); + UserBubbleData userBubbleData = new UserBubbleData(); // Add in all active bubbles for the current user. for (Bubble bubble : mBubbleData.getBubbles()) { - mSavedBubbleKeysPerUser.add(userId, bubble.getKey()); + userBubbleData.add(bubble.getKey(), bubble.showInShade()); } + mSavedUserBubbleData.put(userId, userBubbleData); } /** @@ -787,22 +796,23 @@ public class BubbleController implements ConfigurationChangeListener { * @param userId the id of the user */ private void restoreBubbles(@UserIdInt int userId) { - ArraySet savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId); - if (savedBubbleKeys == null) { + UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId); + if (savedBubbleData == null) { // There were no bubbles saved for this used. return; } - mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> { + mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> { mMainExecutor.execute(() -> { for (BubbleEntry e : entries) { if (canLaunchInTaskView(mContext, e)) { - updateBubble(e, true /* suppressFlyout */, false /* showInShade */); + boolean showInShade = savedBubbleData.isShownInShade(e.getKey()); + updateBubble(e, true /* suppressFlyout */, showInShade); } } }); }); // Finally, remove the entries for this user now that bubbles are restored. - mSavedBubbleKeysPerUser.remove(userId); + mSavedUserBubbleData.remove(userId); } @Override @@ -993,7 +1003,19 @@ public class BubbleController implements ConfigurationChangeListener { */ @VisibleForTesting public void updateBubble(BubbleEntry notif) { - updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + int bubbleUserId = notif.getStatusBarNotification().getUserId(); + if (isCurrentProfile(bubbleUserId)) { + updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); + } else { + // Skip update, but store it in user bubbles so it gets restored after user switch + mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(), + true /* shownInShade */); + if (DEBUG_BUBBLE_CONTROLLER) { + Log.d(TAG, + "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId + + " current userId=" + mCurrentUserId); + } + } } /** @@ -1842,4 +1864,33 @@ public class BubbleController implements ConfigurationChangeListener { } } } + + /** + * Bubble data that is stored per user. + * Used to store and restore active bubbles during user switching. + */ + private static class UserBubbleData { + private final Map mKeyToShownInShadeMap = new HashMap<>(); + + /** + * Add bubble key and whether it should be shown in notification shade + */ + void add(String key, boolean shownInShade) { + mKeyToShownInShadeMap.put(key, shownInShade); + } + + /** + * Get all bubble keys stored for this user + */ + Set getKeys() { + return mKeyToShownInShadeMap.keySet(); + } + + /** + * Check if this bubble with the given key should be shown in the notification shade + */ + boolean isShownInShade(String key) { + return mKeyToShownInShadeMap.get(key); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index f8ccf2364b4c3..cf792cda91b5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -23,12 +23,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.pm.UserInfo; -import android.content.res.Configuration; import android.os.Bundle; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; -import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; @@ -42,6 +40,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -284,7 +283,7 @@ public interface Bubbles { void getPendingOrActiveEntry(String key, Consumer callback); - void getShouldRestoredEntries(ArraySet savedBubbleKeys, + void getShouldRestoredEntries(Set savedBubbleKeys, Consumer> callback); void setNotificationInterruption(String key); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 3e07144dce741..e22a896227ef6 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -40,7 +40,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.ZenModeConfig; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -83,6 +82,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -262,7 +262,7 @@ public class BubblesManager implements Dumpable { } @Override - public void getShouldRestoredEntries(ArraySet savedBubbleKeys, + public void getShouldRestoredEntries(Set savedBubbleKeys, Consumer> callback) { sysuiMainExecutor.execute(() -> { List result = new ArrayList<>(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 59a9a3c74718c..4fa45076a47c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.IActivityManager; import android.app.INotificationManager; import android.app.Notification; @@ -143,7 +144,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -252,6 +256,8 @@ public class BubblesTest extends SysuiTestCase { private TaskViewTransitions mTaskViewTransitions; @Mock private Optional mOneHandedOptional; + @Mock + private UserManager mUserManager; private TestableBubblePositioner mPositioner; @@ -314,6 +320,9 @@ public class BubblesTest extends SysuiTestCase { mPositioner.setMaxBubbles(5); mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor); + when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn( + Collections.singletonList(mock(UserInfo.class))); + TestableNotificationInterruptStateProviderImpl interruptionStateProvider = new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(), mock(PowerManager.class), @@ -339,7 +348,7 @@ public class BubblesTest extends SysuiTestCase { mStatusBarService, mWindowManager, mWindowManagerShellWrapper, - mock(UserManager.class), + mUserManager, mLauncherApps, mBubbleLogger, mTaskStackListener, @@ -1025,7 +1034,7 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull(); // Switch users - mBubbleController.onUserChanged(secondUserId); + switchUser(secondUserId); assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); // Give this user some bubbles @@ -1042,6 +1051,41 @@ public class BubblesTest extends SysuiTestCase { verify(mDataRepository, times(2)).loadBubbles(anyInt(), any()); } + @Test + public void testOnUserChanged_bubblesRestored() { + int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(); + int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier(); + // Mock current profile + when(mLockscreenUserManager.isCurrentProfile(firstUserId)).thenReturn(true); + when(mLockscreenUserManager.isCurrentProfile(secondUserId)).thenReturn(false); + + mBubbleController.updateBubble(mBubbleEntry); + assertThat(mBubbleController.hasBubbles()).isTrue(); + // We start with 1 bubble + assertThat(mBubbleData.getBubbles()).hasSize(1); + + // Switch to second user + switchUser(secondUserId); + + // Second user has no bubbles + assertThat(mBubbleController.hasBubbles()).isFalse(); + + // Send bubble update for first user, ensure it does not show up + mBubbleController.updateBubble(mBubbleEntry2); + assertThat(mBubbleController.hasBubbles()).isFalse(); + + // Start returning notif for first user again + when(mCommonNotifCollection.getAllNotifs()).thenReturn(Arrays.asList(mRow, mRow2)); + + // Switch back to first user + switchUser(firstUserId); + + // Check we now have two bubbles, one previous and one new that came in + assertThat(mBubbleController.hasBubbles()).isTrue(); + // Now there are 2 bubbles + assertThat(mBubbleData.getBubbles()).hasSize(2); + } + /** * Verifies we only load the overflow data once. */ @@ -1443,6 +1487,14 @@ public class BubblesTest extends SysuiTestCase { .build(); } + private void switchUser(int userId) { + when(mLockscreenUserManager.isCurrentProfile(anyInt())).thenAnswer( + (Answer) invocation -> invocation.getArgument(0) == userId); + SparseArray userInfos = new SparseArray<>(1); + userInfos.put(userId, mock(UserInfo.class)); + mBubbleController.onCurrentProfilesChanged(userInfos); + mBubbleController.onUserChanged(userId); + } /** * Asserts that the bubble stack is expanded and also validates the cached state is updated.