BubbleData [7/n]: BubbleData impl and tests!!
This adds the full implementation of BubbleData along with
test cases developed against the designed behavior of Bubbles.
See: go/bubble-order for the full policy and rules behind this.
Since BubbleData.Listener#onOrderChanged is not yet connected,
the behavior is not yet applied to UI. This will happen in a
following change (along with new BubbleStackView tests).
Bug: 123542488
Test: atest BubbleControllerTest BubbleDataTest
Change-Id: I79d4452c196945735f081d6ae0fdc673de7ae102
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 0332477..7094d28 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -16,9 +16,12 @@
package com.android.systemui.bubbles;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
import android.os.UserHandle;
import android.view.LayoutInflater;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -40,15 +43,24 @@
public NotificationEntry entry;
BubbleView iconView;
BubbleExpandedView expandedView;
+ private long mLastUpdated;
+ private long mLastAccessed;
private static String groupId(NotificationEntry entry) {
UserHandle user = entry.notification.getUser();
- return user.getIdentifier() + '|' + entry.notification.getPackageName();
+ return user.getIdentifier() + "|" + entry.notification.getPackageName();
+ }
+
+ /** Used in tests when no UI is required. */
+ @VisibleForTesting(visibility = PRIVATE)
+ Bubble(NotificationEntry e) {
+ this (e, null);
}
Bubble(NotificationEntry e, BubbleExpandedView.OnBubbleBlockedListener listener) {
entry = e;
mKey = e.key;
+ mLastUpdated = e.notification.getPostTime();
mGroupId = groupId(e);
mListener = listener;
}
@@ -101,12 +113,37 @@
void setEntry(NotificationEntry entry) {
this.entry = entry;
+ mLastUpdated = entry.notification.getPostTime();
if (mInflated) {
iconView.update(entry);
expandedView.update(entry);
}
}
+ public long getLastActivity() {
+ return Math.max(mLastUpdated, mLastAccessed);
+ }
+
+ /**
+ * Should be invoked whenever a Bubble is accessed (selected while expanded).
+ */
+ void markAsAccessedAt(long lastAccessedMillis) {
+ mLastAccessed = lastAccessedMillis;
+ entry.setShowInShadeWhenBubble(false);
+ }
+
+ /**
+ * @return whether bubble is from a notification associated with a foreground service.
+ */
+ public boolean isOngoing() {
+ return entry.isForegroundService();
+ }
+
+ @Override
+ public String toString() {
+ return "Bubble{" + mKey + '}';
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index ef383ad..d071363 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -41,6 +41,7 @@
import android.os.ServiceManager;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
+import android.util.Log;
import android.view.Display;
import android.view.IPinnedStackController;
import android.view.IPinnedStackListener;
@@ -83,6 +84,7 @@
public class BubbleController implements ConfigurationController.ConfigurationListener {
private static final String TAG = "BubbleController";
+ private static final boolean DEBUG = true;
@Retention(SOURCE)
@IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
@@ -203,6 +205,9 @@
configurationController.addCallback(this /* configurationListener */);
+ mBubbleData = data;
+ mBubbleData.setListener(mBubbleDataListener);
+
mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
@@ -219,9 +224,6 @@
} catch (RemoteException e) {
e.printStackTrace();
}
-
- mBubbleData = data;
- mBubbleData.setListener(mBubbleDataListener);
mSurfaceSynchronizer = synchronizer;
mBarService = IStatusBarService.Stub.asInterface(
@@ -482,7 +484,7 @@
}
@Override
- public void onSelectionChanged(Bubble selectedBubble) {
+ public void onSelectionChanged(@Nullable Bubble selectedBubble) {
if (mStackView != null) {
mStackView.setSelectedBubble(selectedBubble);
}
@@ -506,6 +508,18 @@
public void apply() {
mNotificationEntryManager.updateNotifications();
updateVisibility();
+
+ if (DEBUG) {
+ Log.d(TAG, "[BubbleData]");
+ Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(),
+ mBubbleData.getSelectedBubble()));
+
+ if (mStackView != null) {
+ Log.d(TAG, "[BubbleStackView]");
+ Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(),
+ mStackView.getExpandedBubble()));
+ }
+ }
}
};
@@ -623,6 +637,23 @@
entry.setShowInShadeWhenBubble(!suppressNotification);
}
+ static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
+ StringBuilder sb = new StringBuilder();
+ for (Bubble bubble : bubbles) {
+ if (bubble == null) {
+ sb.append(" <null> !!!!!\n");
+ } else {
+ boolean isSelected = (bubble == selected);
+ sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n",
+ ((isSelected) ? "->" : " "),
+ bubble.getLastActivity(),
+ (bubble.isOngoing() ? 1 : 0),
+ bubble.getKey()));
+ }
+ }
+ return sb.toString();
+ }
+
/**
* Return true if the applications with the package name is running in foreground.
*
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 259665d..38ba91e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -17,21 +17,26 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-import android.app.ActivityManager;
+import static java.util.stream.Collectors.toList;
+
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.bubbles.BubbleController.DismissReason;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
@@ -44,6 +49,15 @@
public class BubbleData {
private static final String TAG = "BubbleData";
+ private static final boolean DEBUG = false;
+
+ 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<Map.Entry<String, Long>> GROUPS_BY_LAST_ACTIVITY_DESCENDING =
+ Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed();
/**
* This interface reports changes to the state and appearance of bubbles which should be applied
@@ -83,7 +97,7 @@
void onOrderChanged(List<Bubble> bubbles);
/** Indicates the selected bubble changed. */
- void onSelectionChanged(Bubble selectedBubble);
+ void onSelectionChanged(@Nullable Bubble selectedBubble);
/**
* The UI should transition to the given state, incorporating any pending changes during
@@ -98,16 +112,28 @@
void apply();
}
+ interface TimeSource {
+ long currentTimeMillis();
+ }
+
private final Context mContext;
- private final List<Bubble> mBubbles = new ArrayList<>();
+ private List<Bubble> mBubbles;
private Bubble mSelectedBubble;
private boolean mExpanded;
+
+ // TODO: ensure this is invalidated at the appropriate time
+ private int mSelectedBubbleExpandedPosition = -1;
+
+ private TimeSource mTimeSource = System::currentTimeMillis;
+
+ @Nullable
private Listener mListener;
@VisibleForTesting
@Inject
public BubbleData(Context context) {
mContext = context;
+ mBubbles = new ArrayList<>();
}
public boolean hasBubbles() {
@@ -122,29 +148,41 @@
return getBubbleWithKey(key) != null;
}
+ @Nullable
+ public Bubble getSelectedBubble() {
+ return mSelectedBubble;
+ }
+
public void setExpanded(boolean expanded) {
if (setExpandedInternal(expanded)) {
- mListener.apply();
+ dispatchApply();
}
}
public void setSelectedBubble(Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "setSelectedBubble: " + bubble);
+ }
if (setSelectedBubbleInternal(bubble)) {
- mListener.apply();
+ dispatchApply();
}
}
public void notificationEntryUpdated(NotificationEntry entry) {
+ if (DEBUG) {
+ Log.d(TAG, "notificationEntryUpdated: " + entry);
+ }
Bubble bubble = getBubbleWithKey(entry.key);
if (bubble == null) {
// Create a new bubble
bubble = new Bubble(entry, this::onBubbleBlocked);
- mBubbles.add(0, bubble); // TODO: reorder/group
- mListener.onBubbleAdded(bubble);
+ doAdd(bubble);
+ dispatchOnBubbleAdded(bubble);
} else {
// Updates an existing bubble
bubble.setEntry(entry);
- mListener.onBubbleUpdated(bubble);
+ doUpdate(bubble);
+ dispatchOnBubbleUpdated(bubble);
}
if (shouldAutoExpand(entry)) {
setSelectedBubbleInternal(bubble);
@@ -154,49 +192,148 @@
} else if (mSelectedBubble == null) {
setSelectedBubbleInternal(bubble);
}
- // TODO: reorder/group
- mListener.apply();
+ dispatchApply();
+ }
+
+ private void doAdd(Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "doAdd: " + bubble);
+ }
+ 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());
+ }
+ insertBubble(minInsertPoint, bubble);
+ if (!isExpanded()) {
+ packGroup(findFirstIndexForGroup(bubble.getGroupId()));
+ }
+ if (mBubbles.size() > MAX_BUBBLES) {
+ mBubbles.stream()
+ // sort oldest first (ascending lastActivity)
+ .sorted(Comparator.comparingLong(Bubble::getLastActivity))
+ // skip the selected bubble
+ .filter((b) -> !b.equals(mSelectedBubble))
+ .findFirst()
+ .ifPresent((b) -> {
+ doRemove(b.getKey(), BubbleController.DISMISS_AGED);
+ dispatchApply();
+ });
+ }
+ }
+
+ private void doUpdate(Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "doUpdate: " + bubble);
+ }
+ if (!isExpanded()) {
+ // while collapsed, update causes re-sort
+ mBubbles.remove(bubble);
+ insertBubble(0, bubble);
+ packGroup(findFirstIndexForGroup(bubble.getGroupId()));
+ }
}
public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
- int indexToRemove = indexForKey(entry.key);
- if (indexToRemove >= 0) {
- Bubble removed = mBubbles.remove(indexToRemove);
- removed.setDismissed();
- mListener.onBubbleRemoved(removed, reason);
- maybeSendDeleteIntent(reason, removed.entry);
+ if (DEBUG) {
+ Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason);
+ }
+ doRemove(entry.key, reason);
+ dispatchApply();
+ }
- if (mBubbles.isEmpty()) {
+ 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);
- } else if (removed == mSelectedBubble) {
+ }
+ 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);
}
- // TODO: reorder/group
- mListener.apply();
+ bubbleToRemove.setDismissed();
+ maybeSendDeleteIntent(reason, bubbleToRemove.entry);
}
}
public void dismissAll(@DismissReason int reason) {
- boolean changed = setExpandedInternal(false);
+ if (DEBUG) {
+ Log.d(TAG, "dismissAll: reason=" + reason);
+ }
+ if (mBubbles.isEmpty()) {
+ return;
+ }
+ setExpandedInternal(false);
+ setSelectedBubbleInternal(null);
while (!mBubbles.isEmpty()) {
Bubble bubble = mBubbles.remove(0);
bubble.setDismissed();
maybeSendDeleteIntent(reason, bubble.entry);
- mListener.onBubbleRemoved(bubble, reason);
- changed = true;
+ dispatchOnBubbleRemoved(bubble, reason);
}
- if (setSelectedBubbleInternal(null)) {
- changed = true;
- }
- if (changed) {
- // TODO: reorder/group
+ dispatchApply();
+ }
+
+ private void dispatchApply() {
+ if (mListener != null) {
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<Bubble> 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.
@@ -204,7 +341,10 @@
* @param bubble the new selected bubble
* @return true if the state changed as a result
*/
- private boolean setSelectedBubbleInternal(Bubble bubble) {
+ private boolean setSelectedBubbleInternal(@Nullable Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
+ }
if (Objects.equals(bubble, mSelectedBubble)) {
return false;
}
@@ -213,16 +353,17 @@
+ " (" + bubble + ") bubbles=" + mBubbles);
return false;
}
- if (mExpanded) {
- // TODO: bubble.markAsActive() ?
- bubble.entry.setShowInShadeWhenBubble(false);
+ if (mExpanded && bubble != null) {
+ mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
}
mSelectedBubble = bubble;
- mListener.onSelectionChanged(mSelectedBubble);
+ dispatchOnSelectionChanged(mSelectedBubble);
+ if (!mExpanded || mSelectedBubble == null) {
+ mSelectedBubbleExpandedPosition = -1;
+ }
return true;
}
-
/**
* Requests a change to the expanded state. Calls {@link Listener#onExpandedChanged} if
* the value changes.
@@ -231,9 +372,15 @@
* @return true if the state changed as a result
*/
private boolean setExpandedInternal(boolean shouldExpand) {
+ if (DEBUG) {
+ Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
+ }
if (mExpanded == shouldExpand) {
return false;
}
+ if (mSelectedBubble != null) {
+ mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
+ }
if (shouldExpand) {
if (mBubbles.isEmpty()) {
Log.e(TAG, "Attempt to expand stack when empty!");
@@ -243,15 +390,126 @@
Log.e(TAG, "Attempt to expand stack without selected bubble!");
return false;
}
- // TODO: bubble.markAsActive() ?
- mSelectedBubble.entry.setShowInShadeWhenBubble(false);
+ } else {
+ repackAll();
}
- // TODO: reorder/regroup
mExpanded = shouldExpand;
- mListener.onExpandedChanged(mExpanded);
+ dispatchOnExpandedChanged(mExpanded);
return true;
}
+ private static long sortKey(Bubble bubble) {
+ long key = bubble.getLastActivity();
+ if (bubble.isOngoing()) {
+ // Set 2nd highest bit (signed long int), to partition between ongoing and regular
+ key |= 0x4000000000000000L;
+ }
+ return key;
+ }
+
+ /**
+ * Locates and inserts the bubble into a sorted position. The is inserted
+ * based on sort key, groupId is not considered. A call to {@link #packGroup(int)} may be
+ * required to keep grouping intact.
+ *
+ * @param minPosition the first insert point to consider
+ * @param newBubble the bubble to insert
+ * @return the position where the bubble was inserted
+ */
+ private int insertBubble(int minPosition, Bubble newBubble) {
+ long newBubbleSortKey = sortKey(newBubble);
+ String previousGroupId = null;
+
+ for (int pos = minPosition; pos < mBubbles.size(); pos++) {
+ Bubble bubbleAtPos = mBubbles.get(pos);
+ String groupIdAtPos = bubbleAtPos.getGroupId();
+ boolean atStartOfGroup = !groupIdAtPos.equals(previousGroupId);
+
+ if (atStartOfGroup && newBubbleSortKey > sortKey(bubbleAtPos)) {
+ // Insert before the start of first group which has older bubbles.
+ mBubbles.add(pos, newBubble);
+ return pos;
+ }
+ previousGroupId = groupIdAtPos;
+ }
+ mBubbles.add(newBubble);
+ return mBubbles.size() - 1;
+ }
+
+ private boolean hasBubbleWithGroupId(String groupId) {
+ return mBubbles.stream().anyMatch(b -> b.getGroupId().equals(groupId));
+ }
+
+ private int findFirstIndexForGroup(String appId) {
+ for (int i = 0; i < mBubbles.size(); i++) {
+ Bubble bubbleAtPos = mBubbles.get(i);
+ if (bubbleAtPos.getGroupId().equals(appId)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Starting at the given position, moves all bubbles with the same group id to follow. Bubbles
+ * at positions lower than {@code position} are unchanged. Relative order within the group
+ * unchanged. Relative order of any other bubbles are also unchanged.
+ *
+ * @param position the position of the first bubble for the group
+ */
+ private void packGroup(int position) {
+ if (DEBUG) {
+ Log.d(TAG, "packGroup: position=" + position);
+ }
+ Bubble groupStart = mBubbles.get(position);
+ final String groupAppId = groupStart.getGroupId();
+ List<Bubble> moving = new ArrayList<>();
+
+ // Walk backward, collect bubbles within the group
+ for (int i = mBubbles.size() - 1; i > position; i--) {
+ if (mBubbles.get(i).getGroupId().equals(groupAppId)) {
+ moving.add(0, mBubbles.get(i));
+ }
+ }
+ mBubbles.removeAll(moving);
+ mBubbles.addAll(position + 1, moving);
+ }
+
+ private void repackAll() {
+ if (DEBUG) {
+ Log.d(TAG, "repackAll()");
+ }
+ if (mBubbles.isEmpty()) {
+ return;
+ }
+ Map<String, Long> groupLastActivity = new HashMap<>();
+ for (Bubble bubble : mBubbles) {
+ long maxSortKeyForGroup = groupLastActivity.getOrDefault(bubble.getGroupId(), 0L);
+ long sortKeyForBubble = sortKey(bubble);
+ if (sortKeyForBubble > maxSortKeyForGroup) {
+ groupLastActivity.put(bubble.getGroupId(), sortKeyForBubble);
+ }
+ }
+
+ // Sort groups by their most recently active bubble
+ List<String> groupsByMostRecentActivity =
+ groupLastActivity.entrySet().stream()
+ .sorted(GROUPS_BY_LAST_ACTIVITY_DESCENDING)
+ .map(Map.Entry::getKey)
+ .collect(toList());
+
+ List<Bubble> repacked = new ArrayList<>(mBubbles.size());
+
+ // For each group, add bubbles, freshest to oldest
+ for (String appId : groupsByMostRecentActivity) {
+ mBubbles.stream()
+ .filter((b) -> b.getGroupId().equals(appId))
+ .sorted(BUBBLES_BY_LAST_ACTIVITY_DESCENDING)
+ .forEachOrdered(repacked::add);
+ }
+ mBubbles = repacked;
+ }
+
private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) {
if (reason == BubbleController.DISMISS_USER_GESTURE) {
Notification.BubbleMetadata bubbleMetadata = entry.getBubbleMetadata();
@@ -275,13 +533,14 @@
Bubble bubble = i.next();
if (bubble.getPackageName().equals(blockedPackage)) {
i.remove();
- mListener.onBubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED);
+ // TODO: handle removal of selected bubble, and collapse safely if emptied (see
+ // dismissAll)
+ dispatchOnBubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED);
changed = true;
}
}
if (changed) {
- // TODO: reorder/group
- mListener.apply();
+ dispatchApply();
}
}
@@ -295,24 +554,11 @@
return -1;
}
- private Bubble removeBubbleWithKey(String key) {
- for (int i = 0; i < mBubbles.size(); i++) {
- Bubble bubble = mBubbles.get(i);
- if (bubble.getKey().equals(key)) {
- mBubbles.remove(i);
- return bubble;
- }
- }
- return null;
- }
-
/**
* The set of bubbles.
- *
- * @deprecated
*/
- @Deprecated
- public Collection<Bubble> getBubbles() {
+ @VisibleForTesting(visibility = PRIVATE)
+ public List<Bubble> getBubbles() {
return Collections.unmodifiableList(mBubbles);
}
@@ -327,6 +573,11 @@
return null;
}
+ @VisibleForTesting(visibility = PRIVATE)
+ void setTimeSource(TimeSource timeSource) {
+ mTimeSource = timeSource;
+ }
+
public void setListener(Listener listener) {
mListener = listener;
}
@@ -334,17 +585,6 @@
boolean shouldAutoExpand(NotificationEntry entry) {
Notification.BubbleMetadata metadata = entry.getBubbleMetadata();
return metadata != null && metadata.getAutoExpandBubble()
- && isForegroundApp(entry.notification.getPackageName());
- }
-
- /**
- * Return true if the applications with the package name is running in foreground.
- *
- * @param pkgName application package name.
- */
- boolean isForegroundApp(String pkgName) {
- ActivityManager am = mContext.getSystemService(ActivityManager.class);
- List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1 /* maxNum */);
- return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
+ && BubbleController.isForegroundApp(mContext, entry.notification.getPackageName());
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index d9fe47a..ad4419c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -60,6 +60,7 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -211,7 +212,7 @@
mBubbleData = data;
mInflater = LayoutInflater.from(context);
- mTouchHandler = new BubbleTouchHandler(context, this);
+ mTouchHandler = new BubbleTouchHandler(this, data, context);
setOnTouchListener(mTouchHandler);
mInflater = LayoutInflater.from(context);
@@ -484,6 +485,9 @@
// via BubbleData.Listener
void addBubble(Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "addBubble: " + bubble);
+ }
bubble.inflate(mInflater, this);
mBubbleContainer.addView(bubble.iconView, 0,
new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
@@ -494,10 +498,17 @@
// via BubbleData.Listener
void removeBubble(Bubble bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "removeBubble: " + bubble);
+ }
// Remove it from the views
int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView);
- mBubbleContainer.removeViewAt(removedIndex);
- logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
+ if (removedIndex >= 0) {
+ mBubbleContainer.removeViewAt(removedIndex);
+ logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
+ } else {
+ Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
+ }
}
// via BubbleData.Listener
@@ -512,7 +523,10 @@
* position of any bubble.
*/
// via BubbleData.Listener
- public void setSelectedBubble(Bubble bubbleToSelect) {
+ public void setSelectedBubble(@Nullable Bubble bubbleToSelect) {
+ if (DEBUG) {
+ Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
+ }
if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
return;
}
@@ -543,6 +557,9 @@
*/
// via BubbleData.Listener
public void setExpanded(boolean shouldExpand) {
+ if (DEBUG) {
+ Log.d(TAG, "setExpanded: " + shouldExpand);
+ }
boolean wasExpanded = mIsExpanded;
if (shouldExpand == wasExpanded) {
return;
@@ -567,6 +584,9 @@
*/
@Deprecated
void stackDismissed(int reason) {
+ if (DEBUG) {
+ Log.d(TAG, "stackDismissed: reason=" + reason);
+ }
mBubbleData.dismissAll(reason);
logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
@@ -614,6 +634,9 @@
@Deprecated
@MainThread
void collapseStack() {
+ if (DEBUG) {
+ Log.d(TAG, "collapseStack()");
+ }
mBubbleData.setExpanded(false);
}
@@ -623,6 +646,9 @@
@Deprecated
@MainThread
void collapseStack(Runnable endRunnable) {
+ if (DEBUG) {
+ Log.d(TAG, "collapseStack(endRunnable)");
+ }
collapseStack();
// TODO - use the runnable at end of animation
endRunnable.run();
@@ -638,6 +664,9 @@
@Deprecated
@MainThread
void expandStack() {
+ if (DEBUG) {
+ Log.d(TAG, "expandStack()");
+ }
mBubbleData.setExpanded(true);
}
@@ -645,6 +674,9 @@
* Tell the stack to animate to collapsed or expanded state.
*/
private void animateExpansion(boolean shouldExpand) {
+ if (DEBUG) {
+ Log.d(TAG, "animateExpansion: shouldExpand=" + shouldExpand);
+ }
if (mIsExpanded != shouldExpand) {
hideFlyoutImmediate();
@@ -726,6 +758,9 @@
/** Called when a drag operation on an individual bubble has started. */
public void onBubbleDragStart(View bubble) {
+ if (DEBUG) {
+ Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
+ }
mExpandedAnimationController.prepareForBubbleDrag(bubble);
}
@@ -741,6 +776,9 @@
/** Called when a drag operation on an individual bubble has finished. */
public void onBubbleDragFinish(
View bubble, float x, float y, float velX, float velY, boolean dismissed) {
+ if (DEBUG) {
+ Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble + ", dismissed=" + dismissed);
+ }
if (!mIsExpanded || mIsExpansionAnimating) {
return;
}
@@ -753,6 +791,9 @@
}
void onDragStart() {
+ if (DEBUG) {
+ Log.d(TAG, "onDragStart()");
+ }
if (mIsExpanded || mIsExpansionAnimating) {
return;
}
@@ -773,6 +814,9 @@
}
void onDragFinish(float x, float y, float velX, float velY) {
+ if (DEBUG) {
+ Log.d(TAG, "onDragFinish");
+ }
// TODO: Add fling to bottom to dismiss.
mIsDragging = false;
@@ -914,6 +958,9 @@
}
private void updateExpandedBubble() {
+ if (DEBUG) {
+ Log.d(TAG, "updateExpandedBubble()");
+ }
mExpandedViewContainer.removeAllViews();
if (mExpandedBubble != null && mIsExpanded) {
mExpandedViewContainer.addView(mExpandedBubble.expandedView);
@@ -924,7 +971,9 @@
}
private void applyCurrentState() {
- Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
+ if (DEBUG) {
+ Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
+ }
mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
if (mIsExpanded) {
// First update the view so that it calculates a new height (ensuring the y position
@@ -963,10 +1012,14 @@
}
private void updatePointerPosition() {
- if (mExpandedBubble != null) {
- float pointerPosition = mExpandedBubble.iconView.getTranslationX()
- + (mExpandedBubble.iconView.getWidth() / 2f);
- mExpandedBubble.expandedView.setPointerPosition((int) pointerPosition);
+ if (DEBUG) {
+ Log.d(TAG, "updatePointerPosition()");
+ }
+ Bubble expandedBubble = getExpandedBubble();
+ if (expandedBubble != null) {
+ BubbleView iconView = expandedBubble.iconView;
+ float pointerPosition = iconView.getTranslationX() + (iconView.getWidth() / 2f);
+ expandedBubble.expandedView.setPointerPosition((int) pointerPosition);
}
}
@@ -1062,4 +1115,18 @@
}
return mExpandedBubble.expandedView.performBackPressIfNeeded();
}
+
+ /** For debugging only */
+ List<Bubble> getBubblesOnScreen() {
+ List<Bubble> bubbles = new ArrayList<>();
+ for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
+ View child = mBubbleContainer.getChildAt(i);
+ if (child instanceof BubbleView) {
+ String key = ((BubbleView) child).getKey();
+ Bubble bubble = mBubbleData.getBubbleWithKey(key);
+ bubbles.add(bubble);
+ }
+ }
+ return bubbles;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index a51d46c..82e6279 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -37,9 +37,12 @@
/** Velocity required to dismiss a bubble without dragging it into the dismiss target. */
private static final float DISMISS_MIN_VELOCITY = 4000f;
+ private static final String TAG = "BubbleTouchHandler";
+
private final PointF mTouchDown = new PointF();
private final PointF mViewPositionOnTouchDown = new PointF();
private final BubbleStackView mStack;
+ private final BubbleData mBubbleData;
private BubbleController mController = Dependency.get(BubbleController.class);
private PipDismissViewController mDismissViewController;
@@ -60,10 +63,12 @@
/** View that was initially touched, when we received the first ACTION_DOWN event. */
private View mTouchedView;
- BubbleTouchHandler(Context context, BubbleStackView stackView) {
+ BubbleTouchHandler(BubbleStackView stackView,
+ BubbleData bubbleData, Context context) {
final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mTouchSlopSquared = touchSlop * touchSlop;
mDismissViewController = new PipDismissViewController(context);
+ mBubbleData = bubbleData;
mStack = stackView;
}
@@ -80,7 +85,7 @@
// If this is an ACTION_OUTSIDE event, or the stack reported that we aren't touching
// anything, collapse the stack.
if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
- mStack.collapseStack();
+ mBubbleData.setExpanded(false);
resetForNextGesture();
return false;
}
@@ -151,8 +156,8 @@
mStack.onDragFinishAsDismiss();
} else if (isFlyout) {
// TODO(b/129768381): Expand if tapped, dismiss if swiped away.
- if (!mStack.isExpanded() && !mMovedEnough) {
- mStack.expandStack();
+ if (!mBubbleData.isExpanded() && !mMovedEnough) {
+ mBubbleData.setExpanded(true);
}
} else if (mMovedEnough) {
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
@@ -170,15 +175,13 @@
}
}
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
- mStack.collapseStack();
+ mBubbleData.setExpanded(false);
} else if (isStack) {
- if (mStack.isExpanded()) {
- mStack.collapseStack();
- } else {
- mStack.expandStack();
- }
+ // Toggle expansion
+ mBubbleData.setExpanded(!mBubbleData.isExpanded());
} else {
- mStack.setExpandedBubble(((BubbleView) mTouchedView).getKey());
+ final String key = ((BubbleView) mTouchedView).getKey();
+ mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
}
resetForNextGesture();