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