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

Commit da13ac47 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Regroup app-grouped notifications after classification" into main

parents b4be9771 7b433205
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -66,3 +66,10 @@ flag {
   description: "Allows the NAS to create and modify conversation notifications"
   bug: "373599715"
}

flag {
  name: "notification_regroup_on_classification"
  namespace: "systemui"
  description: "This flag controls regrouping after notification classification"
  bug: "372775153"
}
+170 −4
Original line number Diff line number Diff line
@@ -25,8 +25,10 @@ import static android.app.Notification.FLAG_ONGOING_EVENT;
import static android.app.Notification.VISIBILITY_PRIVATE;
import static android.app.Notification.VISIBILITY_PUBLIC;
import static android.service.notification.Flags.notificationForceGrouping;
import static android.service.notification.Flags.notificationRegroupOnClassification;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -49,6 +51,9 @@ import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -83,10 +88,22 @@ public class GroupHelper {
    //  with less than this value, they will be forced grouped
    private static final int MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING = 3;

    // Regrouping needed because the channel was updated, ie. importance changed
    static final int REGROUP_REASON_CHANNEL_UPDATE = 0;
    // Regrouping needed because of notification bundling
    static final int REGROUP_REASON_BUNDLE = 1;

    @IntDef(prefix = { "REGROUP_REASON_" }, value = {
        REGROUP_REASON_CHANNEL_UPDATE,
        REGROUP_REASON_BUNDLE,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface RegroupingReason {}

    private final Callback mCallback;
    private final int mAutoGroupAtCount;
    private final int mAutogroupSparseGroupsAtCount;
    private final int mAutoGroupRegroupingAtCount;
    private final Context mContext;
    private final PackageManager mPackageManager;
    private boolean mIsTestHarnessExempted;
@@ -173,6 +190,11 @@ public class GroupHelper {
        mContext = context;
        mPackageManager = packageManager;
        mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount;
        if (notificationRegroupOnClassification()) {
            mAutoGroupRegroupingAtCount = 1;
        } else {
            mAutoGroupRegroupingAtCount = mAutoGroupAtCount;
        }
        NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections();
    }

@@ -865,7 +887,8 @@ public class GroupHelper {
                }
            }

            regroupNotifications(userId, pkgName, notificationsToCheck);
            regroupNotifications(userId, pkgName, notificationsToCheck,
                    REGROUP_REASON_CHANNEL_UPDATE);
        }
    }

