1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.notification.modes; 18 19 import android.app.Flags; 20 import android.content.Context; 21 import android.service.notification.SystemZenRules; 22 import android.service.notification.ZenModeConfig; 23 import android.text.format.DateFormat; 24 import android.util.ArraySet; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.TextView; 28 import android.widget.ToggleButton; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.VisibleForTesting; 32 import androidx.fragment.app.Fragment; 33 import androidx.preference.Preference; 34 35 import com.android.settings.R; 36 import com.android.settingslib.widget.LayoutPreference; 37 38 import java.text.SimpleDateFormat; 39 import java.time.Duration; 40 import java.util.Arrays; 41 import java.util.Calendar; 42 import java.util.function.Function; 43 44 /** 45 * Preference controller for setting the start and end time and days of the week associated with 46 * an automatic zen mode. 47 */ 48 class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceController { 49 // per-instance to ensure we're always using the current locale 50 // E = day of the week; "EEEEE" is the shortest version; "EEEE" is the full name 51 private final SimpleDateFormat mShortDayFormat = new SimpleDateFormat("EEEEE"); 52 private final SimpleDateFormat mLongDayFormat = new SimpleDateFormat("EEEE"); 53 54 private static final String TAG = "ZenModeSetSchedulePreferenceController"; 55 private Fragment mParent; 56 private ZenModeConfig.ScheduleInfo mSchedule; 57 ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key, ZenModesBackend backend)58 ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key, 59 ZenModesBackend backend) { 60 super(context, key, backend); 61 mParent = parent; 62 } 63 64 @Override updateState(Preference preference, @NonNull ZenMode zenMode)65 public void updateState(Preference preference, @NonNull ZenMode zenMode) { 66 mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId()); 67 LayoutPreference layoutPref = (LayoutPreference) preference; 68 69 TextView start = layoutPref.findViewById(R.id.start_time); 70 start.setText(timeString(mSchedule.startHour, mSchedule.startMinute)); 71 start.setOnClickListener( 72 timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter)); 73 74 TextView end = layoutPref.findViewById(R.id.end_time); 75 end.setText(timeString(mSchedule.endHour, mSchedule.endMinute)); 76 end.setOnClickListener( 77 timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter)); 78 79 TextView durationView = layoutPref.findViewById(R.id.schedule_duration); 80 durationView.setText(getScheduleDurationDescription(mSchedule)); 81 82 ViewGroup daysContainer = layoutPref.findViewById(R.id.days_of_week_container); 83 setupDayToggles(daysContainer, mSchedule, Calendar.getInstance()); 84 } 85 timeString(int hour, int minute)86 private String timeString(int hour, int minute) { 87 final Calendar c = Calendar.getInstance(); 88 c.set(Calendar.HOUR_OF_DAY, hour); 89 c.set(Calendar.MINUTE, minute); 90 return DateFormat.getTimeFormat(mContext).format(c.getTime()); 91 } 92 isValidTime(int hour, int minute)93 private boolean isValidTime(int hour, int minute) { 94 return ZenModeConfig.isValidHour(hour) && ZenModeConfig.isValidMinute(minute); 95 } 96 getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule)97 private String getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule) { 98 final int startMin = 60 * schedule.startHour + schedule.startMinute; 99 final int endMin = 60 * schedule.endHour + schedule.endMinute; 100 final boolean nextDay = startMin >= endMin; 101 102 Duration scheduleDuration; 103 if (nextDay) { 104 // add one day's worth of minutes (24h x 60min) to end minute for end time calculation 105 int endMinNextDay = endMin + (24 * 60); 106 scheduleDuration = Duration.ofMinutes(endMinNextDay - startMin); 107 } else { 108 scheduleDuration = Duration.ofMinutes(endMin - startMin); 109 } 110 111 int hours = scheduleDuration.toHoursPart(); 112 int minutes = scheduleDuration.minusHours(hours).toMinutesPart(); 113 return mContext.getString(R.string.zen_mode_schedule_duration, hours, minutes); 114 } 115 116 @VisibleForTesting updateScheduleMode(ZenModeConfig.ScheduleInfo schedule)117 protected Function<ZenMode, ZenMode> updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) { 118 return (zenMode) -> { 119 zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule)); 120 if (Flags.modesApi() && Flags.modesUi()) { 121 zenMode.getRule().setTriggerDescription( 122 SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule)); 123 } 124 return zenMode; 125 }; 126 } 127 128 private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> { 129 if (!isValidTime(hour, minute)) { 130 return; 131 } 132 if (hour == mSchedule.startHour && minute == mSchedule.startMinute) { 133 return; 134 } 135 mSchedule.startHour = hour; 136 mSchedule.startMinute = minute; 137 saveMode(updateScheduleMode(mSchedule)); 138 }; 139 140 private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> { 141 if (!isValidTime(hour, minute)) { 142 return; 143 } 144 if (hour == mSchedule.endHour && minute == mSchedule.endMinute) { 145 return; 146 } 147 mSchedule.endHour = hour; 148 mSchedule.endMinute = minute; 149 saveMode(updateScheduleMode(mSchedule)); 150 }; 151 timePickerLauncher(int hour, int minute, ZenModeTimePickerFragment.TimeSetter timeSetter)152 private View.OnClickListener timePickerLauncher(int hour, int minute, 153 ZenModeTimePickerFragment.TimeSetter timeSetter) { 154 return v -> { 155 final ZenModeTimePickerFragment frag = new ZenModeTimePickerFragment(mContext, hour, 156 minute, timeSetter); 157 frag.show(mParent.getParentFragmentManager(), TAG); 158 }; 159 } 160 161 protected static int[] getDaysOfWeekForLocale(Calendar c) { 162 int[] daysOfWeek = new int[7]; 163 int currentDay = c.getFirstDayOfWeek(); 164 for (int i = 0; i < daysOfWeek.length; i++) { 165 if (currentDay > 7) currentDay = 1; 166 daysOfWeek[i] = currentDay; 167 currentDay++; 168 } 169 return daysOfWeek; 170 } 171 172 @VisibleForTesting 173 protected void setupDayToggles(ViewGroup dayContainer, ZenModeConfig.ScheduleInfo schedule, 174 Calendar c) { 175 int[] daysOfWeek = getDaysOfWeekForLocale(c); 176 177 // Index in daysOfWeek is associated with the [idx]'th object in the list of days in the 178 // layout. Note that because the order of the days of the week may differ per locale, this 179 // is not necessarily the same as the actual value of the day number at that index. 180 for (int i = 0; i < daysOfWeek.length; i++) { 181 ToggleButton dayToggle = dayContainer.findViewById(resIdForDayIndex(i)); 182 if (dayToggle == null) { 183 continue; 184 } 185 186 final int day = daysOfWeek[i]; 187 c.set(Calendar.DAY_OF_WEEK, day); 188 189 // find current setting for this day 190 boolean dayEnabled = false; 191 if (schedule.days != null) { 192 for (int idx = 0; idx < schedule.days.length; idx++) { 193 if (schedule.days[idx] == day) { 194 dayEnabled = true; 195 break; 196 } 197 } 198 } 199 200 // On/off is indicated by visuals, and both states share the shortest (one-character) 201 // day label. 202 dayToggle.setTextOn(mShortDayFormat.format(c.getTime())); 203 dayToggle.setTextOff(mShortDayFormat.format(c.getTime())); 204 dayToggle.setContentDescription(mLongDayFormat.format(c.getTime())); 205 206 dayToggle.setChecked(dayEnabled); 207 dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> { 208 if (updateScheduleDays(schedule, day, isChecked)) { 209 saveMode(updateScheduleMode(schedule)); 210 } 211 }); 212 213 // If display and text settings cause the text to be larger than its containing box, 214 // don't show scrollbars. 215 dayToggle.setVerticalScrollBarEnabled(false); 216 dayToggle.setHorizontalScrollBarEnabled(false); 217 } 218 } 219 220 // Updates the set of enabled days in provided schedule to either turn on or off the given day. 221 // The format of days in ZenModeConfig.ScheduleInfo is an array of days, where inclusion means 222 // the schedule is set to run on that day. Returns whether anything was changed. 223 @VisibleForTesting 224 protected static boolean updateScheduleDays(ZenModeConfig.ScheduleInfo schedule, int day, 225 boolean set) { 226 // Build a set representing the days that are currently set in mSchedule. 227 ArraySet<Integer> daySet = new ArraySet(); 228 if (schedule.days != null) { 229 for (int i = 0; i < schedule.days.length; i++) { 230 daySet.add(schedule.days[i]); 231 } 232 } 233 234 if (daySet.contains(day) != set) { 235 if (set) { 236 daySet.add(day); 237 } else { 238 daySet.remove(day); 239 } 240 241 // rebuild days array for mSchedule 242 final int[] out = new int[daySet.size()]; 243 for (int i = 0; i < daySet.size(); i++) { 244 out[i] = daySet.valueAt(i); 245 } 246 Arrays.sort(out); 247 schedule.days = out; 248 return true; 249 } 250 // If the setting is the same as it was before, no need to update anything. 251 return false; 252 } 253 254 protected static int resIdForDayIndex(int idx) { 255 switch (idx) { 256 case 0: 257 return R.id.day0; 258 case 1: 259 return R.id.day1; 260 case 2: 261 return R.id.day2; 262 case 3: 263 return R.id.day3; 264 case 4: 265 return R.id.day4; 266 case 5: 267 return R.id.day5; 268 case 6: 269 return R.id.day6; 270 default: 271 return 0; // unknown 272 } 273 } 274 } 275