CalendarUtilities.java revision 6f3013b78708321879728c28db044ab233cb2016
1/* 2 * Copyright (C) 2010 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 com.android.exchange.utility; 18 19import com.android.exchange.Eas; 20import com.android.exchange.adapter.Serializer; 21import com.android.exchange.adapter.Tags; 22 23import org.bouncycastle.util.encoders.Base64; 24 25import android.util.Log; 26 27import java.io.IOException; 28import java.util.Calendar; 29import java.util.Date; 30import java.util.GregorianCalendar; 31import java.util.HashMap; 32import java.util.TimeZone; 33 34public class CalendarUtilities { 35 // NOTE: Most definitions in this class are have package visibility for testing purposes 36 private static final String TAG = "CalendarUtility"; 37 38 // Time related convenience constants, in milliseconds 39 static final int SECONDS = 1000; 40 static final int MINUTES = SECONDS*60; 41 static final int HOURS = MINUTES*60; 42 static final long DAYS = HOURS*24; 43 44 // NOTE All Microsoft data structures are little endian 45 46 // The following constants relate to standard Microsoft data sizes 47 // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx 48 static final int MSFT_LONG_SIZE = 4; 49 static final int MSFT_WCHAR_SIZE = 2; 50 static final int MSFT_WORD_SIZE = 2; 51 52 // The following constants relate to Microsoft's SYSTEMTIME structure 53 // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4 54 55 static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE; 56 static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE; 57 static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE; 58 static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE; 59 static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE; 60 static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE; 61 //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE; 62 //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE; 63 static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE; 64 65 // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure 66 // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx 67 static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0; 68 static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET = 69 MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE; 70 static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET = 71 MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 72 static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET = 73 MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 74 static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET = 75 MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE; 76 static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET = 77 MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 78 static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET = 79 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 80 static final int MSFT_TIME_ZONE_SIZE = 81 MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE; 82 83 // TimeZone cache; we parse/decode as little as possible, because the process is quite slow 84 private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>(); 85 // TZI string cache; we keep around our encoded TimeZoneInformation strings 86 private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>(); 87 88 // There is no type 4 (thus, the "") 89 static final String[] sTypeToFreq = 90 new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"}; 91 92 static final String[] sDayTokens = 93 new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; 94 95 static final String[] sTwoCharacterNumbers = 96 new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; 97 98 static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR); 99 static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT"); 100 101 // Return a 4-byte long from a byte array (little endian) 102 static int getLong(byte[] bytes, int offset) { 103 return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) | 104 ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24); 105 } 106 107 // Put a 4-byte long into a byte array (little endian) 108 static void setLong(byte[] bytes, int offset, int value) { 109 bytes[offset++] = (byte) (value & 0xFF); 110 bytes[offset++] = (byte) ((value >> 8) & 0xFF); 111 bytes[offset++] = (byte) ((value >> 16) & 0xFF); 112 bytes[offset] = (byte) ((value >> 24) & 0xFF); 113 } 114 115 // Return a 2-byte word from a byte array (little endian) 116 static int getWord(byte[] bytes, int offset) { 117 return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8); 118 } 119 120 // Put a 2-byte word into a byte array (little endian) 121 static void setWord(byte[] bytes, int offset, int value) { 122 bytes[offset++] = (byte) (value & 0xFF); 123 bytes[offset] = (byte) ((value >> 8) & 0xFF); 124 } 125 126 // Internal structure for storing a time zone date from a SYSTEMTIME structure 127 // This date represents either the start or the end time for DST 128 static class TimeZoneDate { 129 String year; 130 int month; 131 int dayOfWeek; 132 int day; 133 int time; 134 int hour; 135 int minute; 136 } 137 138 // Write SYSTEMTIME data into a byte array (this will either be for the standard or daylight 139 // transition) 140 static void putTimeInMillisIntoSystemTime(byte[] bytes, int offset, long millis) { 141 GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault()); 142 // Round to the next highest minute; we always write seconds as zero 143 cal.setTimeInMillis(millis + 30*SECONDS); 144 145 // MSFT months are 1 based; TimeZone is 0 based 146 setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1); 147 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 148 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1); 149 150 // Get the "day" in TimeZone format 151 int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH); 152 // 5 means "last" in MSFT land; for TimeZone, it's -1 153 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom); 154 155 // Turn hours/minutes into ms from midnight (per TimeZone) 156 setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, cal.get(Calendar.HOUR)); 157 setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, cal.get(Calendar.MINUTE)); 158 } 159 160 // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset 161 static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) { 162 TimeZoneDate tzd = new TimeZoneDate(); 163 164 // MSFT year is an int; TimeZone is a String 165 int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR); 166 tzd.year = Integer.toString(num); 167 168 // MSFT month = 0 means no daylight time 169 // MSFT months are 1 based; TimeZone is 0 based 170 num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH); 171 if (num == 0) { 172 return null; 173 } else { 174 tzd.month = num -1; 175 } 176 177 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 178 tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1; 179 180 // Get the "day" in TimeZone format 181 num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY); 182 // 5 means "last" in MSFT land; for TimeZone, it's -1 183 if (num == 5) { 184 tzd.day = -1; 185 } else { 186 tzd.day = num; 187 } 188 189 // Turn hours/minutes into ms from midnight (per TimeZone) 190 int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR); 191 tzd.hour = hour; 192 int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE); 193 tzd.minute = minute; 194 tzd.time = (hour*HOURS) + (minute*MINUTES); 195 196 return tzd; 197 } 198 199 // Return a String from within a byte array at the given offset with max characters 200 // Unused for now, but might be helpful for debugging 201 // String getString(byte[] bytes, int offset, int max) { 202 // StringBuilder sb = new StringBuilder(); 203 // while (max-- > 0) { 204 // int b = bytes[offset]; 205 // if (b == 0) break; 206 // sb.append((char)b); 207 // offset += 2; 208 // } 209 // return sb.toString(); 210 // } 211 212 /** 213 * Build a GregorianCalendar, based on a time zone and TimeZoneDate. 214 * @param timeZone the time zone we're checking 215 * @param tzd the TimeZoneDate we're interested in 216 * @return a GregorianCalendar with the given time zone and date 217 */ 218 static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) { 219 GregorianCalendar testCalendar = new GregorianCalendar(timeZone); 220 testCalendar.set(GregorianCalendar.YEAR, sCurrentYear); 221 testCalendar.set(GregorianCalendar.MONTH, tzd.month); 222 testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek); 223 testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day); 224 testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour); 225 testCalendar.set(GregorianCalendar.MINUTE, tzd.minute); 226 return testCalendar; 227 } 228 229 /** 230 * Find a standard/daylight transition between a start time and an end time 231 * @param tz a TimeZone 232 * @param startTime the start time for the test 233 * @param endTime the end time for the test 234 * @param startInDaylightTime whether daylight time is in effect at the startTime 235 * @return the time in millis of the first transition, or 0 if none 236 */ 237 static private long findTransition(TimeZone tz, long startTime, long endTime, 238 boolean startInDaylightTime) { 239 long startingEndTime = endTime; 240 Date date = null; 241 while ((endTime - startTime) > MINUTES) { 242 long checkTime = ((startTime + endTime) / 2) + 1; 243 date = new Date(checkTime); 244 if (tz.inDaylightTime(date) != startInDaylightTime) { 245 endTime = checkTime; 246 } else { 247 startTime = checkTime; 248 } 249 } 250 if (endTime == startingEndTime) { 251 // Really, this shouldn't happen 252 return 0; 253 } 254 return startTime; 255 } 256 257 /** 258 * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 259 * that might be found in an Event; use cached result, if possible 260 * @param tz the TimeZone 261 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 262 */ 263 static public String timeZoneToTziString(TimeZone tz) { 264 String tziString = sTziStringCache.get(tz); 265 if (tziString != null) { 266 if (Eas.USER_LOG) { 267 Log.d(TAG, "TZI string for " + tz.getDisplayName() + " found in cache."); 268 } 269 return tziString; 270 } 271 tziString = timeZoneToTziStringImpl(tz); 272 sTziStringCache.put(tz, tziString); 273 return tziString; 274 } 275 276 /** 277 * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 278 * that might be found in an Event. Since the internal representation of the TimeZone is hidden 279 * from us we'll find the DST transitions and build the structure from that information 280 * @param tz the TimeZone 281 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 282 */ 283 static public String timeZoneToTziStringImpl(TimeZone tz) { 284 String tziString; 285 long time = System.currentTimeMillis(); 286 byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE]; 287 int standardBias = - tz.getRawOffset(); 288 standardBias /= 60*SECONDS; 289 setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias); 290 // If this time zone has daylight savings time, we need to do a bunch more work 291 if (tz.useDaylightTime()) { 292 long standardTransition = 0; 293 long daylightTransition = 0; 294 GregorianCalendar cal = new GregorianCalendar(); 295 cal.set(sCurrentYear, Calendar.JANUARY, 1, 0, 0, 0); 296 cal.setTimeZone(tz); 297 long startTime = cal.getTimeInMillis(); 298 // Calculate rough end of year; no need to do the calculation 299 long endOfYearTime = startTime + 365*DAYS; 300 Date date = new Date(startTime); 301 boolean startInDaylightTime = tz.inDaylightTime(date); 302 // Find the first transition, and store 303 startTime = findTransition(tz, startTime, endOfYearTime, startInDaylightTime); 304 if (startInDaylightTime) { 305 standardTransition = startTime; 306 } else { 307 daylightTransition = startTime; 308 } 309 // Find the second transition, and store 310 startTime = findTransition(tz, startTime, endOfYearTime, !startInDaylightTime); 311 if (startInDaylightTime) { 312 daylightTransition = startTime; 313 } else { 314 standardTransition = startTime; 315 } 316 if (standardTransition != 0 && daylightTransition != 0) { 317 putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, 318 standardTransition); 319 putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, 320 daylightTransition); 321 int dstOffset = tz.getDSTSavings(); 322 setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES); 323 } 324 } 325 // TODO Use a more efficient Base64 API 326 byte[] tziEncodedBytes = Base64.encode(tziBytes); 327 tziString = new String(tziEncodedBytes); 328 if (Eas.USER_LOG) { 329 Log.d(TAG, "Calculated TZI String for " + tz.getDisplayName() + " in " + 330 (System.currentTimeMillis() - time) + "ms"); 331 } 332 return tziString; 333 } 334 335 /** 336 * Given a String as directly read from EAS, returns a TimeZone corresponding to that String 337 * @param timeZoneString the String read from the server 338 * @return the TimeZone, or TimeZone.getDefault() if not found 339 */ 340 static public TimeZone tziStringToTimeZone(String timeZoneString) { 341 // If we have this time zone cached, use that value and return 342 TimeZone timeZone = sTimeZoneCache.get(timeZoneString); 343 if (timeZone != null) { 344 if (Eas.USER_LOG) { 345 Log.d(TAG, " Using cached TimeZone " + timeZone.getDisplayName()); 346 } 347 } else { 348 timeZone = tziStringToTimeZoneImpl(timeZoneString); 349 if (timeZone == null) { 350 // If we don't find a match, we just return the current TimeZone. In theory, this 351 // shouldn't be happening... 352 Log.w(TAG, "TimeZone not found using default: " + timeZoneString); 353 timeZone = TimeZone.getDefault(); 354 } 355 sTimeZoneCache.put(timeZoneString, timeZone); 356 } 357 return timeZone; 358 } 359 360 /** 361 * Given a String as directly read from EAS, tries to find a TimeZone in the database of all 362 * time zones that corresponds to that String. 363 * @param timeZoneString the String read from the server 364 * @return the TimeZone, or TimeZone.getDefault() if not found 365 */ 366 static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) { 367 TimeZone timeZone = null; 368 // TODO Remove after we're comfortable with performance 369 long time = System.currentTimeMillis(); 370 // First, we need to decode the base64 string 371 byte[] timeZoneBytes = Base64.decode(timeZoneString); 372 373 // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms 374 // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added 375 // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so 376 // we need to change the sign 377 int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES; 378 379 // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return 380 // the default time zone 381 String[] zoneIds = TimeZone.getAvailableIDs(bias); 382 if (zoneIds.length > 0) { 383 // Try to find an existing TimeZone from the data provided by EAS 384 // We start by pulling out the date that standard time begins 385 TimeZoneDate dstEnd = 386 getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET); 387 if (dstEnd == null) { 388 // In this case, there is no daylight savings time, so the only interesting data 389 // is the offset, and we know that all of the zoneId's match; we'll take the first 390 timeZone = TimeZone.getTimeZone(zoneIds[0]); 391 String dn = timeZone.getDisplayName(); 392 sTimeZoneCache.put(timeZoneString, timeZone); 393 if (Eas.USER_LOG) { 394 Log.d(TAG, "TimeZone without DST found by offset: " + dn); 395 } 396 } else { 397 TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes, 398 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET); 399 // See comment above for bias... 400 long dstSavings = 401 -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES; 402 403 // We'll go through each time zone to find one with the same DST transitions and 404 // savings length 405 for (String zoneId: zoneIds) { 406 // Get the TimeZone using the zoneId 407 timeZone = TimeZone.getTimeZone(zoneId); 408 409 // Our strategy here is to check just before and just after the transitions 410 // and see whether the check for daylight time matches the expectation 411 // If both transitions match, then we have a match for the offset and start/end 412 // of dst. That's the best we can do for now, since there's no other info 413 // provided by EAS (i.e. we can't get dynamic transitions, etc.) 414 415 int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES; 416 int errorBoundsMinutes = (testSavingsMinutes * 2) + 1; 417 418 // Check start DST transition 419 GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart); 420 testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes); 421 Date before = testCalendar.getTime(); 422 testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes); 423 Date after = testCalendar.getTime(); 424 if (timeZone.inDaylightTime(before)) continue; 425 if (!timeZone.inDaylightTime(after)) continue; 426 427 // Check end DST transition 428 testCalendar = getCheckCalendar(timeZone, dstEnd); 429 testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes); 430 before = testCalendar.getTime(); 431 testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes); 432 after = testCalendar.getTime(); 433 if (!timeZone.inDaylightTime(before)) continue; 434 if (timeZone.inDaylightTime(after)) continue; 435 436 // Check that the savings are the same 437 if (dstSavings != timeZone.getDSTSavings()) continue; 438 439 // If we're here, it's the right time zone, modulo dynamic DST 440 String dn = timeZone.getDisplayName(); 441 // TODO Remove timing when we're comfortable with performance 442 if (Eas.USER_LOG) { 443 Log.d(TAG, "TimeZone found by rules: " + dn + " in " + 444 (System.currentTimeMillis() - time) + "ms"); 445 } 446 break; 447 } 448 } 449 } 450 return timeZone; 451 } 452 453 /** 454 * Generate a time in milliseconds from a date string that represents a date/time in GMT 455 * @param DateTime string from Exchange server 456 * @return the time in milliseconds (since Jan 1, 1970) 457 */ 458 static public long parseDateTimeToMillis(String date) { 459 // Format for calendar date strings is 20090211T180303Z 460 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 461 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), 462 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), 463 Integer.parseInt(date.substring(13, 15))); 464 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 465 return cal.getTimeInMillis(); 466 } 467 468 /** 469 * Generate a GregorianCalendar from a date string that represents a date/time in GMT 470 * @param DateTime string from Exchange server 471 * @return the GregorianCalendar 472 */ 473 static public GregorianCalendar parseDateTimeToCalendar(String date) { 474 // Format for calendar date strings is 20090211T180303Z 475 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 476 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), 477 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), 478 Integer.parseInt(date.substring(13, 15))); 479 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 480 return cal; 481 } 482 483 static String formatTwo(int num) { 484 if (num <= 12) { 485 return sTwoCharacterNumbers[num]; 486 } else 487 return Integer.toString(num); 488 } 489 490 /** 491 * Generate an EAS formatted date/time string based on GMT. See below for details. 492 */ 493 static public String millisToEasDateTime(long millis) { 494 return millisToEasDateTime(millis, sGmtTimeZone); 495 } 496 497 /** 498 * Generate an EAS formatted local date/time string from a time and a time zone 499 * @param millis a time in milliseconds 500 * @param tz a time zone 501 * @return an EAS formatted string indicating the date/time in the given time zone 502 */ 503 static public String millisToEasDateTime(long millis, TimeZone tz) { 504 StringBuilder sb = new StringBuilder(); 505 GregorianCalendar cal = new GregorianCalendar(tz); 506 cal.setTimeInMillis(millis); 507 sb.append(cal.get(Calendar.YEAR)); 508 sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); 509 sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); 510 sb.append('T'); 511 sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); 512 sb.append(formatTwo(cal.get(Calendar.MINUTE))); 513 sb.append(formatTwo(cal.get(Calendar.SECOND))); 514 sb.append('Z'); 515 return sb.toString(); 516 } 517 518 static void addByDay(StringBuilder rrule, int dow, int wom) { 519 rrule.append(";BYDAY="); 520 boolean addComma = false; 521 for (int i = 0; i < 7; i++) { 522 if ((dow & 1) == 1) { 523 if (addComma) { 524 rrule.append(','); 525 } 526 if (wom > 0) { 527 // 5 = last week -> -1 528 // So -1SU = last sunday 529 rrule.append(wom == 5 ? -1 : wom); 530 } 531 rrule.append(sDayTokens[i]); 532 addComma = true; 533 } 534 dow >>= 1; 535 } 536 } 537 538 static void addByMonthDay(StringBuilder rrule, int dom) { 539 // 127 means last day of the month 540 if (dom == 127) { 541 dom = -1; 542 } 543 rrule.append(";BYMONTHDAY=" + dom); 544 } 545 546 /** 547 * Generate the String version of the EAS integer for a given BYDAY value in an rrule 548 * @param dow the BYDAY value of the rrule 549 * @return the String version of the EAS value of these days 550 */ 551 static String generateEasDayOfWeek(String dow) { 552 int bits = 0; 553 int bit = 1; 554 for (String token: sDayTokens) { 555 // If we can find the day in the dow String, add the bit to our bits value 556 if (dow.indexOf(token) >= 0) { 557 bits |= bit; 558 } 559 bit <<= 1; 560 } 561 return Integer.toString(bits); 562 } 563 564 /** 565 * Extract the value of a token in an RRULE string 566 * @param rrule an RRULE string 567 * @param token a token to look for in the RRULE 568 * @return the value of that token 569 */ 570 static String tokenFromRrule(String rrule, String token) { 571 int start = rrule.indexOf(token); 572 if (start < 0) return null; 573 int len = rrule.length(); 574 start += token.length(); 575 int end = start; 576 char c; 577 do { 578 c = rrule.charAt(end++); 579 if (!Character.isLetterOrDigit(c) || (end == len)) { 580 if (end == len) end++; 581 return rrule.substring(start, end -1); 582 } 583 } while (true); 584 } 585 586 /** 587 * Write recurrence information to EAS based on the RRULE in CalendarProvider 588 * @param rrule the RRULE, from CalendarProvider 589 * @param startTime, the DTSTART of this Event 590 * @param s the Serializer we're using to write WBXML data 591 * @throws IOException 592 */ 593 // NOTE: For the moment, we're only parsing recurrence types that are supported by the 594 // Calendar app UI, which is a small subset of possible recurrence types 595 // This code must be updated when the Calendar adds new functionality 596 static public void recurrenceFromRrule(String rrule, long startTime, Serializer s) 597 throws IOException { 598 Log.d("RRULE", "rule: " + rrule); 599 String freq = tokenFromRrule(rrule, "FREQ="); 600 // If there's no FREQ=X, then we don't write a recurrence 601 // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the 602 // possibility of writing out a partial recurrence stanza 603 if (freq != null) { 604 if (freq.equals("DAILY")) { 605 s.start(Tags.CALENDAR_RECURRENCE); 606 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0"); 607 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 608 s.end(); 609 } else if (freq.equals("WEEKLY")) { 610 s.start(Tags.CALENDAR_RECURRENCE); 611 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); 612 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 613 // Requires a day of week (whereas RRULE does not) 614 String byDay = tokenFromRrule(rrule, "BYDAY="); 615 if (byDay != null) { 616 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay)); 617 } 618 s.end(); 619 } else if (freq.equals("MONTHLY")) { 620 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 621 if (byMonthDay != null) { 622 // The nth day of the month 623 s.start(Tags.CALENDAR_RECURRENCE); 624 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2"); 625 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 626 s.end(); 627 } else { 628 String byDay = tokenFromRrule(rrule, "BYDAY="); 629 String bareByDay; 630 if (byDay != null) { 631 // This can be 1WE (1st Wednesday) or -1FR (last Friday) 632 int wom = byDay.charAt(0); 633 if (wom == '-') { 634 // -1 is the only legal case (last week) Use "5" for EAS 635 wom = 5; 636 bareByDay = byDay.substring(2); 637 } else { 638 wom = wom - '0'; 639 bareByDay = byDay.substring(1); 640 } 641 s.start(Tags.CALENDAR_RECURRENCE); 642 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3"); 643 s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom)); 644 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay)); 645 s.end(); 646 } 647 } 648 } else if (freq.equals("YEARLY")) { 649 String byMonth = tokenFromRrule(rrule, "BYMONTH="); 650 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 651 if (byMonth == null || byMonthDay == null) { 652 // Calculate the month and day from the startDate 653 GregorianCalendar cal = new GregorianCalendar(); 654 cal.setTimeInMillis(startTime); 655 cal.setTimeZone(TimeZone.getDefault()); 656 byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1); 657 byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); 658 } 659 s.start(Tags.CALENDAR_RECURRENCE); 660 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5"); 661 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 662 s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth); 663 s.end(); 664 } 665 } 666 } 667 668 /** 669 * Build an RRULE String from EAS recurrence information 670 * @param type the type of recurrence 671 * @param occurrences how many recurrences (instances) 672 * @param interval the interval between recurrences 673 * @param dow day of the week 674 * @param dom day of the month 675 * @param wom week of the month 676 * @param moy month of the year 677 * @param until the last recurrence time 678 * @return a valid RRULE String 679 */ 680 static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow, 681 int dom, int wom, int moy, String until) { 682 StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]); 683 684 // INTERVAL and COUNT 685 if (interval > 0) { 686 rrule.append(";INTERVAL=" + interval); 687 } 688 if (occurrences > 0) { 689 rrule.append(";COUNT=" + occurrences); 690 } 691 692 // Days, weeks, months, etc. 693 switch(type) { 694 case 0: // DAILY 695 case 1: // WEEKLY 696 if (dow > 0) addByDay(rrule, dow, -1); 697 break; 698 case 2: // MONTHLY 699 if (dom > 0) addByMonthDay(rrule, dom); 700 break; 701 case 3: // MONTHLY (on the nth day) 702 if (dow > 0) addByDay(rrule, dow, wom); 703 break; 704 case 5: // YEARLY 705 if (dom > 0) addByMonthDay(rrule, dom); 706 if (moy > 0) { 707 // TODO MAKE SURE WE'RE 1 BASED 708 rrule.append(";BYMONTH=" + moy); 709 } 710 break; 711 case 6: // YEARLY (on the nth day) 712 if (dow > 0) addByDay(rrule, dow, wom); 713 if (moy > 0) addByMonthDay(rrule, dow); 714 break; 715 default: 716 break; 717 } 718 719 // UNTIL comes last 720 // TODO Add UNTIL code 721 if (until != null) { 722 // *** until probably needs reformatting 723 //rrule.append(";UNTIL=" + until); 724 } 725 726 return rrule.toString(); 727 } 728}