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