1 package com.android.exchange.adapter; 2 3 import android.content.ContentProviderOperation; 4 import android.content.ContentProviderResult; 5 import android.content.ContentResolver; 6 import android.content.ContentUris; 7 import android.content.ContentValues; 8 import android.content.Context; 9 import android.content.OperationApplicationException; 10 import android.database.Cursor; 11 import android.net.Uri; 12 import android.os.RemoteException; 13 import android.os.TransactionTooLargeException; 14 import android.provider.CalendarContract; 15 import android.provider.CalendarContract.Attendees; 16 import android.provider.CalendarContract.Calendars; 17 import android.provider.CalendarContract.Events; 18 import android.provider.CalendarContract.ExtendedProperties; 19 import android.provider.CalendarContract.Reminders; 20 import android.provider.CalendarContract.SyncState; 21 import android.provider.SyncStateContract; 22 import android.text.format.DateUtils; 23 24 import com.android.emailcommon.provider.Account; 25 import com.android.emailcommon.provider.Mailbox; 26 import com.android.emailcommon.utility.Utility; 27 import com.android.exchange.Eas; 28 import com.android.exchange.adapter.AbstractSyncAdapter.Operation; 29 import com.android.exchange.eas.EasSyncCalendar; 30 import com.android.exchange.utility.CalendarUtilities; 31 import com.android.mail.utils.LogUtils; 32 import com.google.common.annotations.VisibleForTesting; 33 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.text.ParseException; 37 import java.util.ArrayList; 38 import java.util.GregorianCalendar; 39 import java.util.Map.Entry; 40 import java.util.TimeZone; 41 42 public class CalendarSyncParser extends AbstractSyncParser { 43 private static final String TAG = Eas.LOG_TAG; 44 45 private final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); 46 private final TimeZone mLocalTimeZone = TimeZone.getDefault(); 47 48 private final long mCalendarId; 49 private final android.accounts.Account mAccountManagerAccount; 50 private final Uri mAsSyncAdapterAttendees; 51 private final Uri mAsSyncAdapterEvents; 52 53 private final String[] mBindArgument = new String[1]; 54 private final CalendarOperations mOps; 55 56 57 private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1; 58 // Since exceptions will have the same _SYNC_ID as the original event we have to check that 59 // there's no original event when finding an item by _SYNC_ID 60 private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " + 61 Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?"; 62 private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?"; 63 private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " + 64 Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER; 65 private static final String[] ID_PROJECTION = new String[] {Events._ID}; 66 private static final String EVENT_ID_AND_NAME = 67 ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?"; 68 69 private static final String[] EXTENDED_PROPERTY_PROJECTION = 70 new String[] {ExtendedProperties._ID}; 71 private static final int EXTENDED_PROPERTY_ID = 0; 72 73 private static final String CATEGORY_TOKENIZER_DELIMITER = "\\"; 74 private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER; 75 76 private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus"; 77 private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees"; 78 private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp"; 79 private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status"; 80 private static final String EXTENDED_PROPERTY_CATEGORIES = "categories"; 81 // Used to indicate that we removed the attendee list because it was too large 82 private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted"; 83 // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges) 84 private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited"; 85 86 private static final Operation PLACEHOLDER_OPERATION = 87 new Operation(ContentProviderOperation.newInsert(Uri.EMPTY)); 88 89 private static final long SEPARATOR_ID = Long.MAX_VALUE; 90 91 // Maximum number of allowed attendees; above this number, we mark the Event with the 92 // attendeesRedacted extended property and don't allow the event to be upsynced to the server 93 private static final int MAX_SYNCED_ATTENDEES = 50; 94 // We set the organizer to this when the user is the organizer and we've redacted the 95 // attendee list. By making the meeting organizer OTHER than the user, we cause the UI to 96 // prevent edits to this event (except local changes like reminder). 97 private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed (at) uploadisdisallowed.aaa"; 98 // Maximum number of CPO's before we start redacting attendees in exceptions 99 // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before 100 // binder failures occur, but we need room at any point for additional events/exceptions so 101 // we set our limit at 1/3 of the apparent maximum for extra safety 102 // TODO Find a better solution to this workaround 103 private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500; 104 105 public CalendarSyncParser(final Context context, final ContentResolver resolver, 106 final InputStream in, final Mailbox mailbox, final Account account, 107 final android.accounts.Account accountManagerAccount, 108 final long calendarId) throws IOException { 109 super(context, resolver, in, mailbox, account); 110 mAccountManagerAccount = accountManagerAccount; 111 mCalendarId = calendarId; 112 mAsSyncAdapterAttendees = asSyncAdapter(Attendees.CONTENT_URI, 113 mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 114 mAsSyncAdapterEvents = asSyncAdapter(Events.CONTENT_URI, 115 mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 116 mOps = new CalendarOperations(resolver, mAsSyncAdapterAttendees, mAsSyncAdapterEvents, 117 asSyncAdapter(Reminders.CONTENT_URI, mAccount.mEmailAddress, 118 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 119 asSyncAdapter(ExtendedProperties.CONTENT_URI, mAccount.mEmailAddress, 120 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)); 121 } 122 123 protected static class CalendarOperations extends ArrayList<Operation> { 124 private static final long serialVersionUID = 1L; 125 public int mCount = 0; 126 private int mEventStart = 0; 127 private final ContentResolver mContentResolver; 128 private final Uri mAsSyncAdapterAttendees; 129 private final Uri mAsSyncAdapterEvents; 130 private final Uri mAsSyncAdapterReminders; 131 private final Uri mAsSyncAdapterExtendedProperties; 132 133 public CalendarOperations(final ContentResolver contentResolver, 134 final Uri asSyncAdapterAttendees, final Uri asSyncAdapterEvents, 135 final Uri asSyncAdapterReminders, final Uri asSyncAdapterExtendedProperties) { 136 mContentResolver = contentResolver; 137 mAsSyncAdapterAttendees = asSyncAdapterAttendees; 138 mAsSyncAdapterEvents = asSyncAdapterEvents; 139 mAsSyncAdapterReminders = asSyncAdapterReminders; 140 mAsSyncAdapterExtendedProperties = asSyncAdapterExtendedProperties; 141 } 142 143 @Override 144 public boolean add(Operation op) { 145 super.add(op); 146 mCount++; 147 return true; 148 } 149 150 public int newEvent(Operation op) { 151 mEventStart = mCount; 152 add(op); 153 return mEventStart; 154 } 155 156 public int newDelete(long id, String serverId) { 157 int offset = mCount; 158 delete(id, serverId); 159 return offset; 160 } 161 162 public void newAttendee(ContentValues cv) { 163 newAttendee(cv, mEventStart); 164 } 165 166 public void newAttendee(ContentValues cv, int eventStart) { 167 add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees) 168 .withValues(cv), 169 Attendees.EVENT_ID, 170 eventStart)); 171 } 172 173 public void updatedAttendee(ContentValues cv, long id) { 174 cv.put(Attendees.EVENT_ID, id); 175 add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees) 176 .withValues(cv))); 177 } 178 179 public void newException(ContentValues cv) { 180 add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents) 181 .withValues(cv))); 182 } 183 184 public void newExtendedProperty(String name, String value) { 185 add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties) 186 .withValue(ExtendedProperties.NAME, name) 187 .withValue(ExtendedProperties.VALUE, value), 188 ExtendedProperties.EVENT_ID, 189 mEventStart)); 190 } 191 192 public void updatedExtendedProperty(String name, String value, long id) { 193 // Find an existing ExtendedProperties row for this event and property name 194 Cursor c = mContentResolver.query(ExtendedProperties.CONTENT_URI, 195 EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME, 196 new String[] {Long.toString(id), name}, null); 197 long extendedPropertyId = -1; 198 // If there is one, capture its _id 199 if (c != null) { 200 try { 201 if (c.moveToFirst()) { 202 extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID); 203 } 204 } finally { 205 c.close(); 206 } 207 } 208 // Either do an update or an insert, depending on whether one 209 // already exists 210 if (extendedPropertyId >= 0) { 211 add(new Operation(ContentProviderOperation 212 .newUpdate( 213 ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties, 214 extendedPropertyId)) 215 .withValue(ExtendedProperties.VALUE, value))); 216 } else { 217 newExtendedProperty(name, value); 218 } 219 } 220 221 public void newReminder(int mins, int eventStart) { 222 add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders) 223 .withValue(Reminders.MINUTES, mins) 224 .withValue(Reminders.METHOD, Reminders.METHOD_ALERT), 225 ExtendedProperties.EVENT_ID, 226 eventStart)); 227 } 228 229 public void newReminder(int mins) { 230 newReminder(mins, mEventStart); 231 } 232 233 public void delete(long id, String syncId) { 234 add(new Operation(ContentProviderOperation.newDelete( 235 ContentUris.withAppendedId(mAsSyncAdapterEvents, id)))); 236 // Delete the exceptions for this Event (CalendarProvider doesn't do this) 237 add(new Operation(ContentProviderOperation 238 .newDelete(mAsSyncAdapterEvents) 239 .withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId}))); 240 } 241 } 242 243 private static Uri asSyncAdapter(Uri uri, String account, String accountType) { 244 return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 245 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 246 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 247 } 248 249 private static void addOrganizerToAttendees(CalendarOperations ops, long eventId, 250 String organizerName, String organizerEmail) { 251 // Handle the organizer (who IS an attendee on device, but NOT in EAS) 252 if (organizerName != null || organizerEmail != null) { 253 ContentValues attendeeCv = new ContentValues(); 254 if (organizerName != null) { 255 attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName); 256 } 257 if (organizerEmail != null) { 258 attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail); 259 } 260 attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER); 261 attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED); 262 attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED); 263 if (eventId < 0) { 264 ops.newAttendee(attendeeCv); 265 } else { 266 ops.updatedAttendee(attendeeCv, eventId); 267 } 268 } 269 } 270 271 /** 272 * Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event 273 * The follow rules are enforced by CalendarProvider2: 274 * Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION 275 * Recurring events (i.e. events with RRULE) must have a DURATION 276 * All-day recurring events MUST have a DURATION that is in the form P<n>D 277 * Other events MAY have a DURATION in any valid form (we use P<n>M) 278 * All-day events MUST have hour, minute, and second = 0; in addition, they must have 279 * the EVENT_TIMEZONE set to UTC 280 * Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has 281 * hour, minute, and second = 0 and be set in UTC 282 * @param cv the ContentValues for the Event 283 * @param startTime the start time for the Event 284 * @param endTime the end time for the Event 285 * @param allDayEvent whether this is an all day event (1) or not (0) 286 */ 287 /*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime, 288 int allDayEvent) { 289 // If there's no startTime, the event will be found to be invalid, so return 290 if (startTime < 0) return; 291 // EAS events can arrive without an end time, but CalendarProvider requires them 292 // so we'll default to 30 minutes; this will be superceded if this is an all-day event 293 if (endTime < 0) endTime = startTime + (30 * DateUtils.MINUTE_IN_MILLIS); 294 295 // If this is an all-day event, set hour, minute, and second to zero, and use UTC 296 if (allDayEvent != 0) { 297 startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone); 298 endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone); 299 String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE); 300 cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone); 301 cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID()); 302 } 303 304 // If this is an exception, and the original was an all-day event, make sure the 305 // original instance time has hour, minute, and second set to zero, and is in UTC 306 if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) && 307 cv.containsKey(Events.ORIGINAL_ALL_DAY)) { 308 Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY); 309 if (ade != null && ade != 0) { 310 long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 311 final GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE); 312 exceptionTime = CalendarUtilities.getUtcAllDayCalendarTime(exceptionTime, 313 mLocalTimeZone); 314 cal.setTimeInMillis(exceptionTime); 315 cal.set(GregorianCalendar.HOUR_OF_DAY, 0); 316 cal.set(GregorianCalendar.MINUTE, 0); 317 cal.set(GregorianCalendar.SECOND, 0); 318 cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis()); 319 } 320 } 321 322 // Always set DTSTART 323 cv.put(Events.DTSTART, startTime); 324 // For recurring events, set DURATION. Use P<n>D format for all day events 325 if (cv.containsKey(Events.RRULE)) { 326 if (allDayEvent != 0) { 327 cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.DAY_IN_MILLIS) + "D"); 328 } 329 else { 330 cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.MINUTE_IN_MILLIS) + "M"); 331 } 332 // For other events, set DTEND and LAST_DATE 333 } else { 334 cv.put(Events.DTEND, endTime); 335 cv.put(Events.LAST_DATE, endTime); 336 } 337 } 338 339 public void addEvent(CalendarOperations ops, String serverId, boolean update) 340 throws IOException { 341 ContentValues cv = new ContentValues(); 342 cv.put(Events.CALENDAR_ID, mCalendarId); 343 cv.put(Events._SYNC_ID, serverId); 344 cv.put(Events.HAS_ATTENDEE_DATA, 1); 345 cv.put(Events.SYNC_DATA2, "0"); 346 347 int allDayEvent = 0; 348 String organizerName = null; 349 String organizerEmail = null; 350 int eventOffset = -1; 351 int deleteOffset = -1; 352 int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE; 353 int responseType = CalendarUtilities.RESPONSE_TYPE_NONE; 354 355 boolean firstTag = true; 356 long eventId = -1; 357 long startTime = -1; 358 long endTime = -1; 359 TimeZone timeZone = null; 360 361 // Keep track of the attendees; exceptions will need them 362 ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>(); 363 int reminderMins = -1; 364 String dtStamp = null; 365 boolean organizerAdded = false; 366 367 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 368 if (update && firstTag) { 369 // Find the event that's being updated 370 Cursor c = getServerIdCursor(serverId); 371 long id = -1; 372 try { 373 if (c != null && c.moveToFirst()) { 374 id = c.getLong(0); 375 } 376 } finally { 377 if (c != null) c.close(); 378 } 379 if (id > 0) { 380 // DTSTAMP can come first, and we simply need to track it 381 if (tag == Tags.CALENDAR_DTSTAMP) { 382 dtStamp = getValue(); 383 continue; 384 } else if (tag == Tags.CALENDAR_ATTENDEES) { 385 // This is an attendees-only update; just 386 // delete/re-add attendees 387 mBindArgument[0] = Long.toString(id); 388 ops.add(new Operation(ContentProviderOperation 389 .newDelete(mAsSyncAdapterAttendees) 390 .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument))); 391 eventId = id; 392 } else { 393 // Otherwise, delete the original event and recreate it 394 userLog("Changing (delete/add) event ", serverId); 395 deleteOffset = ops.newDelete(id, serverId); 396 // Add a placeholder event so that associated tables can reference 397 // this as a back reference. We add the event at the end of the method 398 eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); 399 } 400 } else { 401 // The changed item isn't found. We'll treat this as a new item 402 eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); 403 userLog(TAG, "Changed item not found; treating as new."); 404 } 405 } else if (firstTag) { 406 // Add a placeholder event so that associated tables can reference 407 // this as a back reference. We add the event at the end of the method 408 eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); 409 } 410 firstTag = false; 411 switch (tag) { 412 case Tags.CALENDAR_ALL_DAY_EVENT: 413 allDayEvent = getValueInt(); 414 if (allDayEvent != 0 && timeZone != null) { 415 // If the event doesn't start at midnight local time, we won't consider 416 // this an all-day event in the local time zone (this is what OWA does) 417 GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone); 418 cal.setTimeInMillis(startTime); 419 userLog("All-day event arrived in: " + timeZone.getID()); 420 if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 || 421 cal.get(GregorianCalendar.MINUTE) != 0) { 422 allDayEvent = 0; 423 userLog("Not an all-day event locally: " + mLocalTimeZone.getID()); 424 } 425 } 426 cv.put(Events.ALL_DAY, allDayEvent); 427 break; 428 case Tags.CALENDAR_ATTACHMENTS: 429 attachmentsParser(); 430 break; 431 case Tags.CALENDAR_ATTENDEES: 432 // If eventId >= 0, this is an update; otherwise, a new Event 433 attendeeValues = attendeesParser(); 434 break; 435 case Tags.BASE_BODY: 436 cv.put(Events.DESCRIPTION, bodyParser()); 437 break; 438 case Tags.CALENDAR_BODY: 439 cv.put(Events.DESCRIPTION, getValue()); 440 break; 441 case Tags.CALENDAR_TIME_ZONE: 442 timeZone = CalendarUtilities.tziStringToTimeZone(getValue()); 443 if (timeZone == null) { 444 timeZone = mLocalTimeZone; 445 } 446 cv.put(Events.EVENT_TIMEZONE, timeZone.getID()); 447 break; 448 case Tags.CALENDAR_START_TIME: 449 try { 450 startTime = Utility.parseDateTimeToMillis(getValue()); 451 } catch (ParseException e) { 452 LogUtils.w(TAG, "Parse error for CALENDAR_START_TIME tag.", e); 453 } 454 break; 455 case Tags.CALENDAR_END_TIME: 456 try { 457 endTime = Utility.parseDateTimeToMillis(getValue()); 458 } catch (ParseException e) { 459 LogUtils.w(TAG, "Parse error for CALENDAR_END_TIME tag.", e); 460 } 461 break; 462 case Tags.CALENDAR_EXCEPTIONS: 463 // For exceptions to show the organizer, the organizer must be added before 464 // we call exceptionsParser 465 addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); 466 organizerAdded = true; 467 exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus, 468 startTime, endTime); 469 break; 470 case Tags.CALENDAR_LOCATION: 471 cv.put(Events.EVENT_LOCATION, getValue()); 472 break; 473 case Tags.CALENDAR_RECURRENCE: 474 String rrule = recurrenceParser(); 475 if (rrule != null) { 476 cv.put(Events.RRULE, rrule); 477 } 478 break; 479 case Tags.CALENDAR_ORGANIZER_EMAIL: 480 organizerEmail = getValue(); 481 cv.put(Events.ORGANIZER, organizerEmail); 482 break; 483 case Tags.CALENDAR_SUBJECT: 484 cv.put(Events.TITLE, getValue()); 485 break; 486 case Tags.CALENDAR_SENSITIVITY: 487 cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); 488 break; 489 case Tags.CALENDAR_ORGANIZER_NAME: 490 organizerName = getValue(); 491 break; 492 case Tags.CALENDAR_REMINDER_MINS_BEFORE: 493 // Save away whether this tag has content; Exchange 2010 sends an empty tag 494 // rather than not sending one (as with Ex07 and Ex03) 495 boolean hasContent = !noContent; 496 reminderMins = getValueInt(); 497 if (hasContent) { 498 ops.newReminder(reminderMins); 499 cv.put(Events.HAS_ALARM, 1); 500 } 501 break; 502 // The following are fields we should save (for changes), though they don't 503 // relate to data used by CalendarProvider at this point 504 case Tags.CALENDAR_UID: 505 cv.put(Events.SYNC_DATA2, getValue()); 506 break; 507 case Tags.CALENDAR_DTSTAMP: 508 dtStamp = getValue(); 509 break; 510 case Tags.CALENDAR_MEETING_STATUS: 511 ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue()); 512 break; 513 case Tags.CALENDAR_BUSY_STATUS: 514 // We'll set the user's status in the Attendees table below 515 // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate 516 // attendee! 517 busyStatus = getValueInt(); 518 break; 519 case Tags.CALENDAR_RESPONSE_TYPE: 520 // EAS 14+ uses this for the user's response status; we'll use this instead 521 // of busy status, if it appears 522 responseType = getValueInt(); 523 break; 524 case Tags.CALENDAR_CATEGORIES: 525 String categories = categoriesParser(); 526 if (categories.length() > 0) { 527 ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories); 528 } 529 break; 530 default: 531 skipTag(); 532 } 533 } 534 535 // Enforce CalendarProvider required properties 536 setTimeRelatedValues(cv, startTime, endTime, allDayEvent); 537 538 // Set user's availability 539 cv.put(Events.AVAILABILITY, CalendarUtilities.availabilityFromBusyStatus(busyStatus)); 540 541 // If we haven't added the organizer to attendees, do it now 542 if (!organizerAdded) { 543 addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); 544 } 545 546 // Note that organizerEmail can be null with a DTSTAMP only change from the server 547 boolean selfOrganizer = (mAccount.mEmailAddress.equals(organizerEmail)); 548 549 // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties 550 // If the user is an attendee, set the attendee status using busyStatus (note that the 551 // busyStatus is inherited from the parent unless it's specified in the exception) 552 // Add the insert/update operation for each attendee (based on whether it's add/change) 553 int numAttendees = attendeeValues.size(); 554 if (numAttendees > MAX_SYNCED_ATTENDEES) { 555 // Indicate that we've redacted attendees. If we're the organizer, disable edit 556 // by setting organizerEmail to a bogus value and by setting the upsync prohibited 557 // extended properly. 558 // Note that we don't set ANY attendees if we're in this branch; however, the 559 // organizer has already been included above, and WILL show up (which is good) 560 if (eventId < 0) { 561 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1"); 562 if (selfOrganizer) { 563 ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1"); 564 } 565 } else { 566 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId); 567 if (selfOrganizer) { 568 ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1", 569 eventId); 570 } 571 } 572 if (selfOrganizer) { 573 organizerEmail = BOGUS_ORGANIZER_EMAIL; 574 cv.put(Events.ORGANIZER, organizerEmail); 575 } 576 // Tell UI that we don't have any attendees 577 cv.put(Events.HAS_ATTENDEE_DATA, "0"); 578 LogUtils.d(TAG, "Maximum number of attendees exceeded; redacting"); 579 } else if (numAttendees > 0) { 580 StringBuilder sb = new StringBuilder(); 581 for (ContentValues attendee: attendeeValues) { 582 String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL); 583 sb.append(attendeeEmail); 584 sb.append(ATTENDEE_TOKENIZER_DELIMITER); 585 if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { 586 int attendeeStatus; 587 // We'll use the response type (EAS 14), if we've got one; otherwise, we'll 588 // try to infer it from busy status 589 if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) { 590 attendeeStatus = 591 CalendarUtilities.attendeeStatusFromResponseType(responseType); 592 } else if (!update) { 593 // For new events in EAS < 14, we have no idea what the busy status 594 // means, so we show "none", allowing the user to select an option. 595 attendeeStatus = Attendees.ATTENDEE_STATUS_NONE; 596 } else { 597 // For updated events, we'll try to infer the attendee status from the 598 // busy status 599 attendeeStatus = 600 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus); 601 } 602 attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus); 603 // If we're an attendee, save away our initial attendee status in the 604 // event's ExtendedProperties (we look for differences between this and 605 // the user's current attendee status to determine whether an email needs 606 // to be sent to the organizer) 607 // organizerEmail will be null in the case that this is an attendees-only 608 // change from the server 609 if (organizerEmail == null || 610 !organizerEmail.equalsIgnoreCase(attendeeEmail)) { 611 if (eventId < 0) { 612 ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, 613 Integer.toString(attendeeStatus)); 614 } else { 615 ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, 616 Integer.toString(attendeeStatus), eventId); 617 618 } 619 } 620 } 621 if (eventId < 0) { 622 ops.newAttendee(attendee); 623 } else { 624 ops.updatedAttendee(attendee, eventId); 625 } 626 } 627 if (eventId < 0) { 628 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString()); 629 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0"); 630 ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0"); 631 } else { 632 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(), 633 eventId); 634 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId); 635 ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId); 636 } 637 } 638 639 // Put the real event in the proper place in the ops ArrayList 640 if (eventOffset >= 0) { 641 // Store away the DTSTAMP here 642 if (dtStamp != null) { 643 ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp); 644 } 645 646 if (isValidEventValues(cv)) { 647 ops.set(eventOffset, 648 new Operation(ContentProviderOperation 649 .newInsert(mAsSyncAdapterEvents).withValues(cv))); 650 } else { 651 // If we can't add this event (it's invalid), remove all of the inserts 652 // we've built for it 653 int cnt = ops.mCount - eventOffset; 654 userLog(TAG, "Removing " + cnt + " inserts from mOps"); 655 for (int i = 0; i < cnt; i++) { 656 ops.remove(eventOffset); 657 } 658 ops.mCount = eventOffset; 659 // If this is a change, we need to also remove the deletion that comes 660 // before the addition 661 if (deleteOffset >= 0) { 662 // Remove the deletion 663 ops.remove(deleteOffset); 664 // And the deletion of exceptions 665 ops.remove(deleteOffset); 666 userLog(TAG, "Removing deletion ops from mOps"); 667 ops.mCount = deleteOffset; 668 } 669 } 670 } 671 // Mark the end of the event 672 addSeparatorOperation(ops, Events.CONTENT_URI); 673 } 674 675 private void logEventColumns(ContentValues cv, String reason) { 676 if (Eas.USER_LOG) { 677 StringBuilder sb = 678 new StringBuilder("Event invalid, " + reason + ", skipping: Columns = "); 679 for (Entry<String, Object> entry: cv.valueSet()) { 680 sb.append(entry.getKey()); 681 sb.append('/'); 682 } 683 userLog(TAG, sb.toString()); 684 } 685 } 686 687 /*package*/ boolean isValidEventValues(ContentValues cv) { 688 boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME); 689 // All events require DTSTART 690 if (!cv.containsKey(Events.DTSTART)) { 691 logEventColumns(cv, "DTSTART missing"); 692 return false; 693 // If we're a top-level event, we must have _SYNC_DATA (uid) 694 } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) { 695 logEventColumns(cv, "_SYNC_DATA missing"); 696 return false; 697 // We must also have DTEND or DURATION if we're not an exception 698 } else if (!isException && !cv.containsKey(Events.DTEND) && 699 !cv.containsKey(Events.DURATION)) { 700 logEventColumns(cv, "DTEND/DURATION missing"); 701 return false; 702 // Exceptions require DTEND 703 } else if (isException && !cv.containsKey(Events.DTEND)) { 704 logEventColumns(cv, "Exception missing DTEND"); 705 return false; 706 // If this is a recurrence, we need a DURATION (in days if an all-day event) 707 } else if (cv.containsKey(Events.RRULE)) { 708 String duration = cv.getAsString(Events.DURATION); 709 if (duration == null) return false; 710 if (cv.containsKey(Events.ALL_DAY)) { 711 Integer ade = cv.getAsInteger(Events.ALL_DAY); 712 if (ade != null && ade != 0 && !duration.endsWith("D")) { 713 return false; 714 } 715 } 716 } 717 return true; 718 } 719 720 public String recurrenceParser() throws IOException { 721 // Turn this information into an RRULE 722 int type = -1; 723 int occurrences = -1; 724 int interval = -1; 725 int dow = -1; 726 int dom = -1; 727 int wom = -1; 728 int moy = -1; 729 String until = null; 730 731 while (nextTag(Tags.CALENDAR_RECURRENCE) != END) { 732 switch (tag) { 733 case Tags.CALENDAR_RECURRENCE_TYPE: 734 type = getValueInt(); 735 break; 736 case Tags.CALENDAR_RECURRENCE_INTERVAL: 737 interval = getValueInt(); 738 break; 739 case Tags.CALENDAR_RECURRENCE_OCCURRENCES: 740 occurrences = getValueInt(); 741 break; 742 case Tags.CALENDAR_RECURRENCE_DAYOFWEEK: 743 dow = getValueInt(); 744 break; 745 case Tags.CALENDAR_RECURRENCE_DAYOFMONTH: 746 dom = getValueInt(); 747 break; 748 case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH: 749 wom = getValueInt(); 750 break; 751 case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR: 752 moy = getValueInt(); 753 break; 754 case Tags.CALENDAR_RECURRENCE_UNTIL: 755 until = getValue(); 756 break; 757 default: 758 skipTag(); 759 } 760 } 761 762 return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval, 763 dow, dom, wom, moy, until); 764 } 765 766 private void exceptionParser(CalendarOperations ops, ContentValues parentCv, 767 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, 768 long startTime, long endTime) throws IOException { 769 ContentValues cv = new ContentValues(); 770 cv.put(Events.CALENDAR_ID, mCalendarId); 771 772 // It appears that these values have to be copied from the parent if they are to appear 773 // Note that they can be overridden below 774 cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER)); 775 cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE)); 776 cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION)); 777 cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY)); 778 cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION)); 779 cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL)); 780 cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE)); 781 // Exceptions should always have this set to zero, since EAS has no concept of 782 // separate attendee lists for exceptions; if we fail to do this, then the UI will 783 // allow the user to change attendee data, and this change would never get reflected 784 // on the server. 785 cv.put(Events.HAS_ATTENDEE_DATA, 0); 786 787 int allDayEvent = 0; 788 789 // This column is the key that links the exception to the serverId 790 cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID)); 791 792 String exceptionStartTime = "_noStartTime"; 793 while (nextTag(Tags.CALENDAR_EXCEPTION) != END) { 794 switch (tag) { 795 case Tags.CALENDAR_ATTACHMENTS: 796 attachmentsParser(); 797 break; 798 case Tags.CALENDAR_EXCEPTION_START_TIME: 799 final String valueStr = getValue(); 800 try { 801 cv.put(Events.ORIGINAL_INSTANCE_TIME, 802 Utility.parseDateTimeToMillis(valueStr)); 803 exceptionStartTime = valueStr; 804 } catch (ParseException e) { 805 LogUtils.w(TAG, "Parse error for CALENDAR_EXCEPTION_START_TIME tag.", e); 806 } 807 break; 808 case Tags.CALENDAR_EXCEPTION_IS_DELETED: 809 if (getValueInt() == 1) { 810 cv.put(Events.STATUS, Events.STATUS_CANCELED); 811 } 812 break; 813 case Tags.CALENDAR_ALL_DAY_EVENT: 814 allDayEvent = getValueInt(); 815 cv.put(Events.ALL_DAY, allDayEvent); 816 break; 817 case Tags.BASE_BODY: 818 cv.put(Events.DESCRIPTION, bodyParser()); 819 break; 820 case Tags.CALENDAR_BODY: 821 cv.put(Events.DESCRIPTION, getValue()); 822 break; 823 case Tags.CALENDAR_START_TIME: 824 try { 825 startTime = Utility.parseDateTimeToMillis(getValue()); 826 } catch (ParseException e) { 827 LogUtils.w(TAG, "Parse error for CALENDAR_START_TIME tag.", e); 828 } 829 break; 830 case Tags.CALENDAR_END_TIME: 831 try { 832 endTime = Utility.parseDateTimeToMillis(getValue()); 833 } catch (ParseException e) { 834 LogUtils.w(TAG, "Parse error for CALENDAR_END_TIME tag.", e); 835 } 836 break; 837 case Tags.CALENDAR_LOCATION: 838 cv.put(Events.EVENT_LOCATION, getValue()); 839 break; 840 case Tags.CALENDAR_RECURRENCE: 841 String rrule = recurrenceParser(); 842 if (rrule != null) { 843 cv.put(Events.RRULE, rrule); 844 } 845 break; 846 case Tags.CALENDAR_SUBJECT: 847 cv.put(Events.TITLE, getValue()); 848 break; 849 case Tags.CALENDAR_SENSITIVITY: 850 cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); 851 break; 852 case Tags.CALENDAR_BUSY_STATUS: 853 busyStatus = getValueInt(); 854 // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate 855 // attendee! 856 break; 857 // TODO How to handle these items that are linked to event id! 858 // case Tags.CALENDAR_DTSTAMP: 859 // ops.newExtendedProperty("dtstamp", getValue()); 860 // break; 861 // case Tags.CALENDAR_REMINDER_MINS_BEFORE: 862 // ops.newReminder(getValueInt()); 863 // break; 864 default: 865 skipTag(); 866 } 867 } 868 869 // We need a _sync_id, but it can't be the parent's id, so we generate one 870 cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' + 871 exceptionStartTime); 872 873 // Enforce CalendarProvider required properties 874 setTimeRelatedValues(cv, startTime, endTime, allDayEvent); 875 876 // Don't insert an invalid exception event 877 if (!isValidEventValues(cv)) return; 878 879 // Add the exception insert 880 int exceptionStart = ops.mCount; 881 ops.newException(cv); 882 // Also add the attendees, because they need to be copied over from the parent event 883 boolean attendeesRedacted = false; 884 if (attendeeValues != null) { 885 for (ContentValues attValues: attendeeValues) { 886 // If this is the user, use his busy status for attendee status 887 String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL); 888 // Note that the exception at which we surpass the redaction limit might have 889 // any number of attendees shown; since this is an edge case and a workaround, 890 // it seems to be an acceptable implementation 891 if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { 892 attValues.put(Attendees.ATTENDEE_STATUS, 893 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus)); 894 ops.newAttendee(attValues, exceptionStart); 895 } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) { 896 ops.newAttendee(attValues, exceptionStart); 897 } else { 898 attendeesRedacted = true; 899 } 900 } 901 } 902 // And add the parent's reminder value 903 if (reminderMins > 0) { 904 ops.newReminder(reminderMins, exceptionStart); 905 } 906 if (attendeesRedacted) { 907 LogUtils.d(TAG, "Attendees redacted in this exception"); 908 } 909 } 910 911 private static int encodeVisibility(int easVisibility) { 912 int visibility = 0; 913 switch(easVisibility) { 914 case 0: 915 visibility = Events.ACCESS_DEFAULT; 916 break; 917 case 1: 918 visibility = Events.ACCESS_PUBLIC; 919 break; 920 case 2: 921 visibility = Events.ACCESS_PRIVATE; 922 break; 923 case 3: 924 visibility = Events.ACCESS_CONFIDENTIAL; 925 break; 926 } 927 return visibility; 928 } 929 930 private void exceptionsParser(CalendarOperations ops, ContentValues cv, 931 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, 932 long startTime, long endTime) throws IOException { 933 while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) { 934 switch (tag) { 935 case Tags.CALENDAR_EXCEPTION: 936 exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus, 937 startTime, endTime); 938 break; 939 default: 940 skipTag(); 941 } 942 } 943 } 944 945 private String categoriesParser() throws IOException { 946 StringBuilder categories = new StringBuilder(); 947 while (nextTag(Tags.CALENDAR_CATEGORIES) != END) { 948 switch (tag) { 949 case Tags.CALENDAR_CATEGORY: 950 // TODO Handle categories (there's no similar concept for gdata AFAIK) 951 // We need to save them and spit them back when we update the event 952 categories.append(getValue()); 953 categories.append(CATEGORY_TOKENIZER_DELIMITER); 954 break; 955 default: 956 skipTag(); 957 } 958 } 959 return categories.toString(); 960 } 961 962 /** 963 * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14 964 */ 965 private void attachmentsParser() throws IOException { 966 while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) { 967 switch (tag) { 968 case Tags.CALENDAR_ATTACHMENT: 969 skipParser(Tags.CALENDAR_ATTACHMENT); 970 break; 971 default: 972 skipTag(); 973 } 974 } 975 } 976 977 private ArrayList<ContentValues> attendeesParser() 978 throws IOException { 979 int attendeeCount = 0; 980 ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>(); 981 while (nextTag(Tags.CALENDAR_ATTENDEES) != END) { 982 switch (tag) { 983 case Tags.CALENDAR_ATTENDEE: 984 ContentValues cv = attendeeParser(); 985 // If we're going to redact these attendees anyway, let's avoid unnecessary 986 // memory pressure, and not keep them around 987 // We still need to parse them all, however 988 attendeeCount++; 989 // Allow one more than MAX_ATTENDEES, so that the check for "too many" will 990 // succeed in addEvent 991 if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) { 992 attendeeValues.add(cv); 993 } 994 break; 995 default: 996 skipTag(); 997 } 998 } 999 return attendeeValues; 1000 } 1001 1002 private ContentValues attendeeParser() 1003 throws IOException { 1004 ContentValues cv = new ContentValues(); 1005 while (nextTag(Tags.CALENDAR_ATTENDEE) != END) { 1006 switch (tag) { 1007 case Tags.CALENDAR_ATTENDEE_EMAIL: 1008 cv.put(Attendees.ATTENDEE_EMAIL, getValue()); 1009 break; 1010 case Tags.CALENDAR_ATTENDEE_NAME: 1011 cv.put(Attendees.ATTENDEE_NAME, getValue()); 1012 break; 1013 case Tags.CALENDAR_ATTENDEE_STATUS: 1014 int status = getValueInt(); 1015 cv.put(Attendees.ATTENDEE_STATUS, 1016 (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE : 1017 (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED : 1018 (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED : 1019 (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED : 1020 Attendees.ATTENDEE_STATUS_NONE); 1021 break; 1022 case Tags.CALENDAR_ATTENDEE_TYPE: 1023 int type = Attendees.TYPE_NONE; 1024 // EAS types: 1 = req'd, 2 = opt, 3 = resource 1025 switch (getValueInt()) { 1026 case 1: 1027 type = Attendees.TYPE_REQUIRED; 1028 break; 1029 case 2: 1030 type = Attendees.TYPE_OPTIONAL; 1031 break; 1032 } 1033 cv.put(Attendees.ATTENDEE_TYPE, type); 1034 break; 1035 default: 1036 skipTag(); 1037 } 1038 } 1039 cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE); 1040 return cv; 1041 } 1042 1043 private String bodyParser() throws IOException { 1044 String body = null; 1045 while (nextTag(Tags.BASE_BODY) != END) { 1046 switch (tag) { 1047 case Tags.BASE_DATA: 1048 body = getValue(); 1049 break; 1050 default: 1051 skipTag(); 1052 } 1053 } 1054 1055 // Handle null data without error 1056 if (body == null) return ""; 1057 // Remove \r's from any body text 1058 return body.replace("\r\n", "\n"); 1059 } 1060 1061 public void addParser(CalendarOperations ops) throws IOException { 1062 String serverId = null; 1063 while (nextTag(Tags.SYNC_ADD) != END) { 1064 switch (tag) { 1065 case Tags.SYNC_SERVER_ID: // same as 1066 serverId = getValue(); 1067 break; 1068 case Tags.SYNC_APPLICATION_DATA: 1069 addEvent(ops, serverId, false); 1070 break; 1071 default: 1072 skipTag(); 1073 } 1074 } 1075 } 1076 1077 private Cursor getServerIdCursor(String serverId) { 1078 return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, 1079 SERVER_ID_AND_CALENDAR_ID, new String[] {serverId, Long.toString(mCalendarId)}, 1080 null); 1081 } 1082 1083 private Cursor getClientIdCursor(String clientId) { 1084 mBindArgument[0] = clientId; 1085 return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, CLIENT_ID_SELECTION, 1086 mBindArgument, null); 1087 } 1088 1089 public void deleteParser(CalendarOperations ops) throws IOException { 1090 while (nextTag(Tags.SYNC_DELETE) != END) { 1091 switch (tag) { 1092 case Tags.SYNC_SERVER_ID: 1093 String serverId = getValue(); 1094 // Find the event with the given serverId 1095 Cursor c = getServerIdCursor(serverId); 1096 try { 1097 if (c.moveToFirst()) { 1098 userLog("Deleting ", serverId); 1099 ops.delete(c.getLong(0), serverId); 1100 } 1101 } finally { 1102 c.close(); 1103 } 1104 break; 1105 default: 1106 skipTag(); 1107 } 1108 } 1109 } 1110 1111 /** 1112 * A change is handled as a delete (including all exceptions) and an add 1113 * This isn't as efficient as attempting to traverse the original and all of its exceptions, 1114 * but changes happen infrequently and this code is both simpler and easier to maintain 1115 * @param ops the array of pending ContactProviderOperations. 1116 * @throws IOException 1117 */ 1118 public void changeParser(CalendarOperations ops) throws IOException { 1119 String serverId = null; 1120 while (nextTag(Tags.SYNC_CHANGE) != END) { 1121 switch (tag) { 1122 case Tags.SYNC_SERVER_ID: 1123 serverId = getValue(); 1124 break; 1125 case Tags.SYNC_APPLICATION_DATA: 1126 userLog("Changing " + serverId); 1127 addEvent(ops, serverId, true); 1128 break; 1129 default: 1130 skipTag(); 1131 } 1132 } 1133 } 1134 1135 @Override 1136 public void commandsParser() throws IOException { 1137 while (nextTag(Tags.SYNC_COMMANDS) != END) { 1138 if (tag == Tags.SYNC_ADD) { 1139 addParser(mOps); 1140 } else if (tag == Tags.SYNC_DELETE) { 1141 deleteParser(mOps); 1142 } else if (tag == Tags.SYNC_CHANGE) { 1143 changeParser(mOps); 1144 } else 1145 skipTag(); 1146 } 1147 } 1148 1149 @Override 1150 public void commit() throws IOException { 1151 userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey); 1152 // Save the syncKey here, using the Helper provider by Calendar provider 1153 mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation( 1154 asSyncAdapter(SyncState.CONTENT_URI, mAccount.mEmailAddress, 1155 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 1156 mAccountManagerAccount, 1157 mMailbox.mSyncKey.getBytes()))); 1158 1159 // Execute our CPO's safely 1160 try { 1161 safeExecute(mContentResolver, CalendarContract.AUTHORITY, mOps); 1162 } catch (RemoteException e) { 1163 throw new IOException("Remote exception caught; will retry"); 1164 } 1165 } 1166 1167 public void addResponsesParser() throws IOException { 1168 String serverId = null; 1169 String clientId = null; 1170 int status = -1; 1171 ContentValues cv = new ContentValues(); 1172 while (nextTag(Tags.SYNC_ADD) != END) { 1173 switch (tag) { 1174 case Tags.SYNC_SERVER_ID: 1175 serverId = getValue(); 1176 break; 1177 case Tags.SYNC_CLIENT_ID: 1178 clientId = getValue(); 1179 break; 1180 case Tags.SYNC_STATUS: 1181 status = getValueInt(); 1182 if (status != 1) { 1183 userLog("Attempt to add event failed with status: " + status); 1184 } 1185 break; 1186 default: 1187 skipTag(); 1188 } 1189 } 1190 1191 if (clientId == null) return; 1192 if (serverId == null) { 1193 // TODO Reconsider how to handle this 1194 serverId = "FAIL:" + status; 1195 } 1196 1197 Cursor c = getClientIdCursor(clientId); 1198 try { 1199 if (c.moveToFirst()) { 1200 cv.put(Events._SYNC_ID, serverId); 1201 cv.put(Events.SYNC_DATA2, clientId); 1202 long id = c.getLong(0); 1203 // Write the serverId into the Event 1204 mOps.add(new Operation(ContentProviderOperation 1205 .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id)) 1206 .withValues(cv))); 1207 userLog("New event " + clientId + " was given serverId: " + serverId); 1208 } 1209 } finally { 1210 c.close(); 1211 } 1212 } 1213 1214 public void changeResponsesParser() throws IOException { 1215 String serverId = null; 1216 String status = null; 1217 while (nextTag(Tags.SYNC_CHANGE) != END) { 1218 switch (tag) { 1219 case Tags.SYNC_SERVER_ID: 1220 serverId = getValue(); 1221 break; 1222 case Tags.SYNC_STATUS: 1223 status = getValue(); 1224 break; 1225 default: 1226 skipTag(); 1227 } 1228 } 1229 if (serverId != null && status != null) { 1230 userLog("Changed event " + serverId + " failed with status: " + status); 1231 } 1232 } 1233 1234 1235 @Override 1236 public void responsesParser() throws IOException { 1237 // Handle server responses here (for Add and Change) 1238 while (nextTag(Tags.SYNC_RESPONSES) != END) { 1239 if (tag == Tags.SYNC_ADD) { 1240 addResponsesParser(); 1241 } else if (tag == Tags.SYNC_CHANGE) { 1242 changeResponsesParser(); 1243 } else 1244 skipTag(); 1245 } 1246 } 1247 1248 /** 1249 * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties, 1250 * and we just return quickly if the service has already been stopped. 1251 */ 1252 private static ContentProviderResult[] execute(final ContentResolver contentResolver, 1253 final String authority, final ArrayList<ContentProviderOperation> ops) 1254 throws RemoteException, OperationApplicationException { 1255 if (!ops.isEmpty()) { 1256 try { 1257 ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); 1258 //mService.userLog("Results: " + result.length); 1259 return result; 1260 } catch (IllegalArgumentException e) { 1261 // Thrown when Calendar Provider is disabled 1262 LogUtils.e(TAG, "Error executing operation; provider is disabled.", e); 1263 } 1264 } 1265 return new ContentProviderResult[0]; 1266 } 1267 1268 /** 1269 * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the 1270 * passed-in offset 1271 */ 1272 @VisibleForTesting 1273 static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) { 1274 if (op.mOp != null) { 1275 return op.mOp; 1276 } else if (op.mBuilder == null) { 1277 throw new IllegalArgumentException("Operation must have CPO.Builder"); 1278 } 1279 ContentProviderOperation.Builder builder = op.mBuilder; 1280 if (op.mColumnName != null) { 1281 builder.withValueBackReference(op.mColumnName, op.mOffset - offset); 1282 } 1283 return builder.build(); 1284 } 1285 1286 /** 1287 * Create a list of CPOs from a list of Operations, and then apply them in a batch 1288 */ 1289 private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, 1290 final String authority, final ArrayList<Operation> ops, final int offset) 1291 throws RemoteException, OperationApplicationException { 1292 // Handle the empty case 1293 if (ops.isEmpty()) { 1294 return new ContentProviderResult[0]; 1295 } 1296 ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>(); 1297 for (Operation op: ops) { 1298 cpos.add(operationToContentProviderOperation(op, offset)); 1299 } 1300 return execute(contentResolver, authority, cpos); 1301 } 1302 1303 /** 1304 * Apply the list of CPO's in the provider and copy the "mini" result into our full result array 1305 */ 1306 private static void applyAndCopyResults(final ContentResolver contentResolver, 1307 final String authority, final ArrayList<Operation> mini, 1308 final ContentProviderResult[] result, final int offset) throws RemoteException { 1309 // Empty lists are ok; we just ignore them 1310 if (mini.isEmpty()) return; 1311 try { 1312 ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini, 1313 offset); 1314 // Copy the results from this mini-batch into our results array 1315 System.arraycopy(miniResult, 0, result, offset, miniResult.length); 1316 } catch (OperationApplicationException e) { 1317 // Not possible since we're building the ops ourselves 1318 } 1319 } 1320 1321 /** 1322 * Called by a sync adapter to execute a list of Operations in the ContentProvider handling 1323 * the passed-in authority. If the attempt to apply the batch fails due to a too-large 1324 * binder transaction, we split the Operations as directed by separators. If any of the 1325 * "mini" batches fails due to a too-large transaction, we're screwed, but this would be 1326 * vanishingly rare. Other, possibly transient, errors are handled by throwing a 1327 * RemoteException, which the caller will likely re-throw as an IOException so that the sync 1328 * can be attempted again. 1329 * 1330 * Callers MAY leave a dangling separator at the end of the list; note that the separators 1331 * themselves are only markers and are not sent to the provider. 1332 */ 1333 protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, 1334 final String authority, final ArrayList<Operation> ops) throws RemoteException { 1335 //mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority); 1336 ContentProviderResult[] result = null; 1337 try { 1338 // Try to execute the whole thing 1339 return applyBatch(contentResolver, authority, ops, 0); 1340 } catch (TransactionTooLargeException e) { 1341 // Nope; split into smaller chunks, demarcated by the separator operation 1342 //mService.userLog("Transaction too large; spliting!"); 1343 ArrayList<Operation> mini = new ArrayList<Operation>(); 1344 // Build a result array with the total size we're sending 1345 result = new ContentProviderResult[ops.size()]; 1346 int count = 0; 1347 int offset = 0; 1348 for (Operation op: ops) { 1349 if (op.mSeparator) { 1350 //mService.userLog("Try mini-batch of ", mini.size(), " CPO's"); 1351 applyAndCopyResults(contentResolver, authority, mini, result, offset); 1352 mini.clear(); 1353 // Save away the offset here; this will need to be subtracted out of the 1354 // value originally set by the adapter 1355 offset = count + 1; // Remember to add 1 for the separator! 1356 } else { 1357 mini.add(op); 1358 } 1359 count++; 1360 } 1361 // Check out what's left; if it's more than just a separator, apply the batch 1362 int miniSize = mini.size(); 1363 if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) { 1364 applyAndCopyResults(contentResolver, authority, mini, result, offset); 1365 } 1366 } catch (RemoteException e) { 1367 throw e; 1368 } catch (OperationApplicationException e) { 1369 // Not possible since we're building the ops ourselves 1370 } 1371 return result; 1372 } 1373 1374 /** 1375 * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's 1376 */ 1377 protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { 1378 Operation op = new Operation( 1379 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); 1380 op.mSeparator = true; 1381 ops.add(op); 1382 } 1383 1384 @Override 1385 protected void wipe() { 1386 LogUtils.w(TAG, "Wiping calendar for account %d", mAccount.mId); 1387 EasSyncCalendar.wipeAccountFromContentProvider(mContext, 1388 mAccount.mEmailAddress); 1389 } 1390 } 1391