From 8b9b0c9ef45796dde552a889ab6657146d043703 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Thu, 21 Jul 2022 10:38:09 -0700 Subject: [PATCH] Ignore notification update for not active user We can receive notification updates for the not active user. Ensure that we do not pop up a bubble in these case. Store the notification key for these notifications in saved bubble data for the user that should get this bubble. When changing user back to the user that should get the notification, bubble will get restored as well. Bug: 237314340 Test: atest BubblesTest Test: Enable bubbles for incoming SMS in Messages app. Switch to secondary user. Get incoming messages and observe that they do not result in a bubble. Switch back to primary user and observe that bubbles are shown for SMS that came in. Change-Id: Ifcabb8458c72edc06f8cff844ac5e2266f5387f9 --- .../wm/shell/bubbles/BubbleController.java | 77 +++++++++++++++---- .../com/android/wm/shell/bubbles/Bubbles.java | 5 +- .../systemui/wmshell/BubblesManager.java | 4 +- .../android/systemui/wmshell/BubblesTest.java | 56 +++++++++++++- 4 files changed, 122 insertions(+), 20 deletions(-) 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.