@@ -883,13 +906,14 @@ public class GroupHelper {
            ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
            notificationsToCheck.put(record.getKey(), record);
            regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
                    notificationsToCheck);
                    notificationsToCheck, REGROUP_REASON_BUNDLE);
        }
    }

    @GuardedBy("mAggregatedNotifications")
    private void regroupNotifications(int userId, String pkgName,
            ArrayMap<String, NotificationRecord> notificationsToCheck) {
            ArrayMap<String, NotificationRecord> notificationsToCheck,
            @RegroupingReason int regroupingReason) {
        // The list of notification operations required after the channel update
        final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();

@@ -904,12 +928,42 @@ public class GroupHelper {
        notificationsToMove.addAll(
                getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));

        // Handle "grouped correctly" notifications that were re-classified (bundled)
        if (notificationRegroupOnClassification()) {
            if (regroupingReason == REGROUP_REASON_BUNDLE) {
                notificationsToMove.addAll(
                        getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
            }
        }

        // Batch move to new section
        if (!notificationsToMove.isEmpty()) {
            moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
        }
    }

    private List<NotificationMoveOp> getReclassifiedNotificationsMoveOps(int userId,
                String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck) {
        final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
        for (NotificationRecord record : notificationsToCheck.values()) {
            if (isChildOfValidAppGroup(record)) {
                // Check if section changes
                NotificationSectioner sectioner = getSection(record);
                if (sectioner != null) {
                    FullyQualifiedGroupKey newFullAggregateGroupKey =
                            new FullyQualifiedGroupKey(userId, pkgName, sectioner);
                    if (DEBUG) {
                        Slog.v(TAG, "Regroup after classification: " + record + " to: "
                                + newFullAggregateGroupKey);
                    }
                    notificationsToMove.add(
                            new NotificationMoveOp(record, null, newFullAggregateGroupKey));
                }
            }
        }
        return notificationsToMove;
    }

    @GuardedBy("mAggregatedNotifications")
    private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName,
            ArrayMap<String, NotificationRecord> notificationsToCheck) {
@@ -1010,6 +1064,10 @@ public class GroupHelper {
        // Bundled operations to apply to groups affected by the channel update
        ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>();

        // App-provided (valid) groups of notifications that were classified (bundled).
        // Summaries will be canceled if all child notifications have been bundled.
        ArrayMap<String, String> originalGroupsOfBundledNotifications = new ArrayMap<>();

        for (NotificationMoveOp moveOp: notificationsToMove) {
            final NotificationRecord record = moveOp.record;
            final FullyQualifiedGroupKey oldFullAggregateGroupKey = moveOp.oldGroup;
@@ -1035,6 +1093,13 @@ public class GroupHelper {
                    groupsToUpdate.put(oldFullAggregateGroupKey,
                        new GroupUpdateOp(oldFullAggregateGroupKey, record, true));
                }
            } else {
                if (notificationRegroupOnClassification()) {
                    // Null "old aggregate group" => this notification was re-classified from
                    // a valid app-provided group => maybe cancel the original summary
                    // if no children are left
                    originalGroupsOfBundledNotifications.put(record.getKey(), record.getGroupKey());
                }
            }

            // Add moved notifications to the ungrouped list for new group and do grouping
@@ -1076,7 +1141,7 @@ public class GroupHelper {
            NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record;
            boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary;
            //Group needs to be created/updated
            if (ungrouped.size() >= mAutoGroupAtCount
            if (ungrouped.size() >= mAutoGroupRegroupingAtCount
                    || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) {
                NotificationSectioner sectioner = getSection(triggeringNotification);
                if (sectioner == null) {
@@ -1092,6 +1157,18 @@ public class GroupHelper {
                }
            }
        }

        if (notificationRegroupOnClassification()) {
            // Cancel the summary if it's the last notification of the original app-provided group
            for (String triggeringKey : originalGroupsOfBundledNotifications.keySet()) {
                NotificationRecord canceledSummary =
                        mCallback.removeAppProvidedSummaryOnClassification(triggeringKey,
                        originalGroupsOfBundledNotifications.getOrDefault(triggeringKey, null));
                if (canceledSummary != null) {
                    cacheCanceledSummary(canceledSummary);
                }
            }
        }
    }

    static String getFullAggregateGroupKey(String pkgName,
@@ -1113,6 +1190,42 @@ public class GroupHelper {
        return (record.mOriginalFlags & Notification.FLAG_AUTOGROUP_SUMMARY) != 0;
    }

    private boolean isNotificationAggregatedInSection(NotificationRecord record,
            NotificationSectioner sectioner) {
        final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(
                record.getUserId(), record.getSbn().getPackageName(), sectioner);
        return record.getGroupKey().equals(fullAggregateGroupKey.toString());
    }

    private boolean isChildOfValidAppGroup(NotificationRecord record) {
        final StatusBarNotification sbn = record.getSbn();
        if (!sbn.isAppGroup()) {
            return false;
        }

        if (!sbn.getNotification().isGroupChild()) {
            return false;
        }

        if (record.isCanceled) {
            return false;
        }

        final NotificationSectioner sectioner = getSection(record);
        if (sectioner == null) {
            if (DEBUG) {
                Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
            }
            return false;
        }

        if (isNotificationAggregatedInSection(record, sectioner)) {
            return false;
        }

        return true;
    }

    private static int getNumChildrenForGroup(@NonNull final String groupKey,
            final List<NotificationRecord> notificationList) {
        //TODO (b/349072751): track grouping state in GroupHelper -> do not use notificationList
@@ -1438,6 +1551,48 @@ public class GroupHelper {
        }
    }

    protected void dump(PrintWriter pw, String prefix) {
        synchronized (mAggregatedNotifications) {
            if (!mUngroupedAbuseNotifications.isEmpty()) {
                pw.println(prefix + "Ungrouped notifications:");
                for (FullyQualifiedGroupKey groupKey: mUngroupedAbuseNotifications.keySet()) {
                    if (!mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>())
                            .isEmpty()) {
                        pw.println(prefix + prefix + groupKey.toString());
                        for (String notifKey : mUngroupedAbuseNotifications.get(groupKey)
                                .keySet()) {
                            pw.println(prefix + prefix + prefix + notifKey);
                        }
                    }
                }
                pw.println("");
            }

            if (!mAggregatedNotifications.isEmpty()) {
                pw.println(prefix + "Autogrouped notifications:");
                for (FullyQualifiedGroupKey groupKey: mAggregatedNotifications.keySet()) {
                    if (!mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>())
                            .isEmpty()) {
                        pw.println(prefix + prefix + groupKey.toString());
                        for (String notifKey : mAggregatedNotifications.get(groupKey).keySet()) {
                            pw.println(prefix + prefix + prefix + notifKey);
                        }
                    }
                }
                pw.println("");
            }

            if (!mCanceledSummaries.isEmpty()) {
                pw.println(prefix + "Cached canceled summaries:");
                for (CachedSummary summary: mCanceledSummaries.values()) {
                    pw.println(prefix + prefix + prefix + summary.key + " -> "
                            + summary.originalGroupKey);
                }
                pw.println("");
            }
        }
    }

    protected static class NotificationSectioner {
        final String mName;
        final int mSummaryId;
@@ -1551,5 +1706,16 @@ public class GroupHelper {

        void removeNotificationFromCanceledGroup(int userId, String pkg, String groupKey,
                int cancelReason);

        /**
         * Cancels the group summary of a notification that was regrouped because of classification
         *  (bundling). Only cancels if the summary is the last notification of the original group.
         * @param triggeringKey the triggering child notification key
         * @param groupKey the original group key
         * @return the canceled group summary or null if the summary was not canceled
         */
        @Nullable
        NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey,
                @Nullable String groupKey);
    }
}
+59 −0
Original line number Diff line number Diff line
@@ -3008,6 +3008,16 @@ public class NotificationManagerService extends SystemService {
                            groupKey, REASON_APP_CANCEL, SystemClock.elapsedRealtime());
                }
            }
            @Override
            @Nullable
            public NotificationRecord removeAppProvidedSummaryOnClassification(String triggeringKey,
                    @Nullable String oldGroupKey) {
                synchronized (mNotificationLock) {
                    return removeAppProvidedSummaryOnClassificationLocked(triggeringKey,
                            oldGroupKey);
                }
            }
        });
    }
