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

Commit d133ec93 authored by Matías Hernández's avatar Matías Hernández Committed by Android (Google) Code Review
Browse files

Merge "Distinguish the profile of DND-bypassing apps" into main

parents de9d7061 da3ae255
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -9357,6 +9357,8 @@
            other {{app_1}, {app_2}, and # more can interrupt}
        }
    </string>
    <!-- Priority Modes: Entry in the "apps that can bypass DND" list that corresponds to a work profile app (e.g. "Chrome (Work)" [CHAR LIMIT=15]. -->
    <string name="zen_mode_apps_work_app"><xliff:g id="app_label" example="Chrome">%s</xliff:g> (Work)</string>
    <!-- Text displayed (for a brief time) while the list of bypassing apps is being fetched. Will be replaced by a zen_mode_apps_subtext. [CHAR_LIMIT=60] -->
    <string name="zen_mode_apps_calculating">Calculating\u2026</string>
+42 −31
Original line number Diff line number Diff line
@@ -19,28 +19,32 @@ package com.android.settings.notification.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;

import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArraySet;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.text.BidiFormatter;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;

import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Preference with a link and summary about what apps can break through the mode
@@ -51,12 +55,21 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr

    private final ZenModeSummaryHelper mSummaryHelper;
    private final ApplicationsState mApplicationsState;
    private final UserManager mUserManager;
    private ApplicationsState.Session mAppSession;
    private final ZenHelperBackend mHelperBackend;
    private ZenMode mZenMode;
    private Preference mPreference;
    private final Fragment mHost;

    ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
            ZenModesBackend backend, ZenHelperBackend helperBackend) {
        this(context, key, host,
                ApplicationsState.getInstance((Application) context.getApplicationContext()),
                backend, helperBackend);
    }

    @VisibleForTesting
    ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
            ApplicationsState applicationsState, ZenModesBackend backend,
            ZenHelperBackend helperBackend) {
@@ -64,6 +77,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
        mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend);
        mHelperBackend = helperBackend;
        mApplicationsState = applicationsState;
        mUserManager = context.getSystemService(UserManager.class);
        mHost = host;
    }

@@ -90,10 +104,10 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
        if (mApplicationsState != null && mHost != null) {
            mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, mHost.getLifecycle());
        }
        triggerUpdateAppsBypassingDndSummaryText();
        triggerUpdateAppsBypassingDnd();
    }

    private void triggerUpdateAppsBypassingDndSummaryText() {
    private void triggerUpdateAppsBypassingDnd() {
        if (mAppSession == null) {
            return;
        }
@@ -108,31 +122,28 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr
        mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR, false);
    }

    private void updateAppsBypassingDndSummaryText(List<ApplicationsState.AppEntry> apps) {
        Set<String> appNames = getAppsBypassingDnd(apps);
        mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, appNames));
    private void displayAppsBypassingDnd(List<AppEntry> allApps) {
        ImmutableList<AppEntry> apps = getAppsBypassingDndSortedByName(allApps);

        mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps));
    }

    @VisibleForTesting
    ArraySet<String> getAppsBypassingDnd(@NonNull List<ApplicationsState.AppEntry> apps) {
        ArraySet<String> appsBypassingDnd = new ArraySet<>();

        Map<String, String> pkgLabelMap = new HashMap<String, String>();
        for (ApplicationsState.AppEntry entry : apps) {
            if (entry.info != null) {
                pkgLabelMap.put(entry.info.packageName, entry.label);
            }
        }
        for (String pkg : mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(),
                /* includeConversationChannels= */ false)) {
            // Settings may hide some packages from the user, so if they're not present here
            // we skip displaying them, even if they bypass dnd.
            if (pkgLabelMap.get(pkg) == null) {
                continue;
    ImmutableList<AppEntry> getAppsBypassingDndSortedByName(@NonNull List<AppEntry> allApps) {
        Multimap<Integer, String> packagesBypassingDnd = HashMultimap.create();
        for (UserHandle userHandle : mUserManager.getUserProfiles()) {
            packagesBypassingDnd.putAll(userHandle.getIdentifier(),
                    mHelperBackend.getPackagesBypassingDnd(userHandle.getIdentifier(),
                            /* includeConversationChannels= */ false));
        }
            appsBypassingDnd.add(BidiFormatter.getInstance().unicodeWrap(pkgLabelMap.get(pkg)));
        }
        return appsBypassingDnd;

        return ImmutableList.copyOf(
                allApps.stream()
                        .filter(app -> packagesBypassingDnd.containsEntry(
                                UserHandle.getUserId(app.info.uid), app.info.packageName))
                        .sorted(Comparator.comparing((AppEntry app) -> app.label)
                                .thenComparing(app -> UserHandle.getUserId(app.info.uid)))
                        .toList());
    }

    @VisibleForTesting
@@ -145,12 +156,12 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr

                @Override
                public void onPackageListChanged() {
                    triggerUpdateAppsBypassingDndSummaryText();
                    triggerUpdateAppsBypassingDnd();
                }

                @Override
                public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
                    updateAppsBypassingDndSummaryText(apps);
                    displayAppsBypassingDnd(apps);
                }

                @Override
