ZenModeConfig.java revision 4db0d98b42a723f2e16c6595e85e866fd26c6d98
1/** 2 * Copyright (c) 2014, 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 17package android.service.notification; 18 19import android.content.ComponentName; 20import android.net.Uri; 21import android.os.Parcel; 22import android.os.Parcelable; 23import android.text.TextUtils; 24import android.util.Slog; 25 26import org.xmlpull.v1.XmlPullParser; 27import org.xmlpull.v1.XmlPullParserException; 28import org.xmlpull.v1.XmlSerializer; 29 30import java.io.IOException; 31import java.util.ArrayList; 32import java.util.Arrays; 33import java.util.Calendar; 34import java.util.Objects; 35 36/** 37 * Persisted configuration for zen mode. 38 * 39 * @hide 40 */ 41public class ZenModeConfig implements Parcelable { 42 private static String TAG = "ZenModeConfig"; 43 44 public static final String SLEEP_MODE_NIGHTS = "nights"; 45 public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; 46 public static final String SLEEP_MODE_DAYS_PREFIX = "days:"; 47 48 public static final int SOURCE_ANYONE = 0; 49 public static final int SOURCE_CONTACT = 1; 50 public static final int SOURCE_STAR = 2; 51 public static final int MAX_SOURCE = SOURCE_STAR; 52 53 public static final int[] ALL_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, 54 Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY }; 55 public static final int[] WEEKNIGHT_DAYS = { Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, 56 Calendar.WEDNESDAY, Calendar.THURSDAY }; 57 58 private static final int XML_VERSION = 1; 59 private static final String ZEN_TAG = "zen"; 60 private static final String ZEN_ATT_VERSION = "version"; 61 private static final String ALLOW_TAG = "allow"; 62 private static final String ALLOW_ATT_CALLS = "calls"; 63 private static final String ALLOW_ATT_MESSAGES = "messages"; 64 private static final String ALLOW_ATT_FROM = "from"; 65 private static final String SLEEP_TAG = "sleep"; 66 private static final String SLEEP_ATT_MODE = "mode"; 67 68 private static final String SLEEP_ATT_START_HR = "startHour"; 69 private static final String SLEEP_ATT_START_MIN = "startMin"; 70 private static final String SLEEP_ATT_END_HR = "endHour"; 71 private static final String SLEEP_ATT_END_MIN = "endMin"; 72 73 private static final String CONDITION_TAG = "condition"; 74 private static final String CONDITION_ATT_COMPONENT = "component"; 75 private static final String CONDITION_ATT_ID = "id"; 76 private static final String CONDITION_ATT_SUMMARY = "summary"; 77 private static final String CONDITION_ATT_LINE1 = "line1"; 78 private static final String CONDITION_ATT_LINE2 = "line2"; 79 private static final String CONDITION_ATT_ICON = "icon"; 80 private static final String CONDITION_ATT_STATE = "state"; 81 private static final String CONDITION_ATT_FLAGS = "flags"; 82 83 private static final String EXIT_CONDITION_TAG = "exitCondition"; 84 private static final String EXIT_CONDITION_ATT_COMPONENT = "component"; 85 86 public boolean allowCalls; 87 public boolean allowMessages; 88 public int allowFrom = SOURCE_ANYONE; 89 90 public String sleepMode; 91 public int sleepStartHour; // 0-23 92 public int sleepStartMinute; // 0-59 93 public int sleepEndHour; 94 public int sleepEndMinute; 95 public ComponentName[] conditionComponents; 96 public Uri[] conditionIds; 97 public Condition exitCondition; 98 public ComponentName exitConditionComponent; 99 100 public ZenModeConfig() { } 101 102 public ZenModeConfig(Parcel source) { 103 allowCalls = source.readInt() == 1; 104 allowMessages = source.readInt() == 1; 105 if (source.readInt() == 1) { 106 sleepMode = source.readString(); 107 } 108 sleepStartHour = source.readInt(); 109 sleepStartMinute = source.readInt(); 110 sleepEndHour = source.readInt(); 111 sleepEndMinute = source.readInt(); 112 int len = source.readInt(); 113 if (len > 0) { 114 conditionComponents = new ComponentName[len]; 115 source.readTypedArray(conditionComponents, ComponentName.CREATOR); 116 } 117 len = source.readInt(); 118 if (len > 0) { 119 conditionIds = new Uri[len]; 120 source.readTypedArray(conditionIds, Uri.CREATOR); 121 } 122 allowFrom = source.readInt(); 123 exitCondition = source.readParcelable(null); 124 exitConditionComponent = source.readParcelable(null); 125 } 126 127 @Override 128 public void writeToParcel(Parcel dest, int flags) { 129 dest.writeInt(allowCalls ? 1 : 0); 130 dest.writeInt(allowMessages ? 1 : 0); 131 if (sleepMode != null) { 132 dest.writeInt(1); 133 dest.writeString(sleepMode); 134 } else { 135 dest.writeInt(0); 136 } 137 dest.writeInt(sleepStartHour); 138 dest.writeInt(sleepStartMinute); 139 dest.writeInt(sleepEndHour); 140 dest.writeInt(sleepEndMinute); 141 if (conditionComponents != null && conditionComponents.length > 0) { 142 dest.writeInt(conditionComponents.length); 143 dest.writeTypedArray(conditionComponents, 0); 144 } else { 145 dest.writeInt(0); 146 } 147 if (conditionIds != null && conditionIds.length > 0) { 148 dest.writeInt(conditionIds.length); 149 dest.writeTypedArray(conditionIds, 0); 150 } else { 151 dest.writeInt(0); 152 } 153 dest.writeInt(allowFrom); 154 dest.writeParcelable(exitCondition, 0); 155 dest.writeParcelable(exitConditionComponent, 0); 156 } 157 158 @Override 159 public String toString() { 160 return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') 161 .append("allowCalls=").append(allowCalls) 162 .append(",allowMessages=").append(allowMessages) 163 .append(",allowFrom=").append(sourceToString(allowFrom)) 164 .append(",sleepMode=").append(sleepMode) 165 .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) 166 .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) 167 .append(",conditionComponents=") 168 .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents)) 169 .append(",conditionIds=") 170 .append(conditionIds == null ? null : TextUtils.join(",", conditionIds)) 171 .append(",exitCondition=").append(exitCondition) 172 .append(",exitConditionComponent=").append(exitConditionComponent) 173 .append(']').toString(); 174 } 175 176 public static String sourceToString(int source) { 177 switch (source) { 178 case SOURCE_ANYONE: 179 return "anyone"; 180 case SOURCE_CONTACT: 181 return "contacts"; 182 case SOURCE_STAR: 183 return "stars"; 184 default: 185 return "UNKNOWN"; 186 } 187 } 188 189 @Override 190 public boolean equals(Object o) { 191 if (!(o instanceof ZenModeConfig)) return false; 192 if (o == this) return true; 193 final ZenModeConfig other = (ZenModeConfig) o; 194 return other.allowCalls == allowCalls 195 && other.allowMessages == allowMessages 196 && other.allowFrom == allowFrom 197 && Objects.equals(other.sleepMode, sleepMode) 198 && other.sleepStartHour == sleepStartHour 199 && other.sleepStartMinute == sleepStartMinute 200 && other.sleepEndHour == sleepEndHour 201 && other.sleepEndMinute == sleepEndMinute 202 && Objects.deepEquals(other.conditionComponents, conditionComponents) 203 && Objects.deepEquals(other.conditionIds, conditionIds) 204 && Objects.equals(other.exitCondition, exitCondition) 205 && Objects.equals(other.exitConditionComponent, exitConditionComponent); 206 } 207 208 @Override 209 public int hashCode() { 210 return Objects.hash(allowCalls, allowMessages, allowFrom, sleepMode, 211 sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute, 212 Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds), 213 exitCondition, exitConditionComponent); 214 } 215 216 public boolean isValid() { 217 return isValidHour(sleepStartHour) && isValidMinute(sleepStartMinute) 218 && isValidHour(sleepEndHour) && isValidMinute(sleepEndMinute) 219 && isValidSleepMode(sleepMode); 220 } 221 222 public static boolean isValidSleepMode(String sleepMode) { 223 return sleepMode == null || sleepMode.equals(SLEEP_MODE_NIGHTS) 224 || sleepMode.equals(SLEEP_MODE_WEEKNIGHTS) || tryParseDays(sleepMode) != null; 225 } 226 227 public static int[] tryParseDays(String sleepMode) { 228 if (sleepMode == null) return null; 229 sleepMode = sleepMode.trim(); 230 if (SLEEP_MODE_NIGHTS.equals(sleepMode)) return ALL_DAYS; 231 if (SLEEP_MODE_WEEKNIGHTS.equals(sleepMode)) return WEEKNIGHT_DAYS; 232 if (!sleepMode.startsWith(SLEEP_MODE_DAYS_PREFIX)) return null; 233 if (sleepMode.equals(SLEEP_MODE_DAYS_PREFIX)) return null; 234 final String[] tokens = sleepMode.substring(SLEEP_MODE_DAYS_PREFIX.length()).split(","); 235 if (tokens.length == 0) return null; 236 final int[] rt = new int[tokens.length]; 237 for (int i = 0; i < tokens.length; i++) { 238 final int day = tryParseInt(tokens[i], -1); 239 if (day == -1) return null; 240 rt[i] = day; 241 } 242 return rt; 243 } 244 245 private static int tryParseInt(String value, int defValue) { 246 if (TextUtils.isEmpty(value)) return defValue; 247 try { 248 return Integer.valueOf(value); 249 } catch (NumberFormatException e) { 250 return defValue; 251 } 252 } 253 254 public static ZenModeConfig readXml(XmlPullParser parser) 255 throws XmlPullParserException, IOException { 256 int type = parser.getEventType(); 257 if (type != XmlPullParser.START_TAG) return null; 258 String tag = parser.getName(); 259 if (!ZEN_TAG.equals(tag)) return null; 260 final ZenModeConfig rt = new ZenModeConfig(); 261 final int version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION); 262 final ArrayList<ComponentName> conditionComponents = new ArrayList<ComponentName>(); 263 final ArrayList<Uri> conditionIds = new ArrayList<Uri>(); 264 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 265 tag = parser.getName(); 266 if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { 267 if (!conditionComponents.isEmpty()) { 268 rt.conditionComponents = conditionComponents 269 .toArray(new ComponentName[conditionComponents.size()]); 270 rt.conditionIds = conditionIds.toArray(new Uri[conditionIds.size()]); 271 } 272 return rt; 273 } 274 if (type == XmlPullParser.START_TAG) { 275 if (ALLOW_TAG.equals(tag)) { 276 rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); 277 rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); 278 rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE); 279 if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) { 280 throw new IndexOutOfBoundsException("bad source in config:" + rt.allowFrom); 281 } 282 } else if (SLEEP_TAG.equals(tag)) { 283 final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); 284 rt.sleepMode = isValidSleepMode(mode)? mode : null; 285 final int startHour = safeInt(parser, SLEEP_ATT_START_HR, 0); 286 final int startMinute = safeInt(parser, SLEEP_ATT_START_MIN, 0); 287 final int endHour = safeInt(parser, SLEEP_ATT_END_HR, 0); 288 final int endMinute = safeInt(parser, SLEEP_ATT_END_MIN, 0); 289 rt.sleepStartHour = isValidHour(startHour) ? startHour : 0; 290 rt.sleepStartMinute = isValidMinute(startMinute) ? startMinute : 0; 291 rt.sleepEndHour = isValidHour(endHour) ? endHour : 0; 292 rt.sleepEndMinute = isValidMinute(endMinute) ? endMinute : 0; 293 } else if (CONDITION_TAG.equals(tag)) { 294 final ComponentName component = 295 safeComponentName(parser, CONDITION_ATT_COMPONENT); 296 final Uri conditionId = safeUri(parser, CONDITION_ATT_ID); 297 if (component != null && conditionId != null) { 298 conditionComponents.add(component); 299 conditionIds.add(conditionId); 300 } 301 } else if (EXIT_CONDITION_TAG.equals(tag)) { 302 rt.exitCondition = readConditionXml(parser); 303 if (rt.exitCondition != null) { 304 rt.exitConditionComponent = 305 safeComponentName(parser, EXIT_CONDITION_ATT_COMPONENT); 306 } 307 } 308 } 309 } 310 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 311 } 312 313 public void writeXml(XmlSerializer out) throws IOException { 314 out.startTag(null, ZEN_TAG); 315 out.attribute(null, ZEN_ATT_VERSION, Integer.toString(XML_VERSION)); 316 317 out.startTag(null, ALLOW_TAG); 318 out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls)); 319 out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages)); 320 out.attribute(null, ALLOW_ATT_FROM, Integer.toString(allowFrom)); 321 out.endTag(null, ALLOW_TAG); 322 323 out.startTag(null, SLEEP_TAG); 324 if (sleepMode != null) { 325 out.attribute(null, SLEEP_ATT_MODE, sleepMode); 326 } 327 out.attribute(null, SLEEP_ATT_START_HR, Integer.toString(sleepStartHour)); 328 out.attribute(null, SLEEP_ATT_START_MIN, Integer.toString(sleepStartMinute)); 329 out.attribute(null, SLEEP_ATT_END_HR, Integer.toString(sleepEndHour)); 330 out.attribute(null, SLEEP_ATT_END_MIN, Integer.toString(sleepEndMinute)); 331 out.endTag(null, SLEEP_TAG); 332 333 if (conditionComponents != null && conditionIds != null 334 && conditionComponents.length == conditionIds.length) { 335 for (int i = 0; i < conditionComponents.length; i++) { 336 out.startTag(null, CONDITION_TAG); 337 out.attribute(null, CONDITION_ATT_COMPONENT, 338 conditionComponents[i].flattenToString()); 339 out.attribute(null, CONDITION_ATT_ID, conditionIds[i].toString()); 340 out.endTag(null, CONDITION_TAG); 341 } 342 } 343 if (exitCondition != null && exitConditionComponent != null) { 344 out.startTag(null, EXIT_CONDITION_TAG); 345 out.attribute(null, EXIT_CONDITION_ATT_COMPONENT, 346 exitConditionComponent.flattenToString()); 347 writeConditionXml(exitCondition, out); 348 out.endTag(null, EXIT_CONDITION_TAG); 349 } 350 out.endTag(null, ZEN_TAG); 351 } 352 353 public static Condition readConditionXml(XmlPullParser parser) { 354 final Uri id = safeUri(parser, CONDITION_ATT_ID); 355 final String summary = parser.getAttributeValue(null, CONDITION_ATT_SUMMARY); 356 final String line1 = parser.getAttributeValue(null, CONDITION_ATT_LINE1); 357 final String line2 = parser.getAttributeValue(null, CONDITION_ATT_LINE2); 358 final int icon = safeInt(parser, CONDITION_ATT_ICON, -1); 359 final int state = safeInt(parser, CONDITION_ATT_STATE, -1); 360 final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1); 361 try { 362 return new Condition(id, summary, line1, line2, icon, state, flags); 363 } catch (IllegalArgumentException e) { 364 Slog.w(TAG, "Unable to read condition xml", e); 365 return null; 366 } 367 } 368 369 public static void writeConditionXml(Condition c, XmlSerializer out) throws IOException { 370 out.attribute(null, CONDITION_ATT_ID, c.id.toString()); 371 out.attribute(null, CONDITION_ATT_SUMMARY, c.summary); 372 out.attribute(null, CONDITION_ATT_LINE1, c.line1); 373 out.attribute(null, CONDITION_ATT_LINE2, c.line2); 374 out.attribute(null, CONDITION_ATT_ICON, Integer.toString(c.icon)); 375 out.attribute(null, CONDITION_ATT_STATE, Integer.toString(c.state)); 376 out.attribute(null, CONDITION_ATT_FLAGS, Integer.toString(c.flags)); 377 } 378 379 public static boolean isValidHour(int val) { 380 return val >= 0 && val < 24; 381 } 382 383 public static boolean isValidMinute(int val) { 384 return val >= 0 && val < 60; 385 } 386 387 private static boolean safeBoolean(XmlPullParser parser, String att, boolean defValue) { 388 final String val = parser.getAttributeValue(null, att); 389 if (TextUtils.isEmpty(val)) return defValue; 390 return Boolean.valueOf(val); 391 } 392 393 private static int safeInt(XmlPullParser parser, String att, int defValue) { 394 final String val = parser.getAttributeValue(null, att); 395 return tryParseInt(val, defValue); 396 } 397 398 private static ComponentName safeComponentName(XmlPullParser parser, String att) { 399 final String val = parser.getAttributeValue(null, att); 400 if (TextUtils.isEmpty(val)) return null; 401 return ComponentName.unflattenFromString(val); 402 } 403 404 private static Uri safeUri(XmlPullParser parser, String att) { 405 final String val = parser.getAttributeValue(null, att); 406 if (TextUtils.isEmpty(val)) return null; 407 return Uri.parse(val); 408 } 409 410 @Override 411 public int describeContents() { 412 return 0; 413 } 414 415 public ZenModeConfig copy() { 416 final Parcel parcel = Parcel.obtain(); 417 try { 418 writeToParcel(parcel, 0); 419 parcel.setDataPosition(0); 420 return new ZenModeConfig(parcel); 421 } finally { 422 parcel.recycle(); 423 } 424 } 425 426 public static final Parcelable.Creator<ZenModeConfig> CREATOR 427 = new Parcelable.Creator<ZenModeConfig>() { 428 @Override 429 public ZenModeConfig createFromParcel(Parcel source) { 430 return new ZenModeConfig(source); 431 } 432 433 @Override 434 public ZenModeConfig[] newArray(int size) { 435 return new ZenModeConfig[size]; 436 } 437 }; 438 439 public DowntimeInfo toDowntimeInfo() { 440 final DowntimeInfo downtime = new DowntimeInfo(); 441 downtime.startHour = sleepStartHour; 442 downtime.startMinute = sleepStartMinute; 443 downtime.endHour = sleepEndHour; 444 downtime.endMinute = sleepEndMinute; 445 return downtime; 446 } 447 448 // For built-in conditions 449 private static final String SYSTEM_AUTHORITY = "android"; 450 451 // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951 452 private static final String COUNTDOWN_PATH = "countdown"; 453 454 public static Uri toCountdownConditionId(long time) { 455 return new Uri.Builder().scheme(Condition.SCHEME) 456 .authority(SYSTEM_AUTHORITY) 457 .appendPath(COUNTDOWN_PATH) 458 .appendPath(Long.toString(time)) 459 .build(); 460 } 461 462 public static long tryParseCountdownConditionId(Uri conditionId) { 463 if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY)) return 0; 464 if (conditionId.getPathSegments().size() != 2 465 || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0; 466 try { 467 return Long.parseLong(conditionId.getPathSegments().get(1)); 468 } catch (RuntimeException e) { 469 Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e); 470 return 0; 471 } 472 } 473 474 public static boolean isValidCountdownConditionId(Uri conditionId) { 475 return tryParseCountdownConditionId(conditionId) != 0; 476 } 477 478 // Built-in downtime conditions, e.g. condition://android/downtime?start=10.00&end=7.00 479 private static final String DOWNTIME_PATH = "downtime"; 480 481 public static Uri toDowntimeConditionId(DowntimeInfo downtime) { 482 return new Uri.Builder().scheme(Condition.SCHEME) 483 .authority(SYSTEM_AUTHORITY) 484 .appendPath(DOWNTIME_PATH) 485 .appendQueryParameter("start", downtime.startHour + "." + downtime.startMinute) 486 .appendQueryParameter("end", downtime.endHour + "." + downtime.endMinute) 487 .build(); 488 } 489 490 public static DowntimeInfo tryParseDowntimeConditionId(Uri conditionId) { 491 if (!Condition.isValidId(conditionId, SYSTEM_AUTHORITY) 492 || conditionId.getPathSegments().size() != 1 493 || !DOWNTIME_PATH.equals(conditionId.getPathSegments().get(0))) { 494 return null; 495 } 496 final int[] start = tryParseHourAndMinute(conditionId.getQueryParameter("start")); 497 final int[] end = tryParseHourAndMinute(conditionId.getQueryParameter("end")); 498 if (start == null || end == null) return null; 499 final DowntimeInfo downtime = new DowntimeInfo(); 500 downtime.startHour = start[0]; 501 downtime.startMinute = start[1]; 502 downtime.endHour = end[0]; 503 downtime.endMinute = end[1]; 504 return downtime; 505 } 506 507 private static int[] tryParseHourAndMinute(String value) { 508 if (TextUtils.isEmpty(value)) return null; 509 final int i = value.indexOf('.'); 510 if (i < 1 || i >= value.length() - 1) return null; 511 final int hour = tryParseInt(value.substring(0, i), -1); 512 final int minute = tryParseInt(value.substring(i + 1), -1); 513 return isValidHour(hour) && isValidMinute(minute) ? new int[] { hour, minute } : null; 514 } 515 516 public static boolean isValidDowntimeConditionId(Uri conditionId) { 517 return tryParseDowntimeConditionId(conditionId) != null; 518 } 519 520 public static class DowntimeInfo { 521 public int startHour; // 0-23 522 public int startMinute; // 0-59 523 public int endHour; 524 public int endMinute; 525 526 @Override 527 public int hashCode() { 528 return 0; 529 } 530 531 @Override 532 public boolean equals(Object o) { 533 if (!(o instanceof DowntimeInfo)) return false; 534 final DowntimeInfo other = (DowntimeInfo) o; 535 return startHour == other.startHour 536 && startMinute == other.startMinute 537 && endHour == other.endHour 538 && endMinute == other.endMinute; 539 } 540 } 541} 542