First bits of "add a mode"

* Preference below the modes list.
* Temporarily triggers addition of a mode with default name and type=SCHEDULE_TIME (type will be "manual only" later).
* Fixed sorting of modes in the list when refreshing (new modes were added at the bottom instead of where they should, the same would've happened for renamed modes).
* Minor polishes (extracted fragment launch to helper class, renamed item controller class for clarity).

Test: atest com.android.settings.notification.modes
Bug: 326442408
Fixes: 347198709
Flag: android.app.modes_ui
Change-Id: Ie276c92181c5374faf74592433595e7e15a5efc0
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c7622a6..fa0adac 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7906,7 +7906,7 @@
     <!-- Sound: Footer hyperlink text to launch the Connected devices settings page. [CHAR LIMIT=NONE]-->
     <string name="spatial_audio_footer_learn_more_text">Connected devices settings</string>
 
-    <!-- Sound: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
+    <!-- Zen Modes: Summary for the Do not Disturb option that describes how many automatic rules (schedules) are enabled [CHAR LIMIT=NONE]-->
     <string name="zen_mode_settings_schedules_summary">
         {count, plural,
             =0    {None}
@@ -7915,13 +7915,16 @@
         }
     </string>
 
-    <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
+    <!-- Zen Modes: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
     <string name="zen_mode_settings_title">Do Not Disturb</string>
 
-    <!-- Sound: Title for the Modes option and associated settings page. [CHAR LIMIT=50]-->
+    <!-- Zen Modes: Title for the Modes option and associated settings page. [CHAR LIMIT=50]-->
     <string name="zen_modes_list_title">Priority Modes</string>
 
-    <!-- Sound: Summary for the Do not Disturb option and associated settings page. [CHAR LIMIT=240]-->
+    <!-- Zen Modes: Caption of the "add a mode" item in the modes list -->
+    <string name="zen_modes_add_mode">Add a mode</string>
+
+    <!-- Zen Modes: Summary for the Do not Disturb option and associated settings page. [CHAR LIMIT=240]-->
     <string name="zen_mode_settings_summary">Only get notified by important people and apps</string>
 
     <!-- Subtitle for the Do not Disturb slice. [CHAR LIMIT=50]-->
diff --git a/res/xml/modes_list_settings.xml b/res/xml/modes_list_settings.xml
index c6b6200..8207af0 100644
--- a/res/xml/modes_list_settings.xml
+++ b/res/xml/modes_list_settings.xml
@@ -15,8 +15,10 @@
   ~ limitations under the License.
   -->
 
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-                  android:title="@string/zen_modes_list_title" >
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/zen_modes_list_title">
 
     <!-- TODO: b/333682392 - add strings for summary as appropriate -->
 
@@ -25,4 +27,10 @@
         <!-- Preferences leading to rules are added in this PreferenceCategory. -->
     </PreferenceCategory>
 
+    <Preference
+        android:key="add_mode"
+        android:title="@string/zen_modes_add_mode"
+        android:icon="@drawable/ic_add_24dp"
+        settings:allowDividerAbove="false"/>
+
 </PreferenceScreen>
diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java
index 8970957..ad36fc1 100644
--- a/src/com/android/settings/notification/modes/ZenModesBackend.java
+++ b/src/com/android/settings/notification/modes/ZenModesBackend.java
@@ -30,6 +30,8 @@
 import android.provider.Settings;
 import android.service.notification.Condition;
 import android.service.notification.ConversationChannelWrapper;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenAdapters;
 import android.service.notification.ZenModeConfig;
 import android.util.Log;
 
@@ -242,4 +244,32 @@
         }
         mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
     }
