Send deleteIntent when a bubble is dismissed
Test: atest BubbleControllerTest Bug: 124381186 Change-Id: Ie332dc394c25a143e75b3f9caa46de6386b392db
This commit is contained in:
@@ -23,6 +23,8 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static com.android.systemui.statusbar.StatusBarState.SHADE;
|
||||
import static com.android.systemui.statusbar.notification.NotificationAlertingManager.alertAgain;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityManager.RunningTaskInfo;
|
||||
import android.app.ActivityTaskManager;
|
||||
@@ -42,6 +44,7 @@ import android.view.IPinnedStackListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.MainThread;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
@@ -59,6 +62,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
||||
import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
|
||||
import com.android.systemui.statusbar.phone.StatusBarWindowController;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@@ -70,10 +75,22 @@ import javax.inject.Singleton;
|
||||
*/
|
||||
@Singleton
|
||||
public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListener {
|
||||
private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
|
||||
|
||||
private static final String TAG = "BubbleController";
|
||||
|
||||
private static final int MAX_BUBBLES = 5; // TODO: actually enforce this
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
|
||||
DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION})
|
||||
@interface DismissReason {}
|
||||
static final int DISMISS_USER_GESTURE = 1;
|
||||
static final int DISMISS_AGED = 2;
|
||||
static final int DISMISS_TASK_FINISHED = 3;
|
||||
static final int DISMISS_BLOCKED = 4;
|
||||
static final int DISMISS_NOTIF_CANCEL = 5;
|
||||
static final int DISMISS_ACCESSIBILITY_ACTION = 6;
|
||||
|
||||
// Enables some subset of notifs to automatically become bubbles
|
||||
private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
|
||||
|
||||
@@ -248,11 +265,11 @@ public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListe
|
||||
/**
|
||||
* Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
|
||||
*/
|
||||
void dismissStack() {
|
||||
void dismissStack(@DismissReason int reason) {
|
||||
if (mStackView == null) {
|
||||
return;
|
||||
}
|
||||
mStackView.stackDismissed();
|
||||
mStackView.stackDismissed(reason);
|
||||
|
||||
updateVisibility();
|
||||
mNotificationEntryManager.updateNotifications();
|
||||
@@ -304,9 +321,9 @@ public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListe
|
||||
* Must be called from the main thread.
|
||||
*/
|
||||
@MainThread
|
||||
void removeBubble(String key) {
|
||||
void removeBubble(String key, int reason) {
|
||||
if (mStackView != null) {
|
||||
mStackView.removeBubble(key);
|
||||
mStackView.removeBubble(key, reason);
|
||||
}
|
||||
mNotificationEntryManager.updateNotifications();
|
||||
updateVisibility();
|
||||
@@ -320,7 +337,7 @@ public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListe
|
||||
boolean samePackage = entry.notification.getPackageName().equals(
|
||||
e.notification.getPackageName());
|
||||
if (samePackage) {
|
||||
removeBubble(entry.key);
|
||||
removeBubble(entry.key, DISMISS_BLOCKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +394,7 @@ public class BubbleController implements BubbleExpandedView.OnBubbleBlockedListe
|
||||
}
|
||||
if (!removedByUser) {
|
||||
// This was a cancel so we should remove the bubble
|
||||
removeBubble(entry.key);
|
||||
removeBubble(entry.key, DISMISS_NOTIF_CANCEL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,7 +135,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
|
||||
public void onTaskRemovalStarted(int taskId) {
|
||||
if (mEntry != null) {
|
||||
// Must post because this is called from a binder thread.
|
||||
post(() -> mBubbleController.removeBubble(mEntry.key));
|
||||
post(() -> mBubbleController.removeBubble(mEntry.key,
|
||||
BubbleController.DISMISS_TASK_FINISHED));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.android.systemui.bubbles;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Outline;
|
||||
@@ -49,6 +51,7 @@ import androidx.dynamicanimation.animation.SpringForce;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.widget.ViewClippingUtil;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.bubbles.BubbleController.DismissReason;
|
||||
import com.android.systemui.bubbles.animation.ExpandedAnimationController;
|
||||
import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
|
||||
import com.android.systemui.bubbles.animation.StackAnimationController;
|
||||
@@ -62,6 +65,7 @@ import java.math.RoundingMode;
|
||||
*/
|
||||
public class BubbleStackView extends FrameLayout {
|
||||
private static final String TAG = "BubbleStackView";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Friction applied to fling animations. Since the stack must land on one of the sides of the
|
||||
@@ -252,7 +256,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_DISMISS:
|
||||
stackDismissed();
|
||||
stackDismissed(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
|
||||
return true;
|
||||
case AccessibilityNodeInfo.ACTION_COLLAPSE:
|
||||
collapseStack();
|
||||
@@ -376,18 +380,12 @@ public class BubbleStackView extends FrameLayout {
|
||||
/**
|
||||
* Remove a bubble from the stack.
|
||||
*/
|
||||
public void removeBubble(String key) {
|
||||
public void removeBubble(String key, int reason) {
|
||||
Bubble b = mBubbleData.removeBubble(key);
|
||||
if (b == null) {
|
||||
return;
|
||||
}
|
||||
b.entry.setBubbleDismissed(true);
|
||||
|
||||
// Remove it from the views
|
||||
int removedIndex = mBubbleContainer.indexOfChild(b.iconView);
|
||||
b.expandedView.cleanUpExpandedState();
|
||||
mBubbleContainer.removeView(b.iconView);
|
||||
|
||||
int removedIndex = dismissBubble(b, reason);
|
||||
int bubbleCount = mBubbleContainer.getChildCount();
|
||||
if (bubbleCount == 0) {
|
||||
// If no bubbles remain, collapse the entire stack.
|
||||
@@ -405,25 +403,62 @@ public class BubbleStackView extends FrameLayout {
|
||||
mExpandedBubble = null;
|
||||
}
|
||||
}
|
||||
// TODO: consider logging reason code
|
||||
logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the stack of bubbles.
|
||||
*/
|
||||
public void stackDismissed() {
|
||||
public void stackDismissed(int reason) {
|
||||
for (Bubble bubble : mBubbleData.getBubbles()) {
|
||||
bubble.entry.setBubbleDismissed(true);
|
||||
bubble.expandedView.cleanUpExpandedState();
|
||||
dismissBubble(bubble, reason);
|
||||
}
|
||||
mBubbleData.clear();
|
||||
collapseStack();
|
||||
mBubbleContainer.removeAllViews();
|
||||
mExpandedViewContainer.removeAllViews();
|
||||
// TODO: consider logging reason code
|
||||
logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
|
||||
StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the notification entry as dismissed, cleans up Bubble icon and expanded view UI
|
||||
* elements and calls deleteIntent if necessary.
|
||||
*
|
||||
* <p>Note: This does not remove the Bubble from BubbleData.
|
||||
*
|
||||
* @param bubble the Bubble being dismissed
|
||||
* @param reason code for the reason the dismiss was triggered
|
||||
* @see BubbleController.DismissReason
|
||||
*/
|
||||
private int dismissBubble(Bubble bubble, @DismissReason int reason) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dismissBubble: " + bubble + " reason=" + reason);
|
||||
}
|
||||
bubble.entry.setBubbleDismissed(true);
|
||||
bubble.expandedView.cleanUpExpandedState();
|
||||
|
||||
// Remove it from the views
|
||||
int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView);
|
||||
mBubbleContainer.removeViewAt(removedIndex);
|
||||
|
||||
if (reason == BubbleController.DISMISS_USER_GESTURE) {
|
||||
Notification.BubbleMetadata bubbleMetadata = bubble.entry.getBubbleMetadata();
|
||||
PendingIntent deleteIntent = bubbleMetadata.getDeleteIntent();
|
||||
if (deleteIntent != null) {
|
||||
try {
|
||||
deleteIntent.send();
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
Log.w(TAG, "Failed to send delete intent for bubble with key: "
|
||||
+ (bubble.entry != null ? bubble.entry.key : " null entry"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return removedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a bubble in the stack.
|
||||
*
|
||||
|
||||
@@ -140,7 +140,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
case MotionEvent.ACTION_UP:
|
||||
trackMovement(event);
|
||||
if (mInDismissTarget && isStack) {
|
||||
mController.dismissStack();
|
||||
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
||||
} else if (mMovedEnough) {
|
||||
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
|
||||
final float velX = mVelocityTracker.getXVelocity();
|
||||
@@ -152,7 +152,8 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
mStack.onBubbleDragFinish(
|
||||
mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed);
|
||||
if (dismissed) {
|
||||
mController.removeBubble(((BubbleView) mTouchedView).getKey());
|
||||
mController.removeBubble(((BubbleView) mTouchedView).getKey(),
|
||||
BubbleController.DISMISS_USER_GESTURE);
|
||||
}
|
||||
}
|
||||
} else if (mTouchedView == mStack.getExpandedBubbleView()) {
|
||||
|
||||
@@ -20,10 +20,13 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.IActivityManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
@@ -83,6 +86,9 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
@Mock
|
||||
private BubbleController.BubbleExpandListener mBubbleExpandListener;
|
||||
|
||||
@Mock
|
||||
private PendingIntent mDeleteIntent;
|
||||
|
||||
private BubbleData mBubbleData;
|
||||
|
||||
@Before
|
||||
@@ -98,9 +104,9 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
|
||||
// Need notifications for bubbles
|
||||
mNotificationTestHelper = new NotificationTestHelper(mContext);
|
||||
mRow = mNotificationTestHelper.createBubble();
|
||||
mRow2 = mNotificationTestHelper.createBubble();
|
||||
mNoChannelRow = mNotificationTestHelper.createBubble();
|
||||
mRow = mNotificationTestHelper.createBubble(mDeleteIntent);
|
||||
mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent);
|
||||
mNoChannelRow = mNotificationTestHelper.createBubble(mDeleteIntent);
|
||||
|
||||
// Return non-null notification data from the NEM
|
||||
when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData);
|
||||
@@ -141,11 +147,10 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
|
||||
verify(mBubbleStateChangeListener).onHasBubblesChanged(true);
|
||||
|
||||
mBubbleController.removeBubble(mRow.getEntry().key);
|
||||
mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE);
|
||||
assertFalse(mStatusBarWindowController.getBubblesShowing());
|
||||
assertTrue(mRow.getEntry().isBubbleDismissed());
|
||||
verify(mNotificationEntryManager).updateNotifications();
|
||||
|
||||
verify(mBubbleStateChangeListener).onHasBubblesChanged(false);
|
||||
}
|
||||
|
||||
@@ -155,7 +160,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
|
||||
assertTrue(mBubbleController.hasBubbles());
|
||||
|
||||
mBubbleController.dismissStack();
|
||||
mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
||||
assertFalse(mStatusBarWindowController.getBubblesShowing());
|
||||
verify(mNotificationEntryManager).updateNotifications();
|
||||
assertTrue(mRow.getEntry().isBubbleDismissed());
|
||||
@@ -271,7 +276,8 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertFalse(mRow2.getEntry().showInShadeWhenBubble());
|
||||
|
||||
// Dismiss currently expanded
|
||||
mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey());
|
||||
mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(),
|
||||
BubbleController.DISMISS_USER_GESTURE);
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key);
|
||||
|
||||
// Make sure next bubble is selected
|
||||
@@ -279,7 +285,8 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key);
|
||||
|
||||
// Dismiss that one
|
||||
mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey());
|
||||
mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(),
|
||||
BubbleController.DISMISS_USER_GESTURE);
|
||||
|
||||
// Make sure state changes and collapse happens
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key);
|
||||
@@ -299,6 +306,28 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mRow.getEntry().showInShadeWhenBubble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
|
||||
mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
|
||||
mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_AGED);
|
||||
verify(mDeleteIntent, never()).send();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
|
||||
mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
|
||||
mBubbleController.removeBubble(mRow.getEntry().key, BubbleController.DISMISS_USER_GESTURE);
|
||||
verify(mDeleteIntent, times(1)).send();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException {
|
||||
mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */);
|
||||
mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */);
|
||||
mBubbleController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
|
||||
verify(mDeleteIntent, times(2)).send();
|
||||
}
|
||||
|
||||
static class TestableBubbleController extends BubbleController {
|
||||
|
||||
TestableBubbleController(Context context,
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.annotation.Nullable;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.app.Notification;
|
||||
import android.app.Notification.BubbleMetadata;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
@@ -152,8 +153,18 @@ public class NotificationTestHelper {
|
||||
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
|
||||
*/
|
||||
public ExpandableNotificationRow createBubble() throws Exception {
|
||||
return createBubble(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link ExpandableNotificationRow} that should be shown as a bubble.
|
||||
*
|
||||
* @param deleteIntent the intent to assign to {@link BubbleMetadata#deleteIntent}
|
||||
*/
|
||||
public ExpandableNotificationRow createBubble(@Nullable PendingIntent deleteIntent)
|
||||
throws Exception {
|
||||
Notification n = createNotification(false /* isGroupSummary */,
|
||||
null /* groupKey */, true /* isBubble */);
|
||||
null /* groupKey */, true /* isBubble */, deleteIntent);
|
||||
return generateRow(n, PKG, UID, USER_HANDLE, 0 /* extraInflationFlags */, IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
@@ -196,7 +207,8 @@ public class NotificationTestHelper {
|
||||
* @return a notification that is in the group specified or standalone if unspecified
|
||||
*/
|
||||
private Notification createNotification(boolean isGroupSummary, @Nullable String groupKey) {
|
||||
return createNotification(isGroupSummary, groupKey, false /* isBubble */);
|
||||
return createNotification(isGroupSummary, groupKey, false /* isBubble */,
|
||||
null /* bubbleDeleteIntent */);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +220,8 @@ public class NotificationTestHelper {
|
||||
* @return a notification that is in the group specified or standalone if unspecified
|
||||
*/
|
||||
private Notification createNotification(boolean isGroupSummary,
|
||||
@Nullable String groupKey, boolean isBubble) {
|
||||
@Nullable String groupKey, boolean isBubble,
|
||||
@Nullable PendingIntent bubbleDeleteIntent) {
|
||||
Notification publicVersion = new Notification.Builder(mContext).setSmallIcon(
|
||||
R.drawable.ic_person)
|
||||
.setCustomContentView(new RemoteViews(mContext.getPackageName(),
|
||||
@@ -227,7 +240,8 @@ public class NotificationTestHelper {
|
||||
notificationBuilder.setGroup(groupKey);
|
||||
}
|
||||
if (isBubble) {
|
||||
notificationBuilder.setBubbleMetadata(makeBubbleMetadata());
|
||||
BubbleMetadata metadata = makeBubbleMetadata(bubbleDeleteIntent);
|
||||
notificationBuilder.setBubbleMetadata(metadata);
|
||||
}
|
||||
return notificationBuilder.build();
|
||||
}
|
||||
@@ -291,11 +305,13 @@ public class NotificationTestHelper {
|
||||
return row;
|
||||
}
|
||||
|
||||
private Notification.BubbleMetadata makeBubbleMetadata() {
|
||||
private BubbleMetadata makeBubbleMetadata(PendingIntent deleteIntent) {
|
||||
Intent target = new Intent(mContext, BubblesTestActivity.class);
|
||||
PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, 0);
|
||||
return new Notification.BubbleMetadata.Builder()
|
||||
|
||||
return new BubbleMetadata.Builder()
|
||||
.setIntent(bubbleIntent)
|
||||
.setDeleteIntent(deleteIntent)
|
||||
.setTitle("bubble title")
|
||||
.setIcon(Icon.createWithResource(mContext, 1))
|
||||
.setDesiredHeight(314)
|
||||
|
||||
Reference in New Issue
Block a user