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