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
This commit is contained in:
@@ -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<UserInfo> mCurrentProfiles;
|
||||
// Saves notification keys of active bubbles when users are switched.
|
||||
private final SparseSetArray<String> mSavedBubbleKeysPerUser;
|
||||
// Saves data about active bubbles when users are switched.
|
||||
private final SparseArray<UserBubbleData> 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<UserInfo> users = mUserManager.getAliveUsers();
|
||||
mDataRepository.sanitizeBubbles(users);
|
||||
|
||||
// Init profiles
|
||||
SparseArray<UserInfo> 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<String> 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<String, Boolean> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BubbleEntry> callback);
|
||||
|
||||
void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys,
|
||||
void getShouldRestoredEntries(Set<String> savedBubbleKeys,
|
||||
Consumer<List<BubbleEntry>> callback);
|
||||
|
||||
void setNotificationInterruption(String key);
|
||||
|
||||
@@ -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<String> savedBubbleKeys,
|
||||
public void getShouldRestoredEntries(Set<String> savedBubbleKeys,
|
||||
Consumer<List<BubbleEntry>> callback) {
|
||||
sysuiMainExecutor.execute(() -> {
|
||||
List<BubbleEntry> result = new ArrayList<>();
|
||||
|
||||
@@ -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<OneHandedController> 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<Boolean>) invocation -> invocation.<Integer>getArgument(0) == userId);
|
||||
SparseArray<UserInfo> 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.
|
||||
|
||||
Reference in New Issue
Block a user