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