Merge "Hide suppressed bubble"
This commit is contained in:
@@ -367,4 +367,9 @@ public class BadgedImageView extends ConstraintLayout {
|
||||
void hideBadge() {
|
||||
mAppIcon.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BadgedImageView{" + mBubble + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user