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