1 /* 2 * Copyright (C) 2006 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 com.android.calendarcommon2; 18 19 import android.text.TextUtils; 20 import android.util.Log; 21 22 import java.util.Calendar; 23 import java.util.HashMap; 24 25 /** 26 * Event recurrence utility functions. 27 */ 28 public class EventRecurrence { 29 private static String TAG = "EventRecur"; 30 31 public static final int SECONDLY = 1; 32 public static final int MINUTELY = 2; 33 public static final int HOURLY = 3; 34 public static final int DAILY = 4; 35 public static final int WEEKLY = 5; 36 public static final int MONTHLY = 6; 37 public static final int YEARLY = 7; 38 39 public static final int SU = 0x00010000; 40 public static final int MO = 0x00020000; 41 public static final int TU = 0x00040000; 42 public static final int WE = 0x00080000; 43 public static final int TH = 0x00100000; 44 public static final int FR = 0x00200000; 45 public static final int SA = 0x00400000; 46 47 public Time startDate; // set by setStartDate(), not parse() 48 49 public int freq; // SECONDLY, MINUTELY, etc. 50 public String until; 51 public int count; 52 public int interval; 53 public int wkst; // SU, MO, TU, etc. 54 55 /* lists with zero entries may be null references */ 56 public int[] bysecond; 57 public int bysecondCount; 58 public int[] byminute; 59 public int byminuteCount; 60 public int[] byhour; 61 public int byhourCount; 62 public int[] byday; 63 public int[] bydayNum; 64 public int bydayCount; 65 public int[] bymonthday; 66 public int bymonthdayCount; 67 public int[] byyearday; 68 public int byyeardayCount; 69 public int[] byweekno; 70 public int byweeknoCount; 71 public int[] bymonth; 72 public int bymonthCount; 73 public int[] bysetpos; 74 public int bysetposCount; 75 76 /** maps a part string to a parser object */ 77 private static HashMap<String,PartParser> sParsePartMap; 78 static { 79 sParsePartMap = new HashMap<String,PartParser>(); 80 sParsePartMap.put("FREQ", new ParseFreq()); 81 sParsePartMap.put("UNTIL", new ParseUntil()); 82 sParsePartMap.put("COUNT", new ParseCount()); 83 sParsePartMap.put("INTERVAL", new ParseInterval()); 84 sParsePartMap.put("BYSECOND", new ParseBySecond()); 85 sParsePartMap.put("BYMINUTE", new ParseByMinute()); 86 sParsePartMap.put("BYHOUR", new ParseByHour()); 87 sParsePartMap.put("BYDAY", new ParseByDay()); 88 sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay()); 89 sParsePartMap.put("BYYEARDAY", new ParseByYearDay()); 90 sParsePartMap.put("BYWEEKNO", new ParseByWeekNo()); 91 sParsePartMap.put("BYMONTH", new ParseByMonth()); 92 sParsePartMap.put("BYSETPOS", new ParseBySetPos()); 93 sParsePartMap.put("WKST", new ParseWkst()); 94 } 95 96 /* values for bit vector that keeps track of what we have already seen */ 97 private static final int PARSED_FREQ = 1 << 0; 98 private static final int PARSED_UNTIL = 1 << 1; 99 private static final int PARSED_COUNT = 1 << 2; 100 private static final int PARSED_INTERVAL = 1 << 3; 101 private static final int PARSED_BYSECOND = 1 << 4; 102 private static final int PARSED_BYMINUTE = 1 << 5; 103 private static final int PARSED_BYHOUR = 1 << 6; 104 private static final int PARSED_BYDAY = 1 << 7; 105 private static final int PARSED_BYMONTHDAY = 1 << 8; 106 private static final int PARSED_BYYEARDAY = 1 << 9; 107 private static final int PARSED_BYWEEKNO = 1 << 10; 108 private static final int PARSED_BYMONTH = 1 << 11; 109 private static final int PARSED_BYSETPOS = 1 << 12; 110 private static final int PARSED_WKST = 1 << 13; 111 112 /** maps a FREQ value to an integer constant */ 113 private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>(); 114 static { 115 sParseFreqMap.put("SECONDLY", SECONDLY); 116 sParseFreqMap.put("MINUTELY", MINUTELY); 117 sParseFreqMap.put("HOURLY", HOURLY); 118 sParseFreqMap.put("DAILY", DAILY); 119 sParseFreqMap.put("WEEKLY", WEEKLY); 120 sParseFreqMap.put("MONTHLY", MONTHLY); 121 sParseFreqMap.put("YEARLY", YEARLY); 122 } 123 124 /** maps a two-character weekday string to an integer constant */ 125 private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>(); 126 static { 127 sParseWeekdayMap.put("SU", SU); 128 sParseWeekdayMap.put("MO", MO); 129 sParseWeekdayMap.put("TU", TU); 130 sParseWeekdayMap.put("WE", WE); 131 sParseWeekdayMap.put("TH", TH); 132 sParseWeekdayMap.put("FR", FR); 133 sParseWeekdayMap.put("SA", SA); 134 } 135 136 /** If set, allow lower-case recurrence rule strings. Minor performance impact. */ 137 private static final boolean ALLOW_LOWER_CASE = true; 138 139 /** If set, validate the value of UNTIL parts. Minor performance impact. */ 140 private static final boolean VALIDATE_UNTIL = false; 141 142 /** If set, require that only one of {UNTIL,COUNT} is present. Breaks compat w/ old parser. */ 143 private static final boolean ONLY_ONE_UNTIL_COUNT = false; 144 145 146 /** 147 * Thrown when a recurrence string provided can not be parsed according 148 * to RFC2445. 149 */ 150 public static class InvalidFormatException extends RuntimeException { InvalidFormatException(String s)151 InvalidFormatException(String s) { 152 super(s); 153 } 154 } 155 156 setStartDate(Time date)157 public void setStartDate(Time date) { 158 startDate = date; 159 } 160 161 /** 162 * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc. 163 * constants. btw, I think we should switch to those here too, to 164 * get rid of this function, if possible. 165 */ calendarDay2Day(int day)166 public static int calendarDay2Day(int day) 167 { 168 switch (day) 169 { 170 case Calendar.SUNDAY: 171 return SU; 172 case Calendar.MONDAY: 173 return MO; 174 case Calendar.TUESDAY: 175 return TU; 176 case Calendar.WEDNESDAY: 177 return WE; 178 case Calendar.THURSDAY: 179 return TH; 180 case Calendar.FRIDAY: 181 return FR; 182 case Calendar.SATURDAY: 183 return SA; 184 default: 185 throw new RuntimeException("bad day of week: " + day); 186 } 187 } 188 timeDay2Day(int day)189 public static int timeDay2Day(int day) 190 { 191 switch (day) 192 { 193 case Time.SUNDAY: 194 return SU; 195 case Time.MONDAY: 196 return MO; 197 case Time.TUESDAY: 198 return TU; 199 case Time.WEDNESDAY: 200 return WE; 201 case Time.THURSDAY: 202 return TH; 203 case Time.FRIDAY: 204 return FR; 205 case Time.SATURDAY: 206 return SA; 207 default: 208 throw new RuntimeException("bad day of week: " + day); 209 } 210 } day2TimeDay(int day)211 public static int day2TimeDay(int day) 212 { 213 switch (day) 214 { 215 case SU: 216 return Time.SUNDAY; 217 case MO: 218 return Time.MONDAY; 219 case TU: 220 return Time.TUESDAY; 221 case WE: 222 return Time.WEDNESDAY; 223 case TH: 224 return Time.THURSDAY; 225 case FR: 226 return Time.FRIDAY; 227 case SA: 228 return Time.SATURDAY; 229 default: 230 throw new RuntimeException("bad day of week: " + day); 231 } 232 } 233 234 /** 235 * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY 236 * constants. btw, I think we should switch to those here too, to 237 * get rid of this function, if possible. 238 */ day2CalendarDay(int day)239 public static int day2CalendarDay(int day) 240 { 241 switch (day) 242 { 243 case SU: 244 return Calendar.SUNDAY; 245 case MO: 246 return Calendar.MONDAY; 247 case TU: 248 return Calendar.TUESDAY; 249 case WE: 250 return Calendar.WEDNESDAY; 251 case TH: 252 return Calendar.THURSDAY; 253 case FR: 254 return Calendar.FRIDAY; 255 case SA: 256 return Calendar.SATURDAY; 257 default: 258 throw new RuntimeException("bad day of week: " + day); 259 } 260 } 261 262 /** 263 * Converts one of the internal day constants (SU, MO, etc.) to the 264 * two-letter string representing that constant. 265 * 266 * @param day one the internal constants SU, MO, etc. 267 * @return the two-letter string for the day ("SU", "MO", etc.) 268 * 269 * @throws IllegalArgumentException Thrown if the day argument is not one of 270 * the defined day constants. 271 */ day2String(int day)272 private static String day2String(int day) { 273 switch (day) { 274 case SU: 275 return "SU"; 276 case MO: 277 return "MO"; 278 case TU: 279 return "TU"; 280 case WE: 281 return "WE"; 282 case TH: 283 return "TH"; 284 case FR: 285 return "FR"; 286 case SA: 287 return "SA"; 288 default: 289 throw new IllegalArgumentException("bad day argument: " + day); 290 } 291 } 292 appendNumbers(StringBuilder s, String label, int count, int[] values)293 private static void appendNumbers(StringBuilder s, String label, 294 int count, int[] values) 295 { 296 if (count > 0) { 297 s.append(label); 298 count--; 299 for (int i=0; i<count; i++) { 300 s.append(values[i]); 301 s.append(","); 302 } 303 s.append(values[count]); 304 } 305 } 306 appendByDay(StringBuilder s, int i)307 private void appendByDay(StringBuilder s, int i) 308 { 309 int n = this.bydayNum[i]; 310 if (n != 0) { 311 s.append(n); 312 } 313 314 String str = day2String(this.byday[i]); 315 s.append(str); 316 } 317 318 @Override toString()319 public String toString() 320 { 321 StringBuilder s = new StringBuilder(); 322 323 s.append("FREQ="); 324 switch (this.freq) 325 { 326 case SECONDLY: 327 s.append("SECONDLY"); 328 break; 329 case MINUTELY: 330 s.append("MINUTELY"); 331 break; 332 case HOURLY: 333 s.append("HOURLY"); 334 break; 335 case DAILY: 336 s.append("DAILY"); 337 break; 338 case WEEKLY: 339 s.append("WEEKLY"); 340 break; 341 case MONTHLY: 342 s.append("MONTHLY"); 343 break; 344 case YEARLY: 345 s.append("YEARLY"); 346 break; 347 } 348 349 if (!TextUtils.isEmpty(this.until)) { 350 s.append(";UNTIL="); 351 s.append(until); 352 } 353 354 if (this.count != 0) { 355 s.append(";COUNT="); 356 s.append(this.count); 357 } 358 359 if (this.interval != 0) { 360 s.append(";INTERVAL="); 361 s.append(this.interval); 362 } 363 364 if (this.wkst != 0) { 365 s.append(";WKST="); 366 s.append(day2String(this.wkst)); 367 } 368 369 appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond); 370 appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute); 371 appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour); 372 373 // day 374 int count = this.bydayCount; 375 if (count > 0) { 376 s.append(";BYDAY="); 377 count--; 378 for (int i=0; i<count; i++) { 379 appendByDay(s, i); 380 s.append(","); 381 } 382 appendByDay(s, count); 383 } 384 385 appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday); 386 appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday); 387 appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno); 388 appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth); 389 appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos); 390 391 return s.toString(); 392 } 393 repeatsOnEveryWeekDay()394 public boolean repeatsOnEveryWeekDay() { 395 if (this.freq != WEEKLY) { 396 return false; 397 } 398 399 int count = this.bydayCount; 400 if (count != 5) { 401 return false; 402 } 403 404 for (int i = 0 ; i < count ; i++) { 405 int day = byday[i]; 406 if (day == SU || day == SA) { 407 return false; 408 } 409 } 410 411 return true; 412 } 413 414 /** 415 * Determines whether this rule specifies a simple monthly rule by weekday, such as 416 * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month). 417 * <p> 418 * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month), 419 * will cause "false" to be returned. 420 * <p> 421 * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every 422 * month) will cause "false" to be returned. (Note these are usually expressed as 423 * WEEKLY rules, and hence are uncommon.) 424 * 425 * @return true if this rule is of the appropriate form 426 */ repeatsMonthlyOnDayCount()427 public boolean repeatsMonthlyOnDayCount() { 428 if (this.freq != MONTHLY) { 429 return false; 430 } 431 432 if (bydayCount != 1 || bymonthdayCount != 0) { 433 return false; 434 } 435 436 if (bydayNum[0] <= 0) { 437 return false; 438 } 439 440 return true; 441 } 442 443 /** 444 * Determines whether two integer arrays contain identical elements. 445 * <p> 446 * The native implementation over-allocated the arrays (and may have stuff left over from 447 * a previous run), so we can't just check the arrays -- the separately-maintained count 448 * field also matters. We assume that a null array will have a count of zero, and that the 449 * array can hold as many elements as the associated count indicates. 450 * <p> 451 * TODO: replace this with Arrays.equals() when the old parser goes away. 452 */ arraysEqual(int[] array1, int count1, int[] array2, int count2)453 private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) { 454 if (count1 != count2) { 455 return false; 456 } 457 458 for (int i = 0; i < count1; i++) { 459 if (array1[i] != array2[i]) 460 return false; 461 } 462 463 return true; 464 } 465 466 @Override equals(Object obj)467 public boolean equals(Object obj) { 468 if (this == obj) { 469 return true; 470 } 471 if (!(obj instanceof EventRecurrence)) { 472 return false; 473 } 474 475 EventRecurrence er = (EventRecurrence) obj; 476 return (startDate == null ? 477 er.startDate == null : startDate.compareTo(er.startDate) == 0) && 478 freq == er.freq && 479 (until == null ? er.until == null : until.equals(er.until)) && 480 count == er.count && 481 interval == er.interval && 482 wkst == er.wkst && 483 arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) && 484 arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) && 485 arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) && 486 arraysEqual(byday, bydayCount, er.byday, er.bydayCount) && 487 arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) && 488 arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) && 489 arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) && 490 arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) && 491 arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) && 492 arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount); 493 } 494 hashCode()495 @Override public int hashCode() { 496 // We overrode equals, so we must override hashCode(). Nobody seems to need this though. 497 throw new UnsupportedOperationException(); 498 } 499 500 /** 501 * Resets parser-modified fields to their initial state. Does not alter startDate. 502 * <p> 503 * The original parser always set all of the "count" fields, "wkst", and "until", 504 * essentially allowing the same object to be used multiple times by calling parse(). 505 * It's unclear whether this behavior was intentional. For now, be paranoid and 506 * preserve the existing behavior by resetting the fields. 507 * <p> 508 * We don't need to touch the integer arrays; they will either be ignored or 509 * overwritten. The "startDate" field is not set by the parser, so we ignore it here. 510 */ resetFields()511 private void resetFields() { 512 until = null; 513 freq = count = interval = bysecondCount = byminuteCount = byhourCount = 514 bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount = 515 bysetposCount = 0; 516 } 517 518 /** 519 * Parses an rfc2445 recurrence rule string into its component pieces. Attempting to parse 520 * malformed input will result in an EventRecurrence.InvalidFormatException. 521 * 522 * @param recur The recurrence rule to parse (in un-folded form). 523 */ parse(String recur)524 public void parse(String recur) { 525 /* 526 * From RFC 2445 section 4.3.10: 527 * 528 * recur = "FREQ"=freq *( 529 * ; either UNTIL or COUNT may appear in a 'recur', 530 * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' 531 * 532 * ( ";" "UNTIL" "=" enddate ) / 533 * ( ";" "COUNT" "=" 1*DIGIT ) / 534 * 535 * ; the rest of these keywords are optional, 536 * ; but MUST NOT occur more than once 537 * 538 * ( ";" "INTERVAL" "=" 1*DIGIT ) / 539 * ( ";" "BYSECOND" "=" byseclist ) / 540 * ( ";" "BYMINUTE" "=" byminlist ) / 541 * ( ";" "BYHOUR" "=" byhrlist ) / 542 * ( ";" "BYDAY" "=" bywdaylist ) / 543 * ( ";" "BYMONTHDAY" "=" bymodaylist ) / 544 * ( ";" "BYYEARDAY" "=" byyrdaylist ) / 545 * ( ";" "BYWEEKNO" "=" bywknolist ) / 546 * ( ";" "BYMONTH" "=" bymolist ) / 547 * ( ";" "BYSETPOS" "=" bysplist ) / 548 * ( ";" "WKST" "=" weekday ) / 549 * ( ";" x-name "=" text ) 550 * ) 551 * 552 * The rule parts are not ordered in any particular sequence. 553 * 554 * Examples: 555 * FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU 556 * FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8 557 * 558 * Strategy: 559 * (1) Split the string at ';' boundaries to get an array of rule "parts". 560 * (2) For each part, find substrings for left/right sides of '=' (name/value). 561 * (3) Call a <name>-specific parsing function to parse the <value> into an 562 * output field. 563 * 564 * By keeping track of which names we've seen in a bit vector, we can verify the 565 * constraints indicated above (FREQ appears first, none of them appear more than once -- 566 * though x-[name] would require special treatment), and we have either UNTIL or COUNT 567 * but not both. 568 * 569 * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must 570 * be handled in a case-insensitive fashion, but case may be significant for other 571 * properties. We don't have any case-sensitive values in RRULE, except possibly 572 * for the custom "X-" properties, but we ignore those anyway. Thus, we can trivially 573 * convert the entire string to upper case and then use simple comparisons. 574 * 575 * Differences from previous version: 576 * - allows lower-case property and enumeration values [optional] 577 * - enforces that FREQ appears first 578 * - enforces that only one of UNTIL and COUNT may be specified 579 * - allows (but ignores) X-* parts 580 * - improved validation on various values (e.g. UNTIL timestamps) 581 * - error messages are more specific 582 * 583 * TODO: enforce additional constraints listed in RFC 5545, notably the "N/A" entries 584 * in section 3.3.10. For example, if FREQ=WEEKLY, we should reject a rule that 585 * includes a BYMONTHDAY part. 586 */ 587 588 /* TODO: replace with "if (freq != 0) throw" if nothing requires this */ 589 resetFields(); 590 591 int parseFlags = 0; 592 String[] parts; 593 if (ALLOW_LOWER_CASE) { 594 parts = recur.toUpperCase().split(";"); 595 } else { 596 parts = recur.split(";"); 597 } 598 for (String part : parts) { 599 // allow empty part (e.g., double semicolon ";;") 600 if (TextUtils.isEmpty(part)) { 601 continue; 602 } 603 int equalIndex = part.indexOf('='); 604 if (equalIndex <= 0) { 605 /* no '=' or no LHS */ 606 throw new InvalidFormatException("Missing LHS in " + part); 607 } 608 609 String lhs = part.substring(0, equalIndex); 610 String rhs = part.substring(equalIndex + 1); 611 if (rhs.length() == 0) { 612 throw new InvalidFormatException("Missing RHS in " + part); 613 } 614 615 /* 616 * In lieu of a "switch" statement that allows string arguments, we use a 617 * map from strings to parsing functions. 618 */ 619 PartParser parser = sParsePartMap.get(lhs); 620 if (parser == null) { 621 if (lhs.startsWith("X-")) { 622 //Log.d(TAG, "Ignoring custom part " + lhs); 623 continue; 624 } 625 throw new InvalidFormatException("Couldn't find parser for " + lhs); 626 } else { 627 int flag = parser.parsePart(rhs, this); 628 if ((parseFlags & flag) != 0) { 629 throw new InvalidFormatException("Part " + lhs + " was specified twice"); 630 } 631 parseFlags |= flag; 632 } 633 } 634 635 // If not specified, week starts on Monday. 636 if ((parseFlags & PARSED_WKST) == 0) { 637 wkst = MO; 638 } 639 640 // FREQ is mandatory. 641 if ((parseFlags & PARSED_FREQ) == 0) { 642 throw new InvalidFormatException("Must specify a FREQ value"); 643 } 644 645 // Can't have both UNTIL and COUNT. 646 if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) { 647 if (ONLY_ONE_UNTIL_COUNT) { 648 throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur); 649 } else { 650 Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur); 651 } 652 } 653 } 654 655 /** 656 * Base class for the RRULE part parsers. 657 */ 658 abstract static class PartParser { 659 /** 660 * Parses a single part. 661 * 662 * @param value The right-hand-side of the part. 663 * @param er The EventRecurrence into which the result is stored. 664 * @return A bit value indicating which part was parsed. 665 */ parsePart(String value, EventRecurrence er)666 public abstract int parsePart(String value, EventRecurrence er); 667 668 /** 669 * Parses an integer, with range-checking. 670 * 671 * @param str The string to parse. 672 * @param minVal Minimum allowed value. 673 * @param maxVal Maximum allowed value. 674 * @param allowZero Is 0 allowed? 675 * @return The parsed value. 676 */ parseIntRange(String str, int minVal, int maxVal, boolean allowZero)677 public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) { 678 try { 679 if (str.charAt(0) == '+') { 680 // Integer.parseInt does not allow a leading '+', so skip it manually. 681 str = str.substring(1); 682 } 683 int val = Integer.parseInt(str); 684 if (val < minVal || val > maxVal || (val == 0 && !allowZero)) { 685 throw new InvalidFormatException("Integer value out of range: " + str); 686 } 687 return val; 688 } catch (NumberFormatException nfe) { 689 throw new InvalidFormatException("Invalid integer value: " + str); 690 } 691 } 692 693 /** 694 * Parses a comma-separated list of integers, with range-checking. 695 * 696 * @param listStr The string to parse. 697 * @param minVal Minimum allowed value. 698 * @param maxVal Maximum allowed value. 699 * @param allowZero Is 0 allowed? 700 * @return A new array with values, sized to hold the exact number of elements. 701 */ parseNumberList(String listStr, int minVal, int maxVal, boolean allowZero)702 public static int[] parseNumberList(String listStr, int minVal, int maxVal, 703 boolean allowZero) { 704 int[] values; 705 706 if (listStr.indexOf(",") < 0) { 707 // Common case: only one entry, skip split() overhead. 708 values = new int[1]; 709 values[0] = parseIntRange(listStr, minVal, maxVal, allowZero); 710 } else { 711 String[] valueStrs = listStr.split(","); 712 int len = valueStrs.length; 713 values = new int[len]; 714 for (int i = 0; i < len; i++) { 715 values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero); 716 } 717 } 718 return values; 719 } 720 } 721 722 /** parses FREQ={SECONDLY,MINUTELY,...} */ 723 private static class ParseFreq extends PartParser { parsePart(String value, EventRecurrence er)724 @Override public int parsePart(String value, EventRecurrence er) { 725 Integer freq = sParseFreqMap.get(value); 726 if (freq == null) { 727 throw new InvalidFormatException("Invalid FREQ value: " + value); 728 } 729 er.freq = freq; 730 return PARSED_FREQ; 731 } 732 } 733 /** parses UNTIL=enddate, e.g. "19970829T021400" */ 734 private static class ParseUntil extends PartParser { parsePart(String value, EventRecurrence er)735 @Override public int parsePart(String value, EventRecurrence er) { 736 if (VALIDATE_UNTIL) { 737 try { 738 // Parse the time to validate it. The result isn't retained. 739 Time until = new Time(); 740 until.parse(value); 741 } catch (IllegalArgumentException iae) { 742 throw new InvalidFormatException("Invalid UNTIL value: " + value); 743 } 744 } 745 er.until = value; 746 return PARSED_UNTIL; 747 } 748 } 749 /** parses COUNT=[non-negative-integer] */ 750 private static class ParseCount extends PartParser { parsePart(String value, EventRecurrence er)751 @Override public int parsePart(String value, EventRecurrence er) { 752 er.count = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); 753 if (er.count < 0) { 754 Log.d(TAG, "Invalid Count. Forcing COUNT to 1 from " + value); 755 er.count = 1; // invalid count. assume one time recurrence. 756 } 757 return PARSED_COUNT; 758 } 759 } 760 /** parses INTERVAL=[non-negative-integer] */ 761 private static class ParseInterval extends PartParser { parsePart(String value, EventRecurrence er)762 @Override public int parsePart(String value, EventRecurrence er) { 763 er.interval = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); 764 if (er.interval < 1) { 765 Log.d(TAG, "Invalid Interval. Forcing INTERVAL to 1 from " + value); 766 er.interval = 1; 767 } 768 return PARSED_INTERVAL; 769 } 770 } 771 /** parses BYSECOND=byseclist */ 772 private static class ParseBySecond extends PartParser { parsePart(String value, EventRecurrence er)773 @Override public int parsePart(String value, EventRecurrence er) { 774 int[] bysecond = parseNumberList(value, 0, 59, true); 775 er.bysecond = bysecond; 776 er.bysecondCount = bysecond.length; 777 return PARSED_BYSECOND; 778 } 779 } 780 /** parses BYMINUTE=byminlist */ 781 private static class ParseByMinute extends PartParser { parsePart(String value, EventRecurrence er)782 @Override public int parsePart(String value, EventRecurrence er) { 783 int[] byminute = parseNumberList(value, 0, 59, true); 784 er.byminute = byminute; 785 er.byminuteCount = byminute.length; 786 return PARSED_BYMINUTE; 787 } 788 } 789 /** parses BYHOUR=byhrlist */ 790 private static class ParseByHour extends PartParser { parsePart(String value, EventRecurrence er)791 @Override public int parsePart(String value, EventRecurrence er) { 792 int[] byhour = parseNumberList(value, 0, 23, true); 793 er.byhour = byhour; 794 er.byhourCount = byhour.length; 795 return PARSED_BYHOUR; 796 } 797 } 798 /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */ 799 private static class ParseByDay extends PartParser { parsePart(String value, EventRecurrence er)800 @Override public int parsePart(String value, EventRecurrence er) { 801 int[] byday; 802 int[] bydayNum; 803 int bydayCount; 804 805 if (value.indexOf(",") < 0) { 806 /* only one entry, skip split() overhead */ 807 bydayCount = 1; 808 byday = new int[1]; 809 bydayNum = new int[1]; 810 parseWday(value, byday, bydayNum, 0); 811 } else { 812 String[] wdays = value.split(","); 813 int len = wdays.length; 814 bydayCount = len; 815 byday = new int[len]; 816 bydayNum = new int[len]; 817 for (int i = 0; i < len; i++) { 818 parseWday(wdays[i], byday, bydayNum, i); 819 } 820 } 821 er.byday = byday; 822 er.bydayNum = bydayNum; 823 er.bydayCount = bydayCount; 824 return PARSED_BYDAY; 825 } 826 827 /** parses [int]weekday, putting the pieces into parallel array entries */ parseWday(String str, int[] byday, int[] bydayNum, int index)828 private static void parseWday(String str, int[] byday, int[] bydayNum, int index) { 829 int wdayStrStart = str.length() - 2; 830 String wdayStr; 831 832 if (wdayStrStart > 0) { 833 /* number is included; parse it out and advance to weekday */ 834 String numPart = str.substring(0, wdayStrStart); 835 int num = parseIntRange(numPart, -53, 53, false); 836 bydayNum[index] = num; 837 wdayStr = str.substring(wdayStrStart); 838 } else { 839 /* just the weekday string */ 840 wdayStr = str; 841 } 842 Integer wday = sParseWeekdayMap.get(wdayStr); 843 if (wday == null) { 844 throw new InvalidFormatException("Invalid BYDAY value: " + str); 845 } 846 byday[index] = wday; 847 } 848 } 849 /** parses BYMONTHDAY=bymodaylist */ 850 private static class ParseByMonthDay extends PartParser { parsePart(String value, EventRecurrence er)851 @Override public int parsePart(String value, EventRecurrence er) { 852 int[] bymonthday = parseNumberList(value, -31, 31, false); 853 er.bymonthday = bymonthday; 854 er.bymonthdayCount = bymonthday.length; 855 return PARSED_BYMONTHDAY; 856 } 857 } 858 /** parses BYYEARDAY=byyrdaylist */ 859 private static class ParseByYearDay extends PartParser { parsePart(String value, EventRecurrence er)860 @Override public int parsePart(String value, EventRecurrence er) { 861 int[] byyearday = parseNumberList(value, -366, 366, false); 862 er.byyearday = byyearday; 863 er.byyeardayCount = byyearday.length; 864 return PARSED_BYYEARDAY; 865 } 866 } 867 /** parses BYWEEKNO=bywknolist */ 868 private static class ParseByWeekNo extends PartParser { parsePart(String value, EventRecurrence er)869 @Override public int parsePart(String value, EventRecurrence er) { 870 int[] byweekno = parseNumberList(value, -53, 53, false); 871 er.byweekno = byweekno; 872 er.byweeknoCount = byweekno.length; 873 return PARSED_BYWEEKNO; 874 } 875 } 876 /** parses BYMONTH=bymolist */ 877 private static class ParseByMonth extends PartParser { parsePart(String value, EventRecurrence er)878 @Override public int parsePart(String value, EventRecurrence er) { 879 int[] bymonth = parseNumberList(value, 1, 12, false); 880 er.bymonth = bymonth; 881 er.bymonthCount = bymonth.length; 882 return PARSED_BYMONTH; 883 } 884 } 885 /** parses BYSETPOS=bysplist */ 886 private static class ParseBySetPos extends PartParser { parsePart(String value, EventRecurrence er)887 @Override public int parsePart(String value, EventRecurrence er) { 888 int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true); 889 er.bysetpos = bysetpos; 890 er.bysetposCount = bysetpos.length; 891 return PARSED_BYSETPOS; 892 } 893 } 894 /** parses WKST={SU,MO,...} */ 895 private static class ParseWkst extends PartParser { parsePart(String value, EventRecurrence er)896 @Override public int parsePart(String value, EventRecurrence er) { 897 Integer wkst = sParseWeekdayMap.get(value); 898 if (wkst == null) { 899 throw new InvalidFormatException("Invalid WKST value: " + value); 900 } 901 er.wkst = wkst; 902 return PARSED_WKST; 903 } 904 } 905 } 906