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