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

Commit dee5fc96 authored by Yuri Lin's avatar Yuri Lin
Browse files

Refactor methods for returning groups with channel lists.

This brings the logic for filling in the channels associated with a group given the list of channels and options for which channels and groups to include. This refactor is in preparation for caching group info on the client side.

Bug: 381131846
Flag: EXEMPT refactor
Test: PreferencesHelperTest, NotificationChannelGroupsHelperTest
Change-Id: I95d27e3c3ded0204cf41e1b8e1f74adc7396850b
parent 78565e1a
Loading
Loading
Loading
Loading
+186 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.notification;

import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationManager.IMPORTANCE_NONE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.service.notification.Flags;
import android.util.ArrayMap;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * NotificationChannelGroupHelper contains helper methods for associating channels with the groups
 * they belong to, matching by ID.
 */
public class NotificationChannelGroupsHelper {
    /**
     * Set of parameters passed into
     * {@link NotificationChannelGroupsHelper#getGroupsWithChannels(Collection, Map, Params)}.
     *
     * @param includeDeleted Whether to include deleted channels.
     * @param includeNonGrouped Whether to include channels that are not associated with a group.
     * @param includeEmpty Whether to include groups containing no channels.
     * @param includeAllBlockedWithFilter Whether to include channels that are blocked from
     *                                    sending notifications along with channels specified by
     *                                    the filter. This setting only takes effect when
     *                                    channelFilter is not {@code null}, and if true will
     *                                    include all blocked channels in the output (regardless
     *                                    of whether they are included in the filter).
     * @param channelFilter If non-null, a specific set of channels to include. If a channel
     *                      matching this filter is blocked, it will still be included even
     *                      if includeAllBlockedWithFilter=false.
     */
    public record Params(
            boolean includeDeleted,
            boolean includeNonGrouped,
            boolean includeEmpty,
            boolean includeAllBlockedWithFilter,
            Set<String> channelFilter
    ) {
        /**
         * Default set of parameters used to specify the behavior of
         * {@link INotificationManager#getNotificationChannelGroups(String)}. This will include
         * output for all groups, including those without channels, but not any ungrouped channels.
         */
        public static Params forAllGroups() {
            return new Params(
                    /* includeDeleted= */ false,
                    /* includeNonGrouped= */ false,
                    /* includeEmpty= */ true,
                    /* includeAllBlockedWithFilter= */ true,
                    /* channelFilter= */ null);
        }

        /**
         * Parameters to get groups for all channels, including those not associated with any groups
         * and optionally including deleted channels as well. Channels not associated with a group
         * are returned inside a group with id {@code null}.
         *
         * @param includeDeleted Whether to include deleted channels.
         */
        public static Params forAllChannels(boolean includeDeleted) {
            return new Params(
                    includeDeleted,
                    /* includeNonGrouped= */ true,
                    /* includeEmpty= */ false,
                    /* includeAllBlockedWithFilter= */ true,
                    /* channelFilter= */ null);
        }

        /**
         * Parameters to collect groups only for channels specified by the channel filter, as well
         * as any blocked channels (independent of whether they exist in the filter).
         * @param channelFilter Specific set of channels to return.
         */
        public static Params onlySpecifiedOrBlockedChannels(Set<String> channelFilter) {
            return new Params(
                    /* includeDeleted= */ false,
                    /* includeNonGrouped= */ true,
                    /* includeEmpty= */ false,
                    /* includeAllBlockedWithFilter= */ true,
                    channelFilter);
        }
    }

    /**
     * Retrieve the {@link NotificationChannelGroup} object specified by the given groupId, if it
     * exists, with the list of channels filled in from the provided available channels.
     *
     * @param groupId The ID of the group to return.
     * @param allChannels A list of all channels associated with the package.
     * @param allGroups A map of group ID -> NotificationChannelGroup objects.
     */
    public static @Nullable NotificationChannelGroup getGroupWithChannels(@NonNull String groupId,
            @NonNull Collection<NotificationChannel> allChannels,
            @NonNull Map<String, NotificationChannelGroup> allGroups,
            boolean includeDeleted) {
        NotificationChannelGroup group = null;
        if (allGroups.containsKey(groupId)) {
            group = allGroups.get(groupId).clone();
            group.setChannels(new ArrayList<>());
            for (NotificationChannel nc : allChannels) {
                if (includeDeleted || !nc.isDeleted()) {
                    if (groupId.equals(nc.getGroup())) {
                        group.addChannel(nc);
                    }
                }
            }
        }
        return group;
    }