+
+    /**
+     * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
+     * not have a schedule), this can be later updated by the user in the mode settings page.
+     *
+     * @return the created mode. Only {@code null} if creation failed due to an internal error
+     */
+    @Nullable
+    ZenMode addCustomMode(String name) {
+        ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
+        schedule.days = ZenModeConfig.ALL_DAYS;
+        schedule.startHour = 22;
+        schedule.endHour = 7;
+
+        // TODO: b/326442408 - Create as "manual" (i.e. no trigger) instead of schedule-time.
+        AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
+                ZenModeConfig.toScheduleConditionId(schedule))
+                .setPackage(ZenModeConfig.getScheduleConditionProvider().getPackageName())
+                .setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR)
+                .setOwner(ZenModeConfig.getScheduleConditionProvider())
+                .setTriggerDescription(SystemZenRules.getTriggerDescriptionForScheduleTime(
+                        mContext, schedule))
+                .setManualInvocationAllowed(true)
+                .build();
+
+        String ruleId = mNotificationManager.addAutomaticZenRule(rule);
+        return getMode(ruleId);
+    }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java
new file mode 100644
index 0000000..c229fb1
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 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.settings.notification.modes;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settings.utils.ZenServiceListing;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.Random;
+
+class ZenModesListAddModePreferenceController extends AbstractPreferenceController {
+
+    private final ZenModesBackend mBackend;
+    private final ZenServiceListing mServiceListing;
+
+    ZenModesListAddModePreferenceController(Context context, ZenModesBackend backend,
+            ZenServiceListing serviceListing) {
+        super(context);
+        mBackend = backend;
+        mServiceListing = serviceListing;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return "add_mode";
+    }
+
+    @Override
+    public void updateState(Preference preference) {
+        preference.setOnPreferenceClickListener(pref -> {
+            // TODO: b/326442408 - Launch the proper mode creation flow (using mServiceListing).
+            ZenMode mode = mBackend.addCustomMode("New mode #" + new Random().nextInt(1000));
+            if (mode != null) {
+                ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch();
+            }
+            return true;
+        });
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java
index 040621e..80678f6 100644
--- a/src/com/android/settings/notification/modes/ZenModesListFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java
@@ -31,12 +31,14 @@
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.search.SearchIndexable;
 
-import java.util.ArrayList;
+import com.google.common.collect.ImmutableList;
+
 import java.util.List;
 
 @SearchIndexable
 public class ZenModesListFragment extends ZenModesFragmentBase {
-    protected final ManagedServiceSettings.Config CONFIG = getConditionProviderConfig();
+
+    private static final ManagedServiceSettings.Config CONFIG = getConditionProviderConfig();
 
     @Override
     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
@@ -50,13 +52,11 @@
         // We need to redefine ZenModesBackend here even though mBackend exists so that this method
         // can be static; it must be static to be able to be used in SEARCH_INDEX_DATA_PROVIDER.
         ZenModesBackend backend = ZenModesBackend.getInstance(context);
-        List<AbstractPreferenceController> controllers = new ArrayList<>();
-        controllers.add(new ZenModesListPreferenceController(
-                context, parent, backend));
 
-        // TODO: b/326442408 - Add controller for "Add Mode" preference/flow, which is what uses
-        //                     the ZenServiceListing.
-        return controllers;
+        return ImmutableList.of(
+                new ZenModesListPreferenceController(context, parent, backend),
+                new ZenModesListAddModePreferenceController(context, backend, serviceListing)
+        );
     }
 
     @Override
@@ -77,7 +77,7 @@
         return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
     }
 
-    protected static ManagedServiceSettings.Config getConditionProviderConfig() {
+    private static ManagedServiceSettings.Config getConditionProviderConfig() {
         return new ManagedServiceSettings.Config.Builder()
                 .setTag(TAG)
                 .setIntentAction(ConditionProviderService.SERVICE_INTERFACE)
@@ -87,8 +87,6 @@
                 .build();
     }
 
-    // TODO: b/322373473 - Add 3-dot options menu with capability to delete modes.
-
     /**
      * For Search.
      */
diff --git a/src/com/android/settings/notification/modes/ZenModeListPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
similarity index 70%
rename from src/com/android/settings/notification/modes/ZenModeListPreference.java
rename to src/com/android/settings/notification/modes/ZenModesListItemPreference.java
index c3daa61..7ecfb3a 100644
--- a/src/com/android/settings/notification/modes/ZenModeListPreference.java
+++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
@@ -15,24 +15,19 @@
  */
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
-
-import android.app.settings.SettingsEnums;
 import android.content.Context;
-import android.os.Bundle;
 
-import com.android.settings.core.SubSettingLauncher;
 import com.android.settingslib.RestrictedPreference;
 
 /**
  * Preference representing a single mode item on the modes aggregator page. Clicking on this
  * preference leads to an individual mode's configuration page.
  */
-class ZenModeListPreference extends RestrictedPreference {
+class ZenModesListItemPreference extends RestrictedPreference {
     final Context mContext;
     ZenMode mZenMode;
 
-    ZenModeListPreference(Context context, ZenMode zenMode) {
+    ZenModesListItemPreference(Context context, ZenMode zenMode) {
         super(context);
         mContext = context;
         setZenMode(zenMode);
@@ -41,13 +36,7 @@
 
     @Override
     public void onClick() {
-        Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, mZenMode.getId());
-        new SubSettingLauncher(mContext)
-                .setDestination(ZenModeFragment.class.getName())
-                .setArguments(bundle)
-                .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION)
-                .launch();
+        ZenSubSettingLauncher.forMode(mContext, mZenMode.getId()).launch();
     }
 
     public void setZenMode(ZenMode zenMode) {
diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
index ca8fe05..5dcd9eb 100644
--- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
@@ -15,7 +15,6 @@
  */
 package com.android.settings.notification.modes;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
 import android.content.res.Resources;
@@ -74,24 +73,27 @@
         // category for each rule that exists.
         PreferenceCategory category = (PreferenceCategory) preference;
 
-        Map<String, ZenModeListPreference> originalPreferences = new HashMap<>();
+        Map<String, ZenModesListItemPreference> originalPreferences = new HashMap<>();
         for (int i = 0; i < category.getPreferenceCount(); i++) {
-            ZenModeListPreference pref = (ZenModeListPreference) category.getPreference(i);
+            ZenModesListItemPreference pref = (ZenModesListItemPreference) category.getPreference(
+                    i);
             originalPreferences.put(pref.getKey(), pref);
         }
 
         // Loop through each rule, either updating the existing rule or creating the rule's
         // preference
-        for (ZenMode mode : mBackend.getModes()) {
-            if (originalPreferences.containsKey(mode.getId())) {
+        List<ZenMode> modes = mBackend.getModes();
+        for (ZenMode mode : modes) {
+            ZenModesListItemPreference modePreference = originalPreferences.get(mode.getId());
+            if (modePreference != null) {
                 // existing rule; update its info if it's changed since the last display
-                AutomaticZenRule rule = mode.getRule();
-                originalPreferences.get(mode.getId()).setZenMode(mode);
+                modePreference.setZenMode(mode);
             } else {
                 // new rule; create a new ZenRulePreference & add it to the preference category
-                Preference pref = new ZenModeListPreference(mContext, mode);
-                category.addPreference(pref);
+                modePreference = new ZenModesListItemPreference(mContext, mode);
+                category.addPreference(modePreference);
             }
+            modePreference.setOrder(modes.indexOf(mode));
 
             originalPreferences.remove(mode.getId());
         }
diff --git a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
new file mode 100644
index 0000000..11f3492
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 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.settings.notification.modes;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.settings.core.SubSettingLauncher;
+
+class ZenSubSettingLauncher {
+
+    static SubSettingLauncher forMode(Context context, String modeId) {
+        return forModeFragment(context, ZenModeFragment.class, modeId,
+                SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION);
+    }
+
+    private static SubSettingLauncher forModeFragment(Context context,
+            Class<? extends ZenModeFragmentBase> fragmentClass, String modeId,
+            int sourceMetricsCategory) {
+        Bundle bundle = new Bundle();
+        bundle.putString(ZenModeFragmentBase.MODE_ID, modeId);
+
+        return new SubSettingLauncher(context)
+                .setDestination(fragmentClass.getName())
+                .setArguments(bundle)
+                .setSourceMetricsCategory(sourceMetricsCategory);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
index 0297841..9a4de60 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
@@ -31,8 +31,16 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
 
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
 import com.android.settingslib.search.SearchIndexableRaw;
 
+import com.google.common.collect.ImmutableList;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -43,6 +51,7 @@
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 @RunWith(RobolectricTestRunner.class)
@@ -75,16 +84,72 @@
     private ZenModesBackend mBackend;
 
     private ZenModesListPreferenceController mPrefController;
+    private PreferenceCategory mPreference;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mContext = RuntimeEnvironment.application;
 
+        mPreference = new PreferenceCategory(mContext);
+        PreferenceManager preferenceManager = new PreferenceManager(mContext);
+        PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
+        preferenceScreen.addPreference(mPreference);
+
         mPrefController = new ZenModesListPreferenceController(mContext, null, mBackend);
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    public void updateState_addsPreferences() {
+        ImmutableList<ZenMode> modes = ImmutableList.of(newMode("One"), newMode("Two"),
+                newMode("Three"), newMode("Four"), newMode("Five"));
+        when(mBackend.getModes()).thenReturn(modes);
+
+        mPrefController.updateState(mPreference);
+
+        assertThat(mPreference.getPreferenceCount()).isEqualTo(5);
+        List<ZenModesListItemPreference> itemPreferences = getModeListItems(mPreference);
+        assertThat(itemPreferences.stream().map(pref -> pref.mZenMode).toList())
+                .containsExactlyElementsIn(modes)
+                .inOrder();
+
+        for (int i = 0; i < modes.size(); i++) {
+            assertThat(((ZenModesListItemPreference) (mPreference.getPreference(i))).mZenMode)
+                    .isEqualTo(modes.get(i));
+        }
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MODES_UI)
+    public void updateState_secondTime_updatesPreferences() {
+        ImmutableList<ZenMode> modes = ImmutableList.of(newMode("One"), newMode("Two"),
+                newMode("Three"), newMode("Four"), newMode("Five"));
+        when(mBackend.getModes()).thenReturn(modes);
+        mPrefController.updateState(mPreference);
+
+        assertThat(mPreference.getPreferenceCount()).isEqualTo(5);
+        List<ZenModesListItemPreference> oldPreferences = getModeListItems(mPreference);
+
+        ImmutableList<ZenMode> updatedModes = ImmutableList.of(modes.get(0), modes.get(1),
+                newMode("Two.1"), newMode("Two.2"), modes.get(2), /* deleted "Four" */
+                modes.get(4));
+        when(mBackend.getModes()).thenReturn(updatedModes);
+        mPrefController.updateState(mPreference);
+
+        List<ZenModesListItemPreference> newPreferences = getModeListItems(mPreference);
+        assertThat(newPreferences.stream().map(pref -> pref.mZenMode).toList())
+                .containsExactlyElementsIn(updatedModes)
+                .inOrder();
+
+        // Verify that the old preference controllers were reused instead of creating new ones.
+        assertThat(newPreferences.get(0)).isSameInstanceAs(oldPreferences.get(0));
+        assertThat(newPreferences.get(1)).isSameInstanceAs(oldPreferences.get(1));
+        assertThat(newPreferences.get(4)).isSameInstanceAs(oldPreferences.get(2));
+        assertThat(newPreferences.get(5)).isSameInstanceAs(oldPreferences.get(4));
+    }
+
+    @Test
     @DisableFlags(Flags.FLAG_MODES_UI)
     public void testModesUiOff_notAvailableAndNoSearchData() {
         // There exist modes
@@ -151,4 +216,28 @@
         assertThat(item1.key).isEqualTo(TEST_MODE_ID);
         assertThat(item1.title).isEqualTo(TEST_MODE_NAME);
     }
+
+    private static ZenMode newMode(String id) {
+        return new ZenMode(
+                id,
+                new AutomaticZenRule.Builder("Mode " + id, Uri.parse("test_uri"))
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .build(),
+                false);
+    }
+
+    /**
+     * Returns the child preferences of the {@code group}, sorted by their
+     * {@link Preference#getOrder} value (which is the order they will be sorted by and displayed
+     * in the UI).
+     */
+    private List<ZenModesListItemPreference> getModeListItems(PreferenceGroup group) {
+        ArrayList<ZenModesListItemPreference> items = new ArrayList<>();
+        for (int i = 0; i < group.getPreferenceCount(); i++) {
+            items.add((ZenModesListItemPreference) group.getPreference(i));
+        }
+        items.sort(Comparator.comparing(Preference::getOrder));
+        return items;
+    }
 }