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