    /**
     * Returns a list of groups with their associated channels filled in.
     *
     * @param allChannels All available channels that may be associated with these groups.
     * @param allGroups Map of group ID -> {@link NotificationChannelGroup} objects.
     * @param params Params indicating which channels and which groups to include.
     */
    public static @NonNull List<NotificationChannelGroup> getGroupsWithChannels(
            @NonNull Collection<NotificationChannel> allChannels,
            @NonNull Map<String, NotificationChannelGroup> allGroups,
            Params params) {
        Map<String, NotificationChannelGroup> outputGroups = new ArrayMap<>();
        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
        for (NotificationChannel nc : allChannels) {
            boolean includeChannel = (params.includeDeleted || !nc.isDeleted())
                    && (params.channelFilter == null
                            || (params.includeAllBlockedWithFilter
                                    && nc.getImportance() == IMPORTANCE_NONE)
                            || params.channelFilter.contains(nc.getId()))
                    && (!Flags.notificationClassification()
                            || !SYSTEM_RESERVED_IDS.contains(nc.getId()));
            if (includeChannel) {
                if (nc.getGroup() != null) {
                    if (allGroups.get(nc.getGroup()) != null) {
                        NotificationChannelGroup ncg = outputGroups.get(nc.getGroup());
                        if (ncg == null) {
                            ncg = allGroups.get(nc.getGroup()).clone();
                            ncg.setChannels(new ArrayList<>());
                            outputGroups.put(nc.getGroup(), ncg);
                        }
                        ncg.addChannel(nc);
                    }
                } else {
                    nonGrouped.addChannel(nc);
                }
            }
        }
        if (params.includeNonGrouped && nonGrouped.getChannels().size() > 0) {
            outputGroups.put(null, nonGrouped);
        }
        if (params.includeEmpty) {
            for (NotificationChannelGroup group : allGroups.values()) {
                if (!outputGroups.containsKey(group.getId())) {
                    outputGroups.put(group.getId(), group);
                }
            }
        }
        return new ArrayList<>(outputGroups.values());
    }
}
+268 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.notification;

import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_NONE;

import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupWithChannels;
import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupsWithChannels;

import static com.google.common.truth.Truth.assertThat;

import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.internal.notification.NotificationChannelGroupsHelper.Params;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

@RunWith(AndroidJUnit4.class)
public class NotificationChannelGroupsHelperTest {
    private Collection<NotificationChannel> mChannels;
    private Map<String, NotificationChannelGroup> mGroups;

    @Before
    public void setUp() {
        // Test data setup.
        // Channels and their corresponding groups:
        //   * "regular": a channel that is not deleted or blocked. In group A.
        //   * "blocked": blocked channel. In group A.
        //   * "deleted": deleted channel. In group A.
        //   * "adrift": regular channel. No group.
        //   * "gone": deleted channel. No group.
        //   * "alternate": regular channel. In group B.
        //   * "another blocked": blocked channel. In group B.
        //   * "another deleted": deleted channel. In group C.
        //   * Additionally, there is an empty group D.
        mChannels = List.of(makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false),
                makeChannel("deleted", "a", false, true),
                makeChannel("adrift", null, false, false),
                makeChannel("gone", null, false, true),
                makeChannel("alternate", "b", false, false),
                makeChannel("anotherBlocked", "b", true, false),
                makeChannel("anotherDeleted", "c", false, true));

