diff --git a/res/color/modes_set_schedule_text_color.xml b/res/color/modes_set_schedule_text_color.xml
new file mode 100644
index 00000000000..5ceb68e709c
--- /dev/null
+++ b/res/color/modes_set_schedule_text_color.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/res/drawable/modes_schedule_day_toggle.xml b/res/drawable/modes_schedule_day_toggle.xml
new file mode 100644
index 00000000000..c09f5972833
--- /dev/null
+++ b/res/drawable/modes_schedule_day_toggle.xml
@@ -0,0 +1,47 @@
+
+
+
+ -
+
+
+
-
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/modes_set_schedule_layout.xml b/res/layout/modes_set_schedule_layout.xml
new file mode 100644
index 00000000000..5758cfb4be2
--- /dev/null
+++ b/res/layout/modes_set_schedule_layout.xml
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1f5c11d82b3..dea2cc6af23 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7963,6 +7963,15 @@
Schedule
+
+ Set a schedule
+
+
+ Schedule
+
+
+ %1$d hr, %2$d min
+
Schedule
diff --git a/res/xml/modes_set_schedule.xml b/res/xml/modes_set_schedule.xml
new file mode 100644
index 00000000000..dd73ec814b6
--- /dev/null
+++ b/res/xml/modes_set_schedule.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
new file mode 100644
index 00000000000..8517af16975
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
@@ -0,0 +1,56 @@
+/*
+ * 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 android.service.notification.ZenModeConfig;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.TwoStatePreference;
+
+/**
+ * Preference controller controlling whether a time schedule-based mode ends at the next alarm.
+ */
+class ZenModeExitAtAlarmPreferenceController extends
+ AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener {
+ private ZenModeConfig.ScheduleInfo mSchedule;
+
+ ZenModeExitAtAlarmPreferenceController(Context context,
+ String key, ZenModesBackend backend) {
+ super(context, key, backend);
+ }
+
+ @Override
+ public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
+ ((TwoStatePreference) preference).setChecked(mSchedule.exitAtAlarm);
+ }
+
+ @Override
+ public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+ final boolean exitAtAlarm = (Boolean) newValue;
+ if (mSchedule.exitAtAlarm != exitAtAlarm) {
+ mSchedule.exitAtAlarm = exitAtAlarm;
+ return saveMode(mode -> {
+ mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(mSchedule));
+ return mode;
+ });
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
new file mode 100644
index 00000000000..4d58097b1dc
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Settings page to set a schedule for a mode that turns on automatically based on specific days
+ * of the week and times of day.
+ */
+public class ZenModeSetScheduleFragment extends ZenModeFragmentBase {
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.modes_set_schedule;
+ }
+
+ @Override
+ protected List createPreferenceControllers(Context context) {
+ List controllers = new ArrayList<>();
+ controllers.add(
+ new ZenModeSetSchedulePreferenceController(mContext, this, "schedule", mBackend));
+ controllers.add(
+ new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm", mBackend));
+ return controllers;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO: b/332937635 - make this the correct metrics category
+ return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
new file mode 100644
index 00000000000..a6008ccd768
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
@@ -0,0 +1,274 @@
+/*
+ * 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.Flags;
+import android.content.Context;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.text.format.DateFormat;
+import android.util.ArraySet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.widget.LayoutPreference;
+
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.function.Function;
+
+/**
+ * Preference controller for setting the start and end time and days of the week associated with
+ * an automatic zen mode.
+ */
+class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceController {
+ // per-instance to ensure we're always using the current locale
+ // E = day of the week; "EEEEE" is the shortest version; "EEEE" is the full name
+ private final SimpleDateFormat mShortDayFormat = new SimpleDateFormat("EEEEE");
+ private final SimpleDateFormat mLongDayFormat = new SimpleDateFormat("EEEE");
+
+ private static final String TAG = "ZenModeSetSchedulePreferenceController";
+ private Fragment mParent;
+ private ZenModeConfig.ScheduleInfo mSchedule;
+
+ ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key,
+ ZenModesBackend backend) {
+ super(context, key, backend);
+ mParent = parent;
+ }
+
+ @Override
+ public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
+ LayoutPreference layoutPref = (LayoutPreference) preference;
+
+ TextView start = layoutPref.findViewById(R.id.start_time);
+ start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
+ start.setOnClickListener(
+ timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
+
+ TextView end = layoutPref.findViewById(R.id.end_time);
+ end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
+ end.setOnClickListener(
+ timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
+
+ TextView durationView = layoutPref.findViewById(R.id.schedule_duration);
+ durationView.setText(getScheduleDurationDescription(mSchedule));
+
+ ViewGroup daysContainer = layoutPref.findViewById(R.id.days_of_week_container);
+ setupDayToggles(daysContainer, mSchedule, Calendar.getInstance());
+ }
+
+ private String timeString(int hour, int minute) {
+ final Calendar c = Calendar.getInstance();
+ c.set(Calendar.HOUR_OF_DAY, hour);
+ c.set(Calendar.MINUTE, minute);
+ return DateFormat.getTimeFormat(mContext).format(c.getTime());
+ }
+
+ private boolean isValidTime(int hour, int minute) {
+ return ZenModeConfig.isValidHour(hour) && ZenModeConfig.isValidMinute(minute);
+ }
+
+ private String getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule) {
+ final int startMin = 60 * schedule.startHour + schedule.startMinute;
+ final int endMin = 60 * schedule.endHour + schedule.endMinute;
+ final boolean nextDay = startMin >= endMin;
+
+ Duration scheduleDuration;
+ if (nextDay) {
+ // add one day's worth of minutes (24h x 60min) to end minute for end time calculation
+ int endMinNextDay = endMin + (24 * 60);
+ scheduleDuration = Duration.ofMinutes(endMinNextDay - startMin);
+ } else {
+ scheduleDuration = Duration.ofMinutes(endMin - startMin);
+ }
+
+ int hours = scheduleDuration.toHoursPart();
+ int minutes = scheduleDuration.minusHours(hours).toMinutesPart();
+ return mContext.getString(R.string.zen_mode_schedule_duration, hours, minutes);
+ }
+
+ @VisibleForTesting
+ protected Function updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) {
+ return (zenMode) -> {
+ zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule));
+ if (Flags.modesApi() && Flags.modesUi()) {
+ zenMode.getRule().setTriggerDescription(
+ SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule));
+ }
+ return zenMode;
+ };
+ }
+
+ private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> {
+ if (!isValidTime(hour, minute)) {
+ return;
+ }
+ if (hour == mSchedule.startHour && minute == mSchedule.startMinute) {
+ return;
+ }
+ mSchedule.startHour = hour;
+ mSchedule.startMinute = minute;
+ saveMode(updateScheduleMode(mSchedule));
+ };
+
+ private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> {
+ if (!isValidTime(hour, minute)) {
+ return;
+ }
+ if (hour == mSchedule.endHour && minute == mSchedule.endMinute) {
+ return;
+ }
+ mSchedule.endHour = hour;
+ mSchedule.endMinute = minute;
+ saveMode(updateScheduleMode(mSchedule));
+ };
+
+ private View.OnClickListener timePickerLauncher(int hour, int minute,
+ ZenModeTimePickerFragment.TimeSetter timeSetter) {
+ return v -> {
+ final ZenModeTimePickerFragment frag = new ZenModeTimePickerFragment(mContext, hour,
+ minute, timeSetter);
+ frag.show(mParent.getParentFragmentManager(), TAG);
+ };
+ }
+
+ protected static int[] getDaysOfWeekForLocale(Calendar c) {
+ int[] daysOfWeek = new int[7];
+ int currentDay = c.getFirstDayOfWeek();
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ if (currentDay > 7) currentDay = 1;
+ daysOfWeek[i] = currentDay;
+ currentDay++;
+ }
+ return daysOfWeek;
+ }
+
+ @VisibleForTesting
+ protected void setupDayToggles(ViewGroup dayContainer, ZenModeConfig.ScheduleInfo schedule,
+ Calendar c) {
+ int[] daysOfWeek = getDaysOfWeekForLocale(c);
+
+ // Index in daysOfWeek is associated with the [idx]'th object in the list of days in the
+ // layout. Note that because the order of the days of the week may differ per locale, this
+ // is not necessarily the same as the actual value of the day number at that index.
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ ToggleButton dayToggle = dayContainer.findViewById(resIdForDayIndex(i));
+ if (dayToggle == null) {
+ continue;
+ }
+
+ final int day = daysOfWeek[i];
+ c.set(Calendar.DAY_OF_WEEK, day);
+
+ // find current setting for this day
+ boolean dayEnabled = false;
+ if (schedule.days != null) {
+ for (int idx = 0; idx < schedule.days.length; idx++) {
+ if (schedule.days[idx] == day) {
+ dayEnabled = true;
+ break;
+ }
+ }
+ }
+
+ // On/off is indicated by visuals, and both states share the shortest (one-character)
+ // day label.
+ dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
+ dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
+ dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
+
+ dayToggle.setChecked(dayEnabled);
+ dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (updateScheduleDays(schedule, day, isChecked)) {
+ saveMode(updateScheduleMode(schedule));
+ }
+ });
+
+ // If display and text settings cause the text to be larger than its containing box,
+ // don't show scrollbars.
+ dayToggle.setVerticalScrollBarEnabled(false);
+ dayToggle.setHorizontalScrollBarEnabled(false);
+ }
+ }
+
+ // Updates the set of enabled days in provided schedule to either turn on or off the given day.
+ // The format of days in ZenModeConfig.ScheduleInfo is an array of days, where inclusion means
+ // the schedule is set to run on that day. Returns whether anything was changed.
+ @VisibleForTesting
+ protected static boolean updateScheduleDays(ZenModeConfig.ScheduleInfo schedule, int day,
+ boolean set) {
+ // Build a set representing the days that are currently set in mSchedule.
+ ArraySet daySet = new ArraySet();
+ if (schedule.days != null) {
+ for (int i = 0; i < schedule.days.length; i++) {
+ daySet.add(schedule.days[i]);
+ }
+ }
+
+ if (daySet.contains(day) != set) {
+ if (set) {
+ daySet.add(day);
+ } else {
+ daySet.remove(day);
+ }
+
+ // rebuild days array for mSchedule
+ final int[] out = new int[daySet.size()];
+ for (int i = 0; i < daySet.size(); i++) {
+ out[i] = daySet.valueAt(i);
+ }
+ Arrays.sort(out);
+ schedule.days = out;
+ return true;
+ }
+ // If the setting is the same as it was before, no need to update anything.
+ return false;
+ }
+
+ protected static int resIdForDayIndex(int idx) {
+ switch (idx) {
+ case 0:
+ return R.id.day0;
+ case 1:
+ return R.id.day1;
+ case 2:
+ return R.id.day2;
+ case 3:
+ return R.id.day3;
+ case 4:
+ return R.id.day4;
+ case 5:
+ return R.id.day5;
+ case 6:
+ return R.id.day6;
+ default:
+ return 0; // unknown
+ }
+ }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
index a3bc508cfbb..14d5d59a19d 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
@@ -16,6 +16,7 @@
package com.android.settings.notification.modes;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
@@ -32,13 +33,13 @@ import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.PrimarySwitchPreference;
/**
- * Preference controller for the link
+ * Preference controller for the link to an individual mode's configuration page.
*/
-public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
@VisibleForTesting
protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings";
- public ZenModeSetTriggerLinkPreferenceController(Context context, String key,
+ ZenModeSetTriggerLinkPreferenceController(Context context, String key,
ZenModesBackend backend) {
super(context, key, backend);
}
@@ -66,6 +67,16 @@ public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePr
// TODO: b/341961712 - direct preference to app-owned intent if available
switch (zenMode.getRule().getType()) {
+ case TYPE_SCHEDULE_TIME:
+ switchPref.setTitle(R.string.zen_mode_set_schedule_link);
+ switchPref.setSummary(zenMode.getRule().getTriggerDescription());
+ switchPref.setIntent(new SubSettingLauncher(mContext)
+ .setDestination(ZenModeSetScheduleFragment.class.getName())
+ // TODO: b/332937635 - set correct metrics category
+ .setSourceMetricsCategory(0)
+ .setArguments(bundle)
+ .toIntent());
+ break;
case TYPE_SCHEDULE_CALENDAR:
switchPref.setTitle(R.string.zen_mode_set_calendar_link);
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
diff --git a/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
new file mode 100644
index 00000000000..d8e1b38875b
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java
@@ -0,0 +1,76 @@
+/*
+ * 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.Dialog;
+import android.app.TimePickerDialog;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.format.DateFormat;
+import android.widget.TimePicker;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Dialog that shows when a user selects a (start or end) time to edit for a schedule-based mode.
+ */
+public class ZenModeTimePickerFragment extends InstrumentedDialogFragment implements
+ TimePickerDialog.OnTimeSetListener {
+ private final Context mContext;
+ private final TimeSetter mTimeSetter;
+ private final int mHour;
+ private final int mMinute;
+
+ public ZenModeTimePickerFragment(Context context, int hour, int minute,
+ @NonNull TimeSetter timeSetter) {
+ super();
+ mContext = context;
+ mHour = hour;
+ mMinute = minute;
+ mTimeSetter = timeSetter;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new TimePickerDialog(mContext, this, mHour, mMinute,
+ DateFormat.is24HourFormat(mContext));
+ }
+
+ /**
+ * Calls the provided TimeSetter's setTime() method when a time is set on the TimePicker.
+ */
+ public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
+ mTimeSetter.setTime(hourOfDay, minute);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO: b/332937635 - set correct metrics category (or decide to keep this one?)
+ return SettingsEnums.DIALOG_ZEN_TIMEPICKER;
+ }
+
+ /**
+ * Interface for a method to pass into the TimePickerFragment that specifies what to do when the
+ * time is updated.
+ */
+ public interface TimeSetter {
+ void setTime(int hour, int minute);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
new file mode 100644
index 00000000000..c1c4d61727f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.service.notification.ZenModeConfig;
+
+import androidx.preference.TwoStatePreference;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+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 java.util.Calendar;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeExitAtAlarmPreferenceControllerTest {
+ private Context mContext;
+ @Mock
+ private ZenModesBackend mBackend;
+
+ private ZenModeExitAtAlarmPreferenceController mPrefController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+ mPrefController = new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm",
+ mBackend);
+ }
+
+ @Test
+ public void testUpdateState() {
+ TwoStatePreference preference = mock(TwoStatePreference.class);
+
+ // previously: don't exit at alarm
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ scheduleInfo.exitAtAlarm = false;
+
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
+ true); // is active
+
+ // need to call updateZenMode for the first call
+ mPrefController.updateZenMode(preference, mode);
+ verify(preference).setChecked(false);
+
+ // Now update state after changing exitAtAlarm
+ scheduleInfo.exitAtAlarm = true;
+ mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo));
+
+ // now can just call updateState
+ mPrefController.updateState(preference, mode);
+ verify(preference).setChecked(true);
+ }
+
+ @Test
+ public void testOnPreferenceChange() {
+ TwoStatePreference preference = mock(TwoStatePreference.class);
+
+ // previously: exit at alarm
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ scheduleInfo.exitAtAlarm = true;
+
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
+ true); // is active
+ mPrefController.updateZenMode(preference, mode);
+
+ // turn off exit at alarm
+ mPrefController.onPreferenceChange(preference, false);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class);
+ verify(mBackend).updateMode(captor.capture());
+ ZenModeConfig.ScheduleInfo newSchedule = ZenModeConfig.tryParseScheduleConditionId(
+ captor.getValue().getRule().getConditionId());
+ assertThat(newSchedule.exitAtAlarm).isFalse();
+
+ // other properties remain the same
+ assertThat(newSchedule.startHour).isEqualTo(1);
+ assertThat(newSchedule.endHour).isEqualTo(2);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
new file mode 100644
index 00000000000..7cf327c983e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.ZenModeConfig;
+import android.view.ViewGroup;
+import android.widget.ToggleButton;
+
+import androidx.fragment.app.Fragment;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+
+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 java.util.Calendar;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeSetSchedulePreferenceControllerTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
+ @Mock
+ private ZenModesBackend mBackend;
+ private Context mContext;
+
+ @Mock
+ private Fragment mParent;
+ @Mock
+ private Calendar mCalendar;
+ @Mock
+ private ViewGroup mDaysContainer;
+ @Mock
+ private ToggleButton mDay0, mDay1, mDay2, mDay3, mDay4, mDay5, mDay6;
+
+ private ZenModeSetSchedulePreferenceController mPrefController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+ mPrefController = new ZenModeSetSchedulePreferenceController(mContext, mParent, "schedule",
+ mBackend);
+ setupMockDayContainer();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
+ public void updateScheduleRule_updatesConditionAndTriggerDescription() {
+ ZenMode mode = new ZenMode("id",
+ new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(),
+ true); // is active
+
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 2;
+ ZenMode out = mPrefController.updateScheduleMode(scheduleInfo).apply(mode);
+
+ assertThat(out.getRule().getConditionId())
+ .isEqualTo(ZenModeConfig.toScheduleConditionId(scheduleInfo));
+ assertThat(out.getRule().getTriggerDescription()).isNotEmpty();
+ }
+
+ @Test
+ public void testUpdateScheduleDays() {
+ // Confirm that adding/subtracting/etc days works as expected
+ // starting from null: no days set
+ ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
+
+ // Unset a day that's already unset: nothing should change
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.TUESDAY, false)).isFalse();
+ // not explicitly checking whether schedule.days is still null here, as we don't necessarily
+ // want to require nullness as distinct from an empty list of days.
+
+ // set a few new days
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.MONDAY, true)).isTrue();
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.FRIDAY, true)).isTrue();
+ assertThat(schedule.days).hasLength(2);
+ assertThat(schedule.days).asList().containsExactly(Calendar.MONDAY, Calendar.FRIDAY);
+
+ // remove an existing day to make sure that works
+ assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
+ Calendar.MONDAY, false)).isTrue();
+ assertThat(schedule.days).hasLength(1);
+ assertThat(schedule.days).asList().containsExactly(Calendar.FRIDAY);
+ }
+
+ @Test
+ public void testSetupDayToggles_daysOfWeekOrder() {
+ // Confirm that days are correctly associated with the actual day of the week independent
+ // of when the first day of the week is for the given calendar.
+ ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
+ schedule.days = new int[] { Calendar.SUNDAY, Calendar.TUESDAY, Calendar.FRIDAY };
+ schedule.startHour = 1;
+ schedule.endHour = 5;
+
+ // Start mCalendar on Wednesday, arbitrarily
+ when(mCalendar.getFirstDayOfWeek()).thenReturn(Calendar.WEDNESDAY);
+
+ // Setup the day toggles
+ mPrefController.setupDayToggles(mDaysContainer, schedule, mCalendar);
+
+ // we should see toggle 0 associated with the first day of the week, etc.
+ // in this week order, schedule turns on friday (2), sunday (4), tuesday (6) so those
+ // should be checked while everything else should not be checked.
+ verify(mDay0).setChecked(false); // weds
+ verify(mDay1).setChecked(false); // thurs
+ verify(mDay2).setChecked(true); // fri
+ verify(mDay3).setChecked(false); // sat
+ verify(mDay4).setChecked(true); // sun
+ verify(mDay5).setChecked(false); // mon
+ verify(mDay6).setChecked(true); // tues
+ }
+
+ private void setupMockDayContainer() {
+ // associate each index (regardless of associated day of the week) with the appropriate
+ // res id in the days container
+ when(mDaysContainer.findViewById(R.id.day0)).thenReturn(mDay0);
+ when(mDaysContainer.findViewById(R.id.day1)).thenReturn(mDay1);
+ when(mDaysContainer.findViewById(R.id.day2)).thenReturn(mDay2);
+ when(mDaysContainer.findViewById(R.id.day3)).thenReturn(mDay3);
+ when(mDaysContainer.findViewById(R.id.day4)).thenReturn(mDay4);
+ when(mDaysContainer.findViewById(R.id.day5)).thenReturn(mDay5);
+ when(mDaysContainer.findViewById(R.id.day6)).thenReturn(mDay6);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
index 7dcec1cfeed..91de4ea8348 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
@@ -17,6 +17,7 @@
package com.android.settings.notification.modes;
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
@@ -53,6 +54,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import java.util.Calendar;
+
@RunWith(RobolectricTestRunner.class)
public class ZenModeSetTriggerLinkPreferenceControllerTest {
@Rule
@@ -167,4 +170,29 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
ZenModeSetCalendarFragment.class.getName());
}
+
+ @Test
+ public void testRuleLink_schedule() {
+ ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
+ scheduleInfo.days = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.THURSDAY };
+ scheduleInfo.startHour = 1;
+ scheduleInfo.endHour = 15;
+ ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
+ ZenModeConfig.toScheduleConditionId(scheduleInfo))
+ .setType(TYPE_SCHEDULE_TIME)
+ .setTriggerDescription("some schedule")
+ .build(),
+ true); // is active
+ mPrefController.updateZenMode(mPrefCategory, mode);
+
+ verify(mPreference).setTitle(R.string.zen_mode_set_schedule_link);
+ verify(mPreference).setSummary(mode.getRule().getTriggerDescription());
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mPreference).setIntent(captor.capture());
+ // Destination as written into the intent by SubSettingLauncher
+ assertThat(
+ captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
+ ZenModeSetScheduleFragment.class.getName());
+ }
}