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 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; + } }