From c1a4abbc510dd9d2275e64968da7f2f69e5ee01c Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Thu, 20 Jun 2024 17:26:23 -0400 Subject: [PATCH] Migrate "duration for quick settings" to new modes page. This mostly continues to use the existing dialogs in settingslib (ZenDurationDialog, SettingsEnableZenModeDialog) and creates a controller to read from the settings value. Also updates the "turn on / turn off" button to respect the preferred duration. Flag: android.app.modes_ui Bug: 343444249 Test: ZenModeButtonPreferenceControllerTest, ManualDurationPreferenceControllerTest Change-Id: I2fd49a79d9a5807fefdd7ec310a6cc60d70f9bb1 --- res/xml/modes_rule_settings.xml | 4 + .../modes/ManualDurationHelper.java | 123 ++++++++++++++++++ .../ManualDurationPreferenceController.java | 86 ++++++++++++ .../ZenModeButtonPreferenceController.java | 31 ++++- .../notification/modes/ZenModeFragment.java | 20 ++- .../modes/ManualDurationHelperTest.java | 77 +++++++++++ ...anualDurationPreferenceControllerTest.java | 91 +++++++++++++ ...ZenModeButtonPreferenceControllerTest.java | 47 ++++++- 8 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/com/android/settings/notification/modes/ManualDurationHelper.java create mode 100644 src/com/android/settings/notification/modes/ManualDurationPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ManualDurationHelperTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ManualDurationPreferenceControllerTest.java diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index 0df9f80f851..5be206e6dd0 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -67,5 +67,9 @@ + + \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ManualDurationHelper.java b/src/com/android/settings/notification/modes/ManualDurationHelper.java new file mode 100644 index 00000000000..da9f42003eb --- /dev/null +++ b/src/com/android/settings/notification/modes/ManualDurationHelper.java @@ -0,0 +1,123 @@ +/* + * 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.annotation.Nullable; +import android.content.Context; +import android.database.ContentObserver; +import android.icu.text.MessageFormat; +import android.net.Uri; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.settings.R; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Class to contain shared utilities for reading and observing the Settings ZEN_DURATION value. + */ +class ManualDurationHelper { + private Context mContext; + + ManualDurationHelper(@NonNull Context context) { + mContext = context; + } + + int getZenDuration() { + return Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, + 0); + } + + /** + * Generates a summary of the duration that manual DND will be on when turned on from + * quick settings, for example "Until you turn off" or "[number] hours", based on the given + * setting value. + */ + public String getSummary() { + int zenDuration = getZenDuration(); + String summary; + if (zenDuration < 0) { + summary = mContext.getString(R.string.zen_mode_duration_summary_always_prompt); + } else if (zenDuration == 0) { + summary = mContext.getString(R.string.zen_mode_duration_summary_forever); + } else { + if (zenDuration >= 60) { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_duration_summary_time_hours), + Locale.getDefault()); + Map msgArgs = new HashMap<>(); + msgArgs.put("count", zenDuration / 60); + summary = msgFormat.format(msgArgs); + } else { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_duration_summary_time_minutes), + Locale.getDefault()); + Map msgArgs = new HashMap<>(); + msgArgs.put("count", zenDuration); + summary = msgFormat.format(msgArgs); + } + } + return summary; + } + + SettingsObserver makeSettingsObserver(@NonNull AbstractZenModePreferenceController controller) { + return new SettingsObserver(controller); + } + + final class SettingsObserver extends ContentObserver { + private static final Uri ZEN_MODE_DURATION_URI = Settings.Secure.getUriFor( + Settings.Secure.ZEN_DURATION); + + private final AbstractZenModePreferenceController mPrefController; + private Preference mPreference; + + /** + * Create a settings observer attached to the provided PreferenceController, whose + * updateState method should be called onChange. + */ + SettingsObserver(@NonNull AbstractZenModePreferenceController prefController) { + super(mContext.getMainExecutor(), 0); + mPrefController = prefController; + } + + void setPreference(Preference preference) { + mPreference = preference; + } + + public void register() { + mContext.getContentResolver().registerContentObserver(ZEN_MODE_DURATION_URI, false, + this); + } + + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + super.onChange(selfChange, uri); + if (ZEN_MODE_DURATION_URI.equals(uri) && mPreference != null) { + mPrefController.updateState(mPreference); + } + } + } +} diff --git a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java new file mode 100644 index 00000000000..073f8ab78f8 --- /dev/null +++ b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java @@ -0,0 +1,86 @@ +/* + * 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.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.notification.zen.SettingsZenDurationDialog; +import com.android.settingslib.notification.modes.ZenMode; +import com.android.settingslib.notification.modes.ZenModesBackend; + +public class ManualDurationPreferenceController extends AbstractZenModePreferenceController { + private static final String TAG = "QsDurationPrefController"; + + private final Fragment mParent; + private final ManualDurationHelper mDurationHelper; + private final ManualDurationHelper.SettingsObserver mSettingsObserver; + + ManualDurationPreferenceController(Context context, String key, Fragment parent, + ZenModesBackend backend) { + super(context, key, backend); + mParent = parent; + mDurationHelper = new ManualDurationHelper(context); + mSettingsObserver = mDurationHelper.makeSettingsObserver(this); + } + + @Override + public boolean isAvailable(ZenMode zenMode) { + if (!super.isAvailable(zenMode)) { + return false; + } + return zenMode.isManualDnd(); + } + + // Called by parent fragment onAttach(). + void registerSettingsObserver() { + mSettingsObserver.register(); + } + + // Called by parent fragment onDetach(). + void unregisterSettingsObserver() { + mSettingsObserver.unregister(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + Preference pref = screen.findPreference(getPreferenceKey()); + if (pref != null) { + mSettingsObserver.setPreference(pref); + } + } + + @Override + public void updateState(Preference preference, ZenMode unusedZenMode) { + // This controller is a link between a Settings value (ZEN_DURATION) and the manual DND + // mode. The status of the zen mode object itself doesn't affect the preference + // value, as that comes from settings; that value from settings will determine the + // condition that is attached to the mode on manual activation. Thus we ignore the actual + // zen mode value provided here. + preference.setSummary(mDurationHelper.getSummary()); + preference.setOnPreferenceClickListener(pref -> { + // The new setting value is set by the dialog, so we don't need to do it here. + final SettingsZenDurationDialog durationDialog = new SettingsZenDurationDialog(); + durationDialog.show(mParent.getParentFragmentManager(), TAG); + return true; + }); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java index 4a99b33749c..6b84414e0d9 100644 --- a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java @@ -18,21 +18,32 @@ package com.android.settings.notification.modes; import android.annotation.NonNull; import android.content.Context; +import android.provider.Settings; import android.widget.Button; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import com.android.settings.R; +import com.android.settings.notification.SettingsEnableZenModeDialog; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; +import java.time.Duration; + class ZenModeButtonPreferenceController extends AbstractZenModePreferenceController { + private static final String TAG = "ZenModeButtonPrefController"; private Button mZenButton; + private Fragment mParent; + private ManualDurationHelper mDurationHelper; - public ZenModeButtonPreferenceController(Context context, String key, ZenModesBackend backend) { + ZenModeButtonPreferenceController(Context context, String key, Fragment parent, + ZenModesBackend backend) { super(context, key, backend); + mParent = parent; + mDurationHelper = new ManualDurationHelper(context); } @Override @@ -49,7 +60,23 @@ class ZenModeButtonPreferenceController extends AbstractZenModePreferenceControl if (zenMode.isActive()) { mBackend.deactivateMode(zenMode); } else { - mBackend.activateMode(zenMode, null); + if (zenMode.isManualDnd()) { + // if manual DND, potentially ask for or use desired duration + int zenDuration = mDurationHelper.getZenDuration(); + switch (zenDuration) { + case Settings.Secure.ZEN_DURATION_PROMPT: + new SettingsEnableZenModeDialog().show( + mParent.getParentFragmentManager(), TAG); + break; + case Settings.Secure.ZEN_DURATION_FOREVER: + mBackend.activateMode(zenMode, null); + break; + default: + mBackend.activateMode(zenMode, Duration.ofMinutes(zenDuration)); + } + } else { + mBackend.activateMode(zenMode, null); + } } }); if (zenMode.isActive()) { diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 63ed8395006..67815b10088 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.List; public class ZenModeFragment extends ZenModeFragmentBase { - // for mode deletion menu private static final int DELETE_MODE = 1; @@ -48,7 +47,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { protected List createPreferenceControllers(Context context) { List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); - prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend)); + prefControllers.add( + new ZenModeButtonPreferenceController(context, "activate", this, mBackend)); prefControllers.add(new ZenModeActionsPreferenceController(context, "actions", mBackend)); prefControllers.add(new ZenModePeopleLinkPreferenceController( context, "zen_mode_people", mBackend, mHelperBackend)); @@ -64,9 +64,19 @@ public class ZenModeFragment extends ZenModeFragmentBase { "zen_automatic_trigger_category", this, mBackend)); prefControllers.add(new InterruptionFilterPreferenceController( context, "allow_filtering", mBackend)); + prefControllers.add(new ManualDurationPreferenceController( + context, "mode_manual_duration", this, mBackend)); return prefControllers; } + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + // allow duration preference controller to listen for settings changes + use(ManualDurationPreferenceController.class).registerSettingsObserver(); + } + @Override public void onStart() { super.onStart(); @@ -78,6 +88,12 @@ public class ZenModeFragment extends ZenModeFragmentBase { } } + @Override + public void onDetach() { + use(ManualDurationPreferenceController.class).unregisterSettingsObserver(); + super.onDetach(); + } + @Override public int getMetricsCategory() { // TODO: b/332937635 - make this the correct metrics category diff --git a/tests/robotests/src/com/android/settings/notification/modes/ManualDurationHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ManualDurationHelperTest.java new file mode 100644 index 00000000000..18ee2cf630e --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ManualDurationHelperTest.java @@ -0,0 +1,77 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ManualDurationHelperTest { + private Context mContext; + private ContentResolver mContentResolver; + + private ManualDurationHelper mHelper; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mContentResolver = RuntimeEnvironment.application.getContentResolver(); + + mHelper = new ManualDurationHelper(mContext); + } + + @Test + public void getDurationSummary_durationForever() { + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, + Settings.Secure.ZEN_DURATION_FOREVER); + assertThat(mHelper.getSummary()).isEqualTo( + mContext.getString(R.string.zen_mode_duration_summary_forever)); + } + + @Test + public void getDurationSummary_durationPrompt() { + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, + Settings.Secure.ZEN_DURATION_PROMPT); + assertThat(mHelper.getSummary()).isEqualTo( + mContext.getString(R.string.zen_mode_duration_summary_always_prompt)); + } + + @Test + public void getDurationSummary_durationCustom() { + // minutes + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, 45); + assertThat(mHelper.getSummary()).isEqualTo("45 minutes"); + + // hours + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, 300); + assertThat(mHelper.getSummary()).isEqualTo("5 hours"); + } + +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ManualDurationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ManualDurationPreferenceControllerTest.java new file mode 100644 index 00000000000..0a600c05688 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ManualDurationPreferenceControllerTest.java @@ -0,0 +1,91 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; + +import com.android.settingslib.notification.modes.ZenMode; +import com.android.settingslib.notification.modes.ZenModesBackend; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public class ManualDurationPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + private ContentResolver mContentResolver; + + @Mock + private ZenModesBackend mBackend; + + @Mock + private Fragment mParent; + + private ManualDurationPreferenceController mPrefController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mContentResolver = RuntimeEnvironment.application.getContentResolver(); + mPrefController = new ManualDurationPreferenceController(mContext, "key", mParent, + mBackend); + } + + @Test + public void testIsAvailable_onlyForManualDnd() { + assertThat(mPrefController.isAvailable(TestModeBuilder.EXAMPLE)).isFalse(); + + ZenMode manualDnd = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("id", Uri.EMPTY).build(), false); + assertThat(mPrefController.isAvailable(manualDnd)).isTrue(); + } + + @Test + public void testUpdateState_durationSummary() { + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, + 45 /* minutes */); + + Preference pref = new Preference(mContext); + mPrefController.updateState(pref, TestModeBuilder.EXAMPLE); + + assertThat(pref.getSummary().toString()).contains("45"); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java index 625f2311d21..5869c6b26ee 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java @@ -23,12 +23,18 @@ 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.ContentResolver; import android.content.Context; +import android.net.Uri; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; import android.widget.Button; +import androidx.fragment.app.Fragment; + import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; @@ -42,6 +48,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.time.Duration; + @EnableFlags(Flags.FLAG_MODES_UI) @RunWith(RobolectricTestRunner.class) public final class ZenModeButtonPreferenceControllerTest { @@ -51,19 +59,24 @@ public final class ZenModeButtonPreferenceControllerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private Context mContext; + private ContentResolver mContentResolver; + @Mock private ZenModesBackend mBackend; + @Mock + private Fragment mParent; + @Before public void setup() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; + mContentResolver = RuntimeEnvironment.application.getContentResolver(); mController = new ZenModeButtonPreferenceController( - mContext, "something", mBackend); + mContext, "something", mParent, mBackend); } @Test @@ -162,4 +175,34 @@ public final class ZenModeButtonPreferenceControllerTest { button.callOnClick(); verify(mBackend).activateMode(zenMode, null); } + + @Test + public void updateStateThenClick_withDuration() { + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, + 45 /* minutes */); + Button button = new Button(mContext); + LayoutPreference pref = mock(LayoutPreference.class); + when(pref.findViewById(anyInt())).thenReturn(button); + ZenMode zenMode = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("manual", Uri.EMPTY).build(), false); + + mController.updateZenMode(pref, zenMode); + button.callOnClick(); + verify(mBackend).activateMode(zenMode, Duration.ofMinutes(45)); + } + + @Test + public void updateStateThenClick_durationForever() { + Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_DURATION, + Settings.Secure.ZEN_DURATION_FOREVER); + Button button = new Button(mContext); + LayoutPreference pref = mock(LayoutPreference.class); + when(pref.findViewById(anyInt())).thenReturn(button); + ZenMode zenMode = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("manual", Uri.EMPTY).build(), false); + + mController.updateZenMode(pref, zenMode); + button.callOnClick(); + verify(mBackend).activateMode(zenMode, null); + } } \ No newline at end of file