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}