Loading res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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> src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java +42 −31 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading @@ -64,6 +77,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); mHelperBackend = helperBackend; mApplicationsState = applicationsState; mUserManager = context.getSystemService(UserManager.class); mHost = host; } Loading @@ -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; } Loading @@ -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 Loading @@ -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 Loading @@ -171,7 +182,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr @Override public void onLoadEntriesCompleted() { triggerUpdateAppsBypassingDndSummaryText(); triggerUpdateAppsBypassingDnd(); } }; } src/com/android/settings/notification/modes/ZenModeFragment.java +1 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading src/com/android/settings/notification/modes/ZenModeSummaryHelper.java +24 −17 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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) { Loading @@ -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)); } } } Loading tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +59 −14 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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) Loading Loading @@ -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() { Loading Loading @@ -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 Loading @@ -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)) Loading @@ -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 Loading
res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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>
src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java +42 −31 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading @@ -64,6 +77,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); mHelperBackend = helperBackend; mApplicationsState = applicationsState; mUserManager = context.getSystemService(UserManager.class); mHost = host; } Loading @@ -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; } Loading @@ -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 Loading @@ -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 Loading @@ -171,7 +182,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr @Override public void onLoadEntriesCompleted() { triggerUpdateAppsBypassingDndSummaryText(); triggerUpdateAppsBypassingDnd(); } }; }
src/com/android/settings/notification/modes/ZenModeFragment.java +1 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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( Loading
src/com/android/settings/notification/modes/ZenModeSummaryHelper.java +24 −17 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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) { Loading @@ -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)); } } } Loading
tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +59 −14 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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) Loading Loading @@ -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() { Loading Loading @@ -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 Loading @@ -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)) Loading @@ -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