diff --git a/res/values/strings.xml b/res/values/strings.xml
index fa0adac9fd9..f92fd2aca84 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9214,6 +9214,16 @@
None
All
+
+
+ {count, plural, offset:2
+ =0 {No apps can interrupt}
+ =1 {{app_1} can interrupt}
+ =2 {{app_1} and {app_2} can interrupt}
+ =3 {{app_1}, {app_2}, and {app_3} can interrupt}
+ other {{app_1}, {app_2}, and # more can interrupt}
+ }
+
Allow apps to override
diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java
index 8e6019c3b48..0661de7a5e7 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -357,6 +357,19 @@ public class NotificationBackend {
}
}
+ /**
+ * Returns all of a user's packages that have at least one channel that will bypass DND
+ */
+ public List getPackagesBypassingDnd(int userId,
+ boolean includeConversationChannels) {
+ try {
+ return sINM.getPackagesBypassingDnd(userId, includeConversationChannels);
+ } catch (Exception e) {
+ Log.w(TAG, "Error calling NoMan", e);
+ return new ArrayList<>();
+ }
+ }
+
public void updateChannel(String pkg, int uid, NotificationChannel channel) {
try {
sINM.updateNotificationChannelForPackage(pkg, uid, channel);
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
index 42b58b1346e..581fff57a26 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
@@ -20,23 +20,44 @@ import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_I
import android.content.Context;
import android.os.Bundle;
+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.core.SubSettingLauncher;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+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
*/
-public class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceController {
+
+ private static final String TAG = "ZenModeAppsLinkPreferenceController";
private final ZenModeSummaryHelper mSummaryHelper;
+ private ApplicationsState.Session mAppSession;
+ private NotificationBackend mNotificationBackend = new NotificationBackend();
+ private ZenMode mZenMode;
+ private Preference mPreference;
- public ZenModeAppsLinkPreferenceController(Context context, String key,
- ZenModesBackend backend) {
+ ZenModeAppsLinkPreferenceController(Context context, String key, Fragment host,
+ ApplicationsState applicationsState, ZenModesBackend backend) {
super(context, key, backend);
mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend);
+ if (applicationsState != null && host != null) {
+ mAppSession = applicationsState.newSession(mAppSessionCallbacks, host.getLifecycle());
+ }
}
@Override
@@ -49,6 +70,84 @@ public class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferen
.setSourceMetricsCategory(0)
.setArguments(bundle)
.toIntent());
- preference.setSummary(mSummaryHelper.getAppsSummary(zenMode));
+ mZenMode = zenMode;
+ mPreference = preference;
+ triggerUpdateAppsBypassingDndSummaryText();
}
+
+ private void triggerUpdateAppsBypassingDndSummaryText() {
+ if (mAppSession == null) {
+ return;
+ }
+
+ ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures()
+ && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()
+ ? ApplicationsState.FILTER_ENABLED_NOT_QUIET
+ : ApplicationsState.FILTER_ALL_ENABLED;
+ // We initiate a rebuild in the background here. Once the rebuild is completed,
+ // the onRebuildComplete() callback will be invoked, which will trigger the summary text
+ // to be initialized.
+ mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR, false);
+ }
+
+ private void updateAppsBypassingDndSummaryText(List apps) {
+ Set appNames = getAppsBypassingDnd(apps);
+ mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, appNames));
+ }
+
+ @VisibleForTesting
+ ArraySet getAppsBypassingDnd(@NonNull List apps) {
+ ArraySet appsBypassingDnd = new ArraySet<>();
+
+ Map pkgLabelMap = new HashMap();
+ for (ApplicationsState.AppEntry entry : apps) {
+ if (entry.info != null) {
+ pkgLabelMap.put(entry.info.packageName, entry.label);
+ }
+ }
+ for (String pkg : mNotificationBackend.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;
+ }
+ appsBypassingDnd.add(BidiFormatter.getInstance().unicodeWrap(pkgLabelMap.get(pkg)));
+ }
+ return appsBypassingDnd;
+ }
+
+ @VisibleForTesting final ApplicationsState.Callbacks mAppSessionCallbacks =
+ new ApplicationsState.Callbacks() {
+
+ @Override
+ public void onRunningStateChanged(boolean running) { }
+
+ @Override
+ public void onPackageListChanged() {
+ triggerUpdateAppsBypassingDndSummaryText();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ updateAppsBypassingDndSummaryText(apps);
+ }
+
+ @Override
+ public void onPackageIconChanged() { }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) { }
+
+ @Override
+ public void onAllSizesComputed() { }
+
+ @Override
+ public void onLauncherInfoChanged() { }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ triggerUpdateAppsBypassingDndSummaryText();
+ }
+ };
}
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 87165b85d72..e4d81fe51d5 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -16,11 +16,13 @@
package com.android.settings.notification.modes;
+import android.app.Application;
import android.app.AutomaticZenRule;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
+import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
@@ -42,7 +44,9 @@ public class ZenModeFragment extends ZenModeFragmentBase {
prefControllers.add(new ZenModePeopleLinkPreferenceController(
context, "zen_mode_people", mBackend));
prefControllers.add(new ZenModeAppsLinkPreferenceController(
- context, "zen_mode_apps", mBackend));
+ context, "zen_mode_apps", this,
+ ApplicationsState.getInstance((Application) context.getApplicationContext()),
+ mBackend));
prefControllers.add(new ZenModeOtherLinkPreferenceController(
context, "zen_other_settings", mBackend));
prefControllers.add(new ZenModeDisplayLinkPreferenceController(
diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
index b4075cde656..77f364cd701 100644
--- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
+++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
@@ -43,13 +43,18 @@ import android.icu.text.MessageFormat;
import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenPolicy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.settings.R;
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 {
@@ -397,12 +402,13 @@ class ZenModeSummaryHelper {
}
/**
- * Generates a summary to display under the top level "Apps" preference for a mode.
+ * Generates a summary to display under the top level "Apps" preference for a mode, based
+ * on the given mode and provided set of apps.
*/
- public String getAppsSummary(ZenMode zenMode) {
- // TODO: b/308819928 - Set summary using priority app list if Selected Apps Chosen.
+ public @NonNull String getAppsSummary(@NonNull ZenMode zenMode,
+ @Nullable Set appsBypassing) {
if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_PRIORITY) {
- return mContext.getResources().getString(R.string.zen_mode_apps_priority_apps);
+ return formatAppsList(appsBypassing);
} else if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) {
return mContext.getResources().getString(R.string.zen_mode_apps_none_apps);
} else if (zenMode.getPolicy().getAllowedChannels() == ZenMode.CHANNEL_POLICY_ALL) {
@@ -410,4 +416,35 @@ class ZenModeSummaryHelper {
}
return "";
}
+
+ /**
+ * 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.
+ */
+ public @NonNull String formatAppsList(@Nullable Set 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);
+ MessageFormat msgFormat = new MessageFormat(
+ mContext.getString(R.string.zen_mode_apps_subtext),
+ Locale.getDefault());
+ Map 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]);
+ }
+ }
+ }
+ return msgFormat.format(args);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
index 67e1f9f919a..c8b11855c72 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
@@ -16,28 +16,51 @@
package com.android.settings.notification.modes;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.net.Uri;
+import android.os.Bundle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
+import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
+import com.android.settings.SettingsActivity;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
@@ -47,7 +70,15 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
private Context mContext;
@Mock
- private ZenModesBackend mBackend;
+ private ZenModesBackend mZenModesBackend;
+
+ @Mock
+ private NotificationBackend mNotificationBackend;
+
+ @Mock
+ private ApplicationsState mApplicationsState;
+ @Mock
+ private ApplicationsState.Session mSession;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -56,21 +87,109 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
+ when(mApplicationsState.newSession(any(), any())).thenReturn(mSession);
mController = new ZenModeAppsLinkPreferenceController(
- mContext, "controller_key", mBackend);
+ mContext, "controller_key", mock(Fragment.class), mApplicationsState,
+ mZenModesBackend);
+ ReflectionHelpers.setField(mController, "mNotificationBackend", mNotificationBackend);
+ }
+
+ 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 ZenMode createPriorityChannelsZenMode() {
+ return new ZenMode("id", new AutomaticZenRule.Builder("Bedtime",
+ Uri.parse("bed"))
+ .setType(AutomaticZenRule.TYPE_BEDTIME)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder()
+ .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
+ .build())
+ .build(), true);
}
@Test
- @EnableFlags(Flags.FLAG_MODES_UI)
- public void testHasSummary() {
- Preference pref = mock(Preference.class);
- ZenMode zenMode = new ZenMode("id",
- new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
- .setType(AutomaticZenRule.TYPE_DRIVING)
- .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
- .build(), true);
- mController.updateZenMode(pref, zenMode);
- verify(pref).setSummary(any());
+ public void testIsAvailable() {
+ assertThat(mController.isAvailable()).isTrue();
}
+ @Test
+ public void testUpdateSetsIntent() {
+ // Creates the preference
+ SelectorWithWidgetPreference preference = mock(SelectorWithWidgetPreference.class);
+ // Create a zen mode that allows priority channels to breakthrough.
+ ZenMode zenMode = createPriorityChannelsZenMode();
+
+ // Capture the intent
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
+ mController.updateState((Preference) preference, zenMode);
+ verify(preference).setIntent(captor.capture());
+ Intent launcherIntent = captor.getValue();
+
+ assertThat(launcherIntent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+ .isEqualTo("com.android.settings.notification.modes.ZenModeAppsFragment");
+ assertThat(launcherIntent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
+ -1)).isEqualTo(0);
+
+ Bundle bundle = launcherIntent.getBundleExtra(
+ SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+ assertThat(bundle).isNotNull();
+ assertThat(bundle.getString(MODE_ID)).isEqualTo("id");
+ }
+
+ @Test
+ public void testGetAppsBypassingDnd() {
+ ApplicationsState.AppEntry entry = createAppEntry("test", "testLabel");
+ ApplicationsState.AppEntry entryConv = createAppEntry("test_conv", "test_convLabel");
+ List appEntries = List.of(entry, entryConv);
+
+ when(mNotificationBackend.getPackagesBypassingDnd(mContext.getUserId(),
+ false)).thenReturn(List.of("test"));
+
+ assertThat(mController.getAppsBypassingDnd(appEntries)).containsExactly("testLabel");
+ }
+
+ @Test
+ public void testUpdateTriggersRebuild() {
+ // Creates the preference
+ SelectorWithWidgetPreference preference = mock(SelectorWithWidgetPreference.class);
+ // Create a zen mode that allows priority channels to breakthrough.
+ ZenMode zenMode = createPriorityChannelsZenMode();
+
+ // Create some applications.
+ ArrayList appEntries =
+ new ArrayList();
+ appEntries.add(createAppEntry("test", "pkgLabel"));
+
+ when(mNotificationBackend.getPackagesBypassingDnd(
+ mContext.getUserId(), false))
+ .thenReturn(List.of("test"));
+
+ // Updates the preference with the zen mode. We expect that this causes the app session
+ // to trigger a rebuild.
+ mController.updateZenMode((Preference) preference, zenMode);
+ verify(mSession).rebuild(any(), any(), eq(false));
+
+ // Manually triggers the callback that will happen on rebuild.
+ mController.mAppSessionCallbacks.onRebuildComplete(appEntries);
+ verify(preference).setSummary("pkgLabel can interrupt");
+ }
+
+ @Test
+ public void testOnPackageListChangedTriggersRebuild() {
+ mController.mAppSessionCallbacks.onPackageListChanged();
+ verify(mSession).rebuild(any(), any(), eq(false));
+ }
+
+ @Test
+ public void testOnLoadEntriesCompletedTriggersRebuild() {
+ mController.mAppSessionCallbacks.onLoadEntriesCompleted();
+ verify(mSession).rebuild(any(), any(), eq(false));
+ }
}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
index d8c8bf0bfb5..ef8290a4873 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
@@ -38,6 +38,9 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
@RunWith(RobolectricTestRunner.class)
public class ZenModesSummaryHelperTest {
private Context mContext;
@@ -339,7 +342,7 @@ public class ZenModesSummaryHelperTest {
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
- assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("All");
+ assertThat(mSummaryHelper.getAppsSummary(zenMode, new LinkedHashSet<>())).isEqualTo("All");
}
@Test
@@ -353,7 +356,56 @@ public class ZenModesSummaryHelperTest {
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
- assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("None");
+ assertThat(mSummaryHelper.getAppsSummary(zenMode, new LinkedHashSet<>())).isEqualTo("None");
+ }
+
+ @Test
+ public void getAppsSummary_priorityAppsNoList() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+ .setType(AutomaticZenRule.TYPE_BEDTIME)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder()
+ .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
+ .build())
+ .build();
+ ZenMode zenMode = new ZenMode("id", rule, true);
+
+ assertThat(mSummaryHelper.getAppsSummary(zenMode, null)).isEqualTo("Selected apps");
+ }
+
+ @Test
+ public void getAppsSummary_formatAppsListEmpty() {
+ Set apps = new LinkedHashSet<>();
+ assertThat(mSummaryHelper.formatAppsList(apps)).isEqualTo("No apps can interrupt");
+ }
+
+ @Test
+ public void getAppsSummary_formatAppsListSingle() {
+ Set apps = Set.of("My App");
+ assertThat(mSummaryHelper.formatAppsList(apps)).isEqualTo("My App can interrupt");
+ }
+
+ @Test
+ public void getAppsSummary_formatAppsListTwo() {
+ Set apps = Set.of("My App", "SecondApp");
+ assertThat(mSummaryHelper.formatAppsList(apps)).isEqualTo("My App and SecondApp "
+ + "can interrupt");
+ }
+
+ @Test
+ public void getAppsSummary_formatAppsListThree() {
+ Set apps = Set.of("My App", "SecondApp", "ThirdApp");
+ assertThat(mSummaryHelper.formatAppsList(apps)).isEqualTo("My App, SecondApp, "
+ + "and ThirdApp can interrupt");
+ }
+
+ @Test
+ public void getAppsSummary_formatAppsListMany() {
+ Set apps = Set.of("My App", "SecondApp", "ThirdApp", "FourthApp",
+ "FifthApp", "SixthApp");
+ // Note that apps are selected alphabetically.
+ assertThat(mSummaryHelper.formatAppsList(apps)).isEqualTo("FifthApp, FourthApp, "
+ + "and 4 more can interrupt");
}
@Test
@@ -366,7 +418,11 @@ public class ZenModesSummaryHelperTest {
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
+ Set apps = Set.of("My App", "SecondApp", "ThirdApp", "FourthApp",
+ "FifthApp", "SixthApp");
- assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("Selected apps");
+ assertThat(mSummaryHelper.getAppsSummary(zenMode, apps)).isEqualTo("FifthApp, FourthApp, "
+ + "and 4 more can interrupt");
}
+
}