diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 7094d28c29f50..ac4a93ba7fb00 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -46,7 +46,7 @@ class Bubble { private long mLastUpdated; private long mLastAccessed; - private static String groupId(NotificationEntry entry) { + public static String groupId(NotificationEntry entry) { UserHandle user = entry.notification.getUser(); return user.getIdentifier() + "|" + entry.notification.getPackageName(); } @@ -120,10 +120,27 @@ class Bubble { } } + /** + * @return the newer of {@link #getLastUpdateTime()} and {@link #getLastAccessTime()} + */ public long getLastActivity() { return Math.max(mLastUpdated, mLastAccessed); } + /** + * @return the timestamp in milliseconds of the most recent notification entry for this bubble + */ + public long getLastUpdateTime() { + return mLastUpdated; + } + + /** + * @return the timestamp in milliseconds when this bubble was last displayed in expanded state + */ + public long getLastAccessTime() { + return mLastAccessed; + } + /** * Should be invoked whenever a Bubble is accessed (selected while expanded). */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 7d189b28aa5e2..2d0944ad246f5 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -29,6 +29,9 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.Nullable; @@ -73,6 +76,7 @@ import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.util.List; import javax.inject.Inject; @@ -88,11 +92,12 @@ import javax.inject.Singleton; public class BubbleController implements ConfigurationController.ConfigurationListener { private static final String TAG = "BubbleController"; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE}) + @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} static final int DISMISS_USER_GESTURE = 1; @@ -510,6 +515,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onOrderChanged(List bubbles) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbles); + } } @Override @@ -526,13 +534,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } - @Override - public void showFlyoutText(Bubble bubble, String text) { - if (mStackView != null) { - mStackView.animateInFlyoutForBubble(bubble); - } - } - @Override public void apply() { mNotificationEntryManager.updateNotifications(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index f15ba6ee673be..9156e06fe54ec 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -23,6 +23,7 @@ import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.util.Log; +import android.util.Pair; import androidx.annotation.Nullable; @@ -53,10 +54,10 @@ public class BubbleData { private static final int MAX_BUBBLES = 5; - private static final Comparator BUBBLES_BY_LAST_ACTIVITY_DESCENDING = - Comparator.comparing(Bubble::getLastActivity).reversed(); + private static final Comparator BUBBLES_BY_SORT_KEY_DESCENDING = + Comparator.comparing(BubbleData::sortKey).reversed(); - private static final Comparator> GROUPS_BY_LAST_ACTIVITY_DESCENDING = + private static final Comparator> GROUPS_BY_MAX_SORT_KEY_DESCENDING = Comparator., Long>comparing(Map.Entry::getValue).reversed(); /** @@ -105,9 +106,6 @@ public class BubbleData { */ void onExpandedChanged(boolean expanded); - /** Flyout text should animate in, showing the given text. */ - void showFlyoutText(Bubble bubble, String text); - /** Commit any pending operations (since last call of apply()) */ void apply(); } @@ -121,15 +119,19 @@ public class BubbleData { private Bubble mSelectedBubble; private boolean mExpanded; - // TODO: ensure this is invalidated at the appropriate time - private int mSelectedBubbleExpandedPosition = -1; + // State tracked during an operation -- keeps track of what listener events to dispatch. + private boolean mExpandedChanged; + private boolean mOrderChanged; + private boolean mSelectionChanged; + private Bubble mUpdatedBubble; + private Bubble mAddedBubble; + private final List> mRemovedBubbles = new ArrayList<>(); private TimeSource mTimeSource = System::currentTimeMillis; @Nullable private Listener mListener; - @VisibleForTesting @Inject public BubbleData(Context context) { mContext = context; @@ -154,18 +156,19 @@ public class BubbleData { } public void setExpanded(boolean expanded) { - if (setExpandedInternal(expanded)) { - dispatchApply(); + if (DEBUG) { + Log.d(TAG, "setExpanded: " + expanded); } + setExpandedInternal(expanded); + dispatchPendingChanges(); } public void setSelectedBubble(Bubble bubble) { if (DEBUG) { Log.d(TAG, "setSelectedBubble: " + bubble); } - if (setSelectedBubbleInternal(bubble)) { - dispatchApply(); - } + setSelectedBubbleInternal(bubble); + dispatchPendingChanges(); } public void notificationEntryUpdated(NotificationEntry entry) { @@ -177,12 +180,12 @@ public class BubbleData { // Create a new bubble bubble = new Bubble(entry, this::onBubbleBlocked); doAdd(bubble); - dispatchOnBubbleAdded(bubble); + trim(); } else { // Updates an existing bubble bubble.setEntry(entry); doUpdate(bubble); - dispatchOnBubbleUpdated(bubble); + mUpdatedBubble = bubble; } if (shouldAutoExpand(entry)) { setSelectedBubbleInternal(bubble); @@ -192,7 +195,15 @@ public class BubbleData { } else if (mSelectedBubble == null) { setSelectedBubbleInternal(bubble); } - dispatchApply(); + dispatchPendingChanges(); + } + + public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { + if (DEBUG) { + Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason); + } + doRemove(entry.key, reason); + dispatchPendingChanges(); } private void doAdd(Bubble bubble) { @@ -202,14 +213,21 @@ public class BubbleData { int minInsertPoint = 0; boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId()); if (isExpanded()) { - // first bubble of a group goes to the end, otherwise it goes within the existing group - minInsertPoint = - newGroup ? mBubbles.size() : findFirstIndexForGroup(bubble.getGroupId()); + // first bubble of a group goes to the beginning, otherwise within the existing group + minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId()); } - insertBubble(minInsertPoint, bubble); + if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) { + mOrderChanged = true; + } + mAddedBubble = bubble; if (!isExpanded()) { - packGroup(findFirstIndexForGroup(bubble.getGroupId())); + mOrderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId())); + // Top bubble becomes selected. + setSelectedBubbleInternal(mBubbles.get(0)); } + } + + private void trim() { if (mBubbles.size() > MAX_BUBBLES) { mBubbles.stream() // sort oldest first (ascending lastActivity) @@ -217,10 +235,7 @@ public class BubbleData { // skip the selected bubble .filter((b) -> !b.equals(mSelectedBubble)) .findFirst() - .ifPresent((b) -> { - doRemove(b.getKey(), BubbleController.DISMISS_AGED); - dispatchApply(); - }); + .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED)); } } @@ -229,43 +244,48 @@ public class BubbleData { Log.d(TAG, "doUpdate: " + bubble); } if (!isExpanded()) { - // while collapsed, update causes re-sort + // while collapsed, update causes re-pack + int prevPos = mBubbles.indexOf(bubble); mBubbles.remove(bubble); - insertBubble(0, bubble); - packGroup(findFirstIndexForGroup(bubble.getGroupId())); + int newPos = insertBubble(0, bubble); + if (prevPos != newPos) { + packGroup(newPos); + mOrderChanged = true; + } + setSelectedBubbleInternal(mBubbles.get(0)); } } - public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { - if (DEBUG) { - Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason); - } - doRemove(entry.key, reason); - dispatchApply(); - } - private void doRemove(String key, @DismissReason int reason) { int indexToRemove = indexForKey(key); - if (indexToRemove >= 0) { - Bubble bubbleToRemove = mBubbles.get(indexToRemove); - if (mBubbles.size() == 1) { - // Going to become empty, handle specially. - setExpandedInternal(false); - setSelectedBubbleInternal(null); - } - mBubbles.remove(indexToRemove); - dispatchOnBubbleRemoved(bubbleToRemove, reason); - - // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. - if (Objects.equals(mSelectedBubble, bubbleToRemove)) { - // Move selection to the new bubble at the same position. - int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); - Bubble newSelected = mBubbles.get(newIndex); - setSelectedBubbleInternal(newSelected); - } - bubbleToRemove.setDismissed(); - maybeSendDeleteIntent(reason, bubbleToRemove.entry); + if (indexToRemove == -1) { + return; } + Bubble bubbleToRemove = mBubbles.get(indexToRemove); + if (mBubbles.size() == 1) { + // Going to become empty, handle specially. + setExpandedInternal(false); + setSelectedBubbleInternal(null); + } + if (indexToRemove < mBubbles.size() - 1) { + // Removing anything but the last bubble means positions will change. + mOrderChanged = true; + } + mBubbles.remove(indexToRemove); + mRemovedBubbles.add(Pair.create(bubbleToRemove, reason)); + if (!isExpanded()) { + mOrderChanged |= repackAll(); + } + + // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. + if (Objects.equals(mSelectedBubble, bubbleToRemove)) { + // Move selection to the new bubble at the same position. + int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); + Bubble newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); + } + bubbleToRemove.setDismissed(); + maybeSendDeleteIntent(reason, bubbleToRemove.entry); } public void dismissAll(@DismissReason int reason) { @@ -281,87 +301,98 @@ public class BubbleData { Bubble bubble = mBubbles.remove(0); bubble.setDismissed(); maybeSendDeleteIntent(reason, bubble.entry); - dispatchOnBubbleRemoved(bubble, reason); + mRemovedBubbles.add(Pair.create(bubble, reason)); } - dispatchApply(); + dispatchPendingChanges(); } - private void dispatchApply() { - if (mListener != null) { + + private void dispatchPendingChanges() { + if (mListener == null) { + mExpandedChanged = false; + mAddedBubble = null; + mSelectionChanged = false; + mRemovedBubbles.clear(); + mUpdatedBubble = null; + mOrderChanged = false; + return; + } + boolean anythingChanged = false; + + if (mAddedBubble != null) { + mListener.onBubbleAdded(mAddedBubble); + mAddedBubble = null; + anythingChanged = true; + } + + // Compat workaround: Always collapse first. + if (mExpandedChanged && !mExpanded) { + mListener.onExpandedChanged(mExpanded); + mExpandedChanged = false; + anythingChanged = true; + } + + if (mSelectionChanged) { + mListener.onSelectionChanged(mSelectedBubble); + mSelectionChanged = false; + anythingChanged = true; + } + + if (!mRemovedBubbles.isEmpty()) { + for (Pair removed : mRemovedBubbles) { + mListener.onBubbleRemoved(removed.first, removed.second); + } + mRemovedBubbles.clear(); + anythingChanged = true; + } + + if (mUpdatedBubble != null) { + mListener.onBubbleUpdated(mUpdatedBubble); + mUpdatedBubble = null; + anythingChanged = true; + } + + if (mOrderChanged) { + mListener.onOrderChanged(mBubbles); + mOrderChanged = false; + anythingChanged = true; + } + + if (mExpandedChanged) { + mListener.onExpandedChanged(mExpanded); + mExpandedChanged = false; + anythingChanged = true; + } + + if (anythingChanged) { mListener.apply(); } } - private void dispatchOnBubbleAdded(Bubble bubble) { - if (mListener != null) { - mListener.onBubbleAdded(bubble); - } - } - - private void dispatchOnBubbleRemoved(Bubble bubble, @DismissReason int reason) { - if (mListener != null) { - mListener.onBubbleRemoved(bubble, reason); - } - } - - private void dispatchOnExpandedChanged(boolean expanded) { - if (mListener != null) { - mListener.onExpandedChanged(expanded); - } - } - - private void dispatchOnSelectionChanged(@Nullable Bubble bubble) { - if (mListener != null) { - mListener.onSelectionChanged(bubble); - } - } - - private void dispatchOnBubbleUpdated(Bubble bubble) { - if (mListener != null) { - mListener.onBubbleUpdated(bubble); - } - } - - private void dispatchOnOrderChanged(List bubbles) { - if (mListener != null) { - mListener.onOrderChanged(bubbles); - } - } - - private void dispatchShowFlyoutText(Bubble bubble, String text) { - if (mListener != null) { - mListener.showFlyoutText(bubble, text); - } - } - /** * Requests a change to the selected bubble. Calls {@link Listener#onSelectionChanged} if * the value changes. * * @param bubble the new selected bubble - * @return true if the state changed as a result */ - private boolean setSelectedBubbleInternal(@Nullable Bubble bubble) { + private void setSelectedBubbleInternal(@Nullable Bubble bubble) { if (DEBUG) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } if (Objects.equals(bubble, mSelectedBubble)) { - return false; + return; } if (bubble != null && !mBubbles.contains(bubble)) { Log.e(TAG, "Cannot select bubble which doesn't exist!" + " (" + bubble + ") bubbles=" + mBubbles); - return false; + return; } if (mExpanded && bubble != null) { bubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); } mSelectedBubble = bubble; - dispatchOnSelectionChanged(mSelectedBubble); - if (!mExpanded || mSelectedBubble == null) { - mSelectedBubbleExpandedPosition = -1; - } - return true; + mSelectionChanged = true; + return; } /** @@ -369,37 +400,53 @@ public class BubbleData { * the value changes. * * @param shouldExpand the new requested state - * @return true if the state changed as a result */ - private boolean setExpandedInternal(boolean shouldExpand) { + private void setExpandedInternal(boolean shouldExpand) { if (DEBUG) { Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); } if (mExpanded == shouldExpand) { - return false; - } - if (mSelectedBubble != null) { - mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); + return; } if (shouldExpand) { if (mBubbles.isEmpty()) { Log.e(TAG, "Attempt to expand stack when empty!"); - return false; + return; } if (mSelectedBubble == null) { Log.e(TAG, "Attempt to expand stack without selected bubble!"); - return false; + return; + } + mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); + mOrderChanged |= repackAll(); + } else if (!mBubbles.isEmpty()) { + // Apply ordering and grouping rules from expanded -> collapsed, then save + // the result. + mOrderChanged |= repackAll(); + // Save the state which should be returned to when expanded (with no other changes) + + if (mBubbles.indexOf(mSelectedBubble) > 0) { + // Move the selected bubble to the top while collapsed. + if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) { + // The selected bubble cannot be raised to the first position because + // there is an ongoing bubble there. Instead, force the top ongoing bubble + // to become selected. + setSelectedBubbleInternal(mBubbles.get(0)); + } else { + // Raise the selected bubble (and it's group) up to the front so the selected + // bubble remains on top. + mBubbles.remove(mSelectedBubble); + mBubbles.add(0, mSelectedBubble); + packGroup(0); + } } - } else { - repackAll(); } mExpanded = shouldExpand; - dispatchOnExpandedChanged(mExpanded); - return true; + mExpandedChanged = true; } private static long sortKey(Bubble bubble) { - long key = bubble.getLastActivity(); + long key = bubble.getLastUpdateTime(); if (bubble.isOngoing()) { // Set 2nd highest bit (signed long int), to partition between ongoing and regular key |= 0x4000000000000000L; @@ -456,8 +503,9 @@ public class BubbleData { * unchanged. Relative order of any other bubbles are also unchanged. * * @param position the position of the first bubble for the group + * @return true if the position of any bubbles has changed as a result */ - private void packGroup(int position) { + private boolean packGroup(int position) { if (DEBUG) { Log.d(TAG, "packGroup: position=" + position); } @@ -471,16 +519,27 @@ public class BubbleData { moving.add(0, mBubbles.get(i)); } } + if (moving.isEmpty()) { + return false; + } mBubbles.removeAll(moving); mBubbles.addAll(position + 1, moving); + return true; } - private void repackAll() { + /** + * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped + * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles + * within each group are then sorted by lastUpdated descending. + * + * @return true if the position of any bubbles changed as a result + */ + private boolean repackAll() { if (DEBUG) { Log.d(TAG, "repackAll()"); } if (mBubbles.isEmpty()) { - return; + return false; } Map groupLastActivity = new HashMap<>(); for (Bubble bubble : mBubbles) { @@ -494,7 +553,7 @@ public class BubbleData { // Sort groups by their most recently active bubble List groupsByMostRecentActivity = groupLastActivity.entrySet().stream() - .sorted(GROUPS_BY_LAST_ACTIVITY_DESCENDING) + .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING) .map(Map.Entry::getKey) .collect(toList()); @@ -504,10 +563,14 @@ public class BubbleData { for (String appId : groupsByMostRecentActivity) { mBubbles.stream() .filter((b) -> b.getGroupId().equals(appId)) - .sorted(BUBBLES_BY_LAST_ACTIVITY_DESCENDING) + .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) .forEachOrdered(repacked::add); } + if (repacked.equals(mBubbles)) { + return false; + } mBubbles = repacked; + return true; } private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) { @@ -527,21 +590,25 @@ public class BubbleData { } private void onBubbleBlocked(NotificationEntry entry) { - boolean changed = false; - final String blockedPackage = entry.notification.getPackageName(); + final String blockedGroupId = Bubble.groupId(entry); + int selectedIndex = mBubbles.indexOf(mSelectedBubble); for (Iterator i = mBubbles.iterator(); i.hasNext(); ) { Bubble bubble = i.next(); - if (bubble.getPackageName().equals(blockedPackage)) { + if (bubble.getGroupId().equals(blockedGroupId)) { + mRemovedBubbles.add(Pair.create(bubble, BubbleController.DISMISS_BLOCKED)); i.remove(); - // TODO: handle removal of selected bubble, and collapse safely if emptied (see - // dismissAll) - dispatchOnBubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED); - changed = true; } } - if (changed) { - dispatchApply(); + if (mBubbles.isEmpty()) { + setExpandedInternal(false); + setSelectedBubbleInternal(null); + } else if (!mBubbles.contains(mSelectedBubble)) { + // choose a new one + int newIndex = Math.min(selectedIndex, mBubbles.size() - 1); + Bubble newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); } + dispatchPendingChanges(); } private int indexForKey(String key) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 123d73dc6432b..35dc1775cb7fb 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -549,6 +549,7 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.addView(bubble.iconView, 0, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); ViewClippingUtil.setClippingDeactivated(bubble.iconView, true, mClippingParameters); + animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED); } @@ -570,10 +571,19 @@ public class BubbleStackView extends FrameLayout { // via BubbleData.Listener void updateBubble(Bubble bubble) { + animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); } + public void updateBubbleOrder(List bubbles) { + for (int i = 0; i < bubbles.size(); i++) { + Bubble bubble = bubbles.get(i); + mBubbleContainer.moveViewTo(bubble.iconView, i); + } + } + + /** * Changes the currently selected bubble. If the stack is already expanded, the newly selected * bubble will be shown immediately. This does not change the expanded state or change the 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 2d697e34c6260..35a15167d2075 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -296,22 +296,22 @@ public class BubbleControllerTest extends SysuiTestCase { BubbleStackView stackView = mBubbleController.getStackView(); mBubbleController.expandStack(); assertTrue(mBubbleController.isStackExpanded()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); - // First added is the one that is expanded - assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow.getEntry().showInShadeWhenBubble()); - - // Switch which bubble is expanded - mBubbleController.selectBubble(mRow2.getEntry().key); - stackView.setExpandedBubble(mRow2.getEntry()); + // Last added is the one that is expanded assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); assertFalse(mRow2.getEntry().showInShadeWhenBubble()); + // Switch which bubble is expanded + mBubbleController.selectBubble(mRow.getEntry().key); + stackView.setExpandedBubble(mRow.getEntry()); + assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); + assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // collapse for previous bubble - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); // expand for selected bubble - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); // Collapse mBubbleController.collapseStack(); @@ -352,27 +352,27 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.expandStack(); assertTrue(mBubbleController.isStackExpanded()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); - // First added is the one that is expanded - assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // Last added is the one that is expanded + assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); + assertFalse(mRow2.getEntry().showInShadeWhenBubble()); // Dismiss currently expanded mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(), BubbleController.DISMISS_USER_GESTURE); - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); - // Make sure next bubble is selected - assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); + // Make sure first bubble is selected + assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); // Dismiss that one mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(), BubbleController.DISMISS_USER_GESTURE); // Make sure state changes and collapse happens - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); verify(mBubbleStateChangeListener).onHasBubblesChanged(false); assertFalse(mBubbleController.hasBubbles()); } 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 d6dac2f36ba1f..33b2e6e59470d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -16,12 +16,18 @@ package com.android.systemui.bubbles; +import static com.android.systemui.bubbles.BubbleController.DISMISS_AGED; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,6 +54,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -108,6 +116,628 @@ public class BubbleDataTest extends SysuiTestCase { // Used by BubbleData to set lastAccessedTime when(mTimeSource.currentTimeMillis()).thenReturn(1000L); mBubbleData.setTimeSource(mTimeSource); + + // Assert baseline starting state + assertThat(mBubbleData.hasBubbles()).isFalse(); + assertThat(mBubbleData.isExpanded()).isFalse(); + assertThat(mBubbleData.getSelectedBubble()).isNull(); + } + + @Test + public void testAddBubble() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + + // Verify + verify(mListener).onBubbleAdded(eq(mBubbleA1)); + verify(mListener).onSelectionChanged(eq(mBubbleA1)); + verify(mListener).apply(); + } + + @Test + public void testRemoveBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + + // Verify + verify(mListener).onBubbleRemoved(eq(mBubbleA1), eq(BubbleController.DISMISS_USER_GESTURE)); + verify(mListener).apply(); + } + + // COLLAPSED / ADD + + /** + * Verifies that the number of bubbles is not allowed to exceed the maximum. The limit is + * enforced by expiring the bubble which was least recently updated (lowest timestamp). + */ + @Test + public void test_collapsed_addBubble_atMaxBubbles_expiresOldest() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); + sendUpdatedEntryAtTime(mEntryB1, 4000); + sendUpdatedEntryAtTime(mEntryB2, 5000); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryC1, 6000); + verify(mListener).onBubbleRemoved(eq(mBubbleA1), eq(DISMISS_AGED)); + } + + /** + * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles. + *

+ * Placement within the list is based on lastUpdate (post time of the notification), descending + * order (with most recent first). + * + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_collapsed_addBubble_sortAndGrouping() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener, never()).onOrderChanged(anyList()); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA1))); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB2, 3000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA1))); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1))); + } + + /** + * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles. + * Additionally, any bubble which is ongoing is considered "newer" than any non-ongoing bubble. + *

+ * Because of the ongoing bubble, the new bubble cannot be placed in the first position. This + * causes the 'B' group to remain last, despite having a new button added. + * + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_collapsed_addBubble_sortAndGrouping_withOngoing() { + // Setup + mBubbleData.setListener(mListener); + + // Test + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener, never()).onOrderChanged(anyList()); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener, never()).onOrderChanged(eq(listOf(mBubbleA1, mBubbleB1))); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB2, 3000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleB2, mBubbleB1))); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleB2, mBubbleB1))); + } + + /** + * Verifies that new bubbles become the selected bubble when they appear when the stack is in + * the collapsed state. + * + * @see #test_collapsed_updateBubble_selectionChanges() + * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing() + */ + @Test + public void test_collapsed_addBubble_selectionChanges() { + // Setup + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener).onSelectionChanged(eq(mBubbleA1)); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener).onSelectionChanged(eq(mBubbleB1)); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryB2, 3000); + verify(mListener).onSelectionChanged(eq(mBubbleB2)); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onSelectionChanged(eq(mBubbleA2)); + } + /** + * Verifies that while collapsed, the selection will not change if the selected bubble is + * ongoing. It remains the top bubble and as such remains selected. + * + * @see #test_collapsed_addBubble_selectionChanges() + */ + @Test + public void test_collapsed_addBubble_noSelectionChanges_withOngoing() { + // Setup + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); // selection unchanged + } + + // COLLAPSED / REMOVE + + /** + * Verifies that groups may reorder when bubbles are removed, while the stack is in the + * collapsed state. + */ + @Test + public void test_collapsed_removeBubble_sortAndGrouping() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA1))); + } + + + /** + * Verifies that onOrderChanged is not called when a bubble is removed if the removal does not + * cause other bubbles to change position. + */ + @Test + public void test_collapsed_removeOldestBubble_doesNotCallOnOrderChanged() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener, never()).onOrderChanged(anyList()); + } + + /** + * Verifies that bubble ordering reverts to normal when an ongoing bubble is removed. A group + * which has a newer bubble may move to the front after the ongoing bubble is removed. + */ + @Test + public void test_collapsed_removeBubble_sortAndGrouping_withOngoing() { + // Setup + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [A1*, A2, B2, B1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA2))); + } + + /** + * Verifies that when the selected bubble is removed with the stack in the collapsed state, + * the selection moves to the next most-recently updated bubble. + */ + @Test + public void test_collapsed_removeBubble_selectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_NOTIF_CANCEL); + verify(mListener).onSelectionChanged(eq(mBubbleB2)); + } + + // COLLAPSED / UPDATE + + /** + * Verifies that bubble and group ordering may change with updates while the stack is in the + * collapsed state. + */ + @Test + public void test_collapsed_updateBubble_orderAndGrouping() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleB2, mBubbleA2, mBubbleA1))); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryA1, 6000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2))); + } + + /** + * Verifies that selection tracks the most recently updated bubble while in the collapsed state. + */ + @Test + public void test_collapsed_updateBubble_selectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verify(mListener).onSelectionChanged(eq(mBubbleB1)); + + reset(mListener); + sendUpdatedEntryAtTime(mEntryA1, 6000); + verify(mListener).onSelectionChanged(eq(mBubbleA1)); + } + + /** + * Verifies that selection does not change in response to updates when collapsed, if the + * selected bubble is ongoing. + */ + @Test + public void test_collapsed_updateBubble_noSelectionChanges_withOngoing() { + // Setup + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A1*, A2, B2, B1] + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryB2, 5000); // [A1*, A2, B2, B1] + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); + } + + /** + * Verifies that a request to expand the stack has no effect if there are no bubbles. + */ + @Test + public void test_collapsed_expansion_whenEmpty_doesNothing() { + assertThat(mBubbleData.hasBubbles()).isFalse(); + changeExpandedStateAtTime(true, 2000L); + + verify(mListener, never()).onExpandedChanged(anyBoolean()); + verify(mListener, never()).apply(); + } + + @Test + public void test_collapsed_removeLastBubble_clearsSelectedBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + + // Verify the selection was cleared. + verify(mListener).onSelectionChanged(isNull()); + } + + // EXPANDED / ADD + + /** + * Verifies that bubbles added as part of a new group insert before existing groups while + * expanded. + *

+ * Placement within the list is based on lastUpdate (post time of the notification), descending + * order (with most recent first). + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_addBubble_sortAndGrouping_newGroup() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1] + changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryC1, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleC1, mBubbleB1, mBubbleA2, mBubbleA1))); + } + + /** + * Verifies that bubbles added as part of a new group insert before existing groups while + * expanded, but not before any groups with ongoing bubbles. + * + * @see #test_collapsed_addBubble_sortAndGrouping_withOngoing() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_addBubble_sortAndGrouping_newGroup_withOngoing() { + // Setup + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [A1*, A2, B1] + changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryC1, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleC1, mBubbleB1))); + } + + /** + * Verifies that bubbles added as part of an existing group insert to the beginning of that + * group. The order of groups within the list must not change while in the expanded state. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + */ + @Test + public void test_expanded_addBubble_sortAndGrouping_existingGroup() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1] + changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA3, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA3, mBubbleA2, mBubbleA1))); + } + + // EXPANDED / UPDATE + + /** + * Verifies that updates to bubbles while expanded do not result in any change to sorting + * or grouping of bubbles or sorting of groups. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_updateBubble_sortAndGrouping_noChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 4000); + verify(mListener, never()).onOrderChanged(anyList()); + } + + /** + * Verifies that updates to bubbles while expanded do not result in any change to selection. + * + * @see #test_collapsed_addBubble_selectionChanges() + * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing() + */ + @Test + public void test_expanded_updateBubble_noSelectionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 6000); + sendUpdatedEntryAtTime(mEntryA2, 7000); + sendUpdatedEntryAtTime(mEntryB1, 8000); + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); + } + + // EXPANDED / REMOVE + + /** + * Verifies that removing a bubble while expanded does not result in reordering of groups + * or any of the remaining bubbles. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_removeBubble_sortAndGrouping() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA2, mBubbleA1))); + } + + /** + * Verifies that removing the selected bubble while expanded causes another bubble to become + * selected. The replacement selection is the bubble which appears at the same index as the + * previous one, or the previous index if this was the last position. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_removeBubble_selectionChanges_whenSelectedRemoved() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, B1, ^A2, A1] + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onSelectionChanged(mBubbleA1); + + reset(mListener); + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onSelectionChanged(mBubbleB1); + } + + @Test + public void test_expandAndCollapse_callsOnExpandedChanged() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + // Test + changeExpandedStateAtTime(true, 3000L); + verify(mListener).onExpandedChanged(eq(true)); + + reset(mListener); + changeExpandedStateAtTime(false, 4000L); + verify(mListener).onExpandedChanged(eq(false)); + } + + /** + * Verifies that transitions between the collapsed and expanded state maintain sorting and + * grouping rules. + *

+ * While collapsing, sorting is applied since no sorting happens while expanded. The resulting + * state is the new expanded ordering. This state is saved and restored if possible when next + * expanded. + *

+ * When the stack transitions to the collapsed state, the selected bubble is brought to the top. + * Bubbles within the same group should move up with it. + *

+ * When the stack transitions back to the expanded state, the previous ordering is restored, as + * long as no changes have been made (adds, removes or updates) while in the collapsed state. + */ + @Test + public void test_expansionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=6000*, A2=3000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); + mBubbleData.setListener(mListener); + assertThat(mBubbleData.getBubbles()).isEqualTo( + listOf(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); + + // Test + + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [B1, B2, A2*, A1] + // + // That state is restored as long as no changes occur (add/remove/update) while in + // the collapsed state. + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2))); + + // expand -> "original" order/grouping restored + reset(mListener); + changeExpandedStateAtTime(true, 10000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleB2, mBubbleA2, mBubbleA1))); + } + + /** + * When a change occurs while collapsed (any update, add, remove), the previous expanded + * order and grouping becomes invalidated, and the order and grouping when next expanded will + * remain the same as collapsed. + */ + @Test + public void test_expansionChanges_withUpdatesWhileCollapsed() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=*6000, A2=3000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, B1, ^A2, A1] + mBubbleData.setListener(mListener); + + // Test + + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [B1, B2, A2*, A1] + // + // That state is restored as long as no changes occur (add/remove/update) while in + // the collapsed state. + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2))); + + // An update occurs, which causes sorting, and this invalidates the previously saved order. + sendUpdatedEntryAtTime(mEntryA2, 9000); + + // No order changes when expanding because the new sorted order remains. + reset(mListener); + changeExpandedStateAtTime(true, 10000L); + verify(mListener, never()).onOrderChanged(anyList()); + } + + @Test + public void test_expanded_removeLastBubble_collapsesStack() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + changeExpandedStateAtTime(true, 2000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onExpandedChanged(eq(false)); } private NotificationEntry createBubbleEntry(int userId, String notifKey, String packageName) { @@ -155,553 +785,22 @@ public class BubbleDataTest extends SysuiTestCase { return new NotificationEntry(sbn); } + private void setCurrentTime(long time) { + when(mTimeSource.currentTimeMillis()).thenReturn(time); + } + private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) { setPostTime(entry, postTime); mBubbleData.notificationEntryUpdated(entry); } private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { - when(mTimeSource.currentTimeMillis()).thenReturn(time); + setCurrentTime(time); mBubbleData.setExpanded(shouldBeExpanded); } - @Test - public void testAddBubble() { - // Setup - mBubbleData.setListener(mListener); - - // Test - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - // Verify - verify(mListener).onBubbleAdded(eq(mBubbleA1)); - verify(mListener).onSelectionChanged(eq(mBubbleA1)); - verify(mListener).apply(); - } - - @Test - public void testRemoveBubble() { - // Setup - mBubbleData.notificationEntryUpdated(mEntryA1); - mBubbleData.notificationEntryUpdated(mEntryA2); - mBubbleData.notificationEntryUpdated(mEntryA3); - mBubbleData.setListener(mListener); - - // Test - mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - - // Verify - verify(mListener).onBubbleRemoved(eq(mBubbleA1), eq(BubbleController.DISMISS_USER_GESTURE)); - verify(mListener).onSelectionChanged(eq(mBubbleA2)); - verify(mListener).apply(); - } - - @Test - public void test_collapsed_addBubble_atMaxBubbles_expiresLeastActive() { - // Given - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryA3, 3000); - sendUpdatedEntryAtTime(mEntryB1, 4000); - sendUpdatedEntryAtTime(mEntryB2, 5000); - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - // When - sendUpdatedEntryAtTime(mEntryC1, 6000); - - // Then - // A2 is removed. A1 is oldest but is the selected bubble. - assertThat(mBubbleData.getBubbles()).doesNotContain(mBubbleA2); - } - - @Test - public void test_collapsed_expand_whenEmpty_doesNothing() { - assertThat(mBubbleData.hasBubbles()).isFalse(); - changeExpandedStateAtTime(true, 2000L); - - verify(mListener, never()).onExpandedChanged(anyBoolean()); - verify(mListener, never()).apply(); - } - - // New bubble while stack is collapsed - @Test - public void test_collapsed_addBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - // When - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - // Then - // New bubbles move to front when collapsed, bringing bubbles from the same app along - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - } - - // New bubble while collapsed with ongoing bubble present - @Test - public void test_collapsed_addBubble_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - // When - setOngoing(mEntryA1, true); - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - // Then - // New bubbles move to front, but stay behind any ongoing bubbles. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB2, mBubbleB1)); - } - - // Remove the selected bubble (middle bubble), while the stack is collapsed. - @Test - public void test_collapsed_removeBubble_selected() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - mBubbleData.setSelectedBubble(mBubbleB2); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); - - // Then - // (Selection remains in the same position) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); - } - - // Remove the selected bubble (last bubble), while the stack is collapsed. - @Test - public void test_collapsed_removeSelectedBubble_inLastPosition() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - mBubbleData.setSelectedBubble(mBubbleB1); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - - // Then - // (Selection is forced to move to previous) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB2); - } - - @Test - public void test_collapsed_addBubble_ongoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - // When - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - setOngoing(mEntryB2, true); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - // Then - // New bubbles move to front, but stay behind any ongoing bubbles. - // Does not break grouping. (A2 is inserted after B1, even though it's newer). - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - } - - @Test - public void test_collapsed_removeBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - // When - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); - - // Then - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB1)); - } - - @Test - public void test_collapsed_updateBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - sendUpdatedEntryAtTime(mEntryB2, 5000); - - // Then - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - } - - @Test - public void test_collapsed_updateBubble_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setOngoing(mEntryA2, true); - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - setPostTime(mEntryB1, 5000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - // Then - // A2 remains in first position, due to being ongoing. B1 moves before B2, Group A - // remains before group B. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2)); - } - - @Test - public void test_collapse_afterUpdateWhileExpanded() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - changeExpandedStateAtTime(true, 5000L); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - sendUpdatedEntryAtTime(mEntryB1, 6000); - - // (No reordering while expanded) - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - changeExpandedStateAtTime(false, 7000L); - - // Then - // A1 moves to front on collapse, since it is the selected bubble (and most recently - // accessed). - // A2 moves next to A1 to maintain grouping. - // B1 moves in front of B2, since it received an update while expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2)); - } - - @Test - public void test_collapse_afterUpdateWhileExpanded_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - - setOngoing(mEntryB2, true); - sendUpdatedEntryAtTime(mEntryB2, 3000); - - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - changeExpandedStateAtTime(true, 5000L); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - - sendUpdatedEntryAtTime(mEntryA1, 6000); - - // No reordering if expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - - // When - changeExpandedStateAtTime(false, 7000L); - - // Then - // B2 remains in first position because it is ongoing. - // B1 remains grouped with B2 - // A1 moves in front of A2, since it is more recently updated (and is selected). - // B1 moves in front of B2, since it has more recent activity. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA1, mBubbleA2)); - } - - @Test - public void test_collapsed_removeLastBubble_clearsSelectedBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryB1, 2000); - sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); - - assertThat(mBubbleData.getSelectedBubble()).isNull(); - } - - @Test - public void test_expanded_addBubble_atMaxBubbles_expiresLeastActive() { - // Given - sendUpdatedEntryAtTime(mEntryA1, 1000); - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - changeExpandedStateAtTime(true, 2000L); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(2000); - - sendUpdatedEntryAtTime(mEntryA2, 3000); - sendUpdatedEntryAtTime(mEntryA3, 4000); - sendUpdatedEntryAtTime(mEntryB1, 5000); - sendUpdatedEntryAtTime(mEntryB2, 6000); - sendUpdatedEntryAtTime(mEntryB3, 7000); - - - // Then - // A1 would be removed, but it is selected and expanded, so it should not go away. - // Instead, fall through to removing A2 (the next oldest). - assertThat(mBubbleData.getBubbles()).doesNotContain(mEntryA2); - } - - @Test - public void test_expanded_removeLastBubble_collapsesStack() { - // Given - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryC1); - - mBubbleData.setExpanded(true); - - mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryC1, BubbleController.DISMISS_USER_GESTURE); - - assertThat(mBubbleData.isExpanded()).isFalse(); - assertThat(mBubbleData.getSelectedBubble()).isNull(); - } - - // Bubbles do not reorder while expanded - @Test - public void test_expanded_selection_collapseToTop() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryB1, 3000); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB1, mBubbleA2, mBubbleA1)); - - changeExpandedStateAtTime(true, 4000L); - - // regrouping only happens when collapsed (after new or update) or expanded->collapsed - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB1, mBubbleA2, mBubbleA1)); - - changeExpandedStateAtTime(false, 6000L); - - // A1 is still selected and it's lastAccessed time has been updated - // on collapse, sorting is applied, keeping the selected bubble at the front - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB1)); - } - - // New bubble from new app while stack is expanded - @Test - public void test_expanded_addBubble_newApp() { - // Given - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryA3, 3000); - sendUpdatedEntryAtTime(mEntryB1, 4000); - sendUpdatedEntryAtTime(mEntryB2, 5000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - changeExpandedStateAtTime(true, 6000L); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(6000L); - - // regrouping only happens when collapsed (after new or update) or expanded->collapsed - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2, mBubbleA1)); - - // When - sendUpdatedEntryAtTime(mEntryC1, 7000); - - // Then - // A2 is expired. A1 was oldest, but lastActivityTime is reset when expanded, since A1 is - // selected. - // C1 is added at the end since bubbles are expanded. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA1, mBubbleC1)); - } - - // New bubble from existing app while stack is expanded - @Test - public void test_expanded_addBubble_existingApp() { - // Given - sendUpdatedEntryAtTime(mEntryB1, 1000); - sendUpdatedEntryAtTime(mEntryB2, 2000); - sendUpdatedEntryAtTime(mEntryA1, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - sendUpdatedEntryAtTime(mEntryA3, 5000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); - - changeExpandedStateAtTime(true, 6000L); - - // B1 is first (newest, since it's just been expanded and is selected) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(6000L); - - // regrouping only happens when collapsed (after new or update) or while collapsing - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA3, mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - sendUpdatedEntryAtTime(mEntryB3, 7000); - - // Then - // (B2 is expired, B1 was oldest, but it's lastActivityTime is updated at the point when - // the stack was expanded, since it is the selected bubble. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA3, mBubbleA2, mBubbleA1, mBubbleB3, mBubbleB1)); - } - - // Updated bubble from existing app while stack is expanded - @Test - public void test_expanded_updateBubble_existingApp() { - sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryB1, 3000); - sendUpdatedEntryAtTime(mEntryB2, 4000); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - mBubbleData.setExpanded(true); - - sendUpdatedEntryAtTime(mEntryA1, 5000); - - // Does not reorder while expanded (for an update). - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - } - - @Test - public void test_expanded_updateBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - mBubbleData.setExpanded(true); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - setPostTime(mEntryB1, 5000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - // Then - // B1 remains in the same place due to being expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + /** Syntactic sugar to keep assertions more readable */ + private static List listOf(T... a) { + return ImmutableList.copyOf(a); } }