        mGroups = Map.of("a", new NotificationChannelGroup("a", "a"),
                "b", new NotificationChannelGroup("b", "b"),
                "c", new NotificationChannelGroup("c", "c"),
                "d", new NotificationChannelGroup("d", "d"));
    }

    @Test
    public void testGetGroup_noDeleted() {
        NotificationChannelGroup res = getGroupWithChannels("a", mChannels, mGroups, false);
        assertThat(res).isNotNull();
        assertThat(res.getChannels()).hasSize(2);  // "regular" & "blocked"
        assertThat(res.getChannels()).containsExactlyElementsIn(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false)));
    }

    @Test
    public void testGetGroup_includeDeleted() {
        NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, true);
        assertThat(res).isNotNull();
        assertThat(res.getChannels()).hasSize(1);
        assertThat(res.getChannels().getFirst()).isEqualTo(
                makeChannel("anotherDeleted", "c", false, true));
    }

    @Test
    public void testGetGroup_empty() {
        NotificationChannelGroup res = getGroupWithChannels("d", mChannels, mGroups, true);
        assertThat(res).isNotNull();
        assertThat(res.getChannels()).isEmpty();
    }

    @Test
    public void testGetGroup_emptyBecauseNoChannelMatch() {
        NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, false);
        assertThat(res).isNotNull();
        assertThat(res.getChannels()).isEmpty();
    }

    @Test
    public void testGetGroup_nonexistent() {
        NotificationChannelGroup res = getGroupWithChannels("e", mChannels, mGroups, true);
        assertThat(res).isNull();
    }

    @Test
    public void testGetGroups_paramsForAllGroups() {
        // deleted=false, nongrouped=false, empty=true, blocked=true, no channel filter
        List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
                Params.forAllGroups());

        NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
        expectedA.setChannels(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false)));

        NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
        expectedB.setChannels(List.of(
                makeChannel("alternate", "b", false, false),
                makeChannel("anotherBlocked", "b", true, false)));

        NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
        expectedC.setChannels(new ArrayList<>());  // empty, no deleted

        NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
        expectedD.setChannels(new ArrayList<>());  // empty

        assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD);
    }

    @Test
    public void testGetGroups_paramsForAllChannels_noDeleted() {
        // Excluding deleted channels to means group C is not included because it's "empty"
        List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
                Params.forAllChannels(false));

        NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
        expectedA.setChannels(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false)));

        NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
        expectedB.setChannels(List.of(
                makeChannel("alternate", "b", false, false),
                makeChannel("anotherBlocked", "b", true, false)));

        NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
        expectedUngrouped.setChannels(List.of(
                makeChannel("adrift", null, false, false),
                makeChannel("gone", null, false, true)));

        assertThat(res).containsExactly(expectedA, expectedB, expectedUngrouped);
    }

    @Test
    public void testGetGroups_paramsForAllChannels_withDeleted() {
        // This will get everything!
        List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
                Params.forAllChannels(true));

        NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
        expectedA.setChannels(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false),
                makeChannel("deleted", "a", false, true)));

        NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
        expectedB.setChannels(List.of(
                makeChannel("alternate", "b", false, false),
                makeChannel("anotherBlocked", "b", true, false)));

        NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
        expectedC.setChannels(List.of(makeChannel("anotherDeleted", "c", false, true)));

        // no D, because D is empty

        NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
        expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));

        assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedUngrouped);
    }

    @Test
    public void testGetGroups_onlySpecifiedOrBlocked() {
        Set<String> filter = Set.of("regular", "blocked", "adrift", "anotherDeleted");

        // also not including deleted channels to check intersection of those params
        List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
                Params.onlySpecifiedOrBlockedChannels(filter));

        NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
        expectedA.setChannels(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false)));

        // While nothing matches the filter from group B, includeBlocked=true means all blocked
        // channels are included even if they are not in the filter.
        NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
        expectedB.setChannels(List.of(makeChannel("anotherBlocked", "b", true, false)));

        NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
        expectedC.setChannels(new ArrayList<>());  // deleted channel not included

        NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
        expectedD.setChannels(new ArrayList<>());  // empty

        NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
        expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));

        assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD,
                expectedUngrouped);
    }


    @Test
    public void testGetGroups_noBlockedWithFilter() {
        Set<String> filter = Set.of("regular", "blocked", "adrift");

        // The includeBlocked setting only takes effect if there is a channel filter.
        List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
                new Params(true, true, true, false, filter));

        // Even though includeBlocked=false, "blocked" is included because it's explicitly specified
        // by the channel filter.
        NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
        expectedA.setChannels(List.of(
                makeChannel("regular", "a", false, false),
                makeChannel("blocked", "a", true, false)));

        NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
        expectedB.setChannels(new ArrayList<>());  // no matches; blocked channel not in filter

        NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
        expectedC.setChannels(new ArrayList<>());  // no matches

        NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
        expectedD.setChannels(new ArrayList<>());  // empty

        NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
        expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));

        assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD,
                expectedUngrouped);
    }

    private NotificationChannel makeChannel(String id, String groupId, boolean blocked,
            boolean deleted) {
        NotificationChannel c = new NotificationChannel(id, id,
                blocked ? IMPORTANCE_NONE : IMPORTANCE_DEFAULT);
        if (deleted) {
            c.setDeleted(true);
        }
        if (groupId != null) {
            c.setGroup(groupId);
        }
        return c;
    }
}
+9 −6
Original line number Diff line number Diff line
@@ -346,6 +346,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.NotificationChannelGroupsHelper;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
@@ -5005,8 +5006,8 @@ public class NotificationManagerService extends SystemService {
        public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(
                String pkg) {
            checkCallerIsSystemOrSameApp(pkg);
            return mPreferencesHelper.getNotificationChannelGroups(
                    pkg, Binder.getCallingUid(), false, false, true, true, null);
            return mPreferencesHelper.getNotificationChannelGroups(pkg, Binder.getCallingUid(),
                    NotificationChannelGroupsHelper.Params.forAllGroups());
        }
        @Override
@@ -5126,8 +5127,9 @@ public class NotificationManagerService extends SystemService {
        public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage(
                String pkg, int uid, boolean includeDeleted) {
            enforceSystemOrSystemUI("getNotificationChannelGroupsForPackage");
            return mPreferencesHelper.getNotificationChannelGroups(
                    pkg, uid, includeDeleted, true, false, true, null);
            return mPreferencesHelper.getNotificationChannelGroups(pkg, uid,
                    new NotificationChannelGroupsHelper.Params(includeDeleted, true, false, true,
                            null));
        }
        @Override
@@ -5155,8 +5157,9 @@ public class NotificationManagerService extends SystemService {
                }
            }
            return mPreferencesHelper.getNotificationChannelGroups(
                    pkg, uid, false, true, false, true, recentlySentChannels);
            return mPreferencesHelper.getNotificationChannelGroups(pkg, uid,
                    NotificationChannelGroupsHelper.Params.onlySpecifiedOrBlockedChannels(
                            recentlySentChannels));
        }
        @Override
+7 −51

File changed.

Preview size limit exceeded, changes collapsed.

+21 −16

File changed.

Preview size limit exceeded, changes collapsed.