Merge "When summary is dismissed, don't remove bubble children" into qt-r1-bubbles-dev

This commit is contained in:
Mady Mellor
2019-07-11 21:55:19 +00:00
committed by Android (Google) Code Review
5 changed files with 232 additions and 17 deletions

View File

@@ -16,12 +16,15 @@
package com.android.systemui.bubbles;
import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
import static android.app.Notification.FLAG_BUBBLE;
import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.View.INVISIBLE;
@@ -82,6 +85,7 @@ import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ZenModeController;
@@ -90,6 +94,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
@@ -109,7 +114,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
@Retention(SOURCE)
@IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
DISMISS_USER_CHANGED})
DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED})
@Target({FIELD, LOCAL_VARIABLE, PARAMETER})
@interface DismissReason {}
@@ -121,6 +126,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
static final int DISMISS_ACCESSIBILITY_ACTION = 6;
static final int DISMISS_NO_LONGER_BUBBLE = 7;
static final int DISMISS_USER_CHANGED = 8;
static final int DISMISS_GROUP_CANCELLED = 9;
public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
@@ -133,6 +139,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
private BubbleStateChangeListener mStateChangeListener;
private BubbleExpandListener mExpandListener;
@Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
private final NotificationGroupManager mNotificationGroupManager;
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
@@ -211,10 +218,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
BubbleData data, ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager) {
NotificationLockscreenUserManager notifUserManager,
NotificationGroupManager groupManager) {
this(context, statusBarWindowController, data, null /* synchronizer */,
configurationController, interruptionStateProvider, zenModeController,
notifUserManager);
notifUserManager, groupManager);
}
public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
@@ -222,7 +230,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager notifUserManager) {
NotificationLockscreenUserManager notifUserManager,
NotificationGroupManager groupManager) {
mContext = context;
mNotificationInterruptionStateProvider = interruptionStateProvider;
mNotifUserManager = notifUserManager;
@@ -251,6 +260,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
mNotificationGroupManager = groupManager;
mStatusBarWindowController = statusBarWindowController;
mStatusBarStateListener = new StatusBarStateListener();
@@ -500,24 +510,38 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
new NotificationRemoveInterceptor() {
@Override
public boolean onNotificationRemoveRequested(String key, int reason) {
if (!mBubbleData.hasBubbleWithKey(key)) {
NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key);
String groupKey = entry != null ? entry.notification.getGroupKey() : null;
ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
boolean inBubbleData = mBubbleData.hasBubbleWithKey(key);
boolean isSummary = entry != null
&& entry.notification.getNotification().isGroupSummary();
boolean isSummaryOfBubbles = isSummary && bubbleChildren != null
&& !bubbleChildren.isEmpty();
if (!inBubbleData && !isSummaryOfBubbles) {
return false;
}
Bubble bubble = mBubbleData.getBubbleWithKey(key);
NotificationEntry entry = bubble.getEntry();
final boolean isClearAll = reason == REASON_CANCEL_ALL;
final boolean isUserDimiss = reason == REASON_CANCEL;
final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK;
final boolean isAppCancel = reason == REASON_APP_CANCEL
|| reason == REASON_APP_CANCEL_ALL;
final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED;
// Need to check for !appCancel here because the notification may have
// previously been dismissed & entry.isRowDismissed would still be true
boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel)
|| isClearAll || isUserDimiss;
|| isClearAll || isUserDimiss || isSummaryCancel;
if (isSummaryOfBubbles) {
return handleSummaryRemovalInterception(entry, userRemovedNotif);
}
// The bubble notification sticks around in the data as long as the bubble is
// not dismissed and the app hasn't cancelled the notification.
Bubble bubble = mBubbleData.getBubbleWithKey(key);
boolean bubbleExtended = entry.isBubble() && userRemovedNotif;
if (bubbleExtended) {
bubble.setShowInShadeWhenBubble(false);
@@ -536,6 +560,43 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
}
};
private boolean handleSummaryRemovalInterception(NotificationEntry summary,
boolean userRemovedNotif) {
String groupKey = summary.notification.getGroupKey();
ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
if (userRemovedNotif) {
// If it's a user dismiss we mark the children to be hidden from the shade.
for (int i = 0; i < bubbleChildren.size(); i++) {
Bubble bubbleChild = bubbleChildren.get(i);
// As far as group manager is concerned, once a child is no longer shown
// in the shade, it is essentially removed.
mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
bubbleChild.setShowInShadeWhenBubble(false);
bubbleChild.setShowBubbleDot(false);
if (mStackView != null) {
mStackView.updateDotVisibility(bubbleChild.getKey());
}
}
// And since all children are removed, remove the summary.
mNotificationGroupManager.onEntryRemoved(summary);
// If the summary was auto-generated we don't need to keep that notification around
// because apps can't cancel it; so we only intercept & suppress real summaries.
boolean isAutogroupSummary = (summary.notification.getNotification().flags
& FLAG_AUTOGROUP_SUMMARY) != 0;
return !isAutogroupSummary;
} else {
// Remove any associated bubble children.
for (int i = 0; i < bubbleChildren.size(); i++) {
Bubble bubbleChild = bubbleChildren.get(i);
mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(),
DISMISS_GROUP_CANCELLED);
}
return false;
}
}
@SuppressWarnings("FieldCanBeLocal")
private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
@Override
@@ -597,7 +658,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
}
// Do removals, if any.
for (Pair<Bubble, Integer> removed : update.removedBubbles) {
ArrayList<Pair<Bubble, Integer>> removedBubbles =
new ArrayList<>(update.removedBubbles);
for (Pair<Bubble, Integer> removed : removedBubbles) {
final Bubble bubble = removed.first;
@DismissReason final int reason = removed.second;
mStackView.removeBubble(bubble);
@@ -622,6 +685,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
// Bad things have happened
}
}
// Check if summary should be removed from NoManGroup
NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(
bubble.getEntry().notification);
if (summary != null) {
ArrayList<NotificationEntry> summaryChildren =
mNotificationGroupManager.getLogicalChildren(summary.notification);
if (summaryChildren == null || summaryChildren.isEmpty()) {
mNotificationEntryManager.performRemoveNotification(
summary.notification, UNDEFINED_DISMISS_REASON);
}
}
}
}

