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