diff --git a/res/values/strings.xml b/res/values/strings.xml
index c7622a6a599..fa0adac9fd9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7906,7 +7906,7 @@
Connected devices settings
-
+
{count, plural,
=0 {None}
@@ -7915,13 +7915,16 @@
}
-
+
Do Not Disturb
-
+
Priority Modes
-
+
+ Add a mode
+
+
Only get notified by important people and apps
diff --git a/res/xml/modes_list_settings.xml b/res/xml/modes_list_settings.xml
index c6b6200bdcb..8207af0f7e8 100644
--- a/res/xml/modes_list_settings.xml
+++ b/res/xml/modes_list_settings.xml
@@ -15,8 +15,10 @@
~ limitations under the License.
-->
-
+
@@ -25,4 +27,10 @@
+
+
diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java
index 89709579b55..ad36fc1d38b 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.ContactsContract;
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 @@ class ZenModesBackend {
}
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 00000000000..c229fb19b22
--- /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 040621e6d93..80678f6bb69 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.settings.utils.ZenServiceListing;
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 createPreferenceControllers(Context context) {
@@ -50,13 +52,11 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
// 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 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 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
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 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
.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 c3daa614343..7ecfb3acb87 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 @@ class ZenModeListPreference extends RestrictedPreference {
@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 ca8fe0558ba..5dcd9eb4675 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 @@ class ZenModesListPreferenceController extends BasePreferenceController {
// category for each rule that exists.
PreferenceCategory category = (PreferenceCategory) preference;
- Map originalPreferences = new HashMap<>();
+ Map 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 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 00000000000..11f3492f36d
--- /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 0297841a2fe..9a4de60613e 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.annotations.EnableFlags;
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.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@@ -75,15 +84,71 @@ public class ZenModesListPreferenceControllerTest {
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 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 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 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 oldPreferences = getModeListItems(mPreference);
+
+ ImmutableList 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 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() {
@@ -151,4 +216,28 @@ public class ZenModesListPreferenceControllerTest {
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 getModeListItems(PreferenceGroup group) {
+ ArrayList 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;
+ }
}