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