• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.service.notification;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.StringRes;
22 import android.app.AutomaticZenRule;
23 import android.app.Flags;
24 import android.content.Context;
25 import android.service.notification.ZenModeConfig.EventInfo;
26 import android.service.notification.ZenModeConfig.ScheduleInfo;
27 import android.service.notification.ZenModeConfig.ZenRule;
28 import android.text.format.DateFormat;
29 import android.util.Log;
30 
31 import com.android.internal.R;
32 
33 import java.text.SimpleDateFormat;
34 import java.util.Calendar;
35 import java.util.Locale;
36 import java.util.Objects;
37 
38 /**
39  * Helper methods for schedule-type (system-owned) rules.
40  * @hide
41  */
42 public final class SystemZenRules {
43 
44     private static final String TAG = "SystemZenRules";
45 
46     public static final String PACKAGE_ANDROID = "android";
47 
48     /** Updates existing system-owned rules to use the new Modes fields (type, etc). */
maybeUpgradeRules(Context context, ZenModeConfig config)49     public static void maybeUpgradeRules(Context context, ZenModeConfig config) {
50         for (ZenRule rule : config.automaticRules.values()) {
51             if (isSystemOwnedRule(rule)) {
52                 if (rule.type == AutomaticZenRule.TYPE_UNKNOWN) {
53                     upgradeSystemProviderRule(context, rule);
54                 }
55                 if (Flags.modesUi()) {
56                     rule.allowManualInvocation = true;
57                 }
58             }
59         }
60     }
61 
62     /**
63      * Returns whether the rule corresponds to a system ConditionProviderService (i.e. it is owned
64      * by the "android" package).
65      */
isSystemOwnedRule(ZenRule rule)66     public static boolean isSystemOwnedRule(ZenRule rule) {
67         return PACKAGE_ANDROID.equals(rule.pkg);
68     }
69 
upgradeSystemProviderRule(Context context, ZenRule rule)70     private static void upgradeSystemProviderRule(Context context, ZenRule rule) {
71         ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
72         if (scheduleInfo != null) {
73             rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
74             rule.triggerDescription = getTriggerDescriptionForScheduleTime(context, scheduleInfo);
75             return;
76         }
77         EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
78         if (eventInfo != null) {
79             rule.type = AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
80             rule.triggerDescription = getTriggerDescriptionForScheduleEvent(context, eventInfo);
81             return;
82         }
83         Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
84     }
85 
86     /**
87      * Updates the {@link ZenRule#triggerDescription} of the system-owned rule based on the schedule
88      * or event condition encoded in its {@link ZenRule#conditionId}.
89      *
90      * @return {@code true} if the trigger description was updated.
91      */
updateTriggerDescription(Context context, ZenRule rule)92     public static boolean updateTriggerDescription(Context context, ZenRule rule) {
93         ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
94         if (scheduleInfo != null) {
95             return updateTriggerDescription(rule,
96                     getTriggerDescriptionForScheduleTime(context, scheduleInfo));
97         }
98         EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
99         if (eventInfo != null) {
100             return updateTriggerDescription(rule,
101                     getTriggerDescriptionForScheduleEvent(context, eventInfo));
102         }
103         Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
104         return false;
105     }
106 
updateTriggerDescription(ZenRule rule, String triggerDescription)107     private static boolean updateTriggerDescription(ZenRule rule, String triggerDescription) {
108         if (!Objects.equals(rule.triggerDescription, triggerDescription)) {
109             rule.triggerDescription = triggerDescription;
110             return true;
111         }
112         return false;
113     }
114 
115     /**
116      * Returns a suitable trigger description for a time-schedule rule (e.g. "Mon-Fri, 8:00-10:00"),
117      * using the Context's current locale.
118      */
119     @Nullable
getTriggerDescriptionForScheduleTime(Context context, @NonNull ScheduleInfo schedule)120     public static String getTriggerDescriptionForScheduleTime(Context context,
121             @NonNull ScheduleInfo schedule) {
122         String daysSummary = getDaysOfWeekShort(context, schedule);
123         if (daysSummary == null) {
124             // no use outputting times without dates
125             return null;
126         }
127         return context.getString(
128                 R.string.zen_mode_trigger_summary_combined,
129                 daysSummary,
130                 getTimeSummary(context, schedule)
131         );
132     }
133 
134     /**
135      * Returns a short, ordered summarized list of the days on which this schedule applies, using
136      * abbreviated week days, with adjacent days grouped together ("Sun-Wed" instead of
137      * "Sun,Mon,Tue,Wed").
138      */
139     @Nullable
getDaysOfWeekShort(Context context, @NonNull ScheduleInfo schedule)140     public static String getDaysOfWeekShort(Context context, @NonNull ScheduleInfo schedule) {
141         return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_symbol_combination,
142                 new SimpleDateFormat("EEE", getLocale(context)), schedule);
143     }
144 
145     /**
146      * Returns a string representing the days on which this schedule applies, using full week days,
147      * with adjacent days grouped together (e.g. "Sunday to Wednesday" instead of
148      * "Sunday,Monday,Tuesday,Wednesday").
149      */
150     @Nullable
getDaysOfWeekFull(Context context, @NonNull ScheduleInfo schedule)151     public static String getDaysOfWeekFull(Context context, @NonNull ScheduleInfo schedule) {
152         return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_words,
153                 new SimpleDateFormat("EEEE", getLocale(context)), schedule);
154     }
155 
156     /**
157      * Returns an ordered summarized list of the days on which this schedule applies, with
158      * adjacent days grouped together. The formatting of each individual day of week is done with
159      * the provided {@link SimpleDateFormat}.
160      */
161     @Nullable
getDaysSummary(Context context, @StringRes int rangeFormatResId, SimpleDateFormat dayOfWeekFormat, @NonNull ScheduleInfo schedule)162     private static String getDaysSummary(Context context, @StringRes int rangeFormatResId,
163             SimpleDateFormat dayOfWeekFormat, @NonNull ScheduleInfo schedule) {
164         // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
165         // "Sun-Mon,Wed,Fri"
166         final int[] days = schedule.days;
167         if (days != null && days.length > 0) {
168             final StringBuilder sb = new StringBuilder();
169             final Calendar cStart = Calendar.getInstance(getLocale(context));
170             final Calendar cEnd = Calendar.getInstance(getLocale(context));
171             int[] daysOfWeek = getDaysOfWeekForLocale(cStart);
172             // the i for loop goes through days in order as determined by locale. as we walk through
173             // the days of the week, keep track of "start" and "last seen"  as indicators for
174             // what's contiguous, and initialize them to something not near actual indices
175             int startDay = Integer.MIN_VALUE;
176             int lastSeenDay = Integer.MIN_VALUE;
177             for (int i = 0; i < daysOfWeek.length; i++) {
178                 final int day = daysOfWeek[i];
179 
180                 // by default, output if this day is *not* included in the schedule, and thus
181                 // ends a previously existing block. if this day is included in the schedule
182                 // after all (as will be determined in the inner for loop), then output will be set
183                 // to false.
184                 boolean output = (i == lastSeenDay + 1);
185                 for (int j = 0; j < days.length; j++) {
186                     if (day == days[j]) {
187                         // match for this day in the schedule (indicated by counter i)
188                         if (i == lastSeenDay + 1) {
189                             // contiguous to the block we're walking through right now, record it
190                             // (specifically, i, the day index) and move on to the next day
191                             lastSeenDay = i;
192                             output = false;
193                         } else {
194                             // it's a match, but not 1 past the last match, we are starting a new
195                             // block
196                             startDay = i;
197                             lastSeenDay = i;
198                         }
199 
200                         // if there is a match on the last day, also make sure to output at the end
201                         // of this loop, and mark the day as the last day we'll have seen in the
202                         // scheduled days.
203                         if (i == daysOfWeek.length - 1) {
204                             output = true;
205                         }
206                         break;
207                     }
208                 }
209 
210                 // output in either of 2 cases: this day is not a match, so has ended any previous
211                 // block, or this day *is* a match but is the last day of the week, so we need to
212                 // summarize
213                 if (output) {
214                     // either describe just the single day if startDay == lastSeenDay, or
215                     // output "startDay - lastSeenDay" as a group
216                     if (sb.length() > 0) {
217                         sb.append(
218                                 context.getString(R.string.zen_mode_trigger_summary_divider_text));
219                     }
220 
221                     if (startDay == lastSeenDay) {
222                         // last group was only one day
223                         cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
224                         sb.append(dayOfWeekFormat.format(cStart.getTime()));
225                     } else {
226                         // last group was a contiguous group of days, so group them together
227                         cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
228                         cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
229                         sb.append(context.getString(
230                                 rangeFormatResId,
231                                 dayOfWeekFormat.format(cStart.getTime()),
232                                 dayOfWeekFormat.format(cEnd.getTime())));
233                     }
234                 }
235             }
236 
237             if (sb.length() > 0) {
238                 return sb.toString();
239             }
240         }
241         return null;
242     }
243 
244     /** Returns the time part of a {@link ScheduleInfo}, e.g. {@code 9:00-17:00}. */
getTimeSummary(Context context, @NonNull ScheduleInfo schedule)245     public static String getTimeSummary(Context context, @NonNull ScheduleInfo schedule) {
246         return context.getString(
247                 R.string.zen_mode_trigger_summary_range_symbol_combination,
248                 timeString(context, schedule.startHour, schedule.startMinute),
249                 timeString(context, schedule.endHour, schedule.endMinute));
250     }
251 
252     /**
253      * Convenience method for representing the specified time in string format.
254      */
timeString(Context context, int hour, int minute)255     private static String timeString(Context context, int hour, int minute) {
256         final Calendar c = Calendar.getInstance(getLocale(context));
257         c.set(Calendar.HOUR_OF_DAY, hour);
258         c.set(Calendar.MINUTE, minute);
259         return DateFormat.getTimeFormat(context).format(c.getTime());
260     }
261 
getDaysOfWeekForLocale(Calendar c)262     private static int[] getDaysOfWeekForLocale(Calendar c) {
263         int[] daysOfWeek = new int[7];
264         int currentDay = c.getFirstDayOfWeek();
265         for (int i = 0; i < daysOfWeek.length; i++) {
266             if (currentDay > 7) currentDay = 1;
267             daysOfWeek[i] = currentDay;
268             currentDay++;
269         }
270         return daysOfWeek;
271     }
272 
getLocale(Context context)273     private static Locale getLocale(Context context) {
274         return context.getResources().getConfiguration().getLocales().get(0);
275     }
276 
277     /**
278      * Returns a suitable trigger description for a calendar-schedule rule (either the name of the
279      * calendar, or a message indicating all calendars are included).
280      */
getTriggerDescriptionForScheduleEvent(Context context, @NonNull EventInfo event)281     public static String getTriggerDescriptionForScheduleEvent(Context context,
282             @NonNull EventInfo event) {
283         if (event.calName != null) {
284             return event.calName;
285         } else {
286             return context.getResources().getString(
287                     R.string.zen_mode_trigger_event_calendar_any);
288         }
289     }
290 
SystemZenRules()291     private SystemZenRules() {}
292 }
293