Home | History | Annotate | Download | only in utility
      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)
    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)
    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)
    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)
    214     static void setWord(byte[] bytes, int offset, int value) {
    215         bytes[offset++] = (byte) (value & 0xFF);
    216         bytes[offset] = (byte) ((value >> 8) & 0xFF);
    217     }
    218 
    219     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
    245     static void clearTimeZoneCache() {
    246         sTimeZoneCache.clear();
    247     }
    248 
    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)
    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
    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      */
    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      */
    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      */
    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          */
    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          */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    781     static public TimeZone tziStringToTimeZone(String timeZoneString) {
    782         return tziStringToTimeZone(timeZoneString, MINUTES);
    783     }
    784 
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
   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      */
   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      */
   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      */
   1054     static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
   1055         return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
   1056     }
   1057 
   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 
   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 
   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 
   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      */
   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      */
   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
   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      */
   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 
   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 
   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
   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      */
   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      */
   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         if (TextUtils.equals(mailbox.mDisplayName, account.mEmailAddress)) {
   1448             cv.put(Calendars.IS_PRIMARY, 1);
   1449         } else {
   1450             cv.put(Calendars.IS_PRIMARY, 0);
   1451         }
   1452         Uri uri = contentResolver.insert(asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
   1453                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
   1454         // We save the id of the calendar into mSyncStatus
   1455         if (uri != null) {
   1456             String stringId = uri.getPathSegments().get(1);
   1457             mailbox.mSyncStatus = stringId;
   1458             return Long.parseLong(stringId);
   1459         }
   1460         return -1;
   1461     }
   1462 
   1463     static Uri asSyncAdapter(Uri uri, String account, String accountType) {
   1464         return uri.buildUpon()
   1465                 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
   1466                         "true")
   1467                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
   1468                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
   1469     }
   1470 
   1471     /**
   1472      * Return the uid for an event based on its globalObjId
   1473      * @param globalObjId the base64 encoded String provided by EAS
   1474      * @return the uid for the calendar event
   1475      */
   1476     static public String getUidFromGlobalObjId(String globalObjId) {
   1477         StringBuilder sb = new StringBuilder();
   1478         // First get the decoded base64
   1479         try {
   1480             byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
   1481             String idString = new String(idBytes);
   1482             // If the base64 decoded string contains the magic substring: "vCal-Uid", then
   1483             // the actual uid is hidden within; the magic substring is never at the start of the
   1484             // decoded base64
   1485             int index = idString.indexOf("vCal-Uid");
   1486             if (index > 0) {
   1487                 // The uid starts after "vCal-Uidxxxx", where xxxx are padding
   1488                 // characters.  And it ends before the last character, which is ascii 0
   1489                 return idString.substring(index + 12, idString.length() - 1);
   1490             } else {
   1491                 // This is an EAS uid. Go through the bytes and write out the hex
   1492                 // values as characters; this is what we'll need to pass back to EAS
   1493                 // when responding to the invitation
   1494                 for (byte b: idBytes) {
   1495                     Utility.byteToHex(sb, b);
   1496                 }
   1497                 return sb.toString();
   1498             }
   1499         } catch (RuntimeException e) {
   1500             // In the worst of cases (bad format, etc.), we can always return the input
   1501             return globalObjId;
   1502         }
   1503     }
   1504 
   1505     /**
   1506      * Get a selfAttendeeStatus from a busy status
   1507      * The default here is NONE (i.e. we don't know the status)
   1508      * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
   1509      * (there would be no event)
   1510      * @param busyStatus the busy status, from EAS
   1511      * @return the corresponding value for selfAttendeeStatus
   1512      */
   1513     static public int attendeeStatusFromBusyStatus(int busyStatus) {
   1514         int attendeeStatus;
   1515         switch (busyStatus) {
   1516             case BUSY_STATUS_BUSY:
   1517                 attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
   1518                 break;
   1519             case BUSY_STATUS_TENTATIVE:
   1520                 attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
   1521                 break;
   1522             case BUSY_STATUS_FREE:
   1523             case BUSY_STATUS_OUT_OF_OFFICE:
   1524             default:
   1525                 attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
   1526         }
   1527         return attendeeStatus;
   1528     }
   1529 
   1530     /**
   1531      * Get a selfAttendeeStatus from a response type (EAS 14+)
   1532      * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
   1533      * @param busyStatus the response status, from EAS
   1534      * @return the corresponding value for selfAttendeeStatus
   1535      */
   1536     static public int attendeeStatusFromResponseType(int responseType) {
   1537         int attendeeStatus;
   1538         switch (responseType) {
   1539             case RESPONSE_TYPE_NOT_RESPONDED:
   1540                 attendeeStatus = Attendees.ATTENDEE_STATUS_INVITED;
   1541                 break;
   1542             case RESPONSE_TYPE_ACCEPTED:
   1543                 attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
   1544                 break;
   1545             case RESPONSE_TYPE_TENTATIVE:
   1546                 attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
   1547                 break;
   1548             case RESPONSE_TYPE_DECLINED:
   1549                 attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
   1550                 break;
   1551             default:
   1552                 attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
   1553         }
   1554         return attendeeStatus;
   1555     }
   1556 
   1557     /** Get a busy status from a selfAttendeeStatus
   1558      * The default here is BUSY
   1559      * @param selfAttendeeStatus from CalendarProvider2
   1560      * @return the corresponding value of busy status
   1561      */
   1562     static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
   1563         int busyStatus;
   1564         switch (selfAttendeeStatus) {
   1565             case Attendees.ATTENDEE_STATUS_DECLINED:
   1566             case Attendees.ATTENDEE_STATUS_NONE:
   1567             case Attendees.ATTENDEE_STATUS_INVITED:
   1568                 busyStatus = BUSY_STATUS_FREE;
   1569                 break;
   1570             case Attendees.ATTENDEE_STATUS_TENTATIVE:
   1571                 busyStatus = BUSY_STATUS_TENTATIVE;
   1572                 break;
   1573             case Attendees.ATTENDEE_STATUS_ACCEPTED:
   1574             default:
   1575                 busyStatus = BUSY_STATUS_BUSY;
   1576                 break;
   1577         }
   1578         return busyStatus;
   1579     }
   1580 
   1581     /** Get a busy status from event availability
   1582      * The default here is TENTATIVE
   1583      * @param availability from CalendarProvider2
   1584      * @return the corresponding value of busy status
   1585      */
   1586     static public int busyStatusFromAvailability(int availability) {
   1587         int busyStatus;
   1588         switch (availability) {
   1589             case Events.AVAILABILITY_BUSY:
   1590                 busyStatus = BUSY_STATUS_BUSY;
   1591                 break;
   1592             case Events.AVAILABILITY_FREE:
   1593                 busyStatus = BUSY_STATUS_FREE;
   1594                 break;
   1595             case Events.AVAILABILITY_TENTATIVE:
   1596             default:
   1597                 busyStatus = BUSY_STATUS_TENTATIVE;
   1598                 break;
   1599         }
   1600         return busyStatus;
   1601     }
   1602 
   1603     /** Get an event availability from busy status
   1604      * The default here is TENTATIVE
   1605      * @param busyStatus from CalendarProvider2
   1606      * @return the corresponding availability value
   1607      */
   1608     static public int availabilityFromBusyStatus(int busyStatus) {
   1609         int availability;
   1610         switch (busyStatus) {
   1611             case BUSY_STATUS_BUSY:
   1612                 availability = Events.AVAILABILITY_BUSY;
   1613                 break;
   1614             case BUSY_STATUS_FREE:
   1615                 availability = Events.AVAILABILITY_FREE;
   1616                 break;
   1617             case BUSY_STATUS_TENTATIVE:
   1618             default:
   1619                 availability = Events.AVAILABILITY_TENTATIVE;
   1620                 break;
   1621         }
   1622         return availability;
   1623     }
   1624 
   1625     static public String buildMessageTextFromEntityValues(Context context,
   1626             ContentValues entityValues, StringBuilder sb) {
   1627         if (sb == null) {
   1628             sb = new StringBuilder();
   1629         }
   1630         Resources resources = context.getResources();
   1631         // TODO: Add more detail to message text
   1632         // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
   1633         // What we're missing is the duration and any recurrence information.  So this should be
   1634         // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
   1635         // This would require code to build complex strings, and it will have to wait
   1636         // For now, we'll just use the meeting_recurring string
   1637 
   1638         boolean allDayEvent = false;
   1639         if (entityValues.containsKey(Events.ALL_DAY)) {
   1640             Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
   1641             allDayEvent = (ade != null) && (ade == 1);
   1642         }
   1643         boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
   1644             entityValues.containsKey(Events.RRULE);
   1645 
   1646         if (entityValues.containsKey(Events.DTSTART)) {
   1647             final String dateTimeString;
   1648             final int res;
   1649             final long startTime = entityValues.getAsLong(Events.DTSTART);
   1650             if (allDayEvent) {
   1651                 final Date date = new Date(getLocalAllDayCalendarTime(startTime,
   1652                                 TimeZone.getDefault()));
   1653                 dateTimeString = DateFormat.getDateInstance().format(date);
   1654                 res = recurringEvent ? R.string.meeting_allday_recurring
   1655                     : R.string.meeting_allday;
   1656             } else {
   1657                 dateTimeString = DateFormat.getDateTimeInstance().format(
   1658                         new Date(startTime));
   1659                 res = recurringEvent ? R.string.meeting_recurring
   1660                     : R.string.meeting_when;
   1661             }
   1662             sb.append(resources.getString(res, dateTimeString));
   1663         }
   1664 
   1665         String location = null;
   1666         if (entityValues.containsKey(Events.EVENT_LOCATION)) {
   1667             location = entityValues.getAsString(Events.EVENT_LOCATION);
   1668             if (!TextUtils.isEmpty(location)) {
   1669                 sb.append("\n");
   1670                 sb.append(resources.getString(R.string.meeting_where, location));
   1671             }
   1672         }
   1673         // If there's a description for this event, append it
   1674         String desc = entityValues.getAsString(Events.DESCRIPTION);
   1675         if (desc != null) {
   1676             sb.append("\n--\n");
   1677             sb.append(desc);
   1678         }
   1679         return sb.toString();
   1680     }
   1681 
   1682     /**
   1683      * Add an attendee to the ics attachment and the to list of the Message being composed
   1684      * @param ics the ics attachment writer
   1685      * @param toList the list of addressees for this email
   1686      * @param attendeeName the name of the attendee
   1687      * @param attendeeEmail the email address of the attendee
   1688      * @param messageFlag the flag indicating the action to be indicated by the message
   1689      * @param account the sending account of the email
   1690      */
   1691     static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
   1692             String attendeeName, String attendeeEmail, int messageFlag, Account account) {
   1693         if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
   1694             String icalTag = ICALENDAR_ATTENDEE_INVITE;
   1695             if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
   1696                 icalTag = ICALENDAR_ATTENDEE_CANCEL;
   1697             }
   1698             if (attendeeName != null) {
   1699                 icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
   1700             }
   1701             ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
   1702             toList.add(attendeeName == null ? new Address(attendeeEmail) :
   1703                 new Address(attendeeEmail, attendeeName));
   1704         } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
   1705             String icalTag = null;
   1706             switch (messageFlag) {
   1707                 case Message.FLAG_OUTGOING_MEETING_ACCEPT:
   1708                     icalTag = ICALENDAR_ATTENDEE_ACCEPT;
   1709                     break;
   1710                 case Message.FLAG_OUTGOING_MEETING_DECLINE:
   1711                     icalTag = ICALENDAR_ATTENDEE_DECLINE;
   1712                     break;
   1713                 case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
   1714                     icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
   1715                     break;
   1716             }
   1717             if (icalTag != null) {
   1718                 if (attendeeName != null) {
   1719                     icalTag += ";CN="
   1720                             + SimpleIcsWriter.quoteParamValue(attendeeName);
   1721                 }
   1722                 ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
   1723             }
   1724         }
   1725     }
   1726 
   1727     /**
   1728      * Create a Message for an (Event) Entity
   1729      * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
   1730      * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
   1731      * @param uid the unique id of this Event, or null if it can be retrieved from the Event
   1732      * @param account the user's account
   1733      * @return a Message with many fields pre-filled (more later)
   1734      */
   1735     static public Message createMessageForEntity(Context context, Entity entity,
   1736             int messageFlag, String uid, Account account) {
   1737         return createMessageForEntity(context, entity, messageFlag, uid, account,
   1738                 null /*specifiedAttendee*/);
   1739     }
   1740 
   1741     static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
   1742             int messageFlag, String uid, Account account, String specifiedAttendee) {
   1743         ContentValues entityValues = entity.getEntityValues();
   1744         ArrayList<NamedContentValues> subValues = entity.getSubValues();
   1745         boolean isException = entityValues.containsKey(Events.ORIGINAL_INSTANCE_TIME);
   1746         boolean isReply = false;
   1747 
   1748         EmailContent.Message msg = new EmailContent.Message();
   1749         msg.mFlags = messageFlag;
   1750         msg.mTimeStamp = System.currentTimeMillis();
   1751 
   1752         String method;
   1753         if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
   1754             method = "REQUEST";
   1755         } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
   1756             method = "CANCEL";
   1757         } else {
   1758             method = "REPLY";
   1759             isReply = true;
   1760         }
   1761 
   1762         try {
   1763             // Create our iCalendar writer and start generating tags
   1764             SimpleIcsWriter ics = new SimpleIcsWriter();
   1765             ics.writeTag("BEGIN", "VCALENDAR");
   1766             ics.writeTag("METHOD", method);
   1767             ics.writeTag("PRODID", "AndroidEmail");
   1768             ics.writeTag("VERSION", "2.0");
   1769 
   1770             // Our default vcalendar time zone is UTC, but this will change (below) if we're
   1771             // sending a recurring event, in which case we use local time
   1772             TimeZone vCalendarTimeZone = sGmtTimeZone;
   1773             String vCalendarDateSuffix = "";
   1774 
   1775             // Check for all day event
   1776             boolean allDayEvent = false;
   1777             if (entityValues.containsKey(Events.ALL_DAY)) {
   1778                 Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
   1779                 allDayEvent = (ade != null) && (ade == 1);
   1780                 if (allDayEvent) {
   1781                     // Example: DTSTART;VALUE=DATE:20100331 (all day event)
   1782                     vCalendarDateSuffix = ";VALUE=DATE";
   1783                 }
   1784             }
   1785 
   1786             // If we're inviting people and the meeting is recurring, we need to send our time zone
   1787             // information and make sure to send DTSTART/DTEND in local time (unless, of course,
   1788             // this is an all-day event).  Recurring, for this purpose, includes exceptions to
   1789             // recurring events
   1790             if (!isReply && !allDayEvent &&
   1791                     (entityValues.containsKey(Events.RRULE) ||
   1792                             entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
   1793                 vCalendarTimeZone = TimeZone.getDefault();
   1794                 // Write the VTIMEZONE block to the writer
   1795                 timeZoneToVTimezone(vCalendarTimeZone, ics);
   1796                 // Example: DTSTART;TZID=US/Pacific:20100331T124500
   1797                 vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
   1798             }
   1799 
   1800             ics.writeTag("BEGIN", "VEVENT");
   1801             if (uid == null) {
   1802                 uid = entityValues.getAsString(Events.SYNC_DATA2);
   1803             }
   1804             if (uid != null) {
   1805                 ics.writeTag("UID", uid);
   1806             }
   1807 
   1808             if (entityValues.containsKey("DTSTAMP")) {
   1809                 ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
   1810             } else {
   1811                 ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
   1812             }
   1813 
   1814             long startTime = entityValues.getAsLong(Events.DTSTART);
   1815             if (startTime != 0) {
   1816                 ics.writeTag("DTSTART" + vCalendarDateSuffix,
   1817                         millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
   1818             }
   1819 
   1820             // If this is an Exception, we send the recurrence-id, which is just the original
   1821             // instance time
   1822             if (isException) {
   1823                 // isException indicates this key is present
   1824                 long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
   1825                 ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
   1826                         millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
   1827             }
   1828 
   1829             if (!entityValues.containsKey(Events.DURATION)) {
   1830                 if (entityValues.containsKey(Events.DTEND)) {
   1831                     ics.writeTag("DTEND" + vCalendarDateSuffix,
   1832                             millisToEasDateTime(
   1833                                     entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
   1834                                     !allDayEvent));
   1835                 }
   1836             } else {
   1837                 // Convert this into millis and add it to DTSTART for DTEND
   1838                 // We'll use 1 hour as a default
   1839                 long durationMillis = HOURS;
   1840                 Duration duration = new Duration();
   1841                 try {
   1842                     duration.parse(entityValues.getAsString(Events.DURATION));
   1843                     durationMillis = duration.getMillis();
   1844                 } catch (DateException e) {
   1845                     // We'll use the default in this case
   1846                 }
   1847                 ics.writeTag("DTEND" + vCalendarDateSuffix,
   1848                         millisToEasDateTime(
   1849                                 startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
   1850             }
   1851 
   1852             String location = null;
   1853             if (entityValues.containsKey(Events.EVENT_LOCATION)) {
   1854                 location = entityValues.getAsString(Events.EVENT_LOCATION);
   1855                 ics.writeTag("LOCATION", location);
   1856             }
   1857 
   1858             String sequence = entityValues.getAsString(SYNC_VERSION);
   1859             if (sequence == null) {
   1860                 sequence = "0";
   1861             }
   1862 
   1863             // We'll use 0 to mean a meeting invitation
   1864             int titleId = 0;
   1865             switch (messageFlag) {
   1866                 case Message.FLAG_OUTGOING_MEETING_INVITE:
   1867                     if (!sequence.equals("0")) {
   1868                         titleId = R.string.meeting_updated;
   1869                     }
   1870                     break;
   1871                 case Message.FLAG_OUTGOING_MEETING_ACCEPT:
   1872                     titleId = R.string.meeting_accepted;
   1873                     break;
   1874                 case Message.FLAG_OUTGOING_MEETING_DECLINE:
   1875                     titleId = R.string.meeting_declined;
   1876                     break;
   1877                 case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
   1878                     titleId = R.string.meeting_tentative;
   1879                     break;
   1880                 case Message.FLAG_OUTGOING_MEETING_CANCEL:
   1881                     titleId = R.string.meeting_canceled;
   1882                     break;
   1883             }
   1884             Resources resources = context.getResources();
   1885             String title = entityValues.getAsString(Events.TITLE);
   1886             if (title == null) {
   1887                 title = "";
   1888             }
   1889             ics.writeTag("SUMMARY", title);
   1890             // For meeting invitations just use the title
   1891             if (titleId == 0) {
   1892                 msg.mSubject = title;
   1893             } else {
   1894                 // Otherwise, use the additional text
   1895                 msg.mSubject = resources.getString(titleId, title);
   1896             }
   1897 
   1898             // Build the text for the message, starting with an initial line describing the
   1899             // exception (if this is one)
   1900             StringBuilder sb = new StringBuilder();
   1901             if (isException && !isReply) {
   1902                 // Add the line, depending on whether this is a cancellation or update
   1903                 // isException indicates this key is present
   1904                 Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
   1905                 String dateString = DateFormat.getDateInstance().format(date);
   1906                 if (titleId == R.string.meeting_canceled) {
   1907                     sb.append(resources.getString(R.string.exception_cancel, dateString));
   1908                 } else {
   1909                     sb.append(resources.getString(R.string.exception_updated, dateString));
   1910                 }
   1911                 sb.append("\n\n");
   1912             }
   1913             String text =
   1914                 CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
   1915 
   1916             if (text.length() > 0) {
   1917                 ics.writeTag("DESCRIPTION", text);
   1918             }
   1919             // And store the message text
   1920             msg.mText = text;
   1921             if (!isReply) {
   1922                 if (entityValues.containsKey(Events.ALL_DAY)) {
   1923                     Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
   1924                     ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
   1925                 }
   1926 
   1927                 String rrule = entityValues.getAsString(Events.RRULE);
   1928                 if (rrule != null) {
   1929                     ics.writeTag("RRULE", rrule);
   1930                 }
   1931 
   1932                 // If we decide to send alarm information in the meeting request ics file,
   1933                 // handle it here by looping through the subvalues
   1934             }
   1935 
   1936             // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
   1937             String organizerName = null;
   1938             String organizerEmail = null;
   1939             ArrayList<Address> toList = new ArrayList<Address>();
   1940             for (NamedContentValues ncv: subValues) {
   1941                 Uri ncvUri = ncv.uri;
   1942                 ContentValues ncvValues = ncv.values;
   1943                 if (ncvUri.equals(Attendees.CONTENT_URI)) {
   1944                     final Integer relationship =
   1945                         ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
   1946                     final String attendeeEmail =
   1947                         ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
   1948                     // If there's no relationship, we can't create this for EAS
   1949                     // Similarly, we need an attendee email for each invitee
   1950                     if (relationship != null && !TextUtils.isEmpty(attendeeEmail)) {
   1951                         // Organizer isn't among attendees in EAS
   1952                         if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
   1953                             organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
   1954                             organizerEmail = attendeeEmail;
   1955                             continue;
   1956                         }
   1957                         String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
   1958 
   1959                         // If we only want to send to the specifiedAttendee, eliminate others here
   1960                         if ((specifiedAttendee != null) &&
   1961                                 !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
   1962                             continue;
   1963                         }
   1964 
   1965                         addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
   1966                                 account);
   1967                     }
   1968                 }
   1969             }
   1970 
   1971             // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
   1972             if (toList.isEmpty() && (specifiedAttendee != null)) {
   1973                 addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
   1974             }
   1975 
   1976             // Create the organizer tag for ical
   1977             if (organizerEmail != null) {
   1978                 String icalTag = "ORGANIZER";
   1979                 // We should be able to find this, assuming the Email is the user's email
   1980                 // TODO Find this in the account
   1981                 if (organizerName != null) {
   1982                     icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
   1983                 }
   1984                 ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
   1985                 if (isReply) {
   1986                     toList.add(organizerName == null ? new Address(organizerEmail) :
   1987                         new Address(organizerEmail, organizerName));
   1988                 }
   1989             }
   1990 
   1991             // If we have no "to" list, we're done
   1992             if (toList.isEmpty()) return null;
   1993 
   1994             // Write out the "to" list
   1995             Address[] toArray = new Address[toList.size()];
   1996             int i = 0;
   1997             for (Address address: toList) {
   1998                 toArray[i++] = address;
   1999             }
   2000             msg.mTo = Address.toHeader(toArray);
   2001 
   2002             ics.writeTag("CLASS", "PUBLIC");
   2003             ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
   2004                     "CANCELLED" : "CONFIRMED");
   2005             ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
   2006             ics.writeTag("PRIORITY", "5");  // 1 to 9, 5 = medium
   2007             ics.writeTag("SEQUENCE", sequence);
   2008             ics.writeTag("END", "VEVENT");
   2009             ics.writeTag("END", "VCALENDAR");
   2010 
   2011             // Create the ics attachment using the "content" field
   2012             Attachment att = new Attachment();
   2013             att.mContentBytes = ics.getBytes();
   2014             att.mMimeType = "text/calendar; method=" + method;
   2015             att.mFileName = "invite.ics";
   2016             att.mSize = att.mContentBytes.length;
   2017             // We don't send content-disposition with this attachment
   2018             att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
   2019 
   2020             // Add the attachment to the message
   2021             msg.mAttachments = new ArrayList<Attachment>();
   2022             msg.mAttachments.add(att);
   2023         } catch (IOException e) {
   2024             LogUtils.w(TAG, "IOException in createMessageForEntity");
   2025             return null;
   2026         }
   2027 
   2028         // Return the new Message to caller
   2029         return msg;
   2030     }
   2031 
   2032     /**
   2033      * Create a Message for an Event that can be retrieved from CalendarProvider
   2034      * by its unique id
   2035      *
   2036      * @param cr a content resolver that can be used to query for the Event
   2037      * @param eventId the unique id of the Event
   2038      * @param messageFlag the Message.FLAG_XXX constant indicating the type of
   2039      *            email to be sent
   2040      * @param the unique id of this Event, or null if it can be retrieved from
   2041      *            the Event
   2042      * @param the user's account
   2043      * @param requireAddressees if true (the default), no Message is returned if
   2044      *            there aren't any addressees; if false, return the Message
   2045      *            regardless (addressees will be filled in later)
   2046      * @return a Message with many fields pre-filled (more later)
   2047      */
   2048     static public EmailContent.Message createMessageForEventId(Context context, long eventId,
   2049             int messageFlag, String uid, Account account) {
   2050         return createMessageForEventId(context, eventId, messageFlag, uid, account,
   2051                 null /* specifiedAttendee */);
   2052     }
   2053 
   2054     static public EmailContent.Message createMessageForEventId(Context context, long eventId,
   2055             int messageFlag, String uid, Account account, String specifiedAttendee) {
   2056         final ContentResolver cr = context.getContentResolver();
   2057         final Cursor cursor = cr.query(ContentUris.withAppendedId(
   2058                         Events.CONTENT_URI, eventId), null, null, null, null);
   2059         if (cursor == null) {
   2060             return null;
   2061         }
   2062         final EntityIterator eventIterator = EventsEntity.newEntityIterator(cursor, cr);
   2063         try {
   2064             while (eventIterator.hasNext()) {
   2065                 Entity entity = eventIterator.next();
   2066                 return createMessageForEntity(context, entity, messageFlag, uid, account,
   2067                         specifiedAttendee);
   2068             }
   2069         } finally {
   2070             eventIterator.close();
   2071         }
   2072         return null;
   2073     }
   2074 
   2075     /**
   2076      * Return a boolean value for an integer ContentValues column
   2077      * @param values a ContentValues object
   2078      * @param columnName the name of a column to be found in the ContentValues
   2079      * @return a boolean representation of the value of columnName in values; null and 0 = false,
   2080      * other integers = true
   2081      */
   2082     static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
   2083         Integer intValue = values.getAsInteger(columnName);
   2084         return (intValue != null && intValue != 0);
   2085     }
   2086 }
   2087