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