Merge "Hide suppressed bubble"

This commit is contained in:
Ats Jenk
2022-02-09 04:47:37 +00:00
committed by Android (Google) Code Review
7 changed files with 388 additions and 88 deletions

View File

@@ -367,4 +367,9 @@ public class BadgedImageView extends ConstraintLayout {
void hideBadge() {
mAppIcon.setVisibility(GONE);
}
@Override
public String toString() {
return "BadgedImageView{" + mBubble + "}";
}
}

View File

@@ -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

View File

@@ -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 <code>key</code>
*/
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 <code>LocusId</code>
*/
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 <code>key</code>
*
* @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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;
}
}

View File

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