From 4203f311dd48952696d8a998b141a087fb57a388 Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Tue, 28 May 2024 17:28:13 -0400 Subject: [PATCH] Add schedule setting page for time-based modes. Creates new layout for setting the start & end time, and days of the week, for a schedule-based mode. This is close to the mocks specified in https://screenshot.googleplex.com/8zmb7PAjjt73VkN, but with the following differences: - the end time is left-aligned with the center rather than on the right side of the screen. This is a side effect of using LinearLayout to evenly space start & end times, and could in theory be fixed by using a ConstraintLayout, but that option seems to cause times to overlap instead of wrap when display size is cranked up. Could be fixed later. - no icons yet on either side of the time display - no Done button. Instead, has the "exit at alarm" switch that exists today. Have not yet checked how this interacts with TalkBack, etc. Flag: android.app.modes_ui Bug: 332730302 Test: ZenModeSetSchedulePreferenceControllerTest, ZenModeExitAtAlarmPreferenceControllerTest, ZenModeSetTriggerLinkPreferenceControllerTest Test: manual: interacting with UI in normal size, with font & display at minimum and maximum, and in locales (fr) where the first day of the week is a different day Change-Id: I0b76f55891d6c12fc27720657c9eea6fe42fbafe --- res/color/modes_set_schedule_text_color.xml | 27 ++ res/drawable/modes_schedule_day_toggle.xml | 47 +++ res/layout/modes_set_schedule_layout.xml | 228 +++++++++++++++ res/values/strings.xml | 9 + res/xml/modes_set_schedule.xml | 38 +++ ...enModeExitAtAlarmPreferenceController.java | 56 ++++ .../modes/ZenModeSetScheduleFragment.java | 54 ++++ ...enModeSetSchedulePreferenceController.java | 274 ++++++++++++++++++ ...odeSetTriggerLinkPreferenceController.java | 17 +- .../modes/ZenModeTimePickerFragment.java | 76 +++++ ...deExitAtAlarmPreferenceControllerTest.java | 115 ++++++++ ...deSetSchedulePreferenceControllerTest.java | 163 +++++++++++ ...etTriggerLinkPreferenceControllerTest.java | 28 ++ 13 files changed, 1129 insertions(+), 3 deletions(-) create mode 100644 res/color/modes_set_schedule_text_color.xml create mode 100644 res/drawable/modes_schedule_day_toggle.xml create mode 100644 res/layout/modes_set_schedule_layout.xml create mode 100644 res/xml/modes_set_schedule.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSetScheduleFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeTimePickerFragment.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java 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 f15137a66e8..ac50d9f614b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7961,6 +7961,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()); + } }