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