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