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