@@ -7150,6 +7160,50 @@ public class NotificationManagerService extends SystemService {
        }
    }
    @GuardedBy("mNotificationLock")
    @Nullable
    NotificationRecord removeAppProvidedSummaryOnClassificationLocked(String triggeringKey,
            @Nullable String oldGroupKey) {
        NotificationRecord canceledSummary = null;
        NotificationRecord r = mNotificationsByKey.get(triggeringKey);
        if (r == null || oldGroupKey == null) {
            return null;
        }
        if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) {
            NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey);
            // We only care about app-provided valid groups
            if (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary)) {
                List<NotificationRecord> notificationsInGroup =
                        findGroupNotificationsLocked(r.getSbn().getPackageName(),
                            oldGroupKey, r.getUserId());
                // Remove the app-provided summary if only the summary is left in the
                // original group, or summary + triggering notification that will be
                // regrouped
                boolean isOnlySummaryLeft =
                        (notificationsInGroup.size() <= 1)
                            || (notificationsInGroup.size() == 2
                            && notificationsInGroup.contains(r)
                            && notificationsInGroup.contains(groupSummary));
                if (isOnlySummaryLeft) {
                    if (DBG) {
                        Slog.i(TAG, "Removing app summary (all children bundled): "
                                + groupSummary);
                    }
                    canceledSummary = groupSummary;
                    mSummaryByGroupKey.remove(oldGroupKey);
                    cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(),
                            groupSummary.getSbn().getPackageName(),
                            groupSummary.getSbn().getTag(),
                            groupSummary.getSbn().getId(), 0, 0, false, groupSummary.getUserId(),
                            NotificationListenerService.REASON_GROUP_OPTIMIZATION, null);
                }
            }
        }
        return canceledSummary;
    }
    @GuardedBy("mNotificationLock")
    private boolean hasAutoGroupSummaryLocked(NotificationRecord record) {
        final String autbundledGroupKey;
@@ -7530,6 +7584,11 @@ public class NotificationManagerService extends SystemService {
                    mTtlHelper.dump(pw, "    ");
                }
            }
            if (notificationForceGrouping()) {
                pw.println("\n  GroupHelper:");
                mGroupHelper.dump(pw, "    ");
            }
        }
    }
+360 −5

File changed.

Preview size limit exceeded, changes collapsed.

+36 −0
Original line number Diff line number Diff line
@@ -113,6 +113,7 @@ import static android.service.notification.Condition.STATE_TRUE;
import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT;
import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
import static android.service.notification.Flags.FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION;
import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
@@ -2682,6 +2683,41 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(0));
    }
    @Test
    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
    public void testAggregateGroups_RemoveAppSummary_onClassification() throws Exception {
        final String originalGroupName = "originalGroup";
        final int summaryId = 0;
        final NotificationRecord r1 = generateNotificationRecord(mTestNotificationChannel,
                summaryId + 1, originalGroupName, false);
        mService.addNotification(r1);
        final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel,
                summaryId + 2, originalGroupName, false);
        mService.addNotification(r2);
        final NotificationRecord summary = generateNotificationRecord(mTestNotificationChannel,
                summaryId, originalGroupName, true);
        mService.addNotification(summary);
        final String originalGroupKey = summary.getGroupKey();
        assertThat(mService.mSummaryByGroupKey).containsEntry(originalGroupKey, summary);
        // Regroup first child notification
        r1.setOverrideGroupKey("newGroup");
        // Check that removeAppProvidedSummaryOnClassificationLocked is null
        //  => there is still one child left in the original group
        assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r1.getKey(),
                originalGroupKey)).isNull();
        // Regroup last child notification
        r2.setOverrideGroupKey("newGroup");
        // Check that removeAppProvidedSummaryOnClassificationLocked returns the original summary
        //  and that the original app-provided summary is canceled
        assertThat(mService.removeAppProvidedSummaryOnClassificationLocked(r2.getKey(),
                originalGroupKey)).isEqualTo(summary);
        waitForIdle();
        verify(mWorkerHandler, times(1)).scheduleCancelNotification(any(), eq(summaryId));
        assertThat(mService.mSummaryByGroupKey).doesNotContainKey(originalGroupKey);
    }
    @Test
    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
    public void testUngroupingAggregateSummary() throws Exception {