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