diff --git a/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java b/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java index 5b02b3b0c18..d46d31a4860 100644 --- a/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java +++ b/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java @@ -42,7 +42,6 @@ import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.core.AbstractPreferenceController; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -56,8 +55,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { public static final String ACTION = Settings.ACTION_ZEN_MODE_SCHEDULE_RULE_SETTINGS; - // per-instance to ensure we're always using the current locale - private final SimpleDateFormat mDayFormat = new SimpleDateFormat("EEE"); + private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper(); private Preference mDays; private TimePickerPreference mStart; @@ -149,30 +147,11 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { } private void updateDays() { - // Compute an ordered, delimited list of day names based on the persisted user config. - final int[] days = mSchedule.days; - if (days != null && days.length > 0) { - final StringBuilder sb = new StringBuilder(); - final Calendar c = Calendar.getInstance(); - int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c); - for (int i = 0; i < daysOfWeek.length; i++) { - final int day = daysOfWeek[i]; - for (int j = 0; j < days.length; j++) { - if (day == days[j]) { - c.set(Calendar.DAY_OF_WEEK, day); - if (sb.length() > 0) { - sb.append(mContext.getString(R.string.summary_divider_text)); - } - sb.append(mDayFormat.format(c.getTime())); - break; - } - } - } - if (sb.length() > 0) { - mDays.setSummary(sb); - mDays.notifyDependencyChange(false); - return; - } + String desc = mScheduleHelper.getDaysDescription(mContext, mSchedule); + if (desc != null) { + mDays.setSummary(desc); + mDays.notifyDependencyChange(false); + return; } mDays.setSummary(R.string.zen_mode_schedule_rule_days_none); mDays.notifyDependencyChange(true); diff --git a/src/com/android/settings/notification/zen/ZenRulePreference.java b/src/com/android/settings/notification/zen/ZenRulePreference.java index a265a0776d2..8cf3106200b 100644 --- a/src/com/android/settings/notification/zen/ZenRulePreference.java +++ b/src/com/android/settings/notification/zen/ZenRulePreference.java @@ -23,22 +23,20 @@ import android.content.Intent; import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.service.notification.ZenModeConfig; -import android.view.View; -import android.widget.CheckBox; +import android.service.notification.ZenModeConfig.ScheduleInfo; import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import com.android.settings.R; import com.android.settings.utils.ManagedServiceSettings; import com.android.settings.utils.ZenServiceListing; +import com.android.settingslib.PrimarySwitchPreference; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.widget.TwoTargetPreference; import java.util.Map; -public class ZenRulePreference extends TwoTargetPreference { +public class ZenRulePreference extends PrimarySwitchPreference { private static final ManagedServiceSettings.Config CONFIG = ZenModeAutomationSettings.getConditionProviderConfig(); final String mId; @@ -53,14 +51,13 @@ public class ZenRulePreference extends TwoTargetPreference { CharSequence mName; private Intent mIntent; - private boolean mChecked; - private CheckBox mCheckBox; + + private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper(); public ZenRulePreference(Context context, final Map.Entry ruleEntry, Fragment parent, MetricsFeatureProvider metricsProvider) { super(context); - setLayoutResource(R.layout.preference_checkable_two_target); mBackend = ZenModeBackend.getInstance(context); mContext = context; mRule = ruleEntry.getValue(); @@ -72,50 +69,11 @@ public class ZenRulePreference extends TwoTargetPreference { mServiceListing.reloadApprovedServices(); mPref = this; mMetricsFeatureProvider = metricsProvider; - mChecked = mRule.isEnabled(); setAttributes(mRule); setWidgetLayoutResource(getSecondTargetResId()); - } - protected int getSecondTargetResId() { - if (mIntent != null) { - return R.layout.zen_rule_widget; - } - return 0; - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - View settingsWidget = view.findViewById(android.R.id.widget_frame); - View divider = view.findViewById(R.id.two_target_divider); - if (mIntent != null) { - divider.setVisibility(View.VISIBLE); - settingsWidget.setVisibility(View.VISIBLE); - settingsWidget.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mContext.startActivity(mIntent); - } - }); - } else { - divider.setVisibility(View.GONE); - settingsWidget.setVisibility(View.GONE); - settingsWidget.setOnClickListener(null); - } - - View checkboxContainer = view.findViewById(R.id.checkbox_container); - if (checkboxContainer != null) { - checkboxContainer.setOnClickListener(mOnCheckBoxClickListener); - } - mCheckBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox); - if (mCheckBox != null) { - mCheckBox.setChecked(mChecked); - } - } - - public boolean isChecked() { - return mChecked; + // initialize the checked state of the preference + super.setChecked(mRule.isEnabled()); } public void updatePreference(AutomaticZenRule rule) { @@ -126,34 +84,24 @@ public class ZenRulePreference extends TwoTargetPreference { if (mRule.isEnabled() != rule.isEnabled()) { setChecked(rule.isEnabled()); - setSummary(computeRuleSummary(rule)); } - + setSummary(computeRuleSummary(rule)); mRule = rule; } @Override public void onClick() { - mOnCheckBoxClickListener.onClick(null); + mContext.startActivity(mIntent); } - private void setChecked(boolean checked) { - mChecked = checked; - if (mCheckBox != null) { - mCheckBox.setChecked(checked); - } + @Override + public void setChecked(boolean checked) { + mRule.setEnabled(checked); + mBackend.updateZenRule(mId, mRule); + setAttributes(mRule); + super.setChecked(checked); } - private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - mRule.setEnabled(!mChecked); - mBackend.updateZenRule(mId, mRule); - setChecked(mRule.isEnabled()); - setAttributes(mRule); - } - }; - protected void setAttributes(AutomaticZenRule rule) { final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId( rule.getConditionId(), true); @@ -178,6 +126,30 @@ public class ZenRulePreference extends TwoTargetPreference { } private String computeRuleSummary(AutomaticZenRule rule) { + if (rule != null) { + // handle schedule-based rules + ScheduleInfo schedule = + ZenModeConfig.tryParseScheduleConditionId(rule.getConditionId()); + if (schedule != null) { + String desc = mScheduleHelper.getDaysAndTimeSummary(mContext, schedule); + return (desc != null) ? desc : + mContext.getResources().getString( + R.string.zen_mode_schedule_rule_days_none); + } + + // handle event-based rules + ZenModeConfig.EventInfo event = + ZenModeConfig.tryParseEventConditionId(rule.getConditionId()); + if (event != null) { + if (event.calName != null) { + return event.calName; + } else { + return mContext.getResources().getString( + R.string.zen_mode_event_rule_calendar_any); + } + } + } + return (rule == null || !rule.isEnabled()) ? mContext.getResources().getString(R.string.switch_off_text) : mContext.getResources().getString(R.string.switch_on_text); diff --git a/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java b/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java new file mode 100644 index 00000000000..9a4f108437a --- /dev/null +++ b/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2022 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.zen; + +import android.content.Context; +import android.service.notification.ZenModeConfig.ScheduleInfo; +import android.text.format.DateFormat; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +/** + * Helper class for shared functionality regarding descriptions of custom zen rule schedules. + */ +public class ZenRuleScheduleHelper { + // per-instance to ensure we're always using the current locale + private SimpleDateFormat mDayFormat; + + // Default constructor, which will use the current locale. + public ZenRuleScheduleHelper() { + mDayFormat = new SimpleDateFormat("EEE"); + } + + // Constructor for tests to provide an explicit locale + @VisibleForTesting + public ZenRuleScheduleHelper(Locale locale) { + mDayFormat = new SimpleDateFormat("EEE", locale); + } + + /** + * Returns an ordered, comma-separated list of the days that a schedule applies, or null if no + * days. + */ + public String getDaysDescription(Context context, ScheduleInfo schedule) { + // Compute an ordered, delimited list of day names based on the persisted user config. + final int[] days = schedule.days; + if (days != null && days.length > 0) { + final StringBuilder sb = new StringBuilder(); + final Calendar c = Calendar.getInstance(); + int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c); + for (int i = 0; i < daysOfWeek.length; i++) { + final int day = daysOfWeek[i]; + for (int j = 0; j < days.length; j++) { + if (day == days[j]) { + c.set(Calendar.DAY_OF_WEEK, day); + if (sb.length() > 0) { + sb.append(context.getString(R.string.summary_divider_text)); + } + sb.append(mDayFormat.format(c.getTime())); + break; + } + } + } + + if (sb.length() > 0) { + return sb.toString(); + } + } + return null; + } + + /** + * Returns an ordered summarized list of the days on which this schedule applies, with + * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed"). + */ + public String getShortDaysSummary(Context context, ScheduleInfo schedule) { + // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or + // "Sun-Mon,Wed,Fri" + final int[] days = schedule.days; + if (days != null && days.length > 0) { + final StringBuilder sb = new StringBuilder(); + final Calendar cStart = Calendar.getInstance(); + final Calendar cEnd = Calendar.getInstance(); + int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(cStart); + // the i for loop goes through days in order as determined by locale. as we walk through + // the days of the week, keep track of "start" and "last seen" as indicators for + // what's contiguous, and initialize them to something not near actual indices + int startDay = Integer.MIN_VALUE; + int lastSeenDay = Integer.MIN_VALUE; + for (int i = 0; i < daysOfWeek.length; i++) { + final int day = daysOfWeek[i]; + + // by default, output if this day is *not* included in the schedule, and thus + // ends a previously existing block. if this day is included in the schedule + // after all (as will be determined in the inner for loop), then output will be set + // to false. + boolean output = (i == lastSeenDay + 1); + for (int j = 0; j < days.length; j++) { + if (day == days[j]) { + // match for this day in the schedule (indicated by counter i) + if (i == lastSeenDay + 1) { + // contiguous to the block we're walking through right now, record it + // (specifically, i, the day index) and move on to the next day + lastSeenDay = i; + output = false; + } else { + // it's a match, but not 1 past the last match, we are starting a new + // block + startDay = i; + lastSeenDay = i; + } + + // if there is a match on the last day, also make sure to output at the end + // of this loop, and mark the day as the last day we'll have seen in the + // scheduled days. + if (i == daysOfWeek.length - 1) { + output = true; + } + break; + } + } + + // output in either of 2 cases: this day is not a match, so has ended any previous + // block, or this day *is* a match but is the last day of the week, so we need to + // summarize + if (output) { + // either describe just the single day if startDay == lastSeenDay, or + // output "startDay - lastSeenDay" as a group + if (sb.length() > 0) { + sb.append(context.getString(R.string.summary_divider_text)); + } + + if (startDay == lastSeenDay) { + // last group was only one day + cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]); + sb.append(mDayFormat.format(cStart.getTime())); + } else { + // last group was a contiguous group of days, so group them together + cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]); + cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]); + sb.append(context.getString(R.string.summary_range_symbol_combination, + mDayFormat.format(cStart.getTime()), + mDayFormat.format(cEnd.getTime()))); + } + } + } + + if (sb.length() > 0) { + return sb.toString(); + } + } + return null; + } + + /** + * Convenience method for representing the specified time in string format. + */ + private String timeString(Context context, int hour, int minute) { + final Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, hour); + c.set(Calendar.MINUTE, minute); + return DateFormat.getTimeFormat(context).format(c.getTime()); + } + + /** + * Combination description for a zen rule schedule including both day summary and time bounds. + */ + public String getDaysAndTimeSummary(Context context, ScheduleInfo schedule) { + final StringBuilder sb = new StringBuilder(); + String daysSummary = getShortDaysSummary(context, schedule); + if (daysSummary == null) { + // no use outputting times without dates + return null; + } + sb.append(daysSummary); + sb.append(context.getString(R.string.summary_divider_text)); + sb.append(context.getString(R.string.summary_range_symbol_combination, + timeString(context, schedule.startHour, schedule.startMinute), + timeString(context, schedule.endHour, schedule.endMinute))); + + return sb.toString(); + } +} diff --git a/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java b/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java new file mode 100644 index 00000000000..2faee080c6e --- /dev/null +++ b/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2022 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.zen; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.LocaleList; +import android.service.notification.ZenModeConfig.ScheduleInfo; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Calendar; +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +public class ZenRuleScheduleHelperTest { + private ZenRuleScheduleHelper mScheduleHelper; + private ScheduleInfo mScheduleInfo; + + private Context mContext; + + @Mock + private Resources mResources; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // explicitly initialize to Locale.US just for ease of explicitly testing the string values + // of the days of the week if the test locale doesn't happen to be in the US + mScheduleHelper = new ZenRuleScheduleHelper(Locale.US); + mScheduleInfo = new ScheduleInfo(); + + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getResources()).thenReturn(mResources); + + // Resources will be called upon to join strings together, either to get a divider + // or a combination of two strings. Conveniently, these have different signatures. + // Divider method calls getString(string divider id) + when(mResources.getString(anyInt())).thenReturn(","); + + // Combination method calls getString(combination id, first item, second item) + // and returns "first - second" + when(mResources.getString(anyInt(), anyString(), anyString())).thenAnswer( + invocation -> { + return invocation.getArgument(1).toString() // first item + + "-" + + invocation.getArgument(2).toString(); // second item + }); + + // for locale used in time format + Configuration config = new Configuration(); + config.setLocales(new LocaleList(Locale.US)); + when(mResources.getConfiguration()).thenReturn(config); + } + + @Test + public void getDaysDescription() { + // Test various cases of where the days are set. + // No days + mScheduleInfo.days = new int[] {}; + assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isNull(); + + // one day + mScheduleInfo.days = new int[] {Calendar.FRIDAY}; + assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isEqualTo("Fri"); + + // Monday through Friday + mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, + Calendar.THURSDAY, Calendar.FRIDAY}; + assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)) + .isEqualTo("Mon,Tue,Wed,Thu,Fri"); + + // Some scattered days of the week + mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, + Calendar.SATURDAY}; + assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)) + .isEqualTo("Sun,Wed,Thu,Sat"); + } + + @Test + public void getShortDaysSummary_noOrSingleDays() { + // Test various cases for grouping and not-grouping of days. + // No days + mScheduleInfo.days = new int[]{}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isNull(); + + // A single day at the beginning of the week + mScheduleInfo.days = new int[]{Calendar.SUNDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sun"); + + // A single day in the middle of the week + mScheduleInfo.days = new int[]{Calendar.THURSDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Thu"); + + // A single day at the end of the week + mScheduleInfo.days = new int[]{Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sat"); + } + + @Test + public void getShortDaysSummary_oneGroup() { + // The whole week + mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, + Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Sun-Sat"); + + // Various cases of one big group + // Sunday through Thursday + mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, + Calendar.WEDNESDAY, Calendar.THURSDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Sun-Thu"); + + // Wednesday through Saturday + mScheduleInfo.days = new int[] {Calendar.WEDNESDAY, Calendar.THURSDAY, + Calendar.FRIDAY, Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Wed-Sat"); + + // Monday through Friday + mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY, + Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Mon-Fri"); + } + + @Test + public void getShortDaysSummary_mixed() { + // cases combining groups and single days scattered around + mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.TUESDAY, + Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Sun,Tue-Thu,Sat"); + + mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, + Calendar.WEDNESDAY, Calendar.FRIDAY, Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Sun-Wed,Fri-Sat"); + + mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.WEDNESDAY, + Calendar.FRIDAY, Calendar.SATURDAY}; + assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)) + .isEqualTo("Mon,Wed,Fri-Sat"); + } + + @Test + public void getDaysAndTimeSummary() { + // Combination days & time settings + // No days, no output, even if the times are set. + mScheduleInfo.startHour = 10; + mScheduleInfo.endHour = 16; + mScheduleInfo.days = new int[]{}; + assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo)).isNull(); + + // If there are days then they are combined with the time combination + mScheduleInfo.days = new int[]{Calendar.SUNDAY, Calendar.MONDAY, Calendar.WEDNESDAY}; + assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo)) + .isEqualTo("Sun-Mon,Wed,10:00 AM-4:00 PM"); + } +}