Modify Summary for Mode's Apps settings page

Adds call to SummaryHelper to set Apps preference summary.

Bug: 308819928
Test: atest ZenModeAppsLinkPreferenceControllerTest
Flag: android.app.modes_ui
Change-Id: Iebec11afc62ecb79386e1866af57cd4e68461a95
diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java
index 8e6019c..0661de7 100644
--- a/src/com/android/settings/notification/NotificationBackend.java
+++ b/src/com/android/settings/notification/NotificationBackend.java
@@ -357,6 +357,19 @@
         }
     }
 
+    /**
+     * Returns all of a user's packages that have at least one channel that will bypass DND
+     */
+    public List<String> 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 42b58b1..581fff5 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
@@ -20,23 +20,44 @@
 
 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 @@
                 .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<ApplicationsState.AppEntry> apps) {
+        Set<String> appNames = getAppsBypassingDnd(apps);
+        mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, appNames));
+    }
+
+    @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 : 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<ApplicationsState.AppEntry> 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 87165b8..e4d81fe 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 @@
         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/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
index 67e1f9f..c8b1185 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 @@
 
     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 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<Intent> 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<ApplicationsState.AppEntry> 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<ApplicationsState.AppEntry> appEntries =
+                new ArrayList<ApplicationsState.AppEntry>();
+        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));
+    }
 }