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