Loading packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +18 −1 Original line number Diff line number Diff line Loading @@ -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(); } Loading Loading @@ -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). */ Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +9 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -510,6 +515,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onOrderChanged(List<Bubble> bubbles) { if (mStackView != null) { mStackView.updateBubbleOrder(bubbles); } } @Override Loading @@ -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(); Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +193 −126 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -53,10 +54,10 @@ public class BubbleData { private static final int MAX_BUBBLES = 5; private static final Comparator<Bubble> BUBBLES_BY_LAST_ACTIVITY_DESCENDING = Comparator.comparing(Bubble::getLastActivity).reversed(); private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = Comparator.comparing(BubbleData::sortKey).reversed(); private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_LAST_ACTIVITY_DESCENDING = private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING = Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed(); /** Loading Loading @@ -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(); } Loading @@ -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<Pair<Bubble, Integer>> mRemovedBubbles = new ArrayList<>(); private TimeSource mTimeSource = System::currentTimeMillis; @Nullable private Listener mListener; @VisibleForTesting @Inject public BubbleData(Context context) { mContext = context; Loading @@ -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) { Loading @@ -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); Loading @@ -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) { Loading @@ -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()); } if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) { mOrderChanged = true; } insertBubble(minInsertPoint, bubble); 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) Loading @@ -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)); } } Loading @@ -229,32 +244,38 @@ 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) { 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); dispatchOnBubbleRemoved(bubbleToRemove, reason); mRemovedBubbles.add(Pair.create(bubbleToRemove, reason)); if (!isExpanded()) { mOrderChanged |= repackAll(); } // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. if (Objects.equals(mSelectedBubble, bubbleToRemove)) { Loading @@ -266,7 +287,6 @@ public class BubbleData { bubbleToRemove.setDismissed(); maybeSendDeleteIntent(reason, bubbleToRemove.entry); } } public void dismissAll(@DismissReason int reason) { if (DEBUG) { Loading @@ -281,56 +301,71 @@ 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) { mListener.apply(); } } private void dispatchOnBubbleAdded(Bubble bubble) { if (mListener != null) { mListener.onBubbleAdded(bubble); } private void dispatchPendingChanges() { if (mListener == null) { mExpandedChanged = false; mAddedBubble = null; mSelectionChanged = false; mRemovedBubbles.clear(); mUpdatedBubble = null; mOrderChanged = false; return; } boolean anythingChanged = false; private void dispatchOnBubbleRemoved(Bubble bubble, @DismissReason int reason) { if (mListener != null) { mListener.onBubbleRemoved(bubble, reason); } if (mAddedBubble != null) { mListener.onBubbleAdded(mAddedBubble); mAddedBubble = null; anythingChanged = true; } private void dispatchOnExpandedChanged(boolean expanded) { if (mListener != null) { mListener.onExpandedChanged(expanded); // Compat workaround: Always collapse first. if (mExpandedChanged && !mExpanded) { mListener.onExpandedChanged(mExpanded); mExpandedChanged = false; anythingChanged = true; } if (mSelectionChanged) { mListener.onSelectionChanged(mSelectedBubble); mSelectionChanged = false; anythingChanged = true; } private void dispatchOnSelectionChanged(@Nullable Bubble bubble) { if (mListener != null) { mListener.onSelectionChanged(bubble); if (!mRemovedBubbles.isEmpty()) { for (Pair<Bubble, Integer> removed : mRemovedBubbles) { mListener.onBubbleRemoved(removed.first, removed.second); } mRemovedBubbles.clear(); anythingChanged = true; } private void dispatchOnBubbleUpdated(Bubble bubble) { if (mListener != null) { mListener.onBubbleUpdated(bubble); } if (mUpdatedBubble != null) { mListener.onBubbleUpdated(mUpdatedBubble); mUpdatedBubble = null; anythingChanged = true; } private void dispatchOnOrderChanged(List<Bubble> bubbles) { if (mListener != null) { mListener.onOrderChanged(bubbles); if (mOrderChanged) { mListener.onOrderChanged(mBubbles); mOrderChanged = false; anythingChanged = true; } if (mExpandedChanged) { mListener.onExpandedChanged(mExpanded); mExpandedChanged = false; anythingChanged = true; } private void dispatchShowFlyoutText(Bubble bubble, String text) { if (mListener != null) { mListener.showFlyoutText(bubble, text); if (anythingChanged) { mListener.apply(); } } Loading @@ -339,29 +374,25 @@ public class BubbleData { * 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; } /** Loading @@ -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 { repackAll(); // 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); } } } 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; Loading Loading @@ -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); } Loading @@ -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<String, Long> groupLastActivity = new HashMap<>(); for (Bubble bubble : mBubbles) { Loading @@ -494,7 +553,7 @@ public class BubbleData { // Sort groups by their most recently active bubble List<String> groupsByMostRecentActivity = groupLastActivity.entrySet().stream() .sorted(GROUPS_BY_LAST_ACTIVITY_DESCENDING) .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING) .map(Map.Entry::getKey) .collect(toList()); Loading @@ -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) { Loading @@ -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<Bubble> 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) { Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +10 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -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<Bubble> 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 Loading packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +19 −19 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +18 −1 Original line number Diff line number Diff line Loading @@ -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(); } Loading Loading @@ -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). */ Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +9 −8 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -510,6 +515,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onOrderChanged(List<Bubble> bubbles) { if (mStackView != null) { mStackView.updateBubbleOrder(bubbles); } } @Override Loading @@ -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(); Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +193 −126 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -53,10 +54,10 @@ public class BubbleData { private static final int MAX_BUBBLES = 5; private static final Comparator<Bubble> BUBBLES_BY_LAST_ACTIVITY_DESCENDING = Comparator.comparing(Bubble::getLastActivity).reversed(); private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = Comparator.comparing(BubbleData::sortKey).reversed(); private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_LAST_ACTIVITY_DESCENDING = private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING = Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed(); /** Loading Loading @@ -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(); } Loading @@ -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<Pair<Bubble, Integer>> mRemovedBubbles = new ArrayList<>(); private TimeSource mTimeSource = System::currentTimeMillis; @Nullable private Listener mListener; @VisibleForTesting @Inject public BubbleData(Context context) { mContext = context; Loading @@ -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) { Loading @@ -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); Loading @@ -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) { Loading @@ -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()); } if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) { mOrderChanged = true; } insertBubble(minInsertPoint, bubble); 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) Loading @@ -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)); } } Loading @@ -229,32 +244,38 @@ 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) { 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); dispatchOnBubbleRemoved(bubbleToRemove, reason); mRemovedBubbles.add(Pair.create(bubbleToRemove, reason)); if (!isExpanded()) { mOrderChanged |= repackAll(); } // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. if (Objects.equals(mSelectedBubble, bubbleToRemove)) { Loading @@ -266,7 +287,6 @@ public class BubbleData { bubbleToRemove.setDismissed(); maybeSendDeleteIntent(reason, bubbleToRemove.entry); } } public void dismissAll(@DismissReason int reason) { if (DEBUG) { Loading @@ -281,56 +301,71 @@ 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) { mListener.apply(); } } private void dispatchOnBubbleAdded(Bubble bubble) { if (mListener != null) { mListener.onBubbleAdded(bubble); } private void dispatchPendingChanges() { if (mListener == null) { mExpandedChanged = false; mAddedBubble = null; mSelectionChanged = false; mRemovedBubbles.clear(); mUpdatedBubble = null; mOrderChanged = false; return; } boolean anythingChanged = false; private void dispatchOnBubbleRemoved(Bubble bubble, @DismissReason int reason) { if (mListener != null) { mListener.onBubbleRemoved(bubble, reason); } if (mAddedBubble != null) { mListener.onBubbleAdded(mAddedBubble); mAddedBubble = null; anythingChanged = true; } private void dispatchOnExpandedChanged(boolean expanded) { if (mListener != null) { mListener.onExpandedChanged(expanded); // Compat workaround: Always collapse first. if (mExpandedChanged && !mExpanded) { mListener.onExpandedChanged(mExpanded); mExpandedChanged = false; anythingChanged = true; } if (mSelectionChanged) { mListener.onSelectionChanged(mSelectedBubble); mSelectionChanged = false; anythingChanged = true; } private void dispatchOnSelectionChanged(@Nullable Bubble bubble) { if (mListener != null) { mListener.onSelectionChanged(bubble); if (!mRemovedBubbles.isEmpty()) { for (Pair<Bubble, Integer> removed : mRemovedBubbles) { mListener.onBubbleRemoved(removed.first, removed.second); } mRemovedBubbles.clear(); anythingChanged = true; } private void dispatchOnBubbleUpdated(Bubble bubble) { if (mListener != null) { mListener.onBubbleUpdated(bubble); } if (mUpdatedBubble != null) { mListener.onBubbleUpdated(mUpdatedBubble); mUpdatedBubble = null; anythingChanged = true; } private void dispatchOnOrderChanged(List<Bubble> bubbles) { if (mListener != null) { mListener.onOrderChanged(bubbles); if (mOrderChanged) { mListener.onOrderChanged(mBubbles); mOrderChanged = false; anythingChanged = true; } if (mExpandedChanged) { mListener.onExpandedChanged(mExpanded); mExpandedChanged = false; anythingChanged = true; } private void dispatchShowFlyoutText(Bubble bubble, String text) { if (mListener != null) { mListener.showFlyoutText(bubble, text); if (anythingChanged) { mListener.apply(); } } Loading @@ -339,29 +374,25 @@ public class BubbleData { * 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; } /** Loading @@ -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 { repackAll(); // 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); } } } 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; Loading Loading @@ -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); } Loading @@ -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<String, Long> groupLastActivity = new HashMap<>(); for (Bubble bubble : mBubbles) { Loading @@ -494,7 +553,7 @@ public class BubbleData { // Sort groups by their most recently active bubble List<String> groupsByMostRecentActivity = groupLastActivity.entrySet().stream() .sorted(GROUPS_BY_LAST_ACTIVITY_DESCENDING) .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING) .map(Map.Entry::getKey) .collect(toList()); Loading @@ -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) { Loading @@ -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<Bubble> 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) { Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +10 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -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<Bubble> 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 Loading
packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +19 −19 File changed.Preview size limit exceeded, changes collapsed. Show changes