Loading src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java +6 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -149,31 +147,12 @@ 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); 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); } Loading src/com/android/settings/notification/zen/ZenRulePreference.java +39 −67 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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<String, AutomaticZenRule> ruleEntry, Fragment parent, MetricsFeatureProvider metricsProvider) { super(context); setLayoutResource(R.layout.preference_checkable_two_target); mBackend = ZenModeBackend.getInstance(context); mContext = context; mRule = ruleEntry.getValue(); Loading @@ -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) { Loading @@ -126,33 +84,23 @@ 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); } private void setChecked(boolean checked) { mChecked = checked; if (mCheckBox != null) { mCheckBox.setChecked(checked); } mContext.startActivity(mIntent); } private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() { @Override public void onClick(View v) { mRule.setEnabled(!mChecked); public void setChecked(boolean checked) { mRule.setEnabled(checked); mBackend.updateZenRule(mId, mRule); setChecked(mRule.isEnabled()); setAttributes(mRule); super.setChecked(checked); } }; protected void setAttributes(AutomaticZenRule rule) { final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId( Loading @@ -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); Loading src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * 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(); } } tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * 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"); } } Loading
src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java +6 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -149,31 +147,12 @@ 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); 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); } Loading
src/com/android/settings/notification/zen/ZenRulePreference.java +39 −67 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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<String, AutomaticZenRule> ruleEntry, Fragment parent, MetricsFeatureProvider metricsProvider) { super(context); setLayoutResource(R.layout.preference_checkable_two_target); mBackend = ZenModeBackend.getInstance(context); mContext = context; mRule = ruleEntry.getValue(); Loading @@ -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) { Loading @@ -126,33 +84,23 @@ 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); } private void setChecked(boolean checked) { mChecked = checked; if (mCheckBox != null) { mCheckBox.setChecked(checked); } mContext.startActivity(mIntent); } private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() { @Override public void onClick(View v) { mRule.setEnabled(!mChecked); public void setChecked(boolean checked) { mRule.setEnabled(checked); mBackend.updateZenRule(mId, mRule); setChecked(mRule.isEnabled()); setAttributes(mRule); super.setChecked(checked); } }; protected void setAttributes(AutomaticZenRule rule) { final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId( Loading @@ -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); Loading
src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * 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(); } }
tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java 0 → 100644 +191 −0 Original line number Diff line number Diff line /* * 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"); } }