From bb4306e62a383fd2c8caf6188828e532229b1fc6 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Mon, 31 Jan 2022 17:42:07 -0800 Subject: [PATCH] Hide suppressed bubble When a bubble is suppressed it should be removed from the stack and expanded view. Previously we were updating the visibility of the suppressed bubble. This kept it in the expanded view. Updating the suppression logic to remove the suppressed bubble view temporarily, while it is suppressed. When it should be visible again, adding it back. Bug: 214090876 Test: atest BubbleDataTest BubblesTest Test: manual, created a single bubble, checked it is hidden when associated activity is opened and shown again after activity closes Test: manual, created multiple bubbles, checked bubble is hidden when associated activity is opened and shown again after activity closes Test: manual, checked that suppressed bubble is not shown in expanded view Change-Id: I591a4330ccbb4d9764100715140d8bd20d0351bf --- .../wm/shell/bubbles/BadgedImageView.java | 5 + .../wm/shell/bubbles/BubbleController.java | 53 ++++-- .../android/wm/shell/bubbles/BubbleData.java | 169 +++++++++++++++--- .../wm/shell/bubbles/BubbleStackView.java | 85 +++++---- .../animation/PhysicsAnimationLayout.java | 5 + .../animation/StackAnimationController.java | 26 ++- .../wm/shell/bubbles/BubbleDataTest.java | 133 +++++++++++++- 7 files changed, 388 insertions(+), 88 deletions(-) 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 a477bd7f8295b..59478ae8a4398 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() { @@ -1841,11 +1847,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 } /** @@ -2190,7 +2215,7 @@ public class BubbleStackView extends FrameLayout PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) .spring(DynamicAnimation.TRANSLATION_Y, mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, - mTranslateSpringConfig) + mTranslateSpringConfig) .start(); } @@ -3039,14 +3064,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();