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