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