CalendarUtilities.java revision 29c38d2c84273851282ae3c799f5bf1845202065
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.exchange.utility; 18 19import com.android.email.Email; 20import com.android.email.R; 21import com.android.email.mail.Address; 22import com.android.email.provider.EmailContent; 23import com.android.email.provider.EmailContent.Account; 24import com.android.email.provider.EmailContent.Attachment; 25import com.android.email.provider.EmailContent.Mailbox; 26import com.android.email.provider.EmailContent.Message; 27import com.android.exchange.Eas; 28import com.android.exchange.EasSyncService; 29import com.android.exchange.adapter.Serializer; 30import com.android.exchange.adapter.Tags; 31 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Entity; 37import android.content.EntityIterator; 38import android.content.Entity.NamedContentValues; 39import android.net.Uri; 40import android.os.RemoteException; 41import android.provider.Calendar.Attendees; 42import android.provider.Calendar.Calendars; 43import android.provider.Calendar.Events; 44import android.provider.Calendar.EventsEntity; 45import android.provider.Calendar.Reminders; 46import android.text.format.Time; 47import android.util.Log; 48import android.util.base64.Base64; 49 50import java.io.IOException; 51import java.text.ParseException; 52import java.util.ArrayList; 53import java.util.Calendar; 54import java.util.Date; 55import java.util.GregorianCalendar; 56import java.util.HashMap; 57import java.util.TimeZone; 58 59public class CalendarUtilities { 60 // NOTE: Most definitions in this class are have package visibility for testing purposes 61 private static final String TAG = "CalendarUtility"; 62 63 // Time related convenience constants, in milliseconds 64 static final int SECONDS = 1000; 65 static final int MINUTES = SECONDS*60; 66 static final int HOURS = MINUTES*60; 67 static final long DAYS = HOURS*24; 68 69 // NOTE All Microsoft data structures are little endian 70 71 // The following constants relate to standard Microsoft data sizes 72 // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx 73 static final int MSFT_LONG_SIZE = 4; 74 static final int MSFT_WCHAR_SIZE = 2; 75 static final int MSFT_WORD_SIZE = 2; 76 77 // The following constants relate to Microsoft's SYSTEMTIME structure 78 // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4 79 80 static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE; 81 static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE; 82 static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE; 83 static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE; 84 static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE; 85 static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE; 86 //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE; 87 //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE; 88 static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE; 89 90 // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure 91 // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx 92 static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0; 93 static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET = 94 MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE; 95 static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET = 96 MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 97 static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET = 98 MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 99 static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET = 100 MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE; 101 static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET = 102 MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32); 103 static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET = 104 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE; 105 static final int MSFT_TIME_ZONE_SIZE = 106 MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE; 107 108 // TimeZone cache; we parse/decode as little as possible, because the process is quite slow 109 private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>(); 110 // TZI string cache; we keep around our encoded TimeZoneInformation strings 111 private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>(); 112 113 // There is no type 4 (thus, the "") 114 static final String[] sTypeToFreq = 115 new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"}; 116 117 static final String[] sDayTokens = 118 new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; 119 120 static final String[] sTwoCharacterNumbers = 121 new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; 122 123 static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR); 124 static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT"); 125 126 private static final String ICALENDAR_ATTENDEE_INVITE = 127 "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"; 128 private static final String ICALENDAR_ATTENDEE_ACCEPT = 129 "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED"; 130 private static final String ICALENDAR_ATTENDEE_DECLINE = 131 "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=DECLINED"; 132 private static final String ICALENDAR_ATTENDEE_TENTATIVE = 133 "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE"; 134 135 // Return a 4-byte long from a byte array (little endian) 136 static int getLong(byte[] bytes, int offset) { 137 return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) | 138 ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24); 139 } 140 141 // Put a 4-byte long into a byte array (little endian) 142 static void setLong(byte[] bytes, int offset, int value) { 143 bytes[offset++] = (byte) (value & 0xFF); 144 bytes[offset++] = (byte) ((value >> 8) & 0xFF); 145 bytes[offset++] = (byte) ((value >> 16) & 0xFF); 146 bytes[offset] = (byte) ((value >> 24) & 0xFF); 147 } 148 149 // Return a 2-byte word from a byte array (little endian) 150 static int getWord(byte[] bytes, int offset) { 151 return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8); 152 } 153 154 // Put a 2-byte word into a byte array (little endian) 155 static void setWord(byte[] bytes, int offset, int value) { 156 bytes[offset++] = (byte) (value & 0xFF); 157 bytes[offset] = (byte) ((value >> 8) & 0xFF); 158 } 159 160 // Internal structure for storing a time zone date from a SYSTEMTIME structure 161 // This date represents either the start or the end time for DST 162 static class TimeZoneDate { 163 String year; 164 int month; 165 int dayOfWeek; 166 int day; 167 int time; 168 int hour; 169 int minute; 170 } 171 172 // Write SYSTEMTIME data into a byte array (this will either be for the standard or daylight 173 // transition) 174 static void putTimeInMillisIntoSystemTime(byte[] bytes, int offset, long millis) { 175 GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault()); 176 // Round to the next highest minute; we always write seconds as zero 177 cal.setTimeInMillis(millis + 30*SECONDS); 178 179 // MSFT months are 1 based; TimeZone is 0 based 180 setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1); 181 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 182 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1); 183 184 // Get the "day" in TimeZone format 185 int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH); 186 // 5 means "last" in MSFT land; for TimeZone, it's -1 187 setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom); 188 189 // Turn hours/minutes into ms from midnight (per TimeZone) 190 setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, cal.get(Calendar.HOUR)); 191 setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, cal.get(Calendar.MINUTE)); 192 } 193 194 // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset 195 static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) { 196 TimeZoneDate tzd = new TimeZoneDate(); 197 198 // MSFT year is an int; TimeZone is a String 199 int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR); 200 tzd.year = Integer.toString(num); 201 202 // MSFT month = 0 means no daylight time 203 // MSFT months are 1 based; TimeZone is 0 based 204 num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH); 205 if (num == 0) { 206 return null; 207 } else { 208 tzd.month = num -1; 209 } 210 211 // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1 212 tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1; 213 214 // Get the "day" in TimeZone format 215 num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY); 216 // 5 means "last" in MSFT land; for TimeZone, it's -1 217 if (num == 5) { 218 tzd.day = -1; 219 } else { 220 tzd.day = num; 221 } 222 223 // Turn hours/minutes into ms from midnight (per TimeZone) 224 int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR); 225 tzd.hour = hour; 226 int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE); 227 tzd.minute = minute; 228 tzd.time = (hour*HOURS) + (minute*MINUTES); 229 230 return tzd; 231 } 232 233 /** 234 * Build a GregorianCalendar, based on a time zone and TimeZoneDate. 235 * @param timeZone the time zone we're checking 236 * @param tzd the TimeZoneDate we're interested in 237 * @return a GregorianCalendar with the given time zone and date 238 */ 239 static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) { 240 GregorianCalendar testCalendar = new GregorianCalendar(timeZone); 241 testCalendar.set(GregorianCalendar.YEAR, sCurrentYear); 242 testCalendar.set(GregorianCalendar.MONTH, tzd.month); 243 testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek); 244 testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day); 245 testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour); 246 testCalendar.set(GregorianCalendar.MINUTE, tzd.minute); 247 return testCalendar; 248 } 249 250 /** 251 * Find a standard/daylight transition between a start time and an end time 252 * @param tz a TimeZone 253 * @param startTime the start time for the test 254 * @param endTime the end time for the test 255 * @param startInDaylightTime whether daylight time is in effect at the startTime 256 * @return the time in millis of the first transition, or 0 if none 257 */ 258 static private long findTransition(TimeZone tz, long startTime, long endTime, 259 boolean startInDaylightTime) { 260 long startingEndTime = endTime; 261 Date date = null; 262 while ((endTime - startTime) > MINUTES) { 263 long checkTime = ((startTime + endTime) / 2) + 1; 264 date = new Date(checkTime); 265 if (tz.inDaylightTime(date) != startInDaylightTime) { 266 endTime = checkTime; 267 } else { 268 startTime = checkTime; 269 } 270 } 271 if (endTime == startingEndTime) { 272 // Really, this shouldn't happen 273 return 0; 274 } 275 return startTime; 276 } 277 278 /** 279 * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 280 * that might be found in an Event; use cached result, if possible 281 * @param tz the TimeZone 282 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 283 */ 284 static public String timeZoneToTziString(TimeZone tz) { 285 String tziString = sTziStringCache.get(tz); 286 if (tziString != null) { 287 if (Eas.USER_LOG) { 288 Log.d(TAG, "TZI string for " + tz.getDisplayName() + " found in cache."); 289 } 290 return tziString; 291 } 292 tziString = timeZoneToTziStringImpl(tz); 293 sTziStringCache.put(tz, tziString); 294 return tziString; 295 } 296 297 /** 298 * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone 299 * that might be found in an Event. Since the internal representation of the TimeZone is hidden 300 * from us we'll find the DST transitions and build the structure from that information 301 * @param tz the TimeZone 302 * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element 303 */ 304 static public String timeZoneToTziStringImpl(TimeZone tz) { 305 String tziString; 306 long time = System.currentTimeMillis(); 307 byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE]; 308 int standardBias = - tz.getRawOffset(); 309 standardBias /= 60*SECONDS; 310 setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias); 311 // If this time zone has daylight savings time, we need to do a bunch more work 312 if (tz.useDaylightTime()) { 313 long standardTransition = 0; 314 long daylightTransition = 0; 315 GregorianCalendar cal = new GregorianCalendar(); 316 cal.set(sCurrentYear, Calendar.JANUARY, 1, 0, 0, 0); 317 cal.setTimeZone(tz); 318 long startTime = cal.getTimeInMillis(); 319 // Calculate rough end of year; no need to do the calculation 320 long endOfYearTime = startTime + 365*DAYS; 321 Date date = new Date(startTime); 322 boolean startInDaylightTime = tz.inDaylightTime(date); 323 // Find the first transition, and store 324 startTime = findTransition(tz, startTime, endOfYearTime, startInDaylightTime); 325 if (startInDaylightTime) { 326 standardTransition = startTime; 327 } else { 328 daylightTransition = startTime; 329 } 330 // Find the second transition, and store 331 startTime = findTransition(tz, startTime, endOfYearTime, !startInDaylightTime); 332 if (startInDaylightTime) { 333 daylightTransition = startTime; 334 } else { 335 standardTransition = startTime; 336 } 337 if (standardTransition != 0 && daylightTransition != 0) { 338 putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, 339 standardTransition); 340 putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, 341 daylightTransition); 342 int dstOffset = tz.getDSTSavings(); 343 setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES); 344 } 345 } 346 byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP); 347 tziString = new String(tziEncodedBytes); 348 if (Eas.USER_LOG) { 349 Log.d(TAG, "Calculated TZI String for " + tz.getDisplayName() + " in " + 350 (System.currentTimeMillis() - time) + "ms"); 351 } 352 return tziString; 353 } 354 355 /** 356 * Given a String as directly read from EAS, returns a TimeZone corresponding to that String 357 * @param timeZoneString the String read from the server 358 * @return the TimeZone, or TimeZone.getDefault() if not found 359 */ 360 static public TimeZone tziStringToTimeZone(String timeZoneString) { 361 // If we have this time zone cached, use that value and return 362 TimeZone timeZone = sTimeZoneCache.get(timeZoneString); 363 if (timeZone != null) { 364 if (Eas.USER_LOG) { 365 Log.d(TAG, " Using cached TimeZone " + timeZone.getDisplayName()); 366 } 367 } else { 368 timeZone = tziStringToTimeZoneImpl(timeZoneString); 369 if (timeZone == null) { 370 // If we don't find a match, we just return the current TimeZone. In theory, this 371 // shouldn't be happening... 372 Log.w(TAG, "TimeZone not found using default: " + timeZoneString); 373 timeZone = TimeZone.getDefault(); 374 } 375 sTimeZoneCache.put(timeZoneString, timeZone); 376 } 377 return timeZone; 378 } 379 380 /** 381 * Given a String as directly read from EAS, tries to find a TimeZone in the database of all 382 * time zones that corresponds to that String. 383 * @param timeZoneString the String read from the server 384 * @return the TimeZone, or TimeZone.getDefault() if not found 385 */ 386 static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) { 387 TimeZone timeZone = null; 388 // TODO Remove after we're comfortable with performance 389 long time = System.currentTimeMillis(); 390 // First, we need to decode the base64 string 391 byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT); 392 393 // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms 394 // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added 395 // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so 396 // we need to change the sign 397 int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES; 398 399 // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return 400 // the default time zone 401 String[] zoneIds = TimeZone.getAvailableIDs(bias); 402 if (zoneIds.length > 0) { 403 // Try to find an existing TimeZone from the data provided by EAS 404 // We start by pulling out the date that standard time begins 405 TimeZoneDate dstEnd = 406 getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET); 407 if (dstEnd == null) { 408 // In this case, there is no daylight savings time, so the only interesting data 409 // is the offset, and we know that all of the zoneId's match; we'll take the first 410 timeZone = TimeZone.getTimeZone(zoneIds[0]); 411 String dn = timeZone.getDisplayName(); 412 sTimeZoneCache.put(timeZoneString, timeZone); 413 if (Eas.USER_LOG) { 414 Log.d(TAG, "TimeZone without DST found by offset: " + dn); 415 } 416 } else { 417 TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes, 418 MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET); 419 // See comment above for bias... 420 long dstSavings = 421 -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES; 422 423 // We'll go through each time zone to find one with the same DST transitions and 424 // savings length 425 for (String zoneId: zoneIds) { 426 // Get the TimeZone using the zoneId 427 timeZone = TimeZone.getTimeZone(zoneId); 428 429 // Our strategy here is to check just before and just after the transitions 430 // and see whether the check for daylight time matches the expectation 431 // If both transitions match, then we have a match for the offset and start/end 432 // of dst. That's the best we can do for now, since there's no other info 433 // provided by EAS (i.e. we can't get dynamic transitions, etc.) 434 435 int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES; 436 int errorBoundsMinutes = (testSavingsMinutes * 2) + 1; 437 438 // Check start DST transition 439 GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart); 440 testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes); 441 Date before = testCalendar.getTime(); 442 testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes); 443 Date after = testCalendar.getTime(); 444 if (timeZone.inDaylightTime(before)) continue; 445 if (!timeZone.inDaylightTime(after)) continue; 446 447 // Check end DST transition 448 testCalendar = getCheckCalendar(timeZone, dstEnd); 449 testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes); 450 before = testCalendar.getTime(); 451 testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes); 452 after = testCalendar.getTime(); 453 if (!timeZone.inDaylightTime(before)) continue; 454 if (timeZone.inDaylightTime(after)) continue; 455 456 // Check that the savings are the same 457 if (dstSavings != timeZone.getDSTSavings()) continue; 458 459 // If we're here, it's the right time zone 460 String dn = timeZone.getDisplayName(); 461 // TODO Remove timing when we're comfortable with performance 462 if (Eas.USER_LOG) { 463 Log.d(TAG, "TimeZone found by rules: " + dn + " in " + 464 (System.currentTimeMillis() - time) + "ms"); 465 } 466 break; 467 } 468 } 469 } 470 return timeZone; 471 } 472 473 /** 474 * Generate a time in milliseconds from an email date string that represents a date/time in GMT 475 * @param Email style DateTime string from Exchange server 476 * @return the time in milliseconds (since Jan 1, 1970) 477 */ 478 static public long parseEmailDateTimeToMillis(String date) { 479 // Format for email date strings is 2010-02-23T16:00:00.000Z 480 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 481 Integer.parseInt(date.substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)), 482 Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date.substring(14, 16)), 483 Integer.parseInt(date.substring(17, 19))); 484 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 485 return cal.getTimeInMillis(); 486 } 487 488 /** 489 * Generate a time in milliseconds from a date string that represents a date/time in GMT 490 * @param DateTime string from Exchange server 491 * @return the time in milliseconds (since Jan 1, 1970) 492 */ 493 static public long parseDateTimeToMillis(String date) { 494 // Format for calendar date strings is 20090211T180303Z 495 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 496 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), 497 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), 498 Integer.parseInt(date.substring(13, 15))); 499 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 500 return cal.getTimeInMillis(); 501 } 502 503 /** 504 * Generate a GregorianCalendar from a date string that represents a date/time in GMT 505 * @param DateTime string from Exchange server 506 * @return the GregorianCalendar 507 */ 508 static public GregorianCalendar parseDateTimeToCalendar(String date) { 509 // Format for calendar date strings is 20090211T180303Z 510 GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)), 511 Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)), 512 Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)), 513 Integer.parseInt(date.substring(13, 15))); 514 cal.setTimeZone(TimeZone.getTimeZone("GMT")); 515 return cal; 516 } 517 518 static public String convertEmailDateTimeToCalendarDateTime(String date) { 519 // Format for email date strings is 2010-02-23T16:00:00.000Z 520 // Format for calendar date strings is 2010-02-23T160000Z 521 return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) + 522 date.substring(14, 16) + date.substring(17, 19) + 'Z'; 523 } 524 525 static String formatTwo(int num) { 526 if (num <= 12) { 527 return sTwoCharacterNumbers[num]; 528 } else 529 return Integer.toString(num); 530 } 531 532 /** 533 * Generate an EAS formatted date/time string based on GMT. See below for details. 534 */ 535 static public String millisToEasDateTime(long millis) { 536 return millisToEasDateTime(millis, sGmtTimeZone); 537 } 538 539 /** 540 * Generate an EAS formatted local date/time string from a time and a time zone 541 * @param millis a time in milliseconds 542 * @param tz a time zone 543 * @return an EAS formatted string indicating the date/time in the given time zone 544 */ 545 static public String millisToEasDateTime(long millis, TimeZone tz) { 546 StringBuilder sb = new StringBuilder(); 547 GregorianCalendar cal = new GregorianCalendar(tz); 548 cal.setTimeInMillis(millis); 549 sb.append(cal.get(Calendar.YEAR)); 550 sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); 551 sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); 552 sb.append('T'); 553 sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); 554 sb.append(formatTwo(cal.get(Calendar.MINUTE))); 555 sb.append(formatTwo(cal.get(Calendar.SECOND))); 556 sb.append('Z'); 557 return sb.toString(); 558 } 559 560 static void addByDay(StringBuilder rrule, int dow, int wom) { 561 rrule.append(";BYDAY="); 562 boolean addComma = false; 563 for (int i = 0; i < 7; i++) { 564 if ((dow & 1) == 1) { 565 if (addComma) { 566 rrule.append(','); 567 } 568 if (wom > 0) { 569 // 5 = last week -> -1 570 // So -1SU = last sunday 571 rrule.append(wom == 5 ? -1 : wom); 572 } 573 rrule.append(sDayTokens[i]); 574 addComma = true; 575 } 576 dow >>= 1; 577 } 578 } 579 580 static void addByMonthDay(StringBuilder rrule, int dom) { 581 // 127 means last day of the month 582 if (dom == 127) { 583 dom = -1; 584 } 585 rrule.append(";BYMONTHDAY=" + dom); 586 } 587 588 /** 589 * Generate the String version of the EAS integer for a given BYDAY value in an rrule 590 * @param dow the BYDAY value of the rrule 591 * @return the String version of the EAS value of these days 592 */ 593 static String generateEasDayOfWeek(String dow) { 594 int bits = 0; 595 int bit = 1; 596 for (String token: sDayTokens) { 597 // If we can find the day in the dow String, add the bit to our bits value 598 if (dow.indexOf(token) >= 0) { 599 bits |= bit; 600 } 601 bit <<= 1; 602 } 603 return Integer.toString(bits); 604 } 605 606 /** 607 * Extract the value of a token in an RRULE string 608 * @param rrule an RRULE string 609 * @param token a token to look for in the RRULE 610 * @return the value of that token 611 */ 612 static String tokenFromRrule(String rrule, String token) { 613 int start = rrule.indexOf(token); 614 if (start < 0) return null; 615 int len = rrule.length(); 616 start += token.length(); 617 int end = start; 618 char c; 619 do { 620 c = rrule.charAt(end++); 621 if (!Character.isLetterOrDigit(c) || (end == len)) { 622 if (end == len) end++; 623 return rrule.substring(start, end -1); 624 } 625 } while (true); 626 } 627 628 /** 629 * Reformat an RRULE style UNTIL to an EAS style until 630 */ 631 static String recurrenceUntilToEasUntil(String until) { 632 StringBuilder sb = new StringBuilder(); 633 sb.append(until.substring(0, 4)); 634 sb.append('-'); 635 sb.append(until.substring(4, 6)); 636 sb.append('-'); 637 if (until.length() == 8) { 638 sb.append(until.substring(6, 8)); 639 sb.append("T00:00:00"); 640 641 } else { 642 sb.append(until.substring(6, 11)); 643 sb.append(':'); 644 sb.append(until.substring(11, 13)); 645 sb.append(':'); 646 sb.append(until.substring(13, 15)); 647 } 648 sb.append(".000Z"); 649 return sb.toString(); 650 } 651 652 /** 653 * Convenience method to add "until" to an EAS calendar stream 654 */ 655 static void addUntil(String rrule, Serializer s) throws IOException { 656 String until = tokenFromRrule(rrule, "UNTIL="); 657 if (until != null) { 658 s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until)); 659 } 660 } 661 662 /** 663 * Write recurrence information to EAS based on the RRULE in CalendarProvider 664 * @param rrule the RRULE, from CalendarProvider 665 * @param startTime, the DTSTART of this Event 666 * @param s the Serializer we're using to write WBXML data 667 * @throws IOException 668 */ 669 // NOTE: For the moment, we're only parsing recurrence types that are supported by the 670 // Calendar app UI, which is a subset of possible recurrence types 671 // This code must be updated when the Calendar adds new functionality 672 static public void recurrenceFromRrule(String rrule, long startTime, Serializer s) 673 throws IOException { 674 Log.d("RRULE", "rule: " + rrule); 675 String freq = tokenFromRrule(rrule, "FREQ="); 676 // If there's no FREQ=X, then we don't write a recurrence 677 // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the 678 // possibility of writing out a partial recurrence stanza 679 if (freq != null) { 680 if (freq.equals("DAILY")) { 681 s.start(Tags.CALENDAR_RECURRENCE); 682 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0"); 683 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 684 s.end(); 685 } else if (freq.equals("WEEKLY")) { 686 s.start(Tags.CALENDAR_RECURRENCE); 687 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); 688 s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 689 // Requires a day of week (whereas RRULE does not) 690 String byDay = tokenFromRrule(rrule, "BYDAY="); 691 if (byDay != null) { 692 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay)); 693 } 694 addUntil(rrule, s); 695 s.end(); 696 } else if (freq.equals("MONTHLY")) { 697 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 698 if (byMonthDay != null) { 699 // The nth day of the month 700 s.start(Tags.CALENDAR_RECURRENCE); 701 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2"); 702 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 703 addUntil(rrule, s); 704 s.end(); 705 } else { 706 String byDay = tokenFromRrule(rrule, "BYDAY="); 707 String bareByDay; 708 if (byDay != null) { 709 // This can be 1WE (1st Wednesday) or -1FR (last Friday) 710 int wom = byDay.charAt(0); 711 if (wom == '-') { 712 // -1 is the only legal case (last week) Use "5" for EAS 713 wom = 5; 714 bareByDay = byDay.substring(2); 715 } else { 716 wom = wom - '0'; 717 bareByDay = byDay.substring(1); 718 } 719 s.start(Tags.CALENDAR_RECURRENCE); 720 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3"); 721 s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom)); 722 s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay)); 723 addUntil(rrule, s); 724 s.end(); 725 } 726 } 727 } else if (freq.equals("YEARLY")) { 728 String byMonth = tokenFromRrule(rrule, "BYMONTH="); 729 String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY="); 730 if (byMonth == null || byMonthDay == null) { 731 // Calculate the month and day from the startDate 732 GregorianCalendar cal = new GregorianCalendar(); 733 cal.setTimeInMillis(startTime); 734 cal.setTimeZone(TimeZone.getDefault()); 735 byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1); 736 byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH)); 737 } 738 s.start(Tags.CALENDAR_RECURRENCE); 739 s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5"); 740 s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay); 741 s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth); 742 addUntil(rrule, s); 743 s.end(); 744 } 745 } 746 } 747 748 /** 749 * Build an RRULE String from EAS recurrence information 750 * @param type the type of recurrence 751 * @param occurrences how many recurrences (instances) 752 * @param interval the interval between recurrences 753 * @param dow day of the week 754 * @param dom day of the month 755 * @param wom week of the month 756 * @param moy month of the year 757 * @param until the last recurrence time 758 * @return a valid RRULE String 759 */ 760 static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow, 761 int dom, int wom, int moy, String until) { 762 StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]); 763 764 // INTERVAL and COUNT 765 if (interval > 0) { 766 rrule.append(";INTERVAL=" + interval); 767 } 768 if (occurrences > 0) { 769 rrule.append(";COUNT=" + occurrences); 770 } 771 772 // Days, weeks, months, etc. 773 switch(type) { 774 case 0: // DAILY 775 case 1: // WEEKLY 776 if (dow > 0) addByDay(rrule, dow, -1); 777 break; 778 case 2: // MONTHLY 779 if (dom > 0) addByMonthDay(rrule, dom); 780 break; 781 case 3: // MONTHLY (on the nth day) 782 if (dow > 0) addByDay(rrule, dow, wom); 783 break; 784 case 5: // YEARLY 785 if (dom > 0) addByMonthDay(rrule, dom); 786 if (moy > 0) { 787 // TODO MAKE SURE WE'RE 1 BASED 788 rrule.append(";BYMONTH=" + moy); 789 } 790 break; 791 case 6: // YEARLY (on the nth day) 792 if (dow > 0) addByDay(rrule, dow, wom); 793 if (moy > 0) addByMonthDay(rrule, dow); 794 break; 795 default: 796 break; 797 } 798 799 // UNTIL comes last 800 if (until != null) { 801 rrule.append(";UNTIL=" + until); 802 } 803 804 return rrule.toString(); 805 } 806 807 /** 808 * Create a Calendar in CalendarProvider to which synced Events will be linked 809 * @param service the sync service requesting Calendar creation 810 * @param account the account being synced 811 * @param mailbox the Exchange mailbox for the calendar 812 * @return the unique id of the Calendar 813 */ 814 static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) { 815 // Create a Calendar object 816 ContentValues cv = new ContentValues(); 817 // TODO How will this change if the user changes his account display name? 818 cv.put(Calendars.DISPLAY_NAME, account.mDisplayName); 819 cv.put(Calendars._SYNC_ACCOUNT, account.mEmailAddress); 820 cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 821 cv.put(Calendars.SYNC_EVENTS, 1); 822 cv.put(Calendars.SELECTED, 1); 823 cv.put(Calendars.HIDDEN, 0); 824 // TODO Coordinate account colors w/ Calendar, if possible 825 // Make Email account color opaque 826 cv.put(Calendars.COLOR, 0xFF000000 | Email.getAccountColor(account.mId)); 827 cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone()); 828 cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS); 829 cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress); 830 831 Uri uri = service.mContentResolver.insert(Calendars.CONTENT_URI, cv); 832 // We save the id of the calendar into mSyncStatus 833 if (uri != null) { 834 String stringId = uri.getPathSegments().get(1); 835 mailbox.mSyncStatus = stringId; 836 return Long.parseLong(stringId); 837 } 838 return -1; 839 } 840 841 /** 842 * Create a Message for an (Event) Entity 843 * @param entity the Entity for the Event (as might be retrieved by CalendarProvider) 844 * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent 845 * @param the unique id of this Event, or null if it can be retrieved from the Event 846 * @param the user's account 847 * @return a Message with many fields pre-filled (more later) 848 */ 849 static public EmailContent.Message createMessageForEntity(Context context, Entity entity, 850 int messageFlag, String uid, Account account) { 851 // TODO Handle exceptions; will be a nightmare 852 // TODO Cries out for unit test 853 ContentValues entityValues = entity.getEntityValues(); 854 ArrayList<NamedContentValues> subValues = entity.getSubValues(); 855 856 EmailContent.Message msg = new EmailContent.Message(); 857 msg.mFlags = messageFlag; 858 msg.mTimeStamp = System.currentTimeMillis(); 859 860 String method; 861 if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) { 862 method = "REQUEST"; 863 } else { 864 method = "REPLY"; 865 } 866 867 try { 868 // Create our iCalendar writer and start generating tags 869 SimpleIcsWriter ics = new SimpleIcsWriter(); 870 ics.writeTag("BEGIN", "VCALENDAR"); 871 ics.writeTag("METHOD", method); 872 ics.writeTag("PRODID", "AndroidEmail"); 873 ics.writeTag("VERSION", "2.0"); 874 ics.writeTag("BEGIN", "VEVENT"); 875 ics.writeTag("CLASS", "PUBLIC"); 876 ics.writeTag("STATUS", "CONFIRMED"); 877 ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses 878 ics.writeTag("PRIORITY", "5"); // 1 to 9, 5 = medium 879 ics.writeTag("SEQUENCE", "0"); 880 if (uid == null) { 881 uid = entityValues.getAsString(Events._SYNC_DATA); 882 } 883 if (uid != null) { 884 ics.writeTag("UID", uid); 885 } 886 887 if (entityValues.containsKey("DTSTAMP")) { 888 ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP")); 889 } else { 890 ics.writeTag("DTSTAMP", 891 CalendarUtilities.millisToEasDateTime(System.currentTimeMillis())); 892 } 893 894 895 long startTime = entityValues.getAsLong(Events.DTSTART); 896 if (startTime != 0) { 897 ics.writeTag("DTSTART", CalendarUtilities.millisToEasDateTime(startTime)); 898 } 899 900 if (!entityValues.containsKey(Events.DURATION)) { 901 if (entityValues.containsKey(Events.DTEND)) { 902 ics.writeTag("DTEND", CalendarUtilities.millisToEasDateTime( 903 entityValues.getAsLong(Events.DTEND))); 904 } 905 } else { 906 // Convert this into millis and add it to DTSTART for DTEND 907 // We'll use 1 hour as a default 908 long durationMillis = HOURS; 909 Duration duration = new Duration(); 910 try { 911 duration.parse(entityValues.getAsString(Events.DURATION)); 912 } catch (ParseException e) { 913 // We'll use the default in this case 914 } 915 ics.writeTag("DTEND", 916 CalendarUtilities.millisToEasDateTime(startTime + durationMillis)); 917 } 918 919 if (entityValues.containsKey(Events.EVENT_LOCATION)) { 920 ics.writeTag("LOCATION", entityValues.getAsString(Events.EVENT_LOCATION)); 921 } 922 923 int titleId = 0; 924 switch (messageFlag) { 925 case Message.FLAG_OUTGOING_MEETING_INVITE: 926 titleId = R.string.meeting_invitation; 927 break; 928 case Message.FLAG_OUTGOING_MEETING_ACCEPT: 929 titleId = R.string.meeting_accepted; 930 break; 931 case Message.FLAG_OUTGOING_MEETING_DECLINE: 932 titleId = R.string.meeting_declined; 933 break; 934 case Message.FLAG_OUTGOING_MEETING_TENTATIVE: 935 titleId = R.string.meeting_tentative; 936 break; 937 case Message.FLAG_OUTGOING_MEETING_CANCEL: 938 titleId = R.string.meeting_canceled; 939 break; 940 } 941 String title = entityValues.getAsString(Events.TITLE); 942 if (title == null) { 943 title = ""; 944 } 945 ics.writeTag("SUMMARY", title); 946 if (titleId != 0) { 947 msg.mSubject = context.getResources().getString(titleId, title); 948 } 949 if (method.equals("REQUEST")) { 950 if (entityValues.containsKey(Events.ALL_DAY)) { 951 Integer ade = entityValues.getAsInteger(Events.ALL_DAY); 952 ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE"); 953 } 954 955 // TODO Handle time zone 956 957 String desc = entityValues.getAsString(Events.DESCRIPTION); 958 if (desc != null) { 959 // TODO Do we add some description of the event here? 960 ics.writeTag("DESCRIPTION", desc); 961 msg.mText = desc; 962 } else { 963 msg.mText = ""; 964 } 965 966 String rrule = entityValues.getAsString(Events.RRULE); 967 if (rrule != null) { 968 ics.writeTag("RRULE", rrule); 969 } 970 971 // Handle associated data EXCEPT for attendees, which have to be grouped 972 for (NamedContentValues ncv: subValues) { 973 Uri ncvUri = ncv.uri; 974 if (ncvUri.equals(Reminders.CONTENT_URI)) { 975 // TODO Consider sending alarm information in the meeting request, though 976 // it's not obviously appropriate (i.e. telling the user what alarm to use) 977 // This should be for REQUEST only 978 // Here's what the VALARM would look like: 979 // BEGIN:VALARM 980 // ACTION:DISPLAY 981 // DESCRIPTION:REMINDER 982 // TRIGGER;RELATED=START:-PT15M 983 // END:VALARM 984 } 985 } 986 } 987 988 // Handle attendee data here; keep track of organizer and stream it afterward 989 String organizerName = null; 990 String organizerEmail = null; 991 ArrayList<Address> toList = new ArrayList<Address>(); 992 for (NamedContentValues ncv: subValues) { 993 Uri ncvUri = ncv.uri; 994 ContentValues ncvValues = ncv.values; 995 if (ncvUri.equals(Attendees.CONTENT_URI)) { 996 Integer relationship = 997 ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 998 // If there's no relationship, we can't create this for EAS 999 // Similarly, we need an attendee email for each invitee 1000 if (relationship != null && 1001 ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 1002 // Organizer isn't among attendees in EAS 1003 if (relationship == Attendees.RELATIONSHIP_ORGANIZER) { 1004 organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 1005 organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 1006 continue; 1007 } 1008 String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 1009 String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 1010 // This shouldn't be possible, but allow for it 1011 if (attendeeEmail == null) continue; 1012 1013 if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) { 1014 String icalTag = ICALENDAR_ATTENDEE_INVITE; 1015 if (attendeeName != null) { 1016 icalTag += ";CN=" + attendeeName; 1017 } 1018 ics.writeTag(icalTag, "MAILTO:" + attendeeEmail); 1019 toList.add(attendeeName == null ? new Address(attendeeEmail) : 1020 new Address(attendeeEmail, attendeeName)); 1021 } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) { 1022 String icalTag = null; 1023 switch (messageFlag) { 1024 case Message.FLAG_OUTGOING_MEETING_ACCEPT: 1025 icalTag = ICALENDAR_ATTENDEE_ACCEPT; 1026 break; 1027 case Message.FLAG_OUTGOING_MEETING_DECLINE: 1028 icalTag = ICALENDAR_ATTENDEE_DECLINE; 1029 break; 1030 case Message.FLAG_OUTGOING_MEETING_TENTATIVE: 1031 icalTag = ICALENDAR_ATTENDEE_TENTATIVE; 1032 break; 1033 } 1034 if (icalTag != null) { 1035 if (attendeeName != null) { 1036 icalTag += ";CN=" + attendeeName; 1037 } 1038 ics.writeTag(icalTag, "MAILTO:" + attendeeEmail); 1039 } 1040 } 1041 } 1042 } 1043 } 1044 1045 // Create the organizer tag for ical 1046 if (organizerEmail != null) { 1047 String icalTag = "ORGANIZER"; 1048 // We should be able to find this, assuming the Email is the user's email 1049 // TODO Find this in the account 1050 if (organizerName != null) { 1051 icalTag += ";CN=" + organizerName; 1052 } 1053 ics.writeTag(icalTag, "MAILTO:" + organizerEmail); 1054 if (method.equals("REPLY")) { 1055 toList.add(organizerName == null ? new Address(organizerEmail) : 1056 new Address(organizerEmail, organizerName)); 1057 } 1058 } 1059 1060 // If we have no "to" list, we're done 1061 if (toList.isEmpty()) return null; 1062 // Write out the "to" list 1063 Address[] toArray = new Address[toList.size()]; 1064 int i = 0; 1065 for (Address address: toList) { 1066 toArray[i++] = address; 1067 } 1068 msg.mTo = Address.pack(toArray); 1069 1070 ics.writeTag("END", "VEVENT"); 1071 ics.writeTag("END", "VCALENDAR"); 1072 ics.flush(); 1073 ics.close(); 1074 1075 // Create the ics attachment using the "content" field 1076 Attachment att = new Attachment(); 1077 att.mContent = ics.toString(); 1078 att.mMimeType = "text/calendar; method=" + method; 1079 att.mFileName = "invite.ics"; 1080 att.mSize = att.mContent.length(); 1081 // We don't send content-disposition with this attachment 1082 att.mFlags = Attachment.FLAG_SUPPRESS_DISPOSITION; 1083 1084 // Add the attachment to the message 1085 msg.mAttachments = new ArrayList<Attachment>(); 1086 msg.mAttachments.add(att); 1087 } catch (IOException e) { 1088 Log.w(TAG, "IOException in createMessageForEntity"); 1089 return null; 1090 } 1091 1092 // Return the new Message to caller 1093 return msg; 1094 } 1095 1096 /** 1097 * Create a Message for an Event that can be retrieved from CalendarProvider by its unique id 1098 * @param cr a content resolver that can be used to query for the Event 1099 * @param eventId the unique id of the Event 1100 * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent 1101 * @param the unique id of this Event, or null if it can be retrieved from the Event 1102 * @param the user's account 1103 * @return a Message with many fields pre-filled (more later) 1104 * @throws RemoteException if there is an issue retrieving the Event from CalendarProvider 1105 */ 1106 static public EmailContent.Message createMessageForEventId(Context context, long eventId, 1107 int messageFlag, String uid, Account account) throws RemoteException { 1108 ContentResolver cr = context.getContentResolver(); 1109 EntityIterator eventIterator = 1110 EventsEntity.newEntityIterator( 1111 cr.query(ContentUris.withAppendedId(Events.CONTENT_URI.buildUpon() 1112 .appendQueryParameter(android.provider.Calendar.CALLER_IS_SYNCADAPTER, 1113 "true").build(), eventId), null, null, null, null), cr); 1114 try { 1115 while (eventIterator.hasNext()) { 1116 Entity entity = eventIterator.next(); 1117 return createMessageForEntity(context, entity, messageFlag, uid, account); 1118 } 1119 } finally { 1120 eventIterator.close(); 1121 } 1122 return null; 1123 } 1124}