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 17 package com.android.exchange.utility; 18 19 import com.android.email.Email; 20 import com.android.email.R; 21 import com.android.email.Utility; 22 import com.android.email.mail.Address; 23 import com.android.email.provider.EmailContent; 24 import com.android.email.provider.EmailContent.Account; 25 import com.android.email.provider.EmailContent.Attachment; 26 import com.android.email.provider.EmailContent.Mailbox; 27 import com.android.email.provider.EmailContent.Message; 28 import com.android.exchange.Eas; 29 import com.android.exchange.EasSyncService; 30 import com.android.exchange.SyncManager; 31 import com.android.exchange.adapter.Serializer; 32 import com.android.exchange.adapter.Tags; 33 import com.android.internal.util.ArrayUtils; 34 35 import android.content.ContentResolver; 36 import android.content.ContentUris; 37 import android.content.ContentValues; 38 import android.content.Context; 39 import android.content.Entity; 40 import android.content.Entity.NamedContentValues; 41 import android.content.EntityIterator; 42 import android.content.res.Resources; 43 import android.net.Uri; 44 import android.os.RemoteException; 45 import android.provider.Calendar.Attendees; 46 import android.provider.Calendar.Calendars; 47 import android.provider.Calendar.Events; 48 import android.provider.Calendar.EventsEntity; 49 import android.text.TextUtils; 50 import android.text.format.Time; 51 import android.util.Base64; 52 import android.util.Log; 53 54 import java.io.IOException; 55 import java.text.DateFormat; 56 import java.text.ParseException; 57 import java.util.ArrayList; 58 import java.util.Calendar; 59 import java.util.Date; 60 import java.util.GregorianCalendar; 61 import java.util.HashMap; 62 import java.util.TimeZone; 63 64 public class CalendarUtilities { 65 // NOTE: Most definitions in this class are have package visibility for testing purposes 66 private static final String TAG = "CalendarUtility"; 67 68 // Time related convenience constants, in milliseconds 69 static final int SECONDS = 1000; 70 static final int MINUTES = SECONDS*60; 71 static final int HOURS = MINUTES*60; 72 static final long DAYS = HOURS*24; 73 74 // NOTE All Microsoft data structures are little endian 75 76 // The following constants relate to standard Microsoft data sizes 77 // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx 78 static final int MSFT_LONG_SIZE = 4; 79 static final int MSFT_WCHAR_SIZE = 2; 80 static final int MSFT_WORD_SIZE = 2; 81 82 // The following constants relate to Microsoft's SYSTEMTIME structure 83 // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4 84 85 static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE; 86 static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE; 87 static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE; 88 static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE; 89 static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE; 90 static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE; 91 //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE; 92 //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE; 93 static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE; 94 95 // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure 96 // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx 97 static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0; 98 static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET = 99 MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE; 100 static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET = 101 MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 102 static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET = 103 MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 104 static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET = 105 MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE; 106 static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET = 107 MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 108 static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET = 109 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 110 static final int MSFT_TIME_ZONE_SIZE = 111 MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE; 112 113 // TimeZone cache; we parse/decode as little as possible, because the process is quite slow 114 private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>(); 115 // TZI string cache; we keep around our encoded TimeZoneInformation strings 116 private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>(); 117 118 private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); 119 120 // There is no type 4 (thus, the "") 121 static final String[] sTypeToFreq = 122 new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"}; 123 124 static final String[] sDayTokens = 125 new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; 126 127 static final String[] sTwoCharacterNumbers = 128 new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; 129 130 static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR); 131 static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT"); 132 133 private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT"; 134 static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE; 135 static final String ICALENDAR_ATTENDEE_INVITE = 136 ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE"; 137 static final String ICALENDAR_ATTENDEE_ACCEPT = 138 ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED"; 139 static final String ICALENDAR_ATTENDEE_DECLINE = 140 ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED"; 141 static final String ICALENDAR_ATTENDEE_TENTATIVE = 142 ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE"; 143 144 // Note that these constants apply to Calendar items 145 // For future reference: MeetingRequest data can also include free/busy information, but the 146 // constants for these four options in MeetingRequest data have different values! 147 // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus 148 // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus 149 public static final int BUSY_STATUS_FREE = 0; 150 public static final int BUSY_STATUS_TENTATIVE = 1; 151 public static final int BUSY_STATUS_BUSY = 2; 152 public static final int BUSY_STATUS_OUT_OF_OFFICE = 3; 153 154 // Return a 4-byte long from a byte array (little endian) getLong(byte[] bytes, int offset)155 static int getLong(byte[] bytes, int offset) { 156 return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) | 157 ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24); 158 } 159 160 // Put a 4-byte long into a byte array (little endian) setLong(byte[] bytes, int offset, int value)161 static void setLong(byte[] bytes, int offset, int value) { 162 bytes[offset++] = (byte) (value & 0xFF); 163 bytes[offset++] = (byte) ((value >> 8) & 0xFF); 164 bytes[offset++] = (byte) ((value >> 16) & 0xFF); 165 bytes[offset] = (byte) ((value >> 24) & 0xFF); 166 } 167 168 // Return a 2-byte word from a byte array (little endian) getWord(byte[] bytes, int offset)169 static int getWord(byte[] bytes, int offset) { 170 return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8); 171 } 172 173 // Put a 2-byte word into a byte array (little endian) setWord(byte[] bytes, int offset, int value)174 static void setWord(byte[] bytes, int offset, int value) { 175 bytes[offset++] = (byte) (value & 0xFF); 176 bytes[offset] = (byte) ((value >> 8) & 0xFF); 177 } 178 179 // Internal structure for storing a time zone date from a SYSTEMTIME structure 180 // This date represents either the start or the end time for DST 181 static class TimeZoneDate { 182 String year; 183 int month; 184 int dayOfWeek; 185 int day; 186 int time; 187 int hour; 188 int minute; 189 } 190 putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour, int minute)191 static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour, 192 int minute) { 193 // MSFT months are 1 based, same as RRule 194 setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month); 195 // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1 196 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1); 197 // 5 means "last" in MSFT land; for RRule, it's -1 198 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week); 199 // Turn hours/minutes into ms from midnight (per TimeZone) 200 setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour); 201 setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute); 202 } 203 204 // Write a transition time into SYSTEMTIME data (via an offset into a byte array) putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis)205 static void putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis) { 206 GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault()); 207 // Round to the next highest minute; we always write seconds as zero 208 cal.setTimeInMillis(millis + 30*SECONDS); 209 210 // MSFT months are 1 based; TimeZone is 0 based 211 setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1); 212 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 213 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1); 214 215 // Get the "day" in TimeZone format 216 int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH); 217 // 5 means "last" in MSFT land; for TimeZone, it's -1 218 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom); 219 220 // Turn hours/minutes into ms from midnight (per TimeZone) 221 setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, getTrueTransitionHour(cal)); 222 setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal)); 223 } 224 225 // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset getTimeZoneDateFromSystemTime(byte[] bytes, int offset)226 static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) { 227 TimeZoneDate tzd = new TimeZoneDate(); 228 229 // MSFT year is an int; TimeZone is a String 230 int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR); 231 tzd.year = Integer.toString(num); 232 233 // MSFT month = 0 means no daylight time 234 // MSFT months are 1 based; TimeZone is 0 based 235 num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH); 236 if (num == 0) { 237 return null; 238 } else { 239 tzd.month = num -1; 240 } 241 242 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 243 tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1; 244 245 // Get the "day" in TimeZone format 246 num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY); 247 // 5 means "last" in MSFT land; for TimeZone, it's -1 248 if (num == 5) { 249 tzd.day = -1; 250 } else { 251 tzd.day = num; 252 } 253 254 // Turn hours/minutes into ms from midnight (per TimeZone) 255 int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR); 256 tzd.hour = hour; 257 int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE); 258 tzd.minute = minute; 259 tzd.time = (hour*HOURS) + (minute*MINUTES); 260 261 return tzd; 262 } 263 264 /** 265 * Build a GregorianCalendar, based on a time zone and TimeZoneDate. 266 * @param timeZone the time zone we're checking 267 * @param tzd the TimeZoneDate we're interested in 268 * @return a GregorianCalendar with the given time zone and date 269 */ getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd)270 static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) { 271 GregorianCalendar testCalendar = new GregorianCalendar(timeZone); 272 testCalendar.set(GregorianCalendar.YEAR, sCurrentYear); 273 testCalendar.set(GregorianCalendar.MONTH, tzd.month); 274 testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek); 275 testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day); 276 testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour); 277 testCalendar.set(GregorianCalendar.MINUTE, tzd.minute); 278 testCalendar.set(GregorianCalendar.SECOND, 0); 279 return testCalendar.getTimeInMillis(); 280 } 281 282 /** 283 * Return a GregorianCalendar representing the first standard/daylight transition between a 284 * start time and an end time in the given time zone 285 * @param tz a TimeZone the time zone in which we're looking for transitions 286 * @param startTime the start time for the test 287 * @param endTime the end time for the test 288 * @param startInDaylightTime whether daylight time is in effect at the startTime 289 * @return a GregorianCalendar representing the transition or null if none 290 */ findTransitionDate(TimeZone tz, long startTime, long endTime, boolean startInDaylightTime)291 static GregorianCalendar findTransitionDate(TimeZone tz, long startTime, 292 long endTime, boolean startInDaylightTime) { 293 long startingEndTime = endTime; 294 Date date = null; 295 296 // We'll keep splitting the difference until we're within a minute 297 while ((endTime - startTime) > MINUTES) { 298 long checkTime = ((startTime + endTime) / 2) + 1; 299 date = new Date(checkTime); 300 boolean inDaylightTime = tz.inDaylightTime(date); 301 if (inDaylightTime != startInDaylightTime) { 302 endTime = checkTime; 303 } else { 304 startTime = checkTime; 305 } 306 } 307 308 // If these are the same, we're really messed up; return null 309 if (endTime == startingEndTime) { 310 return null; 311 } 312 313 // Set up our calendar and return it 314 GregorianCalendar calendar = new GregorianCalendar(tz); 315 calendar.setTimeInMillis(startTime); 316 return calendar; 317 } 318 319 /** 320 * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 321 * that might be found in an Event; use cached result, if possible 322 * @param tz the TimeZone 323 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 324 */ timeZoneToTziString(TimeZone tz)325 static public String timeZoneToTziString(TimeZone tz) { 326 String tziString = sTziStringCache.get(tz); 327 if (tziString != null) { 328 if (Eas.USER_LOG) { 329 SyncManager.log(TAG, "TZI string for " + tz.getDisplayName() + " found in cache."); 330 } 331 return tziString; 332 } 333 tziString = timeZoneToTziStringImpl(tz); 334 sTziStringCache.put(tz, tziString); 335 return tziString; 336 } 337 338 /** 339 * A class for storing RRULE information. The RRULE members can be accessed individually or 340 * an RRULE string can be created with toString() 341 */ 342 static class RRule { 343 static final int RRULE_NONE = 0; 344 static final int RRULE_DAY_WEEK = 1; 345 static final int RRULE_DATE = 2; 346 347 int type; 348 int dayOfWeek; 349 int week; 350 int month; 351 int date; 352 353 /** 354 * Create an RRULE based on month and date 355 * @param _month the month (1 = JAN, 12 = DEC) 356 * @param _date the date in the month (1-31) 357 */ RRule(int _month, int _date)358 RRule(int _month, int _date) { 359 type = RRULE_DATE; 360 month = _month; 361 date = _date; 362 } 363 364 /** 365 * Create an RRULE based on month, day of week, and week # 366 * @param _month the month (1 = JAN, 12 = DEC) 367 * @param _dayOfWeek the day of the week (1 = SU, 7 = SA) 368 * @param _week the week in the month (1-5 or -1 for last) 369 */ RRule(int _month, int _dayOfWeek, int _week)370 RRule(int _month, int _dayOfWeek, int _week) { 371 type = RRULE_DAY_WEEK; 372 month = _month; 373 dayOfWeek = _dayOfWeek; 374 week = _week; 375 } 376 377 @Override toString()378 public String toString() { 379 if (type == RRULE_DAY_WEEK) { 380 return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week + 381 sDayTokens[dayOfWeek - 1]; 382 } else { 383 return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date; 384 } 385 } 386 } 387 388 /** 389 * Generate an RRULE string for an array of GregorianCalendars, if possible. For now, we are 390 * only looking for rules based on the same date in a month or a specific instance of a day of 391 * the week in a month (e.g. 2nd Tuesday or last Friday). Indeed, these are the only kinds of 392 * rules used in the current tzinfo database. 393 * @param calendars an array of GregorianCalendar, set to a series of transition times in 394 * consecutive years starting with the current year 395 * @return an RRULE or null if none could be inferred from the calendars 396 */ inferRRuleFromCalendars(GregorianCalendar[] calendars)397 static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) { 398 // Let's see if we can make a rule about these 399 GregorianCalendar calendar = calendars[0]; 400 if (calendar == null) return null; 401 int month = calendar.get(Calendar.MONTH); 402 int date = calendar.get(Calendar.DAY_OF_MONTH); 403 int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); 404 int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH); 405 int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH); 406 boolean dateRule = false; 407 boolean dayOfWeekRule = false; 408 for (int i = 1; i < calendars.length; i++) { 409 GregorianCalendar cal = calendars[i]; 410 if (cal == null) return null; 411 // If it's not the same month, there's no rule 412 if (cal.get(Calendar.MONTH) != month) { 413 return null; 414 } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) { 415 // Ok, it seems to be the same day of the week 416 if (dateRule) { 417 return null; 418 } 419 dayOfWeekRule = true; 420 int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH); 421 if (week != thisWeek) { 422 if (week < 0 || week == maxWeek) { 423 int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH); 424 if (thisWeek == thisMaxWeek) { 425 // We'll use -1 (i.e. last) week 426 week = -1; 427 continue; 428 } 429 } 430 return null; 431 } 432 } else if (date == cal.get(Calendar.DAY_OF_MONTH)) { 433 // Maybe the same day of the month? 434 if (dayOfWeekRule) { 435 return null; 436 } 437 dateRule = true; 438 } else { 439 return null; 440 } 441 } 442 443 if (dateRule) { 444 return new RRule(month + 1, date); 445 } 446 // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1) 447 // iCalendar months are 1 based; Calendar months are 0 based 448 // So we adjust these when building the string 449 return new RRule(month + 1, dayOfWeek, week); 450 } 451 452 /** 453 * Generate an rfc2445 utcOffset from minutes offset from GMT 454 * These look like +0800 or -0100 455 * @param offsetMinutes minutes offset from GMT (east is positive, west is negative 456 * @return a utcOffset 457 */ utcOffsetString(int offsetMinutes)458 static String utcOffsetString(int offsetMinutes) { 459 StringBuilder sb = new StringBuilder(); 460 int hours = offsetMinutes / 60; 461 if (hours < 0) { 462 sb.append('-'); 463 hours = 0 - hours; 464 } else { 465 sb.append('+'); 466 } 467 int minutes = offsetMinutes % 60; 468 if (hours < 10) { 469 sb.append('0'); 470 } 471 sb.append(hours); 472 if (minutes < 10) { 473 sb.append('0'); 474 } 475 sb.append(minutes); 476 return sb.toString(); 477 } 478 479 /** 480 * Fill the passed in GregorianCalendars arrays with DST transition information for this and 481 * the following years (based on the length of the arrays) 482 * @param tz the time zone 483 * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing 484 * the transition to daylight time 485 * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing 486 * the transition to standard time 487 * @return true if transitions could be found for all years, false otherwise 488 */ getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars, GregorianCalendar[] toStandardCalendars)489 static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars, 490 GregorianCalendar[] toStandardCalendars) { 491 // We'll use the length of the arrays to determine how many years to check 492 int maxYears = toDaylightCalendars.length; 493 if (toStandardCalendars.length != maxYears) { 494 return false; 495 } 496 // Get the transitions for this year and the next few years 497 for (int i = 0; i < maxYears; i++) { 498 GregorianCalendar cal = new GregorianCalendar(tz); 499 cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0); 500 long startTime = cal.getTimeInMillis(); 501 // Calculate end of year; no need to be insanely precise 502 long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2); 503 Date date = new Date(startTime); 504 boolean startInDaylightTime = tz.inDaylightTime(date); 505 // Find the first transition, and store 506 cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime); 507 if (cal == null) { 508 return false; 509 } else if (startInDaylightTime) { 510 toStandardCalendars[i] = cal; 511 } else { 512 toDaylightCalendars[i] = cal; 513 } 514 // Find the second transition, and store 515 cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime); 516 if (cal == null) { 517 return false; 518 } else if (startInDaylightTime) { 519 toDaylightCalendars[i] = cal; 520 } else { 521 toStandardCalendars[i] = cal; 522 } 523 } 524 return true; 525 } 526 527 /** 528 * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE 529 * @param writer the SimpleIcsWriter we're using 530 * @param tz the time zone 531 * @param offsetString the offset string in VTIMEZONE format (e.g. +0800) 532 * @throws IOException 533 */ writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)534 static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString) 535 throws IOException { 536 writer.writeTag("BEGIN", "STANDARD"); 537 writer.writeTag("TZOFFSETFROM", offsetString); 538 writer.writeTag("TZOFFSETTO", offsetString); 539 // Might as well use start of epoch for start date 540 writer.writeTag("DTSTART", millisToEasDateTime(0L)); 541 writer.writeTag("END", "STANDARD"); 542 writer.writeTag("END", "VTIMEZONE"); 543 } 544 545 /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter 546 * @param tz the TimeZone to be used in the conversion 547 * @param writer the SimpleIcsWriter to be used 548 * @throws IOException 549 */ timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)550 static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer) 551 throws IOException { 552 // We'll use these regardless of whether there's DST in this time zone or not 553 int rawOffsetMinutes = tz.getRawOffset() / MINUTES; 554 String standardOffsetString = utcOffsetString(rawOffsetMinutes); 555 556 // Preamble for all of our VTIMEZONEs 557 writer.writeTag("BEGIN", "VTIMEZONE"); 558 writer.writeTag("TZID", tz.getID()); 559 writer.writeTag("X-LIC-LOCATION", tz.getDisplayName()); 560 561 // Simplest case is no daylight time 562 if (!tz.useDaylightTime()) { 563 writeNoDST(writer, tz, standardOffsetString); 564 return; 565 } 566 567 int maxYears = 3; 568 GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears]; 569 GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears]; 570 if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) { 571 writeNoDST(writer, tz, standardOffsetString); 572 return; 573 } 574 // Try to find a rule to cover these yeras 575 RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars); 576 RRule standardRule = inferRRuleFromCalendars(toStandardCalendars); 577 String daylightOffsetString = 578 utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES)); 579 // We'll use RRULE's if we found both 580 // Otherwise we write the first as DTSTART and the others as RDATE 581 boolean hasRule = daylightRule != null && standardRule != null; 582 583 // Write the DAYLIGHT block 584 writer.writeTag("BEGIN", "DAYLIGHT"); 585 writer.writeTag("TZOFFSETFROM", standardOffsetString); 586 writer.writeTag("TZOFFSETTO", daylightOffsetString); 587 writer.writeTag("DTSTART", 588 transitionMillisToVCalendarTime( 589 toDaylightCalendars[0].getTimeInMillis(), tz, true)); 590 if (hasRule) { 591 writer.writeTag("RRULE", daylightRule.toString()); 592 } else { 593 for (int i = 1; i < maxYears; i++) { 594 writer.writeTag("RDATE", transitionMillisToVCalendarTime( 595 toDaylightCalendars[i].getTimeInMillis(), tz, true)); 596 } 597 } 598 writer.writeTag("END", "DAYLIGHT"); 599 // Write the STANDARD block 600 writer.writeTag("BEGIN", "STANDARD"); 601 writer.writeTag("TZOFFSETFROM", daylightOffsetString); 602 writer.writeTag("TZOFFSETTO", standardOffsetString); 603 writer.writeTag("DTSTART", 604 transitionMillisToVCalendarTime( 605 toStandardCalendars[0].getTimeInMillis(), tz, false)); 606 if (hasRule) { 607 writer.writeTag("RRULE", standardRule.toString()); 608 } else { 609 for (int i = 1; i < maxYears; i++) { 610 writer.writeTag("RDATE", transitionMillisToVCalendarTime( 611 toStandardCalendars[i].getTimeInMillis(), tz, true)); 612 } 613 } 614 writer.writeTag("END", "STANDARD"); 615 // And we're done 616 writer.writeTag("END", "VTIMEZONE"); 617 } 618 619 /** 620 * Find the next transition to occur (i.e. after the current date/time) 621 * @param transitions calendars representing transitions to/from DST 622 * @return millis for the first transition after the current date/time 623 */ findNextTransition(long startingMillis, GregorianCalendar[] transitions)624 static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) { 625 for (GregorianCalendar transition: transitions) { 626 long transitionMillis = transition.getTimeInMillis(); 627 if (transitionMillis > startingMillis) { 628 return transitionMillis; 629 } 630 } 631 return 0; 632 } 633 634 /** 635 * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 636 * that might be found in an Event. Since the internal representation of the TimeZone is hidden 637 * from us we'll find the DST transitions and build the structure from that information 638 * @param tz the TimeZone 639 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 640 */ timeZoneToTziStringImpl(TimeZone tz)641 static String timeZoneToTziStringImpl(TimeZone tz) { 642 String tziString; 643 byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE]; 644 int standardBias = - tz.getRawOffset(); 645 standardBias /= 60*SECONDS; 646 setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias); 647 // If this time zone has daylight savings time, we need to do more work 648 if (tz.useDaylightTime()) { 649 GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3]; 650 GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3]; 651 // See if we can get transitions for a few years; if not, we can't generate DST info 652 // for this time zone 653 if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) { 654 // Try to find a rule to cover these years 655 RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars); 656 RRule standardRule = inferRRuleFromCalendars(toStandardCalendars); 657 if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) && 658 (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) { 659 // We need both rules and they have to be DAY/WEEK type 660 // Write month, day of week, week, hour, minute 661 putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, 662 standardRule, 663 getTrueTransitionHour(toStandardCalendars[0]), 664 getTrueTransitionMinute(toStandardCalendars[0])); 665 putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, 666 daylightRule, 667 getTrueTransitionHour(toDaylightCalendars[0]), 668 getTrueTransitionMinute(toDaylightCalendars[0])); 669 } else { 670 // If there's no rule, we'll use the first transition to standard/to daylight 671 // And indicate that it's just for this year... 672 long now = System.currentTimeMillis(); 673 long standardTransition = findNextTransition(now, toStandardCalendars); 674 long daylightTransition = findNextTransition(now, toDaylightCalendars); 675 // If we can't find transitions, we can't do DST 676 if (standardTransition != 0 && daylightTransition != 0) { 677 putTransitionMillisIntoSystemTime(tziBytes, 678 MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition); 679 putTransitionMillisIntoSystemTime(tziBytes, 680 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition); 681 } 682 } 683 } 684 int dstOffset = tz.getDSTSavings(); 685 setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES); 686 } 687 byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP); 688 tziString = new String(tziEncodedBytes); 689 return tziString; 690 } 691 692 /** 693 * Given a String as directly read from EAS, returns a TimeZone corresponding to that String 694 * @param timeZoneString the String read from the server 695 * @return the TimeZone, or TimeZone.getDefault() if not found 696 */ tziStringToTimeZone(String timeZoneString)697 static public TimeZone tziStringToTimeZone(String timeZoneString) { 698 // If we have this time zone cached, use that value and return 699 TimeZone timeZone = sTimeZoneCache.get(timeZoneString); 700 if (timeZone != null) { 701 if (Eas.USER_LOG) { 702 SyncManager.log(TAG, " Using cached TimeZone " + timeZone.getID()); 703 } 704 } else { 705 timeZone = tziStringToTimeZoneImpl(timeZoneString); 706 if (timeZone == null) { 707 // If we don't find a match, we just return the current TimeZone. In theory, this 708 // shouldn't be happening... 709 SyncManager.alwaysLog("TimeZone not found using default: " + timeZoneString); 710 timeZone = TimeZone.getDefault(); 711 } 712 sTimeZoneCache.put(timeZoneString, timeZone); 713 } 714 return timeZone; 715 } 716 717 /** 718 * Given a String as directly read from EAS, tries to find a TimeZone in the database of all 719 * time zones that corresponds to that String. 720 * @param timeZoneString the String read from the server 721 * @return the TimeZone, or null if not found 722 */ tziStringToTimeZoneImpl(String timeZoneString)723 static TimeZone tziStringToTimeZoneImpl(String timeZoneString) { 724 TimeZone timeZone = null; 725 // First, we need to decode the base64 string 726 byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT); 727 728 // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms 729 // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added 730 // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so 731 // we need to change the sign 732 int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES; 733 734 // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return 735 // the default time zone 736 String[] zoneIds = TimeZone.getAvailableIDs(bias); 737 if (zoneIds.length > 0) { 738 // Try to find an existing TimeZone from the data provided by EAS 739 // We start by pulling out the date that standard time begins 740 TimeZoneDate dstEnd = 741 getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET); 742 if (dstEnd == null) { 743 // If the default time zone is a match 744 TimeZone defaultTimeZone = TimeZone.getDefault(); 745 if (!defaultTimeZone.useDaylightTime() && 746 ArrayUtils.contains(zoneIds, defaultTimeZone.getID())) { 747 if (Eas.USER_LOG) { 748 SyncManager.log(TAG, "TimeZone without DST found to be default: " + 749 defaultTimeZone.getID()); 750 } 751 return defaultTimeZone; 752 } 753 // In this case, there is no daylight savings time, so the only interesting data 754 // for possible matches is the offset and DST availability; we'll take the first 755 // match for those 756 for (String zoneId: zoneIds) { 757 timeZone = TimeZone.getTimeZone(zoneId); 758 if (!timeZone.useDaylightTime()) { 759 if (Eas.USER_LOG) { 760 SyncManager.log(TAG, "TimeZone without DST found by offset: " + 761 timeZone.getID()); 762 } 763 return timeZone; 764 } 765 } 766 // None found, return null 767 return null; 768 } else { 769 TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes, 770 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET); 771 // See comment above for bias... 772 long dstSavings = 773 -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES; 774 775 // We'll go through each time zone to find one with the same DST transitions and 776 // savings length 777 for (String zoneId: zoneIds) { 778 // Get the TimeZone using the zoneId 779 timeZone = TimeZone.getTimeZone(zoneId); 780 781 // Our strategy here is to check just before and just after the transitions 782 // and see whether the check for daylight time matches the expectation 783 // If both transitions match, then we have a match for the offset and start/end 784 // of dst. That's the best we can do for now, since there's no other info 785 // provided by EAS (i.e. we can't get dynamic transitions, etc.) 786 787 // Check one minute before and after DST start transition 788 long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart); 789 Date before = new Date(millisAtTransition - MINUTES); 790 Date after = new Date(millisAtTransition + MINUTES); 791 if (timeZone.inDaylightTime(before)) continue; 792 if (!timeZone.inDaylightTime(after)) continue; 793 794 // Check one minute before and after DST end transition 795 millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd); 796 // Note that we need to subtract an extra hour here, because we end up with 797 // gaining an hour in the transition BACK to standard time 798 before = new Date(millisAtTransition - (dstSavings + MINUTES)); 799 after = new Date(millisAtTransition + MINUTES); 800 if (!timeZone.inDaylightTime(before)) continue; 801 if (timeZone.inDaylightTime(after)) continue; 802 803 // Check that the savings are the same 804 if (dstSavings != timeZone.getDSTSavings()) continue; 805 return timeZone; 806 } 807 // In this case, there is no daylight savings time, so the only interesting data 808 // is the offset, and we know that all of the zoneId's match; we'll take the first 809 timeZone = TimeZone.getTimeZone(zoneIds[0]); 810 if (Eas.USER_LOG) { 811 SyncManager.log(TAG, "No TimeZone with correct DST settings; using first: " + 812 timeZone.getID()); 813 } 814 return timeZone; 815 } 816 } 817 return null; 818 } 819 convertEmailDateTimeToCalendarDateTime(String date)820 static public String convertEmailDateTimeToCalendarDateTime(String date) { 821 // Format for email date strings is 2010-02-23T16:00:00.000Z 822 // Format for calendar date strings is 20100223T160000Z 823 return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) + 824 date.substring(14, 16) + date.substring(17, 19) + 'Z'; 825 } 826 formatTwo(int num)827 static String formatTwo(int num) { 828 if (num <= 12) { 829 return sTwoCharacterNumbers[num]; 830 } else 831 return Integer.toString(num); 832 } 833 834 /** 835 * Generate an EAS formatted date/time string based on GMT. See below for details. 836 */ millisToEasDateTime(long millis)837 static public String millisToEasDateTime(long millis) { 838 return millisToEasDateTime(millis, sGmtTimeZone, true); 839 } 840 841 /** 842 * Generate an EAS formatted local date/time string from a time and a time zone. If the final 843 * argument is false, only a date will be returned (e.g. 20100331) 844 * @param millis a time in milliseconds 845 * @param tz a time zone 846 * @param withTime if the time is to be included in the string 847 * @return an EAS formatted string indicating the date (and time) in the given time zone 848 */ millisToEasDateTime(long millis, TimeZone tz, boolean withTime)849 static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) { 850 StringBuilder sb = new StringBuilder(); 851 GregorianCalendar cal = new GregorianCalendar(tz); 852 cal.setTimeInMillis(millis); 853 sb.append(cal.get(Calendar.YEAR)); 854 sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); 855 sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); 856 if (withTime) { 857 sb.append('T'); 858 sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); 859 sb.append(formatTwo(cal.get(Calendar.MINUTE))); 860 sb.append(formatTwo(cal.get(Calendar.SECOND))); 861 if (tz == sGmtTimeZone) { 862 sb.append('Z'); 863 } 864 } 865 return sb.toString(); 866 } 867 868 /** 869 * Return the true minute at which a transition occurs 870 * Our transition time should be the in the minute BEFORE the transition 871 * If this minute is 59, set minute to 0 and increment the hour 872 * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because 873 * Calendar time will itself be influenced by the transition! So adding 1 minute to 874 * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00) 875 * 876 * @param calendar the calendar holding the transition date/time 877 * @return the true minute of the transition 878 */ getTrueTransitionMinute(GregorianCalendar calendar)879 static int getTrueTransitionMinute(GregorianCalendar calendar) { 880 int minute = calendar.get(Calendar.MINUTE); 881 if (minute == 59) { 882 minute = 0; 883 } 884 return minute; 885 } 886 887 /** 888 * Return the true hour at which a transition occurs 889 * See description for getTrueTransitionMinute, above 890 * @param calendar the calendar holding the transition date/time 891 * @return the true hour of the transition 892 */ getTrueTransitionHour(GregorianCalendar calendar)893 static int getTrueTransitionHour(GregorianCalendar calendar) { 894 int hour = calendar.get(Calendar.HOUR_OF_DAY); 895 hour++; 896 if (hour == 24) { 897 hour = 0; 898 } 899 return hour; 900 } 901 902 /** 903 * Generate a date/time string suitable for VTIMEZONE from a transition time in millis 904 * The format is YYYYMMDDTHHMMSS 905 * @param millis a transition time in milliseconds 906 * @param tz a time zone 907 * @param dst whether we're entering daylight time 908 */ transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst)909 static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) { 910 StringBuilder sb = new StringBuilder(); 911 GregorianCalendar cal = new GregorianCalendar(tz); 912 cal.setTimeInMillis(millis); 913 sb.append(cal.get(Calendar.YEAR)); 914 sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); 915 sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); 916 sb.append('T'); 917 sb.append(formatTwo(getTrueTransitionHour(cal))); 918 sb.append(formatTwo(getTrueTransitionMinute(cal))); 919 sb.append(formatTwo(0)); 920 return sb.toString(); 921 } 922 923 /** 924 * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0 925 * @param time the time in seconds of an all-day event in local time 926 * @return the time in seconds in UTC 927 */ getUtcAllDayCalendarTime(long time, TimeZone localTimeZone)928 static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) { 929 return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE); 930 } 931 932 /** 933 * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0 934 * @param time the time in seconds of an all-day event in UTC 935 * @return the time in seconds in local time 936 */ getLocalAllDayCalendarTime(long time, TimeZone localTimeZone)937 static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) { 938 return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone); 939 } 940 transposeAllDayTime(long time, TimeZone fromTimeZone, TimeZone toTimeZone)941 static private long transposeAllDayTime(long time, TimeZone fromTimeZone, 942 TimeZone toTimeZone) { 943 GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone); 944 fromCalendar.setTimeInMillis(time); 945 GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone); 946 // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds 947 toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR), 948 fromCalendar.get(GregorianCalendar.MONTH), 949 fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0); 950 return toCalendar.getTimeInMillis(); 951 } 952 addByDay(StringBuilder rrule, int dow, int wom)953 static void addByDay(StringBuilder rrule, int dow, int wom) { 954 rrule.append(";BYDAY="); 955 boolean addComma = false; 956 for (int i = 0; i < 7; i++) { 957 if ((dow & 1) == 1) { 958 if (addComma) { 959 rrule.append(','); 960 } 961 if (wom > 0) { 962 // 5 = last week -> -1 963 // So -1SU = last sunday 964 rrule.append(wom == 5 ? -1 : wom); 965 } 966 rrule.append(sDayTokens[i]); 967 addComma = true; 968 } 969 dow >>= 1; 970 } 971 } 972 addByMonthDay(StringBuilder rrule, int dom)973 static void addByMonthDay(StringBuilder rrule, int dom) { 974 // 127 means last day of the month 975 if (dom == 127) { 976 dom = -1; 977 } 978 rrule.append(";BYMONTHDAY=" + dom); 979 } 980 981 /** 982 * Generate the String version of the EAS integer for a given BYDAY value in an rrule 983 * @param dow the BYDAY value of the rrule 984 * @return the String version of the EAS value of these days 985 */ generateEasDayOfWeek(String dow)986 static String generateEasDayOfWeek(String dow) { 987 int bits = 0; 988 int bit = 1; 989 for (String token: sDayTokens) { 990 // If we can find the day in the dow String, add the bit to our bits value 991 if (dow.indexOf(token) >= 0) { 992 bits |= bit; 993 } 994 bit <<= 1; 995 } 996 return Integer.toString(bits); 997 } 998 999 /** 1000 * Extract the value of a token in an RRULE string 1001 * @param rrule an RRULE string 1002 * @param token a token to look for in the RRULE 1003 * @return the value of that token 1004 */ tokenFromRrule(String rrule, String token)1005 static String tokenFromRrule(String rrule, String token) { 1006 int start = rrule.indexOf(token); 1007 if (start < 0) return null; 1008 int len = rrule.length(); 1009 start += token.length(); 1010 int end = start; 1011 char c; 1012 do { 1013 c = rrule.charAt(end++); 1014 if ((c == ';') || (end == len)) { 1015 if (end == len) end++; 1016 return rrule.substring(start, end -1); 1017 } 1018 } while (true); 1019 } 1020 1021 /** 1022 * Reformat an RRULE style UNTIL to an EAS style until 1023 */ recurrenceUntilToEasUntil(String until)1024 static String recurrenceUntilToEasUntil(String until) { 1025 StringBuilder sb = new StringBuilder(); 1026 sb.append(until.substring(0, 4)); 1027 sb.append(until.substring(4, 6)); 1028 sb.append(until.substring(6, 8)); 1029 sb.append("T000000Z"); 1030 return sb.toString(); 1031 } 1032 1033 /** 1034 * Convenience method to add "until" to an EAS calendar stream 1035 */ addUntil(String rrule, Serializer s)1036 static void addUntil(String rrule, Serializer s) throws IOException { 1037 String until = tokenFromRrule(rrule, "UNTIL="); 1038 if (until != null) { 1039 s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until)); 1040 } 1041 } 1042 1043 /** 1044 * Write recurrence information to EAS based on the RRULE in CalendarProvider 1045 * @param rrule the RRULE, from CalendarProvider 1046 * @param startTime, the DTSTART of this Event 1047 * @param s the Serializer we're using to write WBXML data 1048 * @throws IOException 1049 */ 1050 // NOTE: For the moment, we're only parsing recurrence types that are supported by the 1051 // Calendar app UI, which is a subset of possible recurrence types 1052 // This code must be updated when the Calendar adds new functionality recurrenceFromRrule(String rrule, long startTime, Serializer s)1053 static public void recurrenceFromRrule(String rrule, long startTime, Serializer s) 1054 throws IOException { 1055 if (Eas.USER_LOG) { 1056 SyncManager.log(TAG, "RRULE: " + rrule); 1057 } 1058 String freq = tokenFromRrule(rrule, "FREQ="); 1059 // If there's no FREQ=X, then we don't write a recurrence 1060 // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the 1061 // possibility of writing out a partial recurrence stanza 1062 if (freq != null) { 1063 if (freq.equals("DAILY")) { 1064 s.start(Tags.CALENDAR_RECURRENCE); 1065 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0"); 1066 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 1067 addUntil(rrule, s); 1068 s.end(); 1069 } else if (freq.equals("WEEKLY")) { 1070 s.start(Tags.CALENDAR_RECURRENCE); 1071 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); 1072 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 1073 // Requires a day of week (whereas RRULE does not) 1074 String byDay = tokenFromRrule(rrule, "BYDAY="); 1075 if (byDay != null) { 1076 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay)); 1077 } 1078 addUntil(rrule, s); 1079 s.end(); 1080 } else if (freq.equals("MONTHLY")) { 1081 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 1082 if (byMonthDay != null) { 1083 // The nth day of the month 1084 s.start(Tags.CALENDAR_RECURRENCE); 1085 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2"); 1086 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 1087 addUntil(rrule, s); 1088 s.end(); 1089 } else { 1090 String byDay = tokenFromRrule(rrule, "BYDAY="); 1091 String bareByDay; 1092 if (byDay != null) { 1093 // This can be 1WE (1st Wednesday) or -1FR (last Friday) 1094 int wom = byDay.charAt(0); 1095 if (wom == '-') { 1096 // -1 is the only legal case (last week) Use "5" for EAS 1097 wom = 5; 1098 bareByDay = byDay.substring(2); 1099 } else { 1100 wom = wom - '0'; 1101 bareByDay = byDay.substring(1); 1102 } 1103 s.start(Tags.CALENDAR_RECURRENCE); 1104 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3"); 1105 s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom)); 1106 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay)); 1107 addUntil(rrule, s); 1108 s.end(); 1109 } 1110 } 1111 } else if (freq.equals("YEARLY")) { 1112 String byMonth = tokenFromRrule(rrule, "BYMONTH="); 1113 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 1114 if (byMonth == null || byMonthDay == null) { 1115 // Calculate the month and day from the startDate 1116 GregorianCalendar cal = new GregorianCalendar(); 1117 cal.setTimeInMillis(startTime); 1118 cal.setTimeZone(TimeZone.getDefault()); 1119 byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1); 1120 byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); 1121 } 1122 s.start(Tags.CALENDAR_RECURRENCE); 1123 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5"); 1124 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 1125 s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth); 1126 addUntil(rrule, s); 1127 s.end(); 1128 } 1129 } 1130 } 1131 1132 /** 1133 * Build an RRULE String from EAS recurrence information 1134 * @param type the type of recurrence 1135 * @param occurrences how many recurrences (instances) 1136 * @param interval the interval between recurrences 1137 * @param dow day of the week 1138 * @param dom day of the month 1139 * @param wom week of the month 1140 * @param moy month of the year 1141 * @param until the last recurrence time 1142 * @return a valid RRULE String 1143 */ rruleFromRecurrence(int type, int occurrences, int interval, int dow, int dom, int wom, int moy, String until)1144 static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow, 1145 int dom, int wom, int moy, String until) { 1146 StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]); 1147 1148 // INTERVAL and COUNT 1149 if (interval > 0) { 1150 rrule.append(";INTERVAL=" + interval); 1151 } 1152 if (occurrences > 0) { 1153 rrule.append(";COUNT=" + occurrences); 1154 } 1155 1156 // Days, weeks, months, etc. 1157 switch(type) { 1158 case 0: // DAILY 1159 case 1: // WEEKLY 1160 if (dow > 0) addByDay(rrule, dow, -1); 1161 break; 1162 case 2: // MONTHLY 1163 if (dom > 0) addByMonthDay(rrule, dom); 1164 break; 1165 case 3: // MONTHLY (on the nth day) 1166 if (dow > 0) addByDay(rrule, dow, wom); 1167 break; 1168 case 5: // YEARLY (specific day) 1169 if (dom > 0) addByMonthDay(rrule, dom); 1170 if (moy > 0) { 1171 rrule.append(";BYMONTH=" + moy); 1172 } 1173 break; 1174 case 6: // YEARLY 1175 if (dow > 0) addByDay(rrule, dow, wom); 1176 if (dom > 0) addByMonthDay(rrule, dom); 1177 if (moy > 0) { 1178 rrule.append(";BYMONTH=" + moy); 1179 } 1180 break; 1181 default: 1182 break; 1183 } 1184 1185 // UNTIL comes last 1186 if (until != null) { 1187 rrule.append(";UNTIL=" + until); 1188 } 1189 1190 return rrule.toString(); 1191 } 1192 1193 /** 1194 * Create a Calendar in CalendarProvider to which synced Events will be linked 1195 * @param service the sync service requesting Calendar creation 1196 * @param account the account being synced 1197 * @param mailbox the Exchange mailbox for the calendar 1198 * @return the unique id of the Calendar 1199 */ createCalendar(EasSyncService service, Account account, Mailbox mailbox)1200 static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) { 1201 // Create a Calendar object 1202 ContentValues cv = new ContentValues(); 1203 // TODO How will this change if the user changes his account display name? 1204 cv.put(Calendars.DISPLAY_NAME, account.mDisplayName); 1205 cv.put(Calendars._SYNC_ACCOUNT, account.mEmailAddress); 1206 cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 1207 cv.put(Calendars.SYNC_EVENTS, 1); 1208 cv.put(Calendars.SELECTED, 1); 1209 cv.put(Calendars.HIDDEN, 0); 1210 // Don't show attendee status if we're the organizer 1211 cv.put(Calendars.ORGANIZER_CAN_RESPOND, 0); 1212 1213 // TODO Coordinate account colors w/ Calendar, if possible 1214 // Make Email account color opaque 1215 cv.put(Calendars.COLOR, 0xFF000000 | Email.getAccountColor(account.mId)); 1216 cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone()); 1217 cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS); 1218 cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress); 1219 1220 Uri uri = service.mContentResolver.insert(Calendars.CONTENT_URI, cv); 1221 // We save the id of the calendar into mSyncStatus 1222 if (uri != null) { 1223 String stringId = uri.getPathSegments().get(1); 1224 mailbox.mSyncStatus = stringId; 1225 return Long.parseLong(stringId); 1226 } 1227 return -1; 1228 } 1229 1230 /** 1231 * Return the uid for an event based on its globalObjId 1232 * @param globalObjId the base64 encoded String provided by EAS 1233 * @return the uid for the calendar event 1234 */ getUidFromGlobalObjId(String globalObjId)1235 static public String getUidFromGlobalObjId(String globalObjId) { 1236 StringBuilder sb = new StringBuilder(); 1237 // First get the decoded base64 1238 try { 1239 byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT); 1240 String idString = new String(idBytes); 1241 // If the base64 decoded string contains the magic substring: "vCal-Uid", then 1242 // the actual uid is hidden within; the magic substring is never at the start of the 1243 // decoded base64 1244 int index = idString.indexOf("vCal-Uid"); 1245 if (index > 0) { 1246 // The uid starts after "vCal-Uidxxxx", where xxxx are padding 1247 // characters. And it ends before the last character, which is ascii 0 1248 return idString.substring(index + 12, idString.length() - 1); 1249 } else { 1250 // This is an EAS uid. Go through the bytes and write out the hex 1251 // values as characters; this is what we'll need to pass back to EAS 1252 // when responding to the invitation 1253 for (byte b: idBytes) { 1254 Utility.byteToHex(sb, b); 1255 } 1256 return sb.toString(); 1257 } 1258 } catch (RuntimeException e) { 1259 // In the worst of cases (bad format, etc.), we can always return the input 1260 return globalObjId; 1261 } 1262 } 1263 1264 /** 1265 * Get a selfAttendeeStatus from a busy status 1266 * The default here is NONE (i.e. we don't know the status) 1267 * Note that a busy status of FREE must mean NONE as well, since it can't mean declined 1268 * (there would be no event) 1269 * @param busyStatus the busy status, from EAS 1270 * @return the corresponding value for selfAttendeeStatus 1271 */ attendeeStatusFromBusyStatus(int busyStatus)1272 static public int attendeeStatusFromBusyStatus(int busyStatus) { 1273 int attendeeStatus; 1274 switch (busyStatus) { 1275 case BUSY_STATUS_BUSY: 1276 attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED; 1277 break; 1278 case BUSY_STATUS_TENTATIVE: 1279 attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE; 1280 break; 1281 case BUSY_STATUS_FREE: 1282 case BUSY_STATUS_OUT_OF_OFFICE: 1283 default: 1284 attendeeStatus = Attendees.ATTENDEE_STATUS_NONE; 1285 } 1286 return attendeeStatus; 1287 } 1288 1289 /** Get a busy status from a selfAttendeeStatus 1290 * The default here is BUSY 1291 * @param selfAttendeeStatus from CalendarProvider2 1292 * @return the corresponding value of busy status 1293 */ busyStatusFromAttendeeStatus(int selfAttendeeStatus)1294 static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) { 1295 int busyStatus; 1296 switch (selfAttendeeStatus) { 1297 case Attendees.ATTENDEE_STATUS_DECLINED: 1298 case Attendees.ATTENDEE_STATUS_NONE: 1299 case Attendees.ATTENDEE_STATUS_INVITED: 1300 busyStatus = BUSY_STATUS_FREE; 1301 break; 1302 case Attendees.ATTENDEE_STATUS_TENTATIVE: 1303 busyStatus = BUSY_STATUS_TENTATIVE; 1304 break; 1305 case Attendees.ATTENDEE_STATUS_ACCEPTED: 1306 default: 1307 busyStatus = BUSY_STATUS_BUSY; 1308 break; 1309 } 1310 return busyStatus; 1311 } 1312 buildMessageTextFromEntityValues(Context context, ContentValues entityValues, StringBuilder sb)1313 static public String buildMessageTextFromEntityValues(Context context, 1314 ContentValues entityValues, StringBuilder sb) { 1315 if (sb == null) { 1316 sb = new StringBuilder(); 1317 } 1318 Resources resources = context.getResources(); 1319 Date date = new Date(entityValues.getAsLong(Events.DTSTART)); 1320 String dateTimeString = DateFormat.getDateTimeInstance().format(date); 1321 // TODO: Add more detail to message text 1322 // Right now, we're using.. When: Tuesday, March 5th at 2:00pm 1323 // What we're missing is the duration and any recurrence information. So this should be 1324 // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm 1325 // This would require code to build complex strings, and it will have to wait 1326 // For now, we'll just use the meeting_recurring string 1327 if (!entityValues.containsKey(Events.ORIGINAL_EVENT) && 1328 entityValues.containsKey(Events.RRULE)) { 1329 sb.append(resources.getString(R.string.meeting_recurring, dateTimeString)); 1330 } else { 1331 sb.append(resources.getString(R.string.meeting_when, dateTimeString)); 1332 } 1333 String location = null; 1334 if (entityValues.containsKey(Events.EVENT_LOCATION)) { 1335 location = entityValues.getAsString(Events.EVENT_LOCATION); 1336 if (!TextUtils.isEmpty(location)) { 1337 sb.append("\n"); 1338 sb.append(resources.getString(R.string.meeting_where, location)); 1339 } 1340 } 1341 // If there's a description for this event, append it 1342 String desc = entityValues.getAsString(Events.DESCRIPTION); 1343 if (desc != null) { 1344 sb.append("\n--\n"); 1345 sb.append(desc); 1346 } 1347 return sb.toString(); 1348 } 1349 1350 /** 1351 * Add an attendee to the ics attachment and the to list of the Message being composed 1352 * @param ics the ics attachment writer 1353 * @param toList the list of addressees for this email 1354 * @param attendeeName the name of the attendee 1355 * @param attendeeEmail the email address of the attendee 1356 * @param messageFlag the flag indicating the action to be indicated by the message 1357 * @param account the sending account of the email 1358 */ addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList, String attendeeName, String attendeeEmail, int messageFlag, Account account)1359 static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList, 1360 String attendeeName, String attendeeEmail, int messageFlag, Account account) { 1361 if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) { 1362 String icalTag = ICALENDAR_ATTENDEE_INVITE; 1363 if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) { 1364 icalTag = ICALENDAR_ATTENDEE_CANCEL; 1365 } 1366 if (attendeeName != null) { 1367 icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName); 1368 } 1369 ics.writeTag(icalTag, "MAILTO:" + attendeeEmail); 1370 toList.add(attendeeName == null ? new Address(attendeeEmail) : 1371 new Address(attendeeEmail, attendeeName)); 1372 } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) { 1373 String icalTag = null; 1374 switch (messageFlag) { 1375 case Message.FLAG_OUTGOING_MEETING_ACCEPT: 1376 icalTag = ICALENDAR_ATTENDEE_ACCEPT; 1377 break; 1378 case Message.FLAG_OUTGOING_MEETING_DECLINE: 1379 icalTag = ICALENDAR_ATTENDEE_DECLINE; 1380 break; 1381 case Message.FLAG_OUTGOING_MEETING_TENTATIVE: 1382 icalTag = ICALENDAR_ATTENDEE_TENTATIVE; 1383 break; 1384 } 1385 if (icalTag != null) { 1386 if (attendeeName != null) { 1387 icalTag += ";CN=" 1388 + SimpleIcsWriter.quoteParamValue(attendeeName); 1389 } 1390 ics.writeTag(icalTag, "MAILTO:" + attendeeEmail); 1391 } 1392 } 1393 } 1394 1395 /** 1396 * Create a Message for an (Event) Entity 1397 * @param entity the Entity for the Event (as might be retrieved by CalendarProvider) 1398 * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent 1399 * @param the unique id of this Event, or null if it can be retrieved from the Event 1400 * @param the user's account 1401 * @return a Message with many fields pre-filled (more later) 1402 */ createMessageForEntity(Context context, Entity entity, int messageFlag, String uid, Account account)1403 static public EmailContent.Message createMessageForEntity(Context context, Entity entity, 1404 int messageFlag, String uid, Account account) { 1405 return createMessageForEntity(context, entity, messageFlag, uid, account, 1406 null /*specifiedAttendee*/); 1407 } 1408 createMessageForEntity(Context context, Entity entity, int messageFlag, String uid, Account account, String specifiedAttendee)1409 static public EmailContent.Message createMessageForEntity(Context context, Entity entity, 1410 int messageFlag, String uid, Account account, String specifiedAttendee) { 1411 ContentValues entityValues = entity.getEntityValues(); 1412 ArrayList<NamedContentValues> subValues = entity.getSubValues(); 1413 boolean isException = entityValues.containsKey(Events.ORIGINAL_EVENT); 1414 boolean isReply = false; 1415 1416 EmailContent.Message msg = new EmailContent.Message(); 1417 msg.mFlags = messageFlag; 1418 msg.mTimeStamp = System.currentTimeMillis(); 1419 1420 String method; 1421 if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) { 1422 method = "REQUEST"; 1423 } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) { 1424 method = "CANCEL"; 1425 } else { 1426 method = "REPLY"; 1427 isReply = true; 1428 } 1429 1430 try { 1431 // Create our iCalendar writer and start generating tags 1432 SimpleIcsWriter ics = new SimpleIcsWriter(); 1433 ics.writeTag("BEGIN", "VCALENDAR"); 1434 ics.writeTag("METHOD", method); 1435 ics.writeTag("PRODID", "AndroidEmail"); 1436 ics.writeTag("VERSION", "2.0"); 1437 1438 // Our default vcalendar time zone is UTC, but this will change (below) if we're 1439 // sending a recurring event, in which case we use local time 1440 TimeZone vCalendarTimeZone = sGmtTimeZone; 1441 String vCalendarDateSuffix = ""; 1442 1443 // Check for all day event 1444 boolean allDayEvent = false; 1445 if (entityValues.containsKey(Events.ALL_DAY)) { 1446 Integer ade = entityValues.getAsInteger(Events.ALL_DAY); 1447 allDayEvent = (ade != null) && (ade == 1); 1448 if (allDayEvent) { 1449 // Example: DTSTART;VALUE=DATE:20100331 (all day event) 1450 vCalendarDateSuffix = ";VALUE=DATE"; 1451 } 1452 } 1453 1454 // If we're inviting people and the meeting is recurring, we need to send our time zone 1455 // information and make sure to send DTSTART/DTEND in local time (unless, of course, 1456 // this is an all-day event). Recurring, for this purpose, includes exceptions to 1457 // recurring events 1458 if (!isReply && !allDayEvent && 1459 (entityValues.containsKey(Events.RRULE) || 1460 entityValues.containsKey(Events.ORIGINAL_EVENT))) { 1461 vCalendarTimeZone = TimeZone.getDefault(); 1462 // Write the VTIMEZONE block to the writer 1463 timeZoneToVTimezone(vCalendarTimeZone, ics); 1464 // Example: DTSTART;TZID=US/Pacific:20100331T124500 1465 vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID(); 1466 } 1467 1468 ics.writeTag("BEGIN", "VEVENT"); 1469 if (uid == null) { 1470 uid = entityValues.getAsString(Events._SYNC_DATA); 1471 } 1472 if (uid != null) { 1473 ics.writeTag("UID", uid); 1474 } 1475 1476 if (entityValues.containsKey("DTSTAMP")) { 1477 ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP")); 1478 } else { 1479 ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis())); 1480 } 1481 1482 long startTime = entityValues.getAsLong(Events.DTSTART); 1483 if (startTime != 0) { 1484 ics.writeTag("DTSTART" + vCalendarDateSuffix, 1485 millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent)); 1486 } 1487 1488 // If this is an Exception, we send the recurrence-id, which is just the original 1489 // instance time 1490 if (isException) { 1491 long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 1492 ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix, 1493 millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent)); 1494 } 1495 1496 if (!entityValues.containsKey(Events.DURATION)) { 1497 if (entityValues.containsKey(Events.DTEND)) { 1498 ics.writeTag("DTEND" + vCalendarDateSuffix, 1499 millisToEasDateTime( 1500 entityValues.getAsLong(Events.DTEND), vCalendarTimeZone, 1501 !allDayEvent)); 1502 } 1503 } else { 1504 // Convert this into millis and add it to DTSTART for DTEND 1505 // We'll use 1 hour as a default 1506 long durationMillis = HOURS; 1507 Duration duration = new Duration(); 1508 try { 1509 duration.parse(entityValues.getAsString(Events.DURATION)); 1510 } catch (ParseException e) { 1511 // We'll use the default in this case 1512 } 1513 ics.writeTag("DTEND" + vCalendarDateSuffix, 1514 millisToEasDateTime( 1515 startTime + durationMillis, vCalendarTimeZone, !allDayEvent)); 1516 } 1517 1518 String location = null; 1519 if (entityValues.containsKey(Events.EVENT_LOCATION)) { 1520 location = entityValues.getAsString(Events.EVENT_LOCATION); 1521 ics.writeTag("LOCATION", location); 1522 } 1523 1524 String sequence = entityValues.getAsString(Events._SYNC_VERSION); 1525 if (sequence == null) { 1526 sequence = "0"; 1527 } 1528 1529 // We'll use 0 to mean a meeting invitation 1530 int titleId = 0; 1531 switch (messageFlag) { 1532 case Message.FLAG_OUTGOING_MEETING_INVITE: 1533 if (!sequence.equals("0")) { 1534 titleId = R.string.meeting_updated; 1535 } 1536 break; 1537 case Message.FLAG_OUTGOING_MEETING_ACCEPT: 1538 titleId = R.string.meeting_accepted; 1539 break; 1540 case Message.FLAG_OUTGOING_MEETING_DECLINE: 1541 titleId = R.string.meeting_declined; 1542 break; 1543 case Message.FLAG_OUTGOING_MEETING_TENTATIVE: 1544 titleId = R.string.meeting_tentative; 1545 break; 1546 case Message.FLAG_OUTGOING_MEETING_CANCEL: 1547 titleId = R.string.meeting_canceled; 1548 break; 1549 } 1550 Resources resources = context.getResources(); 1551 String title = entityValues.getAsString(Events.TITLE); 1552 if (title == null) { 1553 title = ""; 1554 } 1555 ics.writeTag("SUMMARY", title); 1556 // For meeting invitations just use the title 1557 if (titleId == 0) { 1558 msg.mSubject = title; 1559 } else { 1560 // Otherwise, use the additional text 1561 msg.mSubject = resources.getString(titleId, title); 1562 } 1563 1564 // Build the text for the message, starting with an initial line describing the 1565 // exception (if this is one) 1566 StringBuilder sb = new StringBuilder(); 1567 if (isException && !isReply) { 1568 // Add the line, depending on whether this is a cancellation or update 1569 Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME)); 1570 String dateString = DateFormat.getDateInstance().format(date); 1571 if (titleId == R.string.meeting_canceled) { 1572 sb.append(resources.getString(R.string.exception_cancel, dateString)); 1573 } else { 1574 sb.append(resources.getString(R.string.exception_updated, dateString)); 1575 } 1576 sb.append("\n\n"); 1577 } 1578 String text = 1579 CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb); 1580 1581 if (text.length() > 0) { 1582 ics.writeTag("DESCRIPTION", text); 1583 } 1584 // And store the message text 1585 msg.mText = text; 1586 if (!isReply) { 1587 if (entityValues.containsKey(Events.ALL_DAY)) { 1588 Integer ade = entityValues.getAsInteger(Events.ALL_DAY); 1589 ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE"); 1590 } 1591 1592 String rrule = entityValues.getAsString(Events.RRULE); 1593 if (rrule != null) { 1594 ics.writeTag("RRULE", rrule); 1595 } 1596 1597 // If we decide to send alarm information in the meeting request ics file, 1598 // handle it here by looping through the subvalues 1599 } 1600 1601 // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics 1602 String organizerName = null; 1603 String organizerEmail = null; 1604 ArrayList<Address> toList = new ArrayList<Address>(); 1605 for (NamedContentValues ncv: subValues) { 1606 Uri ncvUri = ncv.uri; 1607 ContentValues ncvValues = ncv.values; 1608 if (ncvUri.equals(Attendees.CONTENT_URI)) { 1609 Integer relationship = 1610 ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 1611 // If there's no relationship, we can't create this for EAS 1612 // Similarly, we need an attendee email for each invitee 1613 if (relationship != null && 1614 ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 1615 // Organizer isn't among attendees in EAS 1616 if (relationship == Attendees.RELATIONSHIP_ORGANIZER) { 1617 organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 1618 organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 1619 continue; 1620 } 1621 String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 1622 String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 1623 1624 // This shouldn't be possible, but allow for it 1625 if (attendeeEmail == null) continue; 1626 // If we only want to send to the specifiedAttendee, eliminate others here 1627 if ((specifiedAttendee != null) && 1628 !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) { 1629 continue; 1630 } 1631 1632 addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag, 1633 account); 1634 } 1635 } 1636 } 1637 1638 // Manually add the specifiedAttendee if he wasn't added in the Attendees loop 1639 if (toList.isEmpty() && (specifiedAttendee != null)) { 1640 addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account); 1641 } 1642 1643 // Create the organizer tag for ical 1644 if (organizerEmail != null) { 1645 String icalTag = "ORGANIZER"; 1646 // We should be able to find this, assuming the Email is the user's email 1647 // TODO Find this in the account 1648 if (organizerName != null) { 1649 icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName); 1650 } 1651 ics.writeTag(icalTag, "MAILTO:" + organizerEmail); 1652 if (isReply) { 1653 toList.add(organizerName == null ? new Address(organizerEmail) : 1654 new Address(organizerEmail, organizerName)); 1655 } 1656 } 1657 1658 // If we have no "to" list, we're done 1659 if (toList.isEmpty()) return null; 1660 1661 // Write out the "to" list 1662 Address[] toArray = new Address[toList.size()]; 1663 int i = 0; 1664 for (Address address: toList) { 1665 toArray[i++] = address; 1666 } 1667 msg.mTo = Address.pack(toArray); 1668 1669 ics.writeTag("CLASS", "PUBLIC"); 1670 ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ? 1671 "CANCELLED" : "CONFIRMED"); 1672 ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses 1673 ics.writeTag("PRIORITY", "5"); // 1 to 9, 5 = medium 1674 ics.writeTag("SEQUENCE", sequence); 1675 ics.writeTag("END", "VEVENT"); 1676 ics.writeTag("END", "VCALENDAR"); 1677 1678 // Create the ics attachment using the "content" field 1679 Attachment att = new Attachment(); 1680 att.mContentBytes = ics.getBytes(); 1681 att.mMimeType = "text/calendar; method=" + method; 1682 att.mFileName = "invite.ics"; 1683 att.mSize = att.mContentBytes.length; 1684 // We don't send content-disposition with this attachment 1685 att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART; 1686 1687 // Add the attachment to the message 1688 msg.mAttachments = new ArrayList<Attachment>(); 1689 msg.mAttachments.add(att); 1690 } catch (IOException e) { 1691 Log.w(TAG, "IOException in createMessageForEntity"); 1692 return null; 1693 } 1694 1695 // Return the new Message to caller 1696 return msg; 1697 } 1698 1699 /** 1700 * Create a Message for an Event that can be retrieved from CalendarProvider by its unique id 1701 * @param cr a content resolver that can be used to query for the Event 1702 * @param eventId the unique id of the Event 1703 * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent 1704 * @param the unique id of this Event, or null if it can be retrieved from the Event 1705 * @param the user's account 1706 * @param requireAddressees if true (the default), no Message is returned if there aren't any 1707 * addressees; if false, return the Message regardless (addressees will be filled in later) 1708 * @return a Message with many fields pre-filled (more later) 1709 * @throws RemoteException if there is an issue retrieving the Event from CalendarProvider 1710 */ createMessageForEventId(Context context, long eventId, int messageFlag, String uid, Account account)1711 static public EmailContent.Message createMessageForEventId(Context context, long eventId, 1712 int messageFlag, String uid, Account account) throws RemoteException { 1713 return createMessageForEventId(context, eventId, messageFlag, uid, account, 1714 null /*specifiedAttendee*/); 1715 } 1716 createMessageForEventId(Context context, long eventId, int messageFlag, String uid, Account account, String specifiedAttendee)1717 static public EmailContent.Message createMessageForEventId(Context context, long eventId, 1718 int messageFlag, String uid, Account account, String specifiedAttendee) 1719 throws RemoteException { 1720 ContentResolver cr = context.getContentResolver(); 1721 EntityIterator eventIterator = 1722 EventsEntity.newEntityIterator( 1723 cr.query(ContentUris.withAppendedId(Events.CONTENT_URI.buildUpon() 1724 .appendQueryParameter(android.provider.Calendar.CALLER_IS_SYNCADAPTER, 1725 "true").build(), eventId), null, null, null, null), cr); 1726 try { 1727 while (eventIterator.hasNext()) { 1728 Entity entity = eventIterator.next(); 1729 return createMessageForEntity(context, entity, messageFlag, uid, account, 1730 specifiedAttendee); 1731 } 1732 } finally { 1733 eventIterator.close(); 1734 } 1735 return null; 1736 } 1737 1738 /** 1739 * Return a boolean value for an integer ContentValues column 1740 * @param values a ContentValues object 1741 * @param columnName the name of a column to be found in the ContentValues 1742 * @return a boolean representation of the value of columnName in values; null and 0 = false, 1743 * other integers = true 1744 */ getIntegerValueAsBoolean(ContentValues values, String columnName)1745 static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) { 1746 Integer intValue = values.getAsInteger(columnName); 1747 return (intValue != null && intValue != 0); 1748 } 1749 } 1750