diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index c52d87dde07f3..79e624212f4b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -367,4 +367,9 @@ public class BadgedImageView extends ConstraintLayout {
void hideBadge() {
mAppIcon.setVisibility(GONE);
}
+
+ @Override
+ public String toString() {
+ return "BadgedImageView{" + mBubble + "}";
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 57cb7a5a57d72..6ed1ba9f561fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -24,6 +24,7 @@ import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubblePositioner.TASKBAR_POSITION_BOTTOM;
@@ -616,7 +617,7 @@ public class BubbleController {
return mTaskViewTransitions;
}
- /** Contains information to help position things on the screen. */
+ /** Contains information to help position things on the screen. */
BubblePositioner getPositioner() {
return mBubblePositioner;
}
@@ -659,8 +660,8 @@ public class BubbleController {
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT);
mWmLayoutParams.setTrustedOverlay();
@@ -750,7 +751,7 @@ public class BubbleController {
// First clear any existing keys that might be stored.
mSavedBubbleKeysPerUser.remove(userId);
// Add in all active bubbles for the current user.
- for (Bubble bubble: mBubbleData.getBubbles()) {
+ for (Bubble bubble : mBubbleData.getBubbles()) {
mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
}
}
@@ -982,9 +983,9 @@ public class BubbleController {
/**
* Adds or updates a bubble associated with the provided notification entry.
*
- * @param notif the notification associated with this bubble.
+ * @param notif the notification associated with this bubble.
* @param suppressFlyout this bubble suppress flyout or not.
- * @param showInShade this bubble show in shade or not.
+ * @param showInShade this bubble show in shade or not.
*/
@VisibleForTesting
public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
@@ -992,11 +993,17 @@ public class BubbleController {
mSysuiProxy.setNotificationInterruption(notif.getKey());
if (!notif.getRanking().isTextChanged()
&& (notif.getBubbleMetadata() != null
- && !notif.getBubbleMetadata().getAutoExpandBubble())
+ && !notif.getBubbleMetadata().getAutoExpandBubble())
&& mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
// Update the bubble but don't promote it out of overflow
Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
b.setEntry(notif);
+ } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) {
+ // Update the bubble but don't promote it out of overflow
+ Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey());
+ if (b != null) {
+ b.setEntry(notif);
+ }
} else {
Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
inflateAndAdd(bubble, suppressFlyout, showInShade);
@@ -1170,6 +1177,18 @@ public class BubbleController {
@Override
public void applyUpdate(BubbleData.Update update) {
+ if (DEBUG_BUBBLE_CONTROLLER) {
+ Log.d(TAG, "applyUpdate:" + " bubbleAdded=" + (update.addedBubble != null)
+ + " bubbleRemoved="
+ + (update.removedBubbles != null && update.removedBubbles.size() > 0)
+ + " bubbleUpdated=" + (update.updatedBubble != null)
+ + " orderChanged=" + update.orderChanged
+ + " expandedChanged=" + update.expandedChanged
+ + " selectionChanged=" + update.selectionChanged
+ + " suppressed=" + (update.suppressedBubble != null)
+ + " unsuppressed=" + (update.unsuppressedBubble != null));
+ }
+
ensureStackViewCreated();
// Lazy load overflow bubbles from disk
@@ -1249,6 +1268,14 @@ public class BubbleController {
mStackView.updateBubble(update.updatedBubble);
}
+ if (update.suppressedBubble != null && mStackView != null) {
+ mStackView.setBubbleSuppressed(update.suppressedBubble, true);
+ }
+
+ if (update.unsuppressedBubble != null && mStackView != null) {
+ mStackView.setBubbleSuppressed(update.unsuppressedBubble, false);
+ }
+
// At this point, the correct bubbles are inflated in the stack.
// Make sure the order in bubble data is reflected in bubble row.
if (update.orderChanged && mStackView != null) {
@@ -1263,14 +1290,6 @@ public class BubbleController {
}
}
- if (update.suppressedBubble != null && mStackView != null) {
- mStackView.setBubbleVisibility(update.suppressedBubble, false);
- }
-
- if (update.unsuppressedBubble != null && mStackView != null) {
- mStackView.setBubbleVisibility(update.unsuppressedBubble, true);
- }
-
// Expanding? Apply this last.
if (update.expandedChanged && update.expanded) {
if (mStackView != null) {
@@ -1398,7 +1417,7 @@ public class BubbleController {
* that should filter out any invalid bubbles, but should protect SysUI side just in case.
*
* @param context the context to use.
- * @param entry the entry to bubble.
+ * @param entry the entry to bubble.
*/
static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
PendingIntent intent = entry.getBubbleMetadata() != null
@@ -1531,7 +1550,7 @@ public class BubbleController {
String groupKey) {
return mSuppressedBubbleKeys.contains(key)
|| (mSuppressedGroupToNotifKeys.containsKey(groupKey)
- && key.equals(mSuppressedGroupToNotifKeys.get(groupKey)));
+ && key.equals(mSuppressedGroupToNotifKeys.get(groupKey)));
}
@Nullable
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 51b7eaa8b0e4d..9961ad71e64af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -224,7 +224,8 @@ public class BubbleData {
}
public boolean hasAnyBubbleWithKey(String key) {
- return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key);
+ return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key)
+ || hasSuppressedBubbleWithKey(key);
}
public boolean hasBubbleInStackWithKey(String key) {
@@ -235,6 +236,20 @@ public class BubbleData {
return getOverflowBubbleWithKey(key) != null;
}
+ /**
+ * Check if there are any bubbles suppressed with the given notification key
+ */
+ public boolean hasSuppressedBubbleWithKey(String key) {
+ return mSuppressedBubbles.values().stream().anyMatch(b -> b.getKey().equals(key));
+ }
+
+ /**
+ * Check if there are any bubbles suppressed with the given LocusId
+ */
+ public boolean isSuppressedWithLocusId(LocusId locusId) {
+ return mSuppressedBubbles.get(locusId) != null;
+ }
+
@Nullable
public BubbleViewProvider getSelectedBubble() {
return mSelectedBubble;
@@ -356,11 +371,11 @@ public class BubbleData {
boolean isSuppressed = mSuppressedBubbles.containsKey(locusId);
if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) {
mSuppressedBubbles.remove(locusId);
- mStateChange.unsuppressedBubble = bubble;
+ doUnsuppress(bubble);
} else if (!isSuppressed && (bubble.isSuppressed()
|| bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) {
mSuppressedBubbles.put(locusId, bubble);
- mStateChange.suppressedBubble = bubble;
+ doSuppress(bubble);
}
}
dispatchPendingChanges();
@@ -532,16 +547,19 @@ public class BubbleData {
if (mPendingBubbles.containsKey(key)) {
mPendingBubbles.remove(key);
}
+
+ boolean shouldRemoveHiddenBubble = reason == Bubbles.DISMISS_NOTIF_CANCEL
+ || reason == Bubbles.DISMISS_GROUP_CANCELLED
+ || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
+ || reason == Bubbles.DISMISS_BLOCKED
+ || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
+ || reason == Bubbles.DISMISS_PACKAGE_REMOVED
+ || reason == Bubbles.DISMISS_USER_CHANGED;
+
int indexToRemove = indexForKey(key);
if (indexToRemove == -1) {
if (hasOverflowBubbleWithKey(key)
- && (reason == Bubbles.DISMISS_NOTIF_CANCEL
- || reason == Bubbles.DISMISS_GROUP_CANCELLED
- || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
- || reason == Bubbles.DISMISS_BLOCKED
- || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
- || reason == Bubbles.DISMISS_PACKAGE_REMOVED
- || reason == Bubbles.DISMISS_USER_CHANGED)) {
+ && shouldRemoveHiddenBubble) {
Bubble b = getOverflowBubbleWithKey(key);
if (DEBUG_BUBBLE_DATA) {
@@ -555,6 +573,17 @@ public class BubbleData {
mStateChange.bubbleRemoved(b, reason);
mStateChange.removedOverflowBubble = b;
}
+ if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) {
+ Bubble b = getSuppressedBubbleWithKey(key);
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "Cancel suppressed bubble: " + b);
+ }
+ if (b != null) {
+ mSuppressedBubbles.remove(b.getLocusId());
+ b.stopInflation();
+ mStateChange.bubbleRemoved(b, reason);
+ }
+ }
return;
}
Bubble bubbleToRemove = mBubbles.get(indexToRemove);
@@ -579,19 +608,73 @@ public class BubbleData {
// 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);
- BubbleViewProvider newSelected = mBubbles.get(newIndex);
- setSelectedBubbleInternal(newSelected);
+ setNewSelectedIndex(indexToRemove);
}
maybeSendDeleteIntent(reason, bubbleToRemove);
}
+ private void setNewSelectedIndex(int indexOfSelected) {
+ if (mBubbles.isEmpty()) {
+ Log.w(TAG, "Bubbles list empty when attempting to select index: " + indexOfSelected);
+ return;
+ }
+ // Move selection to the new bubble at the same position.
+ int newIndex = Math.min(indexOfSelected, mBubbles.size() - 1);
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "setNewSelectedIndex: " + indexOfSelected);
+ }
+ BubbleViewProvider newSelected = mBubbles.get(newIndex);
+ setSelectedBubbleInternal(newSelected);
+ }
+
+ private void doSuppress(Bubble bubble) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "doSuppressed: " + bubble);
+ }
+ mStateChange.suppressedBubble = bubble;
+ bubble.setSuppressBubble(true);
+
+ int indexToRemove = mBubbles.indexOf(bubble);
+ // Order changes if we are not suppressing the last bubble
+ mStateChange.orderChanged = !(mBubbles.size() - 1 == indexToRemove);
+ mBubbles.remove(indexToRemove);
+
+ // Update selection if we suppressed the selected bubble
+ if (Objects.equals(mSelectedBubble, bubble)) {
+ if (mBubbles.isEmpty()) {
+ // Don't use setSelectedBubbleInternal because we don't want to trigger an
+ // applyUpdate
+ mSelectedBubble = null;
+ } else {
+ // Mark new first bubble as selected
+ setNewSelectedIndex(0);
+ }
+ }
+ }
+
+ private void doUnsuppress(Bubble bubble) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "doUnsuppressed: " + bubble);
+ }
+ bubble.setSuppressBubble(false);
+ mStateChange.unsuppressedBubble = bubble;
+ mBubbles.add(bubble);
+ if (mBubbles.size() > 1) {
+ // See where the bubble actually lands
+ repackAll();
+ mStateChange.orderChanged = true;
+ }
+ if (mBubbles.get(0) == bubble) {
+ // Unsuppressed bubble is sorted to first position. Mark it as the selected.
+ setNewSelectedIndex(0);
+ }
+ }
+
void overflowBubble(@DismissReason int reason, Bubble bubble) {
if (bubble.getPendingIntentCanceled()
|| !(reason == Bubbles.DISMISS_AGED
- || reason == Bubbles.DISMISS_USER_GESTURE
- || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
+ || reason == Bubbles.DISMISS_USER_GESTURE
+ || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
return;
}
if (DEBUG_BUBBLE_DATA) {
@@ -619,7 +702,7 @@ public class BubbleData {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "dismissAll: reason=" + reason);
}
- if (mBubbles.isEmpty()) {
+ if (mBubbles.isEmpty() && mSuppressedBubbles.isEmpty()) {
return;
}
setExpandedInternal(false);
@@ -627,6 +710,10 @@ public class BubbleData {
while (!mBubbles.isEmpty()) {
doRemove(mBubbles.get(0).getKey(), reason);
}
+ while (!mSuppressedBubbles.isEmpty()) {
+ Bubble bubble = mSuppressedBubbles.removeAt(0);
+ doRemove(bubble.getKey(), reason);
+ }
dispatchPendingChanges();
}
@@ -635,11 +722,15 @@ public class BubbleData {
* and if there's a matching bubble for that locusId then the bubble may be hidden or shown
* depending on the visibility of the locusId.
*
- * @param taskId the taskId associated with the locusId visibility change.
+ * @param taskId the taskId associated with the locusId visibility change.
* @param locusId the locusId whose visibility has changed.
* @param visible whether the task with the locusId is visible or not.
*/
public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) {
+ if (DEBUG_BUBBLE_DATA) {
+ Log.d(TAG, "onLocusVisibilityChanged: " + locusId + " visible=" + visible);
+ }
+
Bubble matchingBubble = getBubbleInStackWithLocusId(locusId);
// Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled.
if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) {
@@ -648,20 +739,22 @@ public class BubbleData {
mVisibleLocusIds.remove(locusId);
}
if (matchingBubble == null) {
- return;
+ // Check if there is a suppressed bubble for this LocusId
+ matchingBubble = mSuppressedBubbles.get(locusId);
+ if (matchingBubble == null) {
+ return;
+ }
}
boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null;
if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable()
&& taskId != matchingBubble.getTaskId()) {
mSuppressedBubbles.put(locusId, matchingBubble);
- matchingBubble.setSuppressBubble(true);
- mStateChange.suppressedBubble = matchingBubble;
+ doSuppress(matchingBubble);
dispatchPendingChanges();
} else if (!visible) {
Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId);
if (unsuppressedBubble != null) {
- unsuppressedBubble.setSuppressBubble(false);
- mStateChange.unsuppressedBubble = unsuppressedBubble;
+ doUnsuppress(unsuppressedBubble);
}
dispatchPendingChanges();
}
@@ -720,14 +813,14 @@ public class BubbleData {
/**
* Logs the bubble UI event.
*
- * @param provider The bubble view provider that is being interacted on. Null value indicates
- * that the user interaction is not specific to one bubble.
- * @param action The user interaction enum
+ * @param provider The bubble view provider that is being interacted on. Null value indicates
+ * that the user interaction is not specific to one bubble.
+ * @param action The user interaction enum
* @param packageName SystemUI package
* @param bubbleCount Number of bubbles in the stack
* @param bubbleIndex Index of bubble in the stack
- * @param normalX Normalized x position of the stack
- * @param normalY Normalized y position of the stack
+ * @param normalX Normalized x position of the stack
+ * @param normalY Normalized y position of the stack
*/
void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName,
int bubbleCount, int bubbleIndex, float normalX, float normalY) {
@@ -869,6 +962,9 @@ public class BubbleData {
if (b == null) {
b = getOverflowBubbleWithKey(key);
}
+ if (b == null) {
+ b = getSuppressedBubbleWithKey(key);
+ }
return b;
}
@@ -946,6 +1042,23 @@ public class BubbleData {
return null;
}
+ /**
+ * Get a suppressed bubble with given notification key
+ *
+ * @param key notification key
+ * @return bubble that matches or null
+ */
+ @Nullable
+ @VisibleForTesting(visibility = PRIVATE)
+ public Bubble getSuppressedBubbleWithKey(String key) {
+ for (Bubble b : mSuppressedBubbles.values()) {
+ if (b.getKey().equals(key)) {
+ return b;
+ }
+ }
+ return null;
+ }
+
@VisibleForTesting(visibility = PRIVATE)
void setTimeSource(TimeSource timeSource) {
mTimeSource = timeSource;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 7ab683513570b..c2eb08ca09ddf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -168,26 +168,27 @@ public class BubbleStackView extends FrameLayout
private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
new SurfaceSynchronizer() {
- @Override
- public void syncSurfaceAndRun(Runnable callback) {
- Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
- // Just wait 2 frames. There is no guarantee, but this is usually enough time that
- // the requested change is reflected on the screen.
- // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
- // surfaces, rewrite this logic with them.
- private int mFrameWait = 2;
-
@Override
- public void doFrame(long frameTimeNanos) {
- if (--mFrameWait > 0) {
- Choreographer.getInstance().postFrameCallback(this);
- } else {
- callback.run();
- }
+ public void syncSurfaceAndRun(Runnable callback) {
+ Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
+ // Just wait 2 frames. There is no guarantee, but this is usually enough
+ // time that the requested change is reflected on the screen.
+ // TODO: Once SurfaceFlinger provide APIs to sync the state of
+ // {@code View} and surfaces, rewrite this logic with them.
+ private int mFrameWait = 2;
+
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ if (--mFrameWait > 0) {
+ Choreographer.getInstance().postFrameCallback(this);
+ } else {
+ callback.run();
+ }
+ }
+ };
+ Choreographer.getInstance().postFrameCallback(frameCallback);
}
- });
- }
- };
+ };
private final BubbleController mBubbleController;
private final BubbleData mBubbleData;
private StackViewState mStackViewState = new StackViewState();
@@ -781,7 +782,7 @@ public class BubbleStackView extends FrameLayout
mPositioner = mBubbleController.getPositioner();
final TypedArray ta = mContext.obtainStyledAttributes(
- new int[] {android.R.attr.dialogCornerRadius});
+ new int[]{android.R.attr.dialogCornerRadius});
mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
@@ -942,7 +943,7 @@ public class BubbleStackView extends FrameLayout
});
// If the stack itself is clicked, it means none of its touchable views (bubbles, flyouts,
- // TaskView, etc.) were touched. Collapse the stack if it's expanded.
+ // TaskView, etc.) were touched. Collapse the stack if it's expanded.
setOnClickListener(view -> {
if (mShowingManage) {
showManageMenu(false /* show */);
@@ -1656,7 +1657,12 @@ public class BubbleStackView extends FrameLayout
return;
}
}
- Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
+ // If a bubble is suppressed, it is not attached to the container. Clean it up.
+ if (bubble.isSuppressed()) {
+ bubble.cleanupViews();
+ } else {
+ Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
+ }
}
private void updateOverflowVisibility() {
@@ -1842,11 +1848,30 @@ public class BubbleStackView extends FrameLayout
}
}
- void setBubbleVisibility(Bubble b, boolean visible) {
- if (b.getIconView() != null) {
- b.getIconView().setVisibility(visible ? VISIBLE : GONE);
+ void setBubbleSuppressed(Bubble bubble, boolean suppressed) {
+ if (DEBUG_BUBBLE_STACK_VIEW) {
+ Log.d(TAG, "setBubbleSuppressed: suppressed=" + suppressed + " bubble=" + bubble);
+ }
+ if (suppressed) {
+ int index = getBubbleIndex(bubble);
+ mBubbleContainer.removeViewAt(index);
+ updateExpandedView();
+ } else {
+ if (bubble.getIconView() == null) {
+ return;
+ }
+ if (bubble.getIconView().getParent() != null) {
+ Log.e(TAG, "Bubble is already added to parent. Can't unsuppress: " + bubble);
+ return;
+ }
+ int index = mBubbleData.getBubbles().indexOf(bubble);
+ // Add the view back to the correct position
+ mBubbleContainer.addView(bubble.getIconView(), index,
+ new LayoutParams(mPositioner.getBubbleSize(),
+ mPositioner.getBubbleSize()));
+ updateBubbleShadows(false /* showForAllBubbles */);
+ requestUpdate();
}
- // TODO(b/181166384): Animate in / out & handle adjusting how the bubbles overlap
}
/**
@@ -2191,7 +2216,7 @@ public class BubbleStackView extends FrameLayout
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer)
.spring(DynamicAnimation.TRANSLATION_Y,
mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize,
- mTranslateSpringConfig)
+ mTranslateSpringConfig)
.start();
}
@@ -3040,14 +3065,14 @@ public class BubbleStackView extends FrameLayout
* Logs the bubble UI event.
*
* @param provider the bubble view provider that is being interacted on. Null value indicates
- * that the user interaction is not specific to one bubble.
- * @param action the user interaction enum.
+ * that the user interaction is not specific to one bubble.
+ * @param action the user interaction enum.
*/
private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) {
final String packageName =
(provider != null && provider instanceof Bubble)
- ? ((Bubble) provider).getPackageName()
- : "null";
+ ? ((Bubble) provider).getPackageName()
+ : "null";
mBubbleData.logBubbleEvent(provider,
action,
packageName,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
index 4ec2c8d4d3622..55052e614458d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
@@ -364,6 +364,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
final int oldIndex = indexOfChild(view);
super.removeView(view);
+ if (view.getParent() != null) {
+ // View still has a parent. This could have been added as a transient view.
+ // Remove it from transient views.
+ super.removeTransientView(view);
+ }
addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */);
if (mController != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 60b64333114e4..3ba056a6b4a29 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -750,6 +750,12 @@ public class StackAnimationController extends
// Otherwise, animate the bubble in if it's the newest bubble. If we're adding a bubble
// to the back of the stack, it'll be largely invisible so don't bother animating it in.
animateInBubble(child, index);
+ } else {
+ // We are not animating the bubble in. Make sure it has the right alpha and scale values
+ // in case this view was previously removed and is being re-added.
+ child.setAlpha(1f);
+ child.setScaleX(1f);
+ child.setScaleY(1f);
}
}
@@ -785,23 +791,24 @@ public class StackAnimationController extends
}
};
+ boolean swapped = false;
for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) {
View view = bubbleViews.get(newIndex);
final int oldIndex = mLayout.indexOfChild(view);
- animateSwap(view, oldIndex, newIndex, updateAllIcons, after);
+ swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after);
+ }
+ if (!swapped) {
+ // All bubbles were at the right position. Make sure badges and z order is correct.
+ updateAllIcons.run();
}
}
- private void animateSwap(View view, int oldIndex, int newIndex,
+ private boolean animateSwap(View view, int oldIndex, int newIndex,
Runnable updateAllIcons, Runnable finishReorder) {
if (newIndex == oldIndex) {
- // Add new bubble to index 0; move existing bubbles down
- updateBadgesAndZOrder(view, newIndex);
- if (newIndex == 0) {
- animateInBubble(view, newIndex);
- } else {
- moveToFinalIndex(view, newIndex, finishReorder);
- }
+ // View order did not change. Make sure position is correct.
+ moveToFinalIndex(view, newIndex, finishReorder);
+ return false;
} else {
// Reorder existing bubbles
if (newIndex == 0) {
@@ -809,6 +816,7 @@ public class StackAnimationController extends
} else {
moveToFinalIndex(view, newIndex, finishReorder);
}
+ return true;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 185479b145af4..169f03e7bc3ed 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.PendingIntent;
+import android.content.LocusId;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
@@ -39,7 +40,6 @@ import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
@@ -82,6 +82,7 @@ public class BubbleDataTest extends ShellTestCase {
private BubbleEntry mEntryC1;
private BubbleEntry mEntryInterruptive;
private BubbleEntry mEntryDismissed;
+ private BubbleEntry mEntryLocusId;
private Bubble mBubbleA1;
private Bubble mBubbleA2;
@@ -92,6 +93,7 @@ public class BubbleDataTest extends ShellTestCase {
private Bubble mBubbleC1;
private Bubble mBubbleInterruptive;
private Bubble mBubbleDismissed;
+ private Bubble mBubbleLocusId;
private BubbleData mBubbleData;
private TestableBubblePositioner mPositioner;
@@ -141,6 +143,10 @@ public class BubbleDataTest extends ShellTestCase {
mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null,
mMainExecutor);
+ mEntryLocusId = createBubbleEntry(1, "keyLocus", "package.e", null,
+ new LocusId("locusId1"));
+ mBubbleLocusId = new Bubble(mEntryLocusId, mSuppressionListener, null, mMainExecutor);
+
mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener,
mMainExecutor);
mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener,
@@ -939,6 +945,102 @@ public class BubbleDataTest extends ShellTestCase {
assertOrderChangedTo(mBubbleB3, mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2);
}
+ /**
+ * There is one bubble in the stack. If a task matching the locusId becomes visible, suppress
+ * the bubble. If it is hidden, unsuppress the bubble.
+ */
+ @Test
+ public void test_onLocusVisibilityChanged_singleBubble() {
+ sendUpdatedEntryAtTime(mEntryLocusId, 1000);
+ mBubbleData.setListener(mListener);
+
+ // Suppress the bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */);
+ verifyUpdateReceived();
+ assertBubbleSuppressed(mBubbleLocusId);
+ assertOrderNotChanged();
+ assertBubbleListContains(/* empty list */);
+
+ // Unsuppress the bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */);
+ verifyUpdateReceived();
+ assertBubbleUnsuppressed(mBubbleLocusId);
+ assertOrderNotChanged();
+ assertBubbleListContains(mBubbleLocusId);
+ }
+
+ /**
+ * Bubble stack has multiple bubbles. Suppress bubble based on matching locusId. Suppressed
+ * bubble is at the top.
+ *
+ * When suppressed:
+ * - hide bubble
+ * - update order
+ * - update selection
+ *
+ * When unsuppressed:
+ * - show bubble
+ * - update order
+ * - update selection
+ */
+ @Test
+ public void test_onLocusVisibilityChanged_multipleBubbles_suppressTopBubble() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ sendUpdatedEntryAtTime(mEntryLocusId, 3000);
+ mBubbleData.setListener(mListener);
+
+ // Suppress bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */);
+ verifyUpdateReceived();
+ assertBubbleSuppressed(mBubbleLocusId);
+ assertSelectionChangedTo(mBubbleA2);
+ assertOrderChangedTo(mBubbleA2, mBubbleA1);
+
+ // Unsuppress bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */);
+ verifyUpdateReceived();
+ assertBubbleUnsuppressed(mBubbleLocusId);
+ assertSelectionChangedTo(mBubbleLocusId);
+ assertOrderChangedTo(mBubbleLocusId, mBubbleA2, mBubbleA1);
+ }
+
+ /**
+ * Bubble stack has multiple bubbles. Suppress bubble based on matching locusId. Suppressed
+ * bubble is not at the top.
+ *
+ * When suppressed:
+ * - hide suppressed bubble
+ * - do not update order
+ * - do not update selection
+ *
+ * When unsuppressed:
+ * - show bubble
+ * - do not update order
+ * - do not update selection
+ */
+ @Test
+ public void test_onLocusVisibilityChanged_multipleBubbles_suppressStackedBubble() {
+ sendUpdatedEntryAtTime(mEntryLocusId, 1000);
+ sendUpdatedEntryAtTime(mEntryA1, 2000);
+ sendUpdatedEntryAtTime(mEntryA2, 3000);
+ mBubbleData.setListener(mListener);
+
+ // Suppress bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), true /* visible */);
+ verifyUpdateReceived();
+ assertBubbleSuppressed(mBubbleLocusId);
+ assertSelectionNotChanged();
+ assertBubbleListContains(mBubbleA2, mBubbleA1);
+
+ // Unsuppress bubble
+ mBubbleData.onLocusVisibilityChanged(100, mEntryLocusId.getLocusId(), false /* visible */);
+ verifyUpdateReceived();
+ assertBubbleUnsuppressed(mBubbleLocusId);
+ assertSelectionNotChanged();
+ assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId);
+ }
+
private void verifyUpdateReceived() {
verify(mListener).applyUpdate(mUpdateCaptor.capture());
reset(mListener);
@@ -995,9 +1097,29 @@ public class BubbleDataTest extends ShellTestCase {
assertThat(update.overflowBubbles).isEqualTo(bubbles);
}
+ private void assertBubbleListContains(Bubble... bubbles) {
+ BubbleData.Update update = mUpdateCaptor.getValue();
+ assertWithMessage("bubbleList").that(update.bubbles).containsExactlyElementsIn(bubbles);
+ }
+
+ private void assertBubbleSuppressed(Bubble expected) {
+ BubbleData.Update update = mUpdateCaptor.getValue();
+ assertWithMessage("suppressedBubble").that(update.suppressedBubble).isEqualTo(expected);
+ }
+
+ private void assertBubbleUnsuppressed(Bubble expected) {
+ BubbleData.Update update = mUpdateCaptor.getValue();
+ assertWithMessage("unsuppressedBubble").that(update.unsuppressedBubble).isEqualTo(expected);
+ }
+
private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
NotificationListenerService.Ranking ranking) {
- return createBubbleEntry(userId, notifKey, packageName, ranking, 1000);
+ return createBubbleEntry(userId, notifKey, packageName, ranking, 1000, null);
+ }
+
+ private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
+ NotificationListenerService.Ranking ranking, LocusId locusId) {
+ return createBubbleEntry(userId, notifKey, packageName, ranking, 1000, locusId);
}
private void setPostTime(BubbleEntry entry, long postTime) {
@@ -1010,15 +1132,18 @@ public class BubbleDataTest extends ShellTestCase {
* as a convenience to create a Notification w/BubbleMetadata.
*/
private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
- NotificationListenerService.Ranking ranking, long postTime) {
+ NotificationListenerService.Ranking ranking, long postTime,
+ LocusId locusId) {
// BubbleMetadata
Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder(
mExpandIntent, Icon.createWithResource("", 0))
.setDeleteIntent(mDeleteIntent)
+ .setSuppressableBubble(true)
.build();
// Notification -> BubbleMetadata
Notification notification = mock(Notification.class);
- notification.setBubbleMetadata(bubbleMetadata);
+ when(notification.getBubbleMetadata()).thenReturn(bubbleMetadata);
+ when(notification.getLocusId()).thenReturn(locusId);
// Notification -> extras
notification.extras = new Bundle();