Loading services/core/java/com/android/server/notification/GroupHelper.java +38 −9 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; Loading Loading @@ -130,8 +131,10 @@ public class GroupHelper { mUngroupedAbuseNotifications = new ArrayMap<>(); // Contains the list of group summaries that were canceled when "singleton groups" were // force grouped. Used to remove the original group's children when an app cancels the // already removed summary. Key is userId|packageName|g:OriginalGroupName // force grouped. Key is userId|packageName|g:OriginalGroupName. Used to: // 1) remove the original group's children when an app cancels the already removed summary. // 2) perform the same side effects that would happen if the group is removed because // all its force-regrouped children are removed (e.g. firing its deleteIntent). @GuardedBy("mAggregatedNotifications") private final ArrayMap<FullyQualifiedGroupKey, CachedSummary> mCanceledSummaries = new ArrayMap<>(); Loading Loading @@ -278,7 +281,11 @@ public class GroupHelper { public void onNotificationRemoved(NotificationRecord record) { try { if (notificationForceGrouping()) { onNotificationRemoved(record, new ArrayList<>()); Slog.wtf(TAG, "This overload of onNotificationRemoved() should not be called if " + "notification_force_grouping is enabled!", new Exception("call stack")); onNotificationRemoved(record, new ArrayList<>(), false); } else { final StatusBarNotification sbn = record.getSbn(); maybeUngroup(sbn, true, sbn.getUserId()); Loading Loading @@ -926,10 +933,12 @@ public class GroupHelper { * * @param record the removed notification * @param notificationList the full notification list from NotificationManagerService * @param sendingDelete whether the removed notification is being removed in a way that sends * its {@code deleteIntent} */ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) protected void onNotificationRemoved(final NotificationRecord record, final List<NotificationRecord> notificationList) { final List<NotificationRecord> notificationList, boolean sendingDelete) { final StatusBarNotification sbn = record.getSbn(); final String pkgName = sbn.getPackageName(); final int userId = record.getUserId(); Loading Loading @@ -973,9 +982,11 @@ public class GroupHelper { } // Try to cleanup cached summaries if notification was canceled (not snoozed) // If the notification was cancelled by an action that fires its delete intent, // also fire it for the cached summary. if (record.isCanceled) { maybeClearCanceledSummariesCache(pkgName, userId, record.getNotification().getGroup(), notificationList); record.getNotification().getGroup(), notificationList, sendingDelete); } } } Loading Loading @@ -1759,13 +1770,18 @@ public class GroupHelper { private void cacheCanceledSummary(NotificationRecord record) { final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(), record.getNotification().getGroup()); mCanceledSummaries.put(groupKey, new CachedSummary(record.getSbn().getId(), record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey())); mCanceledSummaries.put(groupKey, new CachedSummary( record.getSbn().getId(), record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey(), record.getNotification().deleteIntent)); } @GuardedBy("mAggregatedNotifications") private void maybeClearCanceledSummariesCache(String pkgName, int userId, String groupName, List<NotificationRecord> notificationList) { String groupName, List<NotificationRecord> notificationList, boolean sendSummaryDelete) { final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, groupName); CachedSummary summary = mCanceledSummaries.get(findKey); Loading @@ -1786,6 +1802,9 @@ public class GroupHelper { } if (!stillHasChildren) { removeCachedSummary(pkgName, userId, summary); if (sendSummaryDelete && summary.deleteIntent != null) { mCallback.sendAppProvidedSummaryDeleteIntent(pkgName, summary.deleteIntent); } } } } Loading Loading @@ -1965,7 +1984,8 @@ public class GroupHelper { } } record CachedSummary(int id, String tag, String originalGroupKey, String key) {} record CachedSummary(int id, String tag, String originalGroupKey, String key, @Nullable PendingIntent deleteIntent) { } protected static class NotificationAttributes { public final int flags; Loading Loading @@ -2035,6 +2055,15 @@ public class GroupHelper { // New callbacks for API abuse grouping void removeAppProvidedSummary(String key); /** * Send a cached summary's deleteIntent, when the last of its original children is removed. * * <p>While technically the group summary was "canceled" much earlier (because it was the * summary of a sparse group and its children got reparented), the posting package expected * the summary's deleteIntent to fire when the summary is auto-dismissed. */ void sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent); void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason); Loading services/core/java/com/android/server/notification/NotificationManagerService.java +24 −16 Original line number Diff line number Diff line Loading @@ -3210,6 +3210,11 @@ public class NotificationManagerService extends SystemService { } } @Override public void sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent) { sendDeleteIntent(deleteIntent, pkg); } @Override public void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason) { Loading Loading @@ -9898,7 +9903,8 @@ public class NotificationManagerService extends SystemService { if (notificationForceGrouping()) { mHandler.post(() -> { synchronized (mNotificationLock) { mGroupHelper.onNotificationRemoved(r, mNotificationList); mGroupHelper.onNotificationRemoved(r, mNotificationList, /* sendingDelete= */ false); } }); } else { Loading Loading @@ -10826,20 +10832,7 @@ public class NotificationManagerService extends SystemService { // tell the app if (sendDelete) { final PendingIntent deleteIntent = r.getNotification().deleteIntent; if (deleteIntent != null) { try { // make sure deleteIntent cannot be used to start activities from background LocalServices.getService(ActivityManagerInternal.class) .clearPendingIntentAllowBgActivityStarts(deleteIntent.getTarget(), ALLOWLIST_TOKEN); deleteIntent.send(); } catch (PendingIntent.CanceledException ex) { // do nothing - there's no relevant way to recover, and // no reason to let this propagate Slog.w(TAG, "canceled PendingIntent for " + r.getSbn().getPackageName(), ex); } } sendDeleteIntent(r.getNotification().deleteIntent, r.getSbn().getPackageName()); } // Only cancel these if this notification actually got to be posted. Loading @@ -10854,7 +10847,7 @@ public class NotificationManagerService extends SystemService { mHandler.removeCallbacksAndEqualMessages(r.getKey()); mHandler.post(() -> { synchronized (NotificationManagerService.this.mNotificationLock) { mGroupHelper.onNotificationRemoved(r, mNotificationList); mGroupHelper.onNotificationRemoved(r, mNotificationList, sendDelete); } }); Loading Loading @@ -10952,6 +10945,21 @@ public class NotificationManagerService extends SystemService { } } private static void sendDeleteIntent(@Nullable PendingIntent deleteIntent, String fromPkg) { if (deleteIntent != null) { try { // make sure deleteIntent cannot be used to start activities from background LocalServices.getService(ActivityManagerInternal.class) .clearPendingIntentAllowBgActivityStarts(deleteIntent.getTarget(), ALLOWLIST_TOKEN); deleteIntent.send(); } catch (PendingIntent.CanceledException ex) { // There's no relevant way to recover, and no reason to let this propagate Slog.w(TAG, "canceled PendingIntent for " + fromPkg, ex); } } } @VisibleForTesting void updateUriPermissions(@Nullable NotificationRecord newRecord, @Nullable NotificationRecord oldRecord, String targetPkg, int targetUserId) { Loading services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +62 −18 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ import static org.mockito.Mockito.when; import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.AdaptiveIconDrawable; Loading Loading @@ -620,6 +622,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_singleOngoing_removeOngoingChild() { final String pkg = "package"; Loading @@ -639,7 +642,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(0)); mGroupHelper.onNotificationRemoved(notifications.get(0), new ArrayList<>(), false); // Summary is no longer ongoing verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -776,7 +779,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(1)); mGroupHelper.onNotificationRemoved(notifications.get(1), new ArrayList<>(), false); // Summary is still ongoing verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -936,7 +939,7 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(r, false); } mGroupHelper.onNotificationRemoved(notifications.get(0)); mGroupHelper.onNotificationRemoved(notifications.get(0), new ArrayList<>(), false); // Summary should still be autocancelable verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading @@ -944,6 +947,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testDropToZeroRemoveGroup_disableFlag() { final String pkg = "package"; Loading @@ -963,19 +967,20 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) public void testDropToZeroRemoveGroup() { final String pkg = "package"; ArrayList<NotificationRecord> posted = new ArrayList<>(); Loading @@ -994,13 +999,13 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } Loading Loading @@ -1072,6 +1077,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() { final String pkg = "package"; Loading @@ -1091,7 +1097,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); mGroupHelper.onNotificationRemoved(posted.remove(i), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Loading @@ -1114,7 +1120,8 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() { final String pkg = "package"; ArrayList<NotificationRecord> posted = new ArrayList<>(); Loading @@ -1134,7 +1141,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); mGroupHelper.onNotificationRemoved(posted.remove(i), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Loading Loading @@ -1481,7 +1488,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // Remove last notification (the only one with different icon and color) mGroupHelper.onNotificationRemoved(notifications.get(lastIdx)); mGroupHelper.onNotificationRemoved(notifications.get(lastIdx), new ArrayList<>(), false); // Summary should be updated to the common icon and color verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -2162,7 +2169,7 @@ public class GroupHelperTest extends UiServiceTestCase { NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, false); r.setOverrideGroupKey(expectedGroupKey); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Check that the autogroup summary is removed verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), Loading Loading @@ -2206,7 +2213,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (NotificationRecord r: childrenToRemove) { notificationList.remove(r); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Only call onGroupedNotificationRemovedWithDelay with the summary notification mGroupHelper.onGroupedNotificationRemovedWithDelay(summary, notificationList, Loading Loading @@ -2270,7 +2277,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (NotificationRecord r: childrenToRemove) { notificationList.remove(r); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Only call onGroupedNotificationRemovedWithDelay with the summary notification mGroupHelper.onGroupedNotificationRemovedWithDelay(summary, notificationList, Loading Loading @@ -2327,7 +2334,7 @@ public class GroupHelperTest extends UiServiceTestCase { for (NotificationRecord r: notificationList) { if (r.getGroupKey().contains(groupToRemove)) { r.isCanceled = true; mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } } // Only call onGroupedNotificationRemovedWithDelay with the summary notification Loading Loading @@ -2452,7 +2459,7 @@ public class GroupHelperTest extends UiServiceTestCase { true); assertThat(GroupHelper.getSection(notifToInvalidate)).isNull(); notificationList.remove(notifToInvalidate); mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList); mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList, false); // Check that the autogroup was updated verify(mCallback, never()).removeAutoGroup(anyString()); Loading Loading @@ -4023,7 +4030,7 @@ public class GroupHelperTest extends UiServiceTestCase { //Cancel child 0 => remove cached summary childToRemove.isCanceled = true; notificationListAfterGrouping.remove(childToRemove); mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping); mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping, false); CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id, UserHandle.SYSTEM.getIdentifier()); assertThat(cachedSummary).isNull(); Loading Loading @@ -4399,4 +4406,41 @@ public class GroupHelperTest extends UiServiceTestCase { "PeopleSection(priority)"); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void onNotificationRemoved_lastChildOfCachedSummary_firesCachedSummaryDeleteIntent() { final List<NotificationRecord> notificationList = new ArrayList<>(); final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); final String pkg = "package"; NotificationRecord onlyChildOfFirstGroup = null; PendingIntent deleteIntentofFirstSummary = PendingIntent.getActivity(mContext, 1, new Intent(), PendingIntent.FLAG_IMMUTABLE); // Post singleton groups, above forced group limit, so they are force grouped for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { NotificationRecord summary = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true); notificationList.add(summary); NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp " + i, false); notificationList.add(child); summaryByGroup.put(summary.getGroupKey(), summary); if (i == 0) { onlyChildOfFirstGroup = child; summary.getNotification().deleteIntent = deleteIntentofFirstSummary; } mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); summary.isCanceled = true; // simulate removing the app summary mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); } // Sparse group autogrouping would've removed the summary. notificationList.remove(0); // Now remove the only child of the first (force-grouped, cuz sparse) group. notificationList.remove(0); onlyChildOfFirstGroup.isCanceled = true; mGroupHelper.onNotificationRemoved(onlyChildOfFirstGroup, notificationList, true); verify(mCallback).sendAppProvidedSummaryDeleteIntent(eq(pkg), eq(deleteIntentofFirstSummary)); } } services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +43 −10 Original line number Diff line number Diff line Loading @@ -349,6 +349,7 @@ import com.android.server.wm.WindowManagerInternal; import com.google.android.collect.Lists; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; Loading Loading @@ -2788,8 +2789,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { nr1.getSbn().getId(), nr1.getSbn().getUserId()); waitForIdle(); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any(), eq(false)); // GroupHelper would send 'remove summary' event mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(), Loading Loading @@ -3155,8 +3156,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that onGroupedNotificationRemovedWithDelay was called only once verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any(), eq(false)); verify(mGroupHelper, times(1)).onGroupedNotificationRemovedWithDelay(eq(summary), any(), any()); } Loading Loading @@ -3201,9 +3202,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that onGroupedNotificationRemovedWithDelay was never called: summary was canceled verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(summary), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(summary), any(), eq(false)); verify(mGroupHelper, never()).onGroupedNotificationRemovedWithDelay(any(), any(), any()); } Loading Loading @@ -14122,9 +14123,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that child notifications are also removed verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any(), eq(false)); // Make sure the summary was removed and not re-posted assertThat(mService.getNotificationRecordCount()).isEqualTo(0); Loading Loading @@ -18659,4 +18661,35 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void clearAll_fromUser_willSendDeleteIntentForCachedSummaries() throws Exception { NotificationRecord n = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", n.getSbn().getId(), n.getSbn().getNotification(), n.getSbn().getUserId()); waitForIdle(); n = Iterables.getOnlyElement(mService.mNotificationList); mService.mNotificationDelegate.onClearAll(mUid, Binder.getCallingPid(), n.getUserId()); waitForIdle(); verify(mGroupHelper).onNotificationRemoved(eq(n), any(), eq(true)); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void cancel_fromApp_willNotSendDeleteIntentForCachedSummaries() throws Exception { NotificationRecord n = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", n.getSbn().getId(), n.getSbn().getNotification(), n.getSbn().getUserId()); waitForIdle(); n = Iterables.getOnlyElement(mService.mNotificationList); mBinderService.cancelAllNotifications(mPkg, mUserId); waitForIdle(); verify(mGroupHelper).onNotificationRemoved(eq(n), any(), eq(false)); } } Loading
services/core/java/com/android/server/notification/GroupHelper.java +38 −9 Original line number Diff line number Diff line Loading @@ -35,6 +35,7 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; Loading Loading @@ -130,8 +131,10 @@ public class GroupHelper { mUngroupedAbuseNotifications = new ArrayMap<>(); // Contains the list of group summaries that were canceled when "singleton groups" were // force grouped. Used to remove the original group's children when an app cancels the // already removed summary. Key is userId|packageName|g:OriginalGroupName // force grouped. Key is userId|packageName|g:OriginalGroupName. Used to: // 1) remove the original group's children when an app cancels the already removed summary. // 2) perform the same side effects that would happen if the group is removed because // all its force-regrouped children are removed (e.g. firing its deleteIntent). @GuardedBy("mAggregatedNotifications") private final ArrayMap<FullyQualifiedGroupKey, CachedSummary> mCanceledSummaries = new ArrayMap<>(); Loading Loading @@ -278,7 +281,11 @@ public class GroupHelper { public void onNotificationRemoved(NotificationRecord record) { try { if (notificationForceGrouping()) { onNotificationRemoved(record, new ArrayList<>()); Slog.wtf(TAG, "This overload of onNotificationRemoved() should not be called if " + "notification_force_grouping is enabled!", new Exception("call stack")); onNotificationRemoved(record, new ArrayList<>(), false); } else { final StatusBarNotification sbn = record.getSbn(); maybeUngroup(sbn, true, sbn.getUserId()); Loading Loading @@ -926,10 +933,12 @@ public class GroupHelper { * * @param record the removed notification * @param notificationList the full notification list from NotificationManagerService * @param sendingDelete whether the removed notification is being removed in a way that sends * its {@code deleteIntent} */ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) protected void onNotificationRemoved(final NotificationRecord record, final List<NotificationRecord> notificationList) { final List<NotificationRecord> notificationList, boolean sendingDelete) { final StatusBarNotification sbn = record.getSbn(); final String pkgName = sbn.getPackageName(); final int userId = record.getUserId(); Loading Loading @@ -973,9 +982,11 @@ public class GroupHelper { } // Try to cleanup cached summaries if notification was canceled (not snoozed) // If the notification was cancelled by an action that fires its delete intent, // also fire it for the cached summary. if (record.isCanceled) { maybeClearCanceledSummariesCache(pkgName, userId, record.getNotification().getGroup(), notificationList); record.getNotification().getGroup(), notificationList, sendingDelete); } } } Loading Loading @@ -1759,13 +1770,18 @@ public class GroupHelper { private void cacheCanceledSummary(NotificationRecord record) { final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(), record.getNotification().getGroup()); mCanceledSummaries.put(groupKey, new CachedSummary(record.getSbn().getId(), record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey())); mCanceledSummaries.put(groupKey, new CachedSummary( record.getSbn().getId(), record.getSbn().getTag(), record.getNotification().getGroup(), record.getKey(), record.getNotification().deleteIntent)); } @GuardedBy("mAggregatedNotifications") private void maybeClearCanceledSummariesCache(String pkgName, int userId, String groupName, List<NotificationRecord> notificationList) { String groupName, List<NotificationRecord> notificationList, boolean sendSummaryDelete) { final FullyQualifiedGroupKey findKey = new FullyQualifiedGroupKey(userId, pkgName, groupName); CachedSummary summary = mCanceledSummaries.get(findKey); Loading @@ -1786,6 +1802,9 @@ public class GroupHelper { } if (!stillHasChildren) { removeCachedSummary(pkgName, userId, summary); if (sendSummaryDelete && summary.deleteIntent != null) { mCallback.sendAppProvidedSummaryDeleteIntent(pkgName, summary.deleteIntent); } } } } Loading Loading @@ -1965,7 +1984,8 @@ public class GroupHelper { } } record CachedSummary(int id, String tag, String originalGroupKey, String key) {} record CachedSummary(int id, String tag, String originalGroupKey, String key, @Nullable PendingIntent deleteIntent) { } protected static class NotificationAttributes { public final int flags; Loading Loading @@ -2035,6 +2055,15 @@ public class GroupHelper { // New callbacks for API abuse grouping void removeAppProvidedSummary(String key); /** * Send a cached summary's deleteIntent, when the last of its original children is removed. * * <p>While technically the group summary was "canceled" much earlier (because it was the * summary of a sparse group and its children got reparented), the posting package expected * the summary's deleteIntent to fire when the summary is auto-dismissed. */ void sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent); void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason); Loading
services/core/java/com/android/server/notification/NotificationManagerService.java +24 −16 Original line number Diff line number Diff line Loading @@ -3210,6 +3210,11 @@ public class NotificationManagerService extends SystemService { } } @Override public void sendAppProvidedSummaryDeleteIntent(String pkg, PendingIntent deleteIntent) { sendDeleteIntent(deleteIntent, pkg); } @Override public void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey, int cancelReason) { Loading Loading @@ -9898,7 +9903,8 @@ public class NotificationManagerService extends SystemService { if (notificationForceGrouping()) { mHandler.post(() -> { synchronized (mNotificationLock) { mGroupHelper.onNotificationRemoved(r, mNotificationList); mGroupHelper.onNotificationRemoved(r, mNotificationList, /* sendingDelete= */ false); } }); } else { Loading Loading @@ -10826,20 +10832,7 @@ public class NotificationManagerService extends SystemService { // tell the app if (sendDelete) { final PendingIntent deleteIntent = r.getNotification().deleteIntent; if (deleteIntent != null) { try { // make sure deleteIntent cannot be used to start activities from background LocalServices.getService(ActivityManagerInternal.class) .clearPendingIntentAllowBgActivityStarts(deleteIntent.getTarget(), ALLOWLIST_TOKEN); deleteIntent.send(); } catch (PendingIntent.CanceledException ex) { // do nothing - there's no relevant way to recover, and // no reason to let this propagate Slog.w(TAG, "canceled PendingIntent for " + r.getSbn().getPackageName(), ex); } } sendDeleteIntent(r.getNotification().deleteIntent, r.getSbn().getPackageName()); } // Only cancel these if this notification actually got to be posted. Loading @@ -10854,7 +10847,7 @@ public class NotificationManagerService extends SystemService { mHandler.removeCallbacksAndEqualMessages(r.getKey()); mHandler.post(() -> { synchronized (NotificationManagerService.this.mNotificationLock) { mGroupHelper.onNotificationRemoved(r, mNotificationList); mGroupHelper.onNotificationRemoved(r, mNotificationList, sendDelete); } }); Loading Loading @@ -10952,6 +10945,21 @@ public class NotificationManagerService extends SystemService { } } private static void sendDeleteIntent(@Nullable PendingIntent deleteIntent, String fromPkg) { if (deleteIntent != null) { try { // make sure deleteIntent cannot be used to start activities from background LocalServices.getService(ActivityManagerInternal.class) .clearPendingIntentAllowBgActivityStarts(deleteIntent.getTarget(), ALLOWLIST_TOKEN); deleteIntent.send(); } catch (PendingIntent.CanceledException ex) { // There's no relevant way to recover, and no reason to let this propagate Slog.w(TAG, "canceled PendingIntent for " + fromPkg, ex); } } } @VisibleForTesting void updateUriPermissions(@Nullable NotificationRecord newRecord, @Nullable NotificationRecord oldRecord, String targetPkg, int targetUserId) { Loading
services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +62 −18 Original line number Diff line number Diff line Loading @@ -66,6 +66,8 @@ import static org.mockito.Mockito.when; import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationChannel; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.drawable.AdaptiveIconDrawable; Loading Loading @@ -620,6 +622,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testAutoGrouped_singleOngoing_removeOngoingChild() { final String pkg = "package"; Loading @@ -639,7 +642,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(0)); mGroupHelper.onNotificationRemoved(notifications.get(0), new ArrayList<>(), false); // Summary is no longer ongoing verify(mCallback).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -776,7 +779,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // remove ongoing mGroupHelper.onNotificationRemoved(notifications.get(1)); mGroupHelper.onNotificationRemoved(notifications.get(1), new ArrayList<>(), false); // Summary is still ongoing verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -936,7 +939,7 @@ public class GroupHelperTest extends UiServiceTestCase { mGroupHelper.onNotificationPosted(r, false); } mGroupHelper.onNotificationRemoved(notifications.get(0)); mGroupHelper.onNotificationRemoved(notifications.get(0), new ArrayList<>(), false); // Summary should still be autocancelable verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading @@ -944,6 +947,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testDropToZeroRemoveGroup_disableFlag() { final String pkg = "package"; Loading @@ -963,19 +967,20 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) public void testDropToZeroRemoveGroup() { final String pkg = "package"; ArrayList<NotificationRecord> posted = new ArrayList<>(); Loading @@ -994,13 +999,13 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Mockito.reset(mCallback); mGroupHelper.onNotificationRemoved(posted.remove(0)); mGroupHelper.onNotificationRemoved(posted.remove(0), new ArrayList<>(), false); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString(), anyString()); } Loading Loading @@ -1072,6 +1077,7 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() { final String pkg = "package"; Loading @@ -1091,7 +1097,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); mGroupHelper.onNotificationRemoved(posted.remove(i), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Loading @@ -1114,7 +1120,8 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST) @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() { final String pkg = "package"; ArrayList<NotificationRecord> posted = new ArrayList<>(); Loading @@ -1134,7 +1141,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (int i = posted.size() - 2; i >= 0; i--) { mGroupHelper.onNotificationRemoved(posted.remove(i)); mGroupHelper.onNotificationRemoved(posted.remove(i), new ArrayList<>(), false); } verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); Loading Loading @@ -1481,7 +1488,7 @@ public class GroupHelperTest extends UiServiceTestCase { } // Remove last notification (the only one with different icon and color) mGroupHelper.onNotificationRemoved(notifications.get(lastIdx)); mGroupHelper.onNotificationRemoved(notifications.get(lastIdx), new ArrayList<>(), false); // Summary should be updated to the common icon and color verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), Loading Loading @@ -2162,7 +2169,7 @@ public class GroupHelperTest extends UiServiceTestCase { NotificationRecord r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, false); r.setOverrideGroupKey(expectedGroupKey); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Check that the autogroup summary is removed verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), Loading Loading @@ -2206,7 +2213,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (NotificationRecord r: childrenToRemove) { notificationList.remove(r); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Only call onGroupedNotificationRemovedWithDelay with the summary notification mGroupHelper.onGroupedNotificationRemovedWithDelay(summary, notificationList, Loading Loading @@ -2270,7 +2277,7 @@ public class GroupHelperTest extends UiServiceTestCase { Mockito.reset(mCallback); for (NotificationRecord r: childrenToRemove) { notificationList.remove(r); mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } // Only call onGroupedNotificationRemovedWithDelay with the summary notification mGroupHelper.onGroupedNotificationRemovedWithDelay(summary, notificationList, Loading Loading @@ -2327,7 +2334,7 @@ public class GroupHelperTest extends UiServiceTestCase { for (NotificationRecord r: notificationList) { if (r.getGroupKey().contains(groupToRemove)) { r.isCanceled = true; mGroupHelper.onNotificationRemoved(r, notificationList); mGroupHelper.onNotificationRemoved(r, notificationList, false); } } // Only call onGroupedNotificationRemovedWithDelay with the summary notification Loading Loading @@ -2452,7 +2459,7 @@ public class GroupHelperTest extends UiServiceTestCase { true); assertThat(GroupHelper.getSection(notifToInvalidate)).isNull(); notificationList.remove(notifToInvalidate); mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList); mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList, false); // Check that the autogroup was updated verify(mCallback, never()).removeAutoGroup(anyString()); Loading Loading @@ -4023,7 +4030,7 @@ public class GroupHelperTest extends UiServiceTestCase { //Cancel child 0 => remove cached summary childToRemove.isCanceled = true; notificationListAfterGrouping.remove(childToRemove); mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping); mGroupHelper.onNotificationRemoved(childToRemove, notificationListAfterGrouping, false); CachedSummary cachedSummary = mGroupHelper.findCanceledSummary(pkg, String.valueOf(id), id, UserHandle.SYSTEM.getIdentifier()); assertThat(cachedSummary).isNull(); Loading Loading @@ -4399,4 +4406,41 @@ public class GroupHelperTest extends UiServiceTestCase { "PeopleSection(priority)"); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void onNotificationRemoved_lastChildOfCachedSummary_firesCachedSummaryDeleteIntent() { final List<NotificationRecord> notificationList = new ArrayList<>(); final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); final String pkg = "package"; NotificationRecord onlyChildOfFirstGroup = null; PendingIntent deleteIntentofFirstSummary = PendingIntent.getActivity(mContext, 1, new Intent(), PendingIntent.FLAG_IMMUTABLE); // Post singleton groups, above forced group limit, so they are force grouped for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { NotificationRecord summary = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true); notificationList.add(summary); NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp " + i, false); notificationList.add(child); summaryByGroup.put(summary.getGroupKey(), summary); if (i == 0) { onlyChildOfFirstGroup = child; summary.getNotification().deleteIntent = deleteIntentofFirstSummary; } mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); summary.isCanceled = true; // simulate removing the app summary mGroupHelper.onNotificationPostedWithDelay(summary, notificationList, summaryByGroup); } // Sparse group autogrouping would've removed the summary. notificationList.remove(0); // Now remove the only child of the first (force-grouped, cuz sparse) group. notificationList.remove(0); onlyChildOfFirstGroup.isCanceled = true; mGroupHelper.onNotificationRemoved(onlyChildOfFirstGroup, notificationList, true); verify(mCallback).sendAppProvidedSummaryDeleteIntent(eq(pkg), eq(deleteIntentofFirstSummary)); } }
services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +43 −10 Original line number Diff line number Diff line Loading @@ -349,6 +349,7 @@ import com.android.server.wm.WindowManagerInternal; import com.google.android.collect.Lists; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; Loading Loading @@ -2788,8 +2789,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { nr1.getSbn().getId(), nr1.getSbn().getUserId()); waitForIdle(); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any(), eq(false)); // GroupHelper would send 'remove summary' event mService.clearAutogroupSummaryLocked(nr1.getUserId(), nr1.getSbn().getPackageName(), Loading Loading @@ -3155,8 +3156,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that onGroupedNotificationRemovedWithDelay was called only once verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any(), eq(false)); verify(mGroupHelper, times(1)).onGroupedNotificationRemovedWithDelay(eq(summary), any(), any()); } Loading Loading @@ -3201,9 +3202,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that onGroupedNotificationRemovedWithDelay was never called: summary was canceled verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(summary), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r1), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(r2), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(summary), any(), eq(false)); verify(mGroupHelper, never()).onGroupedNotificationRemovedWithDelay(any(), any(), any()); } Loading Loading @@ -14122,9 +14123,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check that child notifications are also removed verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any(), eq(false)); verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any(), eq(false)); // Make sure the summary was removed and not re-posted assertThat(mService.getNotificationRecordCount()).isEqualTo(0); Loading Loading @@ -18659,4 +18661,35 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void clearAll_fromUser_willSendDeleteIntentForCachedSummaries() throws Exception { NotificationRecord n = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", n.getSbn().getId(), n.getSbn().getNotification(), n.getSbn().getUserId()); waitForIdle(); n = Iterables.getOnlyElement(mService.mNotificationList); mService.mNotificationDelegate.onClearAll(mUid, Binder.getCallingPid(), n.getUserId()); waitForIdle(); verify(mGroupHelper).onNotificationRemoved(eq(n), any(), eq(true)); } @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void cancel_fromApp_willNotSendDeleteIntentForCachedSummaries() throws Exception { NotificationRecord n = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", n.getSbn().getId(), n.getSbn().getNotification(), n.getSbn().getUserId()); waitForIdle(); n = Iterables.getOnlyElement(mService.mNotificationList); mBinderService.cancelAllNotifications(mPkg, mUserId); waitForIdle(); verify(mGroupHelper).onNotificationRemoved(eq(n), any(), eq(false)); } }