NotifCollection.dismissNotifications will now remove hidden summaries.
Previously we would take the list of notifications exactly, but that left room that a notification's summary, if it was not included in the list, would be left in the shade. We now check if each entry is the sole logic child of a single summary, and if so we include that summary in the dismissal (assuming it was not already included), and generate the necessary stats object.
Bug: 355967751
Flag: com.android.systemui.notifications_dismiss_pruned_summaries
Test: atest NotifCollectionTest
Change-Id: Id3eda2f7a36227e4d5a921888735dd898d33a61a
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1c29db1..2047919 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -129,6 +129,13 @@
}
flag {
+ name: "notifications_dismiss_pruned_summaries"
+ namespace: "systemui"
+ description: "NotifCollection.dismissNotifications will now dismiss summaries that are pruned from the shade."
+ bug: "355967751"
+}
+
+flag {
name: "notification_transparent_header_fix"
namespace: "systemui"
description: "fix the transparent group header issue for async header inflation."
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 7b3a93a..b5c6c252 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -39,6 +39,7 @@
import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
+import static com.android.systemui.Flags.notificationsDismissPrunedSummaries;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
@@ -69,6 +70,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -111,6 +113,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -277,6 +280,10 @@
Assert.isMainThread();
checkForReentrantCall();
+ if (notificationsDismissPrunedSummaries()) {
+ entriesToDismiss = includeSummariesToDismiss(entriesToDismiss);
+ }
+
final int entryCount = entriesToDismiss.size();
final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>();
for (int i = 0; i < entriesToDismiss.size(); i++) {
@@ -336,6 +343,36 @@
dispatchEventsAndRebuildList("dismissNotifications");
}
+ private List<Pair<NotificationEntry, DismissedByUserStats>> includeSummariesToDismiss(
+ List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) {
+ final HashSet<NotificationEntry> entriesSet = new HashSet<>(entriesToDismiss.size());
+ for (Pair<NotificationEntry, DismissedByUserStats> entryToStats : entriesToDismiss) {
+ entriesSet.add(entryToStats.first);
+ }
+
+ final List<Pair<NotificationEntry, DismissedByUserStats>> entriesPlusSummaries =
+ new ArrayList<>(entriesToDismiss.size() + 1);
+ for (Pair<NotificationEntry, DismissedByUserStats> entryToStats : entriesToDismiss) {
+ entriesPlusSummaries.add(entryToStats);
+ NotificationEntry summary = fetchSummaryToDismiss(entryToStats.first);
+ if (summary != null && !entriesSet.contains(summary)) {
+ DismissedByUserStats currentStats = entryToStats.second;
+ NotificationVisibility summaryVisibility = NotificationVisibility.obtain(
+ summary.getKey(),
+ summary.getRanking().getRank(),
+ currentStats.notificationVisibility.count,
+ /* visible= */ false);
+ DismissedByUserStats summaryStats = new DismissedByUserStats(
+ currentStats.dismissalSurface,
+ currentStats.dismissalSentiment,
+ summaryVisibility
+ );
+ entriesPlusSummaries.add(new Pair<>(summary, summaryStats));
+ }
+ }
+ return entriesPlusSummaries;
+ }
+
/**
* Dismisses a single notification on behalf of the user.
*/
@@ -1062,6 +1099,16 @@
}
}
+ @Nullable
+ private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) {
+ if (isOnlyChildInGroup(entry)) {
+ String group = entry.getSbn().getGroupKey();
+ NotificationEntry summary = getGroupSummary(group);
+ if (summary != null && isDismissable(summary)) return summary;
+ }
+ return null;
+ }
+
/** A single method interface that callers can pass in when registering future dismissals */
public interface DismissedByUserStatsCreator {
DismissedByUserStats createDismissedByUserStats(NotificationEntry entry);
@@ -1092,16 +1139,6 @@
+ ">";
}
- @Nullable
- private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) {
- if (isOnlyChildInGroup(entry)) {
- String group = entry.getSbn().getGroupKey();
- NotificationEntry summary = getGroupSummary(group);
- if (summary != null && isDismissable(summary)) return summary;
- }
- return null;
- }
-
/** called when the entry has been removed from the collection */
public void onSystemServerCancel(@CancellationReason int cancellationReason) {
Assert.isMainThread();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 2cf599a..3893c9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -45,6 +45,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -63,6 +64,7 @@
import android.app.NotificationManager;
import android.os.Handler;
import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
@@ -77,6 +79,7 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.LogBufferEulogizer;
@@ -129,6 +132,7 @@
@Mock private GroupCoalescer mGroupCoalescer;
@Spy private RecordingCollectionListener mCollectionListener;
@Mock private CollectionReadyForBuildListener mBuildListener;
+ @Mock private NotificationDismissibilityProvider mDismissibilityProvider;
@Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
@Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
@@ -160,6 +164,7 @@
allowTestableLooperAsMainThread();
when(mEulogizer.record(any(Exception.class))).thenAnswer(i -> i.getArguments()[0]);
+ doReturn(Boolean.TRUE).when(mDismissibilityProvider).isDismissable(any());
mListenerInOrder = inOrder(mCollectionListener);
@@ -172,7 +177,7 @@
mBgExecutor,
mEulogizer,
mock(DumpManager.class),
- mock(NotificationDismissibilityProvider.class));
+ mDismissibilityProvider);
mCollection.attach(mGroupCoalescer);
mCollection.addCollectionListener(mCollectionListener);
mCollection.setBuildListener(mBuildListener);
@@ -1379,6 +1384,43 @@
}
@Test
+ @EnableFlags(Flags.FLAG_NOTIFICATIONS_DISMISS_PRUNED_SUMMARIES)
+ public void testDismissNotificationsIncludesPrunedParents() {
+ // GIVEN a collection with 2 groups; one has a single child, one has two.
+ mCollection.addNotificationDismissInterceptor(mInterceptor1);
+
+ NotifEvent notif1summary = mNoMan.postNotif(
+ buildNotif(TEST_PACKAGE, 1, "notif1summary").setGroup(mContext, "group1")
+ .setGroupSummary(mContext, true));
+ NotifEvent notif1child = mNoMan.postNotif(
+ buildNotif(TEST_PACKAGE, 1, "notif1child").setGroup(mContext, "group1"));
+ NotifEvent notif2summary = mNoMan.postNotif(
+ buildNotif(TEST_PACKAGE2, 2, "notif2summary").setGroup(mContext, "group2")
+ .setGroupSummary(mContext, true));
+ NotifEvent notif2child1 = mNoMan.postNotif(
+ buildNotif(TEST_PACKAGE2, 2, "notif2child1").setGroup(mContext, "group2"));
+ NotifEvent notif2child2 = mNoMan.postNotif(
+ buildNotif(TEST_PACKAGE2, 2, "notif2child2").setGroup(mContext, "group2"));
+ NotificationEntry entry1summary = mCollectionListener.getEntry(notif1summary.key);
+ NotificationEntry entry1child = mCollectionListener.getEntry(notif1child.key);
+ NotificationEntry entry2summary = mCollectionListener.getEntry(notif2summary.key);
+ NotificationEntry entry2child1 = mCollectionListener.getEntry(notif2child1.key);
+ NotificationEntry entry2child2 = mCollectionListener.getEntry(notif2child2.key);
+
+ // WHEN one child from each group are manually dismissed together
+ mCollection.dismissNotifications(
+ List.of(new Pair<>(entry1child, defaultStats(entry1child)),
+ new Pair<>(entry2child1, defaultStats(entry2child1))));
+
+ // THEN the summary for the singleton child is dismissed, but not the other summary
+ verify(mInterceptor1).shouldInterceptDismissal(entry1summary);
+ verify(mInterceptor1).shouldInterceptDismissal(entry1child);
+ verify(mInterceptor1, never()).shouldInterceptDismissal(entry2summary);
+ verify(mInterceptor1).shouldInterceptDismissal(entry2child1);
+ verify(mInterceptor1, never()).shouldInterceptDismissal(entry2child2);
+ }
+
+ @Test
public void testDismissAllNotificationsCallsRebuildOnce() {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));