Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c6cae217 authored by Valentin Iftime's avatar Valentin Iftime
Browse files

Use previous valid section key of a notification that was updated to an invalid section

 When a notification is updated to an ungroupable/invalid section, aggregate groups need to be updated and the internal GroupHelper state cleaned up.

Flag: android.service.notification.notification_force_grouping

Test: atest GroupHelperTest
Bug: 379467923
Change-Id: I77f040a51fd2cd8f946059b468bd6153aa835fdc
parent 92f601e8
Loading
Loading
Loading
Loading
+100 −19
Original line number Original line Diff line number Diff line
@@ -59,6 +59,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.HashSet;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Objects;
import java.util.Set;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Predicate;
@@ -243,7 +244,7 @@ public class GroupHelper {
                if (!sbn.isAppGroup()) {
                if (!sbn.isAppGroup()) {
                    sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists);
                    sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists);
                } else {
                } else {
                    maybeUngroupWithSections(record);
                    maybeUngroupOnAppGrouped(record);
                }
                }
            } else {
            } else {
                final StatusBarNotification sbn = record.getSbn();
                final StatusBarNotification sbn = record.getSbn();
@@ -553,11 +554,13 @@ public class GroupHelper {
    }
    }


    /**
    /**
     * A non-app grouped notification has been added or updated
     * A non-app-grouped notification has been added or updated
     * Evaluate if:
     * Evaluate if:
     * (a) an existing autogroup summary needs updated attributes
     * (a) an existing autogroup summary needs updated attributes
     * (b) a new autogroup summary needs to be added with correct attributes
     * (b) a new autogroup summary needs to be added with correct attributes
     * (c) other non-app grouped children need to be moved to the autogroup
     * (c) other non-app grouped children need to be moved to the autogroup
     * (d) the notification has been updated from a groupable to a non-groupable section and needs
     *  to trigger a cleanup
     *
     *
     * This method implements autogrouping with sections support.
     * This method implements autogrouping with sections support.
     *
     *
@@ -567,11 +570,11 @@ public class GroupHelper {
            boolean autogroupSummaryExists) {
            boolean autogroupSummaryExists) {
        final StatusBarNotification sbn = record.getSbn();
        final StatusBarNotification sbn = record.getSbn();
        boolean sbnToBeAutogrouped = false;
        boolean sbnToBeAutogrouped = false;

        final NotificationSectioner sectioner = getSection(record);
        final NotificationSectioner sectioner = getSection(record);
        if (sectioner == null) {
        if (sectioner == null) {
            maybeUngroupOnNonGroupableUpdate(record);
            if (DEBUG) {
            if (DEBUG) {
                Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
                Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
            }
            }
            return false;
            return false;
        }
        }
@@ -584,7 +587,6 @@ public class GroupHelper {
        if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
        if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
            return false;
            return false;
        }
        }

        synchronized (mAggregatedNotifications) {
        synchronized (mAggregatedNotifications) {
            ArrayMap<String, NotificationAttributes> ungrouped =
            ArrayMap<String, NotificationAttributes> ungrouped =
                mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
                mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
@@ -601,11 +603,11 @@ public class GroupHelper {
            if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
            if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
                if (DEBUG) {
                if (DEBUG) {
                    if (ungrouped.size() >= mAutoGroupAtCount) {
                    if (ungrouped.size() >= mAutoGroupAtCount) {
                        Log.i(TAG,
                        Slog.i(TAG,
                            "Found >=" + mAutoGroupAtCount
                            "Found >=" + mAutoGroupAtCount
                                + " ungrouped notifications => force grouping");
                                + " ungrouped notifications => force grouping");
                    } else {
                    } else {
                        Log.i(TAG, "Found aggregate summary => force grouping");
                        Slog.i(TAG, "Found aggregate summary => force grouping");
                    }
                    }
                }
                }


@@ -642,7 +644,24 @@ public class GroupHelper {
    }
    }


    /**
    /**
     * A notification was added that's app grouped.
     * A notification was added that was previously part of a valid section and needs to trigger
     * GH state cleanup.
     */
    private void maybeUngroupOnNonGroupableUpdate(NotificationRecord record) {
        maybeUngroupWithSections(record, getPreviousValidSectionKey(record));
    }

    /**
     * A notification was added that is app-grouped.
     */
    private void maybeUngroupOnAppGrouped(NotificationRecord record) {
        maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record));
    }

    /**
     * Called when a notification is posted and is either app-grouped or was previously part of
     * a valid section and needs to trigger GH state cleanup.
     *
     * Evaluate whether:
     * Evaluate whether:
     * (a) an existing autogroup summary needs updated attributes
     * (a) an existing autogroup summary needs updated attributes
     * (b) if we need to remove our autogroup overlay for this notification
     * (b) if we need to remove our autogroup overlay for this notification
@@ -652,13 +671,20 @@ public class GroupHelper {
     *
     *
     * And updates the internal state of un-app-grouped notifications and their flags.
     * And updates the internal state of un-app-grouped notifications and their flags.
     */
     */
    private void maybeUngroupWithSections(NotificationRecord record) {
    private void maybeUngroupWithSections(NotificationRecord record,
            @Nullable FullyQualifiedGroupKey fullAggregateGroupKey) {
        if (fullAggregateGroupKey == null) {
            if (DEBUG) {
                Slog.i(TAG,
                        "Skipping maybeUngroupWithSections for " + record
                            + " no valid section found.");
            }
            return;
        }

        final StatusBarNotification sbn = record.getSbn();
        final StatusBarNotification sbn = record.getSbn();
        final String pkgName = sbn.getPackageName();
        final String pkgName = sbn.getPackageName();
        final int userId = record.getUserId();
        final int userId = record.getUserId();
        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,
                pkgName, getSection(record));

        synchronized (mAggregatedNotifications) {
        synchronized (mAggregatedNotifications) {
            // if this notification still exists and has an autogroup overlay, but is now
            // if this notification still exists and has an autogroup overlay, but is now
            // grouped by the app, clear the overlay
            // grouped by the app, clear the overlay
@@ -675,21 +701,22 @@ public class GroupHelper {
                mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
                mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);


                if (DEBUG) {
                if (DEBUG) {
                    Log.i(TAG, "maybeUngroup removeAutoGroup: " + record);
                    Slog.i(TAG, "maybeUngroup removeAutoGroup: " + record);
                }
                }


                mCallback.removeAutoGroup(sbn.getKey());
                mCallback.removeAutoGroup(sbn.getKey());


                if (aggregatedNotificationsAttrs.isEmpty()) {
                if (aggregatedNotificationsAttrs.isEmpty()) {
                    if (DEBUG) {
                    if (DEBUG) {
                        Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
                        Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
                    }
                    }
                    mCallback.removeAutoGroupSummary(userId, pkgName,
                    mCallback.removeAutoGroupSummary(userId, pkgName,
                            fullAggregateGroupKey.toString());
                            fullAggregateGroupKey.toString());
                    mAggregatedNotifications.remove(fullAggregateGroupKey);
                    mAggregatedNotifications.remove(fullAggregateGroupKey);
                } else {
                } else {
                    if (DEBUG) {
                    if (DEBUG) {
                        Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
                        Slog.i(TAG,
                                "Aggregate group not empty, updating: " + fullAggregateGroupKey);
                    }
                    }
                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
                }
                }
@@ -860,8 +887,15 @@ public class GroupHelper {
        final StatusBarNotification sbn = record.getSbn();
        final StatusBarNotification sbn = record.getSbn();
        final String pkgName = sbn.getPackageName();
        final String pkgName = sbn.getPackageName();
        final int userId = record.getUserId();
        final int userId = record.getUserId();
        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,

                pkgName, getSection(record));
        final FullyQualifiedGroupKey fullAggregateGroupKey = getSectionGroupKeyWithFallback(record);
        if (fullAggregateGroupKey == null) {
            if (DEBUG) {
                Slog.i(TAG,
                        "Skipping autogroup cleanup for " + record + " no valid section found.");
            }
            return;
        }


        synchronized (mAggregatedNotifications) {
        synchronized (mAggregatedNotifications) {
            ArrayMap<String, NotificationAttributes> ungrouped =
            ArrayMap<String, NotificationAttributes> ungrouped =
@@ -879,14 +913,15 @@ public class GroupHelper {


                if (aggregatedNotificationsAttrs.isEmpty()) {
                if (aggregatedNotificationsAttrs.isEmpty()) {
                    if (DEBUG) {
                    if (DEBUG) {
                        Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
                        Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
                    }
                    }
                    mCallback.removeAutoGroupSummary(userId, pkgName,
                    mCallback.removeAutoGroupSummary(userId, pkgName,
                            fullAggregateGroupKey.toString());
                            fullAggregateGroupKey.toString());
                    mAggregatedNotifications.remove(fullAggregateGroupKey);
                    mAggregatedNotifications.remove(fullAggregateGroupKey);
                } else {
                } else {
                    if (DEBUG) {
                    if (DEBUG) {
                        Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
                        Slog.i(TAG,
                                "Aggregate group not empty, updating: " + fullAggregateGroupKey);
                    }
                    }
                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
                    updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
                }
                }
@@ -900,6 +935,52 @@ public class GroupHelper {
        }
        }
    }
    }


    /**
     * Get the section key for a notification. If the section is invalid, ie. notification is not
     * auto-groupable, then return the previous valid section, if any.
     * @param record the notification
     * @return a section group key, null if not found
     */
    @Nullable
    private FullyQualifiedGroupKey getSectionGroupKeyWithFallback(final NotificationRecord record) {
        final NotificationSectioner sectioner = getSection(record);
        if (sectioner != null) {
            return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(),
                sectioner);
        } else {
            return getPreviousValidSectionKey(record);
        }
    }

    /**
     * Get the previous valid section key of a notification that may have been updated to an invalid
     * section. This is needed in case a notification is updated as an ungroupable (invalid section)
     *  => auto-groups need to be updated/GH state cleanup.
     * @param record the notification
     * @return a section group key or null if not found
     */
    @Nullable
    private FullyQualifiedGroupKey getPreviousValidSectionKey(final NotificationRecord record) {
        synchronized (mAggregatedNotifications) {
            final String recordKey = record.getKey();
            // Search in ungrouped
            for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
                        ungroupedSection : mUngroupedAbuseNotifications.entrySet()) {
                if (ungroupedSection.getValue().containsKey(recordKey)) {
                    return ungroupedSection.getKey();
                }
            }
            // Search in aggregated
            for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
                    aggregatedSection : mAggregatedNotifications.entrySet()) {
                if (aggregatedSection.getValue().containsKey(recordKey)) {
                    return aggregatedSection.getKey();
                }
            }
        }
        return null;
    }

    /**
    /**
     * Called when a child notification is removed, after some delay, so that this helper can
     * Called when a child notification is removed, after some delay, so that this helper can
     * trigger a forced grouping if the group has become sparse/singleton
     * trigger a forced grouping if the group has become sparse/singleton
+171 −0
Original line number Original line Diff line number Diff line
@@ -2337,6 +2337,177 @@ public class GroupHelperTest extends UiServiceTestCase {
        verifyZeroInteractions(mCallback);
        verifyZeroInteractions(mCallback);
    }
    }


    @Test
    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
    public void testUpdateToUngroupableSection_cleanupUngrouped() {
        final String pkg = "package";
        // Post notification w/o group in a valid section
        NotificationRecord notification = spy(getNotificationRecord(pkg, 0, "", mUser,
                "", false, IMPORTANCE_LOW));
        Notification n = mock(Notification.class);
        StatusBarNotification sbn = spy(getSbn(pkg, 0, "0", UserHandle.SYSTEM));
        when(notification.getNotification()).thenReturn(n);
        when(notification.getSbn()).thenReturn(sbn);
        when(sbn.getNotification()).thenReturn(n);
        when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
        assertThat(GroupHelper.getSection(notification)).isNotNull();
        mGroupHelper.onNotificationPosted(notification, false);

        // Update notification to invalid section
        when(n.isStyle(Notification.CallStyle.class)).thenReturn(true);
        assertThat(GroupHelper.getSection(notification)).isNull();
        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false);
        assertThat(needsAutogrouping).isFalse();

        // Check that GH internal state (ungrouped list) was cleaned-up
        // Post AUTOGROUP_AT_COUNT-1 notifications => should not autogroup
        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
            int id = 42 + i;
            notification = getNotificationRecord(pkg, id, "" + id, mUser,
                null, false, IMPORTANCE_LOW);
            mGroupHelper.onNotificationPosted(notification, false);
        }

        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
                anyString(), anyInt(), any());
        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
    }

    @Test
    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
    public void testUpdateToUngroupableSection_afterAutogroup_isUngrouped() {
        final String pkg = "package";
        final List<NotificationRecord> notificationList = new ArrayList<>();
        // Post notification w/o group in a valid section
        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
            NotificationRecord notification = spy(getNotificationRecord(pkg, i, "" + i, mUser,
                    "", false, IMPORTANCE_LOW));
            Notification n = mock(Notification.class);
            StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM));
            when(notification.getNotification()).thenReturn(n);
            when(notification.getSbn()).thenReturn(sbn);
            when(sbn.getNotification()).thenReturn(n);
            when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
            assertThat(GroupHelper.getSection(notification)).isNotNull();
            mGroupHelper.onNotificationPosted(notification, false);
            notificationList.add(notification);
        }

        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                eq(expectedGroupKey), anyInt(), any());
        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
                eq(expectedGroupKey), eq(true));

        // Update a notification to invalid section
        Mockito.reset(mCallback);
        final NotificationRecord notifToInvalidate = notificationList.get(0);
        when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
                true);
        assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, true);
        assertThat(needsAutogrouping).isFalse();

        // Check that the updated notification was removed from the autogroup
        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
                eq(expectedGroupKey), any());
    }

    @Test
    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
    public void testUpdateToUngroupableSection_onRemoved_isUngrouped() {
        final String pkg = "package";
        final List<NotificationRecord> notificationList = new ArrayList<>();
        // Post notification w/o group in a valid section
        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
            NotificationRecord notification = spy(getNotificationRecord(pkg, i, "" + i, mUser,
                    "", false, IMPORTANCE_LOW));
            Notification n = mock(Notification.class);
            StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM));
            when(notification.getNotification()).thenReturn(n);
            when(notification.getSbn()).thenReturn(sbn);
            when(sbn.getNotification()).thenReturn(n);
            when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
            assertThat(GroupHelper.getSection(notification)).isNotNull();
            mGroupHelper.onNotificationPosted(notification, false);
            notificationList.add(notification);
        }

        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                eq(expectedGroupKey), anyInt(), any());
        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
                eq(expectedGroupKey), eq(true));

        // Update a notification to invalid section and removed it
        Mockito.reset(mCallback);
        final NotificationRecord notifToInvalidate = notificationList.get(0);
        when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
                true);
        assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
        notificationList.remove(notifToInvalidate);
        mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList);

        // Check that the autogroup was updated
        verify(mCallback, never()).removeAutoGroup(anyString());
        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
                eq(expectedGroupKey), any());
    }

    @Test
    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
    public void testUpdateToUngroupableSection_afterForceGrouping_isUngrouped() {
        final String pkg = "package";
        final String groupName = "testGroup";
        final List<NotificationRecord> notificationList = new ArrayList<>();
        final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
        // Post valid section summary notifications without children => force group
        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
            NotificationRecord notification = spy(getNotificationRecord(mPkg, i, "" + i, mUser,
                    groupName, true, IMPORTANCE_LOW));
            Notification n = mock(Notification.class);
            StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM, groupName));
            when(notification.getNotification()).thenReturn(n);
            when(notification.getSbn()).thenReturn(sbn);
            when(n.getGroup()).thenReturn(groupName);
            when(sbn.getNotification()).thenReturn(n);
            when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
            assertThat(GroupHelper.getSection(notification)).isNotNull();
            notificationList.add(notification);
            mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
                    summaryByGroup);
        }

        final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                eq(expectedGroupKey), anyInt(), any());
        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
                eq(expectedGroupKey), eq(true));

        // Update a notification to invalid section
        Mockito.reset(mCallback);
        final NotificationRecord notifToInvalidate = notificationList.get(0);
        when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
                true);
        assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
        boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, true);
        assertThat(needsAutogrouping).isFalse();

        // Check that GH internal state (ungrouped list) was cleaned-up
        verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
                eq(expectedGroupKey), any());
    }

    @Test
    @Test
    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
    public void testMoveAggregateGroups_updateChannel() {
    public void testMoveAggregateGroups_updateChannel() {