View File

@@ -229,6 +229,23 @@ public class BubbleData {
dispatchPendingChanges();
}
/**
* Retrieves any bubbles that are part of the notification group represented by the provided
* group key.
*/
ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
ArrayList<Bubble> bubbleChildren = new ArrayList<>();
if (groupKey == null) {
return bubbleChildren;
}
for (Bubble b : mBubbles) {
if (groupKey.equals(b.getEntry().notification.getGroupKey())) {
bubbleChildren.add(b);
}
}
return bubbleChildren;
}
private void doAdd(Bubble bubble) {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "doAdd: " + bubble);

View File

@@ -70,6 +70,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBarWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -91,6 +92,8 @@ public class BubbleControllerTest extends SysuiTestCase {
@Mock
private NotificationEntryManager mNotificationEntryManager;
@Mock
private NotificationGroupManager mNotificationGroupManager;
@Mock
private WindowManager mWindowManager;
@Mock
private IActivityManager mActivityManager;
@@ -154,6 +157,7 @@ public class BubbleControllerTest extends SysuiTestCase {
// Return non-null notification data from the NEM
when(mNotificationEntryManager.getNotificationData()).thenReturn(mNotificationData);
when(mNotificationData.get(mRow.getEntry().key)).thenReturn(mRow.getEntry());
when(mNotificationData.getChannel(mRow.getEntry().key)).thenReturn(mRow.getEntry().channel);
mZenModeConfig.suppressedVisualEffects = 0;
@@ -168,9 +172,14 @@ public class BubbleControllerTest extends SysuiTestCase {
mock(HeadsUpManager.class),
mock(NotificationInterruptionStateProvider.HeadsUpSuppressor.class));
mBubbleData = new BubbleData(mContext);
mBubbleController = new TestableBubbleController(mContext, mStatusBarWindowController,
mBubbleData, mConfigurationController, interruptionStateProvider,
mZenModeController, mLockscreenUserManager);
mBubbleController = new TestableBubbleController(mContext,
mStatusBarWindowController,
mBubbleData,
mConfigurationController,
interruptionStateProvider,
mZenModeController,
mLockscreenUserManager,
mNotificationGroupManager);
mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener);
mBubbleController.setExpandListener(mBubbleExpandListener);
@@ -631,10 +640,11 @@ public class BubbleControllerTest extends SysuiTestCase {
ConfigurationController configurationController,
NotificationInterruptionStateProvider interruptionStateProvider,
ZenModeController zenModeController,
NotificationLockscreenUserManager lockscreenUserManager) {
NotificationLockscreenUserManager lockscreenUserManager,
NotificationGroupManager groupManager) {
super(context, statusBarWindowController, data, Runnable::run,
configurationController, interruptionStateProvider, zenModeController,
lockscreenUserManager);
lockscreenUserManager, groupManager);
}
}

View File

@@ -5249,12 +5249,26 @@ public class NotificationManagerService extends SystemService {
return;
}
// Bubbled children get to stick around if the summary was manually cancelled
// (user removed) from systemui.
FlagChecker childrenFlagChecker = null;
if (mReason == REASON_CANCEL
|| mReason == REASON_CLICK
|| mReason == REASON_CANCEL_ALL) {
childrenFlagChecker = (flags) -> {
if ((flags & FLAG_BUBBLE) != 0) {
return false;
}
return true;
};
}
// Cancel the notification.
boolean wasPosted = removeFromNotificationListsLocked(r);
cancelNotificationLocked(
r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName);
cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
mSendDelete, null);
mSendDelete, childrenFlagChecker);
updateLightsLocked();
} else {
// No notification was found, assume that it is snoozed and cancel it.

View File

@@ -20,6 +20,7 @@ import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREG
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
import static android.app.Notification.CATEGORY_CALL;
import static android.app.Notification.FLAG_AUTO_CANCEL;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.NotificationManager.EXTRA_BLOCKED_STATE;
@@ -439,14 +440,22 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
return sbn;
}
private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id,
String groupKey, boolean isSummary) {
return generateNotificationRecord(channel, id, groupKey, isSummary, false /* isBubble */);
}
private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id,
String groupKey, boolean isSummary, boolean isBubble) {
Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
.setContentTitle("foo")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.setGroup(groupKey)
.setGroupSummary(isSummary);
if (isBubble) {
nb.setBubbleMetadata(getBasicBubbleMetadataBuilder().build());
}
StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0,
nb.build(), new UserHandle(mUid), null, 0);
return new NotificationRecord(mContext, sbn, channel);
@@ -542,6 +551,52 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
.setIcon(Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon));
}
private NotificationRecord addGroupWithBubblesAndValidateAdded(boolean summaryAutoCancel)
throws RemoteException {
// Notification that has bubble metadata
NotificationRecord nrBubble = generateNotificationRecord(mTestNotificationChannel, 1,
"BUBBLE_GROUP", false /* isSummary */, true /* isBubble */);
// Make the package foreground so that we're allowed to be a bubble
when(mActivityManager.getPackageImportance(nrBubble.sbn.getPackageName())).thenReturn(
IMPORTANCE_FOREGROUND);
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag",
nrBubble.sbn.getId(), nrBubble.sbn.getNotification(), nrBubble.sbn.getUserId());
waitForIdle();
// Make sure we are a bubble
StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
assertEquals(1, notifsAfter.length);
assertTrue((notifsAfter[0].getNotification().flags & FLAG_BUBBLE) != 0);
// Plain notification without bubble metadata
NotificationRecord nrPlain = generateNotificationRecord(mTestNotificationChannel, 2,
"BUBBLE_GROUP", false /* isSummary */, false /* isBubble */);
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag",
nrPlain.sbn.getId(), nrPlain.sbn.getNotification(), nrPlain.sbn.getUserId());
waitForIdle();
notifsAfter = mBinderService.getActiveNotifications(PKG);
assertEquals(2, notifsAfter.length);
// Summary notification for both of those
NotificationRecord nrSummary = generateNotificationRecord(mTestNotificationChannel, 3,
"BUBBLE_GROUP", true /* isSummary */, false /* isBubble */);
if (summaryAutoCancel) {
nrSummary.getNotification().flags |= FLAG_AUTO_CANCEL;
}
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag",
nrSummary.sbn.getId(), nrSummary.sbn.getNotification(), nrSummary.sbn.getUserId());
waitForIdle();
notifsAfter = mBinderService.getActiveNotifications(PKG);
assertEquals(3, notifsAfter.length);
return nrSummary;
}
@Test
public void testCreateNotificationChannels_SingleChannel() throws Exception {
final NotificationChannel channel =
@@ -5192,4 +5247,48 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
assertTrue(notif.getBubbleMetadata().getAutoExpandBubble());
assertTrue(notif.getBubbleMetadata().isNotificationSuppressed());
}
@Test
public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryDismissed()
throws Exception {
// Bubbles are allowed!
setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded(
true /* summaryAutoCancel */);
// Dismiss summary
final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2,
true);
mService.mNotificationDelegate.onNotificationClear(mUid, 0, PKG, nrSummary.sbn.getTag(),
nrSummary.sbn.getId(), nrSummary.getUserId(), nrSummary.getKey(),
NotificationStats.DISMISSAL_SHADE,
NotificationStats.DISMISS_SENTIMENT_NEUTRAL, nv);
waitForIdle();
// The bubble should still exist
StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
assertEquals(1, notifsAfter.length);
}
@Test
public void testNotificationBubbles_bubbleChildrenStay_whenGroupSummaryClicked()
throws Exception {
// Bubbles are allowed!
setUpPrefsForBubbles(true /* global */, true /* app */, true /* channel */);
NotificationRecord nrSummary = addGroupWithBubblesAndValidateAdded(
true /* summaryAutoCancel */);
// Click summary
final NotificationVisibility nv = NotificationVisibility.obtain(nrSummary.getKey(), 1, 2,
true);
mService.mNotificationDelegate.onNotificationClick(mUid, Binder.getCallingPid(),
nrSummary.getKey(), nv);
waitForIdle();
// The bubble should still exist
StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
assertEquals(1, notifsAfter.length);
}
}