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