@@ -171,7 +182,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr

                @Override
                public void onLoadEntriesCompleted() {
                    triggerUpdateAppsBypassingDndSummaryText();
                    triggerUpdateAppsBypassingDnd();
                }
            };
}
+1 −5
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.settings.notification.modes;

import android.app.AlertDialog;
import android.app.Application;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.view.Menu;
@@ -29,7 +28,6 @@ import androidx.annotation.NonNull;
import androidx.core.view.MenuProvider;

import com.android.settings.R;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.ZenMode;

@@ -58,9 +56,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
        prefControllers.add(new ZenModePeopleLinkPreferenceController(
                context, "zen_mode_people", mHelperBackend));
        prefControllers.add(new ZenModeAppsLinkPreferenceController(
                context, "zen_mode_apps", this,
                ApplicationsState.getInstance((Application) context.getApplicationContext()),
                mBackend, mHelperBackend));
                context, "zen_mode_apps", this, mBackend, mHelperBackend));
        prefControllers.add(new ZenModeOtherLinkPreferenceController(
                context, "zen_other_settings", mHelperBackend));
        prefControllers.add(new ZenModeDisplayLinkPreferenceController(
+24 −17
Original line number Diff line number Diff line
@@ -49,17 +49,18 @@ import android.util.ArrayMap;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.text.BidiFormatter;

import com.android.settings.R;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.notification.modes.ZenMode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

class ZenModeSummaryHelper {
@@ -412,7 +413,7 @@ class ZenModeSummaryHelper {
     * on the given mode and provided set of apps.
     */
    public @NonNull String getAppsSummary(@NonNull ZenMode zenMode,
            @Nullable Set<String> appsBypassing) {
            @Nullable List<AppEntry> appsBypassing) {
        if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_PRIORITY) {
            return formatAppsList(appsBypassing);
        } else if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
@@ -424,28 +425,34 @@ class ZenModeSummaryHelper {
    /**
     * Generates a formatted string declaring which apps can interrupt in the style of
     * "App, App2, and 4 more can interrupt."
     * Apps selected for explicit mention are selected in order from the provided set sorted
     * alphabetically.
     * Apps selected for explicit mention are picked in order from the provided list.
     */
    public @NonNull String formatAppsList(@Nullable Set<String> appsBypassingDnd) {
    @VisibleForTesting
    public @NonNull String formatAppsList(@Nullable List<AppEntry> appsBypassingDnd) {
        if (appsBypassingDnd == null) {
            return mContext.getResources().getString(R.string.zen_mode_apps_priority_apps);
        }
        final int numAppsBypassingDnd = appsBypassingDnd.size();
        String[] appsBypassingDndArr = appsBypassingDnd.toArray(new String[numAppsBypassingDnd]);
        // Sorts the provided apps alphabetically.
        Arrays.sort(appsBypassingDndArr);
        List<String> appNames = appsBypassingDnd.stream().limit(3)
                .map(app -> {
                    String appName = BidiFormatter.getInstance().unicodeWrap(app.label);
                    if (app.isManagedProfile()) {
                        appName = mContext.getString(R.string.zen_mode_apps_work_app, appName);
                    }
                    return appName;
                })
                .toList();

        MessageFormat msgFormat = new MessageFormat(
                mContext.getString(R.string.zen_mode_apps_subtext),
                Locale.getDefault());
        Map<String, Object> args = new HashMap<>();
        args.put("count", numAppsBypassingDnd);
        if (numAppsBypassingDnd >= 1) {
            args.put("app_1", appsBypassingDndArr[0]);
            if (numAppsBypassingDnd >= 2) {
                args.put("app_2", appsBypassingDndArr[1]);
                if (numAppsBypassingDnd == 3) {
                    args.put("app_3", appsBypassingDndArr[2]);
        args.put("count", appsBypassingDnd.size());
        if (appNames.size() >= 1) {
            args.put("app_1", appNames.get(0));
            if (appNames.size() >= 2) {
                args.put("app_2", appNames.get(1));
                if (appNames.size() == 3) {
                    args.put("app_3", appNames.get(2));
                }
            }
        }
+59 −14
Original line number Diff line number Diff line
@@ -22,17 +22,22 @@ import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.app.Flags;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.UserInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
@@ -41,6 +46,7 @@ import androidx.fragment.app.Fragment;

import com.android.settings.SettingsActivity;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.notification.modes.TestModeBuilder;
import com.android.settingslib.notification.modes.ZenMode;
@@ -58,6 +64,7 @@ import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
@@ -90,13 +97,13 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
                mZenModesBackend, mHelperBackend);
    }

    private ApplicationsState.AppEntry createAppEntry(String packageName, String label) {
        ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class);
        entry.info = new ApplicationInfo();
        entry.info.packageName = packageName;
        entry.label = label;
        entry.info.uid = 0;
        return entry;
    private AppEntry createAppEntry(String packageName, int userId) {
        ApplicationInfo applicationInfo = new ApplicationInfo();
        applicationInfo.packageName = packageName;
        applicationInfo.uid = UserHandle.getUid(userId, new Random().nextInt(100));
        AppEntry appEntry = new AppEntry(mContext, applicationInfo, 1);
        appEntry.label = packageName;
        return appEntry;
    }

    private ZenMode createPriorityChannelsZenMode() {
@@ -137,14 +144,52 @@ public final class ZenModeAppsLinkPreferenceControllerTest {

    @Test
    public void testGetAppsBypassingDnd() {
        ApplicationsState.AppEntry entry = createAppEntry("test", "testLabel");
        ApplicationsState.AppEntry entryConv = createAppEntry("test_conv", "test_convLabel");
        List<ApplicationsState.AppEntry> appEntries = List.of(entry, entryConv);
        ApplicationsState.AppEntry app1 = createAppEntry("app1", mContext.getUserId());
        ApplicationsState.AppEntry app2 = createAppEntry("app2", mContext.getUserId());
        List<ApplicationsState.AppEntry> allApps = List.of(app1, app2);

        when(mHelperBackend.getPackagesBypassingDnd(mContext.getUserId(),
                false)).thenReturn(List.of("test"));
                false)).thenReturn(List.of("app1"));

        assertThat(mController.getAppsBypassingDnd(appEntries)).containsExactly("testLabel");
        assertThat(mController.getAppsBypassingDndSortedByName(allApps)).containsExactly(app1);
    }

    @Test
    public void testGetAppsBypassingDnd_sortsByName() {
        ApplicationsState.AppEntry appC = createAppEntry("C", mContext.getUserId());
        ApplicationsState.AppEntry appA = createAppEntry("A", mContext.getUserId());
        ApplicationsState.AppEntry appB = createAppEntry("B", mContext.getUserId());
        List<ApplicationsState.AppEntry> allApps = List.of(appC, appA, appB);

        when(mHelperBackend.getPackagesBypassingDnd(eq(mContext.getUserId()), anyBoolean()))
                .thenReturn(List.of("B", "C", "A"));

        assertThat(mController.getAppsBypassingDndSortedByName(allApps))
                .containsExactly(appA, appB, appC).inOrder();
    }

    @Test
    public void testGetAppsBypassingDnd_withWorkProfile_includesProfileAndSorts() {
        UserInfo workProfile = new UserInfo(10, "Work Profile", 0);
        workProfile.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
        UserManager userManager = mContext.getSystemService(UserManager.class);
        shadowOf(userManager).addProfile(mContext.getUserId(), 10, workProfile);

        ApplicationsState.AppEntry personalCopy = createAppEntry("app", mContext.getUserId());
        ApplicationsState.AppEntry workCopy = createAppEntry("app", 10);
        ApplicationsState.AppEntry otherPersonal = createAppEntry("p2", mContext.getUserId());
        ApplicationsState.AppEntry otherWork = createAppEntry("w2", 10);
        List<ApplicationsState.AppEntry> allApps = List.of(workCopy, personalCopy, otherPersonal,
                otherWork);

        when(mHelperBackend.getPackagesBypassingDnd(eq(mContext.getUserId()), anyBoolean()))
                .thenReturn(List.of("app", "p2"));
        when(mHelperBackend.getPackagesBypassingDnd(eq(10), anyBoolean()))
                .thenReturn(List.of("app"));

        // Personal copy before work copy (names match).
        assertThat(mController.getAppsBypassingDndSortedByName(allApps))
                .containsExactly(personalCopy, workCopy, otherPersonal).inOrder();
    }

    @Test
@@ -156,7 +201,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest {

        // Create some applications.
        ArrayList<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
        appEntries.add(createAppEntry("test", "pkgLabel"));
        appEntries.add(createAppEntry("test", mContext.getUserId()));

        when(mHelperBackend.getPackagesBypassingDnd(
                mContext.getUserId(), false))
@@ -170,7 +215,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest {

        // Manually triggers the callback that will happen on rebuild.
        mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
        assertThat(String.valueOf(preference.getSummary())).isEqualTo("pkgLabel can interrupt");
        assertThat(String.valueOf(preference.getSummary())).isEqualTo("test can interrupt");
    }

    @Test
Loading