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