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();