Bubbles handle user switch
- Removes bubbles on user switch
- Saves notification keys for active bubbles
- Restores bubbles on switch back
- Flyout text is suppressed when bubble is restored
Cleanups:
Removes setRemoved() from Bubble (no effect)
Bug: 131609280
Test: manually, add bubbles, add second user, switch to
second user, switch back to primary
Change-Id: I0f2c6a2f2a46e0c812291e38aa79497a8d36592e
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index be93548..b09cdcb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -74,6 +74,9 @@
*/
private boolean mShowBubbleUpdateDot = true;
+ /** Whether flyout text should be suppressed, regardless of any other flags or state. */
+ private boolean mSuppressFlyout;
+
public static String groupId(NotificationEntry entry) {
UserHandle user = entry.notification.getUser();
return user.getIdentifier() + "|" + entry.notification.getPackageName();
@@ -142,6 +145,12 @@
return mExpandedView;
}
+ void cleanupExpandedState() {
+ if (mExpandedView != null) {
+ mExpandedView.cleanUpExpandedState();
+ }
+ }
+
void inflate(LayoutInflater inflater, BubbleStackView stackView) {
if (mInflated) {
return;
@@ -171,22 +180,6 @@
}
}
- void setRemoved() {
- mIsRemoved = true;
- // TODO: move this somewhere where it can be guaranteed not to run until safe from flicker
- if (mExpandedView != null) {
- mExpandedView.cleanUpExpandedState();
- }
- }
-
- void setRemoved(boolean removed) {
- mIsRemoved = removed;
- }
-
- boolean isRemoved() {
- return mIsRemoved;
- }
-
void updateEntry(NotificationEntry entry) {
mEntry = entry;
mLastUpdated = entry.notification.getPostTime();
@@ -268,7 +261,17 @@
* Whether the flyout for the bubble should be shown.
*/
boolean showFlyoutForBubble() {
- return !mEntry.shouldSuppressPeek() && !mEntry.shouldSuppressNotificationList();
+ return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
+ && !mEntry.shouldSuppressNotificationList();
+ }
+
+ /**
+ * Set whether the flyout text for the bubble should be shown when an update is received.
+ *
+ * @param suppressFlyout whether the flyout text is shown
+ */
+ void setSuppressFlyout(boolean suppressFlyout) {
+ mSuppressFlyout = suppressFlyout;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 6e3c410..1601a51 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -39,6 +39,7 @@
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.annotation.UserIdInt;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -52,8 +53,10 @@
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.SparseSetArray;
import android.view.Display;
import android.view.IPinnedStackController;
import android.view.IPinnedStackListener;
@@ -72,10 +75,12 @@
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.WindowManagerWrapper;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
+import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -101,7 +106,8 @@
@Retention(SOURCE)
@IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
- DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE})
+ DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
+ DISMISS_USER_CHANGED})
@Target({FIELD, LOCAL_VARIABLE, PARAMETER})
@interface DismissReason {}
@@ -112,6 +118,7 @@
static final int DISMISS_NOTIF_CANCEL = 5;
static final int DISMISS_ACCESSIBILITY_ACTION = 6;
static final int DISMISS_NO_LONGER_BUBBLE = 7;
+ static final int DISMISS_USER_CHANGED = 8;
public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
@@ -128,6 +135,11 @@
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
+ // Tracks the id of the current (foreground) user.
+ private int mCurrentUserId;
+ // Saves notification keys of active bubbles when users are switched.
+ private final SparseSetArray<String> mSavedBubbleKeysPerUser;
+
// Bubbles get added to the status bar view
private final StatusBarWindowController mStatusBarWindowController;
private final ZenModeController mZenModeController;
@@ -139,6 +151,9 @@
// Used for determining view rect for touch interaction
private Rect mTempRect = new Rect();
+ // Listens to user switch so bubbles can be saved and restored.
+ private final NotificationLockscreenUserManager mNotifUserManager;
+
/** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
@@ -193,18 +208,22 @@
public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
BubbleData data, ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
- ZenModeController zenModeController) {
+ ZenModeController zenModeController,
+ NotificationLockscreenUserManager notifUserManager) {
this(context, statusBarWindowController, data, null /* synchronizer */,
- configurationController, interruptionStateProvider, zenModeController);
+ configurationController, interruptionStateProvider, zenModeController,
+ notifUserManager);
}
public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
- ZenModeController zenModeController) {
+ ZenModeController zenModeController,
+ NotificationLockscreenUserManager notifUserManager) {
mContext = context;
mNotificationInterruptionStateProvider = interruptionStateProvider;
+ mNotifUserManager = notifUserManager;
mZenModeController = zenModeController;
mZenModeController.addCallback(new ZenModeController.Callback() {
@Override
@@ -247,6 +266,16 @@
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+
+ mSavedBubbleKeysPerUser = new SparseSetArray<>();
+ mCurrentUserId = mNotifUserManager.getCurrentUserId();
+ mNotifUserManager.addUserChangedListener(
+ newUserId -> {
+ saveBubbles(mCurrentUserId);
+ mBubbleData.dismissAll(DISMISS_USER_CHANGED);
+ restoreBubbles(newUserId);
+ mCurrentUserId = newUserId;
+ });
}
/**
@@ -267,6 +296,45 @@
}
}
+ /**
+ * Records the notification key for any active bubbles. These are used to restore active
+ * bubbles when the user returns to the foreground.
+ *
+ * @param userId the id of the user
+ */
+ private void saveBubbles(@UserIdInt int userId) {
+ // First clear any existing keys that might be stored.
+ mSavedBubbleKeysPerUser.remove(userId);
+ // Add in all active bubbles for the current user.
+ for (Bubble bubble: mBubbleData.getBubbles()) {
+ mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
+ }
+ }
+
+ /**
+ * Promotes existing notifications to Bubbles if they were previously bubbles.
+ *
+ * @param userId the id of the user
+ */
+ private void restoreBubbles(@UserIdInt int userId) {
+ NotificationData notificationData =
+ mNotificationEntryManager.getNotificationData();
+ ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
+ if (savedBubbleKeys == null) {
+ // There were no bubbles saved for this used.
+ return;
+ }
+ for (NotificationEntry e : notificationData.getNotificationsForCurrentUser()) {
+ if (savedBubbleKeys.contains(e.key)
+ && mNotificationInterruptionStateProvider.shouldBubbleUp(e)
+ && canLaunchInActivityView(mContext, e)) {
+ updateBubble(e, /* suppressFlyout= */ true);
+ }
+ }
+ // Finally, remove the entries for this user now that bubbles are restored.
+ mSavedBubbleKeysPerUser.remove(mCurrentUserId);
+ }
+
@Override
public void onUiModeChanged() {
if (mStackView != null) {
@@ -400,11 +468,15 @@
* @param notif the notification associated with this bubble.
*/
void updateBubble(NotificationEntry notif) {
+ updateBubble(notif, /* supressFlyout */ false);
+ }
+
+ void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
// If this is an interruptive notif, mark that it's interrupted
if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) {
notif.setInterruption();
}
- mBubbleData.notificationEntryUpdated(notif);
+ mBubbleData.notificationEntryUpdated(notif, suppressFlyout);
}
/**
@@ -444,8 +516,7 @@
// The bubble notification sticks around in the data as long as the bubble is
// not dismissed and the app hasn't cancelled the notification.
- boolean bubbleExtended = entry.isBubble() && !bubble.isRemoved()
- && userRemovedNotif;
+ boolean bubbleExtended = entry.isBubble() && userRemovedNotif;
if (bubbleExtended) {
bubble.setShowInShadeWhenBubble(false);
bubble.setShowBubbleDot(false);
@@ -454,7 +525,7 @@
}
mNotificationEntryManager.updateNotifications();
return true;
- } else if (!userRemovedNotif && !bubble.isRemoved()) {
+ } else if (!userRemovedNotif) {
// This wasn't a user removal so we should remove the bubble as well
mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
return false;
@@ -488,7 +559,6 @@
removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE);
} else if (shouldBubble) {
Bubble b = mBubbleData.getBubbleWithKey(entry.key);
- b.setRemoved(false); // updates come back as bubbles even if dismissed
updateBubble(entry);
}
}
@@ -530,22 +600,25 @@
@DismissReason final int reason = removed.second;
mStackView.removeBubble(bubble);
- if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
- && !bubble.showInShadeWhenBubble()) {
- // The bubble is gone & the notification is gone, time to actually remove it
- mNotificationEntryManager.performRemoveNotification(
- bubble.getEntry().notification, UNDEFINED_DISMISS_REASON);
- } else {
- // Update the flag for SysUI
- bubble.getEntry().notification.getNotification().flags &= ~FLAG_BUBBLE;
+ // If the bubble is removed for user switching, leave the notification in place.
+ if (reason != DISMISS_USER_CHANGED) {
+ if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
+ && !bubble.showInShadeWhenBubble()) {
+ // The bubble is gone & the notification is gone, time to actually remove it
+ mNotificationEntryManager.performRemoveNotification(
+ bubble.getEntry().notification, UNDEFINED_DISMISS_REASON);
+ } else {
+ // Update the flag for SysUI
+ bubble.getEntry().notification.getNotification().flags &= ~FLAG_BUBBLE;
- // Make sure NoMan knows it's not a bubble anymore so anyone querying it will
- // get right result back
- try {
- mBarService.onNotificationBubbleChanged(bubble.getKey(),
- false /* isBubble */);
- } catch (RemoteException e) {
- // Bad things have happened
+ // Make sure NoMan knows it's not a bubble anymore so anyone querying it
+ // will get right result back
+ try {
+ mBarService.onNotificationBubbleChanged(bubble.getKey(),
+ false /* isBubble */);
+ } catch (RemoteException e) {
+ // Bad things have happened
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 3d07057..ec49cdf 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -164,7 +164,7 @@
dispatchPendingChanges();
}
- public void notificationEntryUpdated(NotificationEntry entry) {
+ void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout) {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "notificationEntryUpdated: " + entry);
}
@@ -172,6 +172,7 @@
if (bubble == null) {
// Create a new bubble
bubble = new Bubble(mContext, entry);
+ bubble.setSuppressFlyout(suppressFlyout);
doAdd(bubble);
trim();
} else {
@@ -304,7 +305,6 @@
Bubble newSelected = mBubbles.get(newIndex);
setSelectedBubbleInternal(newSelected);
}
- bubbleToRemove.setRemoved();
maybeSendDeleteIntent(reason, bubbleToRemove.getEntry());
}
@@ -319,7 +319,6 @@
setSelectedBubbleInternal(null);
while (!mBubbles.isEmpty()) {
Bubble bubble = mBubbles.remove(0);
- bubble.setRemoved();
maybeSendDeleteIntent(reason, bubble.getEntry());
mStateChange.bubbleRemoved(bubble, reason);
}
@@ -595,4 +594,4 @@
Notification.BubbleMetadata metadata = entry.getBubbleMetadata();
return metadata != null && metadata.getAutoExpandBubble();
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 44e84b9..b0222a8 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -703,6 +703,7 @@
int removedIndex = mBubbleContainer.indexOfChild(bubble.getIconView());
if (removedIndex >= 0) {
mBubbleContainer.removeViewAt(removedIndex);
+ bubble.cleanupExpandedState();
logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
} else {
Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
@@ -1305,64 +1306,71 @@
void animateInFlyoutForBubble(Bubble bubble) {
final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
- // Show the message if one exists, and we're not expanded or animating expansion.
- if (updateMessage != null
- && !isExpanded()
- && !mIsExpansionAnimating
- && !mIsGestureInProgress
- && bubble.showFlyoutForBubble()) {
- if (bubble.getIconView() != null) {
- // Temporarily suppress the dot while the flyout is visible.
- bubble.getIconView().setSuppressDot(
- true /* suppressDot */, false /* animate */);
+ if (!bubble.showFlyoutForBubble()) {
+ // In case flyout was suppressed for this update, reset now.
+ bubble.setSuppressFlyout(false);
+ return;
+ }
- mFlyoutDragDeltaX = 0f;
- mFlyout.setAlpha(0f);
+ if (updateMessage == null
+ || isExpanded()
+ || mIsExpansionAnimating
+ || mIsGestureInProgress) {
+ // Skip the message if none exists, we're expanded or animating expansion.
+ return;
+ }
- if (mAfterFlyoutHides != null) {
- mAfterFlyoutHides.run();
- }
+ if (bubble.getIconView() != null) {
+ // Temporarily suppress the dot while the flyout is visible.
+ bubble.getIconView().setSuppressDot(
+ true /* suppressDot */, false /* animate */);
- mAfterFlyoutHides = () -> {
- if (bubble.getIconView() == null) {
- return;
- }
+ mFlyoutDragDeltaX = 0f;
+ mFlyout.setAlpha(0f);
- final boolean suppressDot = !bubble.showBubbleDot();
- // If we're going to suppress the dot, make it visible first so it'll
- // visibly animate away.
- if (suppressDot) {
- bubble.getIconView().setSuppressDot(
- false /* suppressDot */, false /* animate */);
- }
- // Reset dot suppression. If we're not suppressing due to DND, then
- // stop suppressing it with no animation (since the flyout has
- // transformed into the dot). If we are suppressing due to DND, animate
- // it away.
- bubble.getIconView().setSuppressDot(
- suppressDot /* suppressDot */,
- suppressDot /* animate */);
- };
-
- // Post in case layout isn't complete and getWidth returns 0.
- post(() -> {
- // An auto-expanding bubble could have been posted during the time it takes to
- // layout.
- if (isExpanded()) {
- return;
- }
-
- mFlyout.showFlyout(
- updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
- mStackAnimationController.isStackOnLeftSide(),
- bubble.getIconView().getBadgeColor(), mAfterFlyoutHides);
- });
+ if (mAfterFlyoutHides != null) {
+ mAfterFlyoutHides.run();
}
- mFlyout.removeCallbacks(mHideFlyout);
- mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
- logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
+ mAfterFlyoutHides = () -> {
+ if (bubble.getIconView() == null) {
+ return;
+ }
+
+ final boolean suppressDot = !bubble.showBubbleDot();
+ // If we're going to suppress the dot, make it visible first so it'll
+ // visibly animate away.
+ if (suppressDot) {
+ bubble.getIconView().setSuppressDot(
+ false /* suppressDot */, false /* animate */);
+ }
+ // Reset dot suppression. If we're not suppressing due to DND, then
+ // stop suppressing it with no animation (since the flyout has
+ // transformed into the dot). If we are suppressing due to DND, animate
+ // it away.
+ bubble.getIconView().setSuppressDot(
+ suppressDot /* suppressDot */,
+ suppressDot /* animate */);
+ };
+
+ // Post in case layout isn't complete and getWidth returns 0.
+ post(() -> {
+ // An auto-expanding bubble could have been posted during the time it takes to
+ // layout.
+ if (isExpanded()) {
+ return;
+ }
+
+ mFlyout.showFlyout(
+ updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
+ mStackAnimationController.isStackOnLeftSide(),
+ bubble.getIconView().getBadgeColor(), mAfterFlyoutHides);
+ });
}
+
+ mFlyout.removeCallbacks(mHideFlyout);
+ mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
+ logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
}
/** Hide the flyout immediately and cancel any pending hide runnables. */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 944b5cd..0df4f0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -58,6 +58,7 @@
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoveInterceptor;
import com.android.systemui.statusbar.NotificationTestHelper;
@@ -103,6 +104,8 @@
private ZenModeConfig mZenModeConfig;
@Mock
private FaceManager mFaceManager;
+ @Mock
+ private NotificationLockscreenUserManager mLockscreenUserManager;
private FrameLayout mStatusBarView;
@Captor
@@ -167,7 +170,7 @@
mBubbleData = new BubbleData(mContext);
mBubbleController = new TestableBubbleController(mContext, mStatusBarWindowController,
mBubbleData, mConfigurationController, interruptionStateProvider,
- mZenModeController);
+ mZenModeController, mLockscreenUserManager);
mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener);
mBubbleController.setExpandListener(mBubbleExpandListener);
@@ -592,9 +595,11 @@
StatusBarWindowController statusBarWindowController, BubbleData data,
ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
- ZenModeController zenModeController) {
+ ZenModeController zenModeController,
+ NotificationLockscreenUserManager lockscreenUserManager) {
super(context, statusBarWindowController, data, Runnable::run,
- configurationController, interruptionStateProvider, zenModeController);
+ configurationController, interruptionStateProvider, zenModeController,
+ lockscreenUserManager);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index 1522c5b..f7d2743 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -785,21 +785,6 @@
assertExpandedChangedTo(false);
}
- @Test
- public void test_doRemove_setsBubbleRemoved() {
- // Setup
- sendUpdatedEntryAtTime(mEntryA1, 1000);
- assertThat(mBubbleA1.isRemoved()).isFalse();
- assertThat(mBubbleData.getBubbleWithKey(mEntryA1.key)).isNotNull();
- Bubble b = mBubbleData.getBubbleWithKey(mEntryA1.key);
- assertThat(b.isRemoved()).isFalse();
-
- // Test
- mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE);
- assertThat(b.isRemoved()).isTrue();
- assertThat(mBubbleData.getBubbleWithKey(mEntryA1.key)).isNull();
- }
-
private void verifyUpdateReceived() {
verify(mListener).applyUpdate(mUpdateCaptor.capture());
reset(mListener);
@@ -902,7 +887,7 @@
private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) {
setPostTime(entry, postTime);
- mBubbleData.notificationEntryUpdated(entry);
+ mBubbleData.notificationEntryUpdated(entry, /* suppressFlyout=*/ false);
}
private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) {