diff options
6 files changed, 135 insertions, 36 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 51b2098ed9c6..eb85589f3781 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -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); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 25ee87a13b74..856b9d6c27e4 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -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)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 84efb9acbcf2..888e3fe282a8 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -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; private Point mDisplaySize; @@ -232,7 +236,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(); @@ -356,18 +360,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. @@ -385,26 +383,63 @@ 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. * * @param entry the entry to update in the stack. diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index c8eebac4da3e..a7170d0256e3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -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()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 0fd783423464..42c221a91422 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -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, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java index cef78db1b16b..de155055b76b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java @@ -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) |