CalendarSyncParser.java revision de6b8b15730f59f3178e4a4223ddf00c6acd16c2
1package com.android.exchange.adapter; 2 3import android.content.ContentProviderOperation; 4import android.content.ContentProviderResult; 5import android.content.ContentResolver; 6import android.content.ContentUris; 7import android.content.ContentValues; 8import android.content.Context; 9import android.content.OperationApplicationException; 10import android.database.Cursor; 11import android.net.Uri; 12import android.os.RemoteException; 13import android.os.TransactionTooLargeException; 14import android.provider.CalendarContract; 15import android.provider.SyncStateContract; 16import android.provider.CalendarContract.Attendees; 17import android.provider.CalendarContract.Calendars; 18import android.provider.CalendarContract.Events; 19import android.provider.CalendarContract.ExtendedProperties; 20import android.provider.CalendarContract.Reminders; 21import android.provider.CalendarContract.SyncState; 22import android.text.format.DateUtils; 23 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.provider.Account; 26import com.android.emailcommon.provider.Mailbox; 27import com.android.emailcommon.utility.Utility; 28import com.android.exchange.Eas; 29import com.android.exchange.adapter.AbstractSyncAdapter.Operation; 30import com.android.exchange.utility.CalendarUtilities; 31import com.android.mail.utils.LogUtils; 32import com.google.common.annotations.VisibleForTesting; 33 34import java.io.IOException; 35import java.io.InputStream; 36import java.util.ArrayList; 37import java.util.GregorianCalendar; 38import java.util.TimeZone; 39import java.util.Map.Entry; 40 41public class CalendarSyncParser extends AbstractSyncParser { 42 private static final String TAG = Logging.LOG_TAG; 43 44 private final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); 45 private final TimeZone mLocalTimeZone = TimeZone.getDefault(); 46 47 private final long mCalendarId; 48 private final android.accounts.Account mAccountManagerAccount; 49 private final Uri mAsSyncAdapterAttendees; 50 private final Uri mAsSyncAdapterEvents; 51 52 private final String[] mBindArgument = new String[1]; 53 private final CalendarOperations mOps; 54 55 56 private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1; 57 // Since exceptions will have the same _SYNC_ID as the original event we have to check that 58 // there's no original event when finding an item by _SYNC_ID 59 private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " + 60 Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?"; 61 private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?"; 62 private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " + 63 Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER; 64 private static final String[] ID_PROJECTION = new String[] {Events._ID}; 65 private static final String EVENT_ID_AND_NAME = 66 ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?"; 67 68 private static final String[] EXTENDED_PROPERTY_PROJECTION = 69 new String[] {ExtendedProperties._ID}; 70 private static final int EXTENDED_PROPERTY_ID = 0; 71 72 private static final String CATEGORY_TOKENIZER_DELIMITER = "\\"; 73 private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER; 74 75 private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus"; 76 private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees"; 77 private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp"; 78 private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status"; 79 private static final String EXTENDED_PROPERTY_CATEGORIES = "categories"; 80 // Used to indicate that we removed the attendee list because it was too large 81 private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted"; 82 // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges) 83 private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited"; 84 85 private static final Operation PLACEHOLDER_OPERATION = 86 new Operation(ContentProviderOperation.newInsert(Uri.EMPTY)); 87 88 private static final long SEPARATOR_ID = Long.MAX_VALUE; 89 90 // Maximum number of allowed attendees; above this number, we mark the Event with the 91 // attendeesRedacted extended property and don't allow the event to be upsynced to the server 92 private static final int MAX_SYNCED_ATTENDEES = 50; 93 // We set the organizer to this when the user is the organizer and we've redacted the 94 // attendee list. By making the meeting organizer OTHER than the user, we cause the UI to 95 // prevent edits to this event (except local changes like reminder). 96 private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa"; 97 // Maximum number of CPO's before we start redacting attendees in exceptions 98 // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before 99 // binder failures occur, but we need room at any point for additional events/exceptions so 100 // we set our limit at 1/3 of the apparent maximum for extra safety 101 // TODO Find a better solution to this workaround 102 private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500; 103 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 startTime = Utility.parseDateTimeToMillis(getValue()); 450 break; 451 case Tags.CALENDAR_END_TIME: 452 endTime = Utility.parseDateTimeToMillis(getValue()); 453 break; 454 case Tags.CALENDAR_EXCEPTIONS: 455 // For exceptions to show the organizer, the organizer must be added before 456 // we call exceptionsParser 457 addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); 458 organizerAdded = true; 459 exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus, 460 startTime, endTime); 461 break; 462 case Tags.CALENDAR_LOCATION: 463 cv.put(Events.EVENT_LOCATION, getValue()); 464 break; 465 case Tags.CALENDAR_RECURRENCE: 466 String rrule = recurrenceParser(); 467 if (rrule != null) { 468 cv.put(Events.RRULE, rrule); 469 } 470 break; 471 case Tags.CALENDAR_ORGANIZER_EMAIL: 472 organizerEmail = getValue(); 473 cv.put(Events.ORGANIZER, organizerEmail); 474 break; 475 case Tags.CALENDAR_SUBJECT: 476 cv.put(Events.TITLE, getValue()); 477 break; 478 case Tags.CALENDAR_SENSITIVITY: 479 cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); 480 break; 481 case Tags.CALENDAR_ORGANIZER_NAME: 482 organizerName = getValue(); 483 break; 484 case Tags.CALENDAR_REMINDER_MINS_BEFORE: 485 // Save away whether this tag has content; Exchange 2010 sends an empty tag 486 // rather than not sending one (as with Ex07 and Ex03) 487 boolean hasContent = !noContent; 488 reminderMins = getValueInt(); 489 if (hasContent) { 490 ops.newReminder(reminderMins); 491 cv.put(Events.HAS_ALARM, 1); 492 } 493 break; 494 // The following are fields we should save (for changes), though they don't 495 // relate to data used by CalendarProvider at this point 496 case Tags.CALENDAR_UID: 497 cv.put(Events.SYNC_DATA2, getValue()); 498 break; 499 case Tags.CALENDAR_DTSTAMP: 500 dtStamp = getValue(); 501 break; 502 case Tags.CALENDAR_MEETING_STATUS: 503 ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue()); 504 break; 505 case Tags.CALENDAR_BUSY_STATUS: 506 // We'll set the user's status in the Attendees table below 507 // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate 508 // attendee! 509 busyStatus = getValueInt(); 510 break; 511 case Tags.CALENDAR_RESPONSE_TYPE: 512 // EAS 14+ uses this for the user's response status; we'll use this instead 513 // of busy status, if it appears 514 responseType = getValueInt(); 515 break; 516 case Tags.CALENDAR_CATEGORIES: 517 String categories = categoriesParser(); 518 if (categories.length() > 0) { 519 ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories); 520 } 521 break; 522 default: 523 skipTag(); 524 } 525 } 526 527 // Enforce CalendarProvider required properties 528 setTimeRelatedValues(cv, startTime, endTime, allDayEvent); 529 530 // Set user's availability 531 cv.put(Events.AVAILABILITY, CalendarUtilities.availabilityFromBusyStatus(busyStatus)); 532 533 // If we haven't added the organizer to attendees, do it now 534 if (!organizerAdded) { 535 addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); 536 } 537 538 // Note that organizerEmail can be null with a DTSTAMP only change from the server 539 boolean selfOrganizer = (mAccount.mEmailAddress.equals(organizerEmail)); 540 541 // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties 542 // If the user is an attendee, set the attendee status using busyStatus (note that the 543 // busyStatus is inherited from the parent unless it's specified in the exception) 544 // Add the insert/update operation for each attendee (based on whether it's add/change) 545 int numAttendees = attendeeValues.size(); 546 if (numAttendees > MAX_SYNCED_ATTENDEES) { 547 // Indicate that we've redacted attendees. If we're the organizer, disable edit 548 // by setting organizerEmail to a bogus value and by setting the upsync prohibited 549 // extended properly. 550 // Note that we don't set ANY attendees if we're in this branch; however, the 551 // organizer has already been included above, and WILL show up (which is good) 552 if (eventId < 0) { 553 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1"); 554 if (selfOrganizer) { 555 ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1"); 556 } 557 } else { 558 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId); 559 if (selfOrganizer) { 560 ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1", 561 eventId); 562 } 563 } 564 if (selfOrganizer) { 565 organizerEmail = BOGUS_ORGANIZER_EMAIL; 566 cv.put(Events.ORGANIZER, organizerEmail); 567 } 568 // Tell UI that we don't have any attendees 569 cv.put(Events.HAS_ATTENDEE_DATA, "0"); 570 LogUtils.i(TAG, "Maximum number of attendees exceeded; redacting"); 571 } else if (numAttendees > 0) { 572 StringBuilder sb = new StringBuilder(); 573 for (ContentValues attendee: attendeeValues) { 574 String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL); 575 sb.append(attendeeEmail); 576 sb.append(ATTENDEE_TOKENIZER_DELIMITER); 577 if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { 578 int attendeeStatus; 579 // We'll use the response type (EAS 14), if we've got one; otherwise, we'll 580 // try to infer it from busy status 581 if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) { 582 attendeeStatus = 583 CalendarUtilities.attendeeStatusFromResponseType(responseType); 584 } else if (!update) { 585 // For new events in EAS < 14, we have no idea what the busy status 586 // means, so we show "none", allowing the user to select an option. 587 attendeeStatus = Attendees.ATTENDEE_STATUS_NONE; 588 } else { 589 // For updated events, we'll try to infer the attendee status from the 590 // busy status 591 attendeeStatus = 592 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus); 593 } 594 attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus); 595 // If we're an attendee, save away our initial attendee status in the 596 // event's ExtendedProperties (we look for differences between this and 597 // the user's current attendee status to determine whether an email needs 598 // to be sent to the organizer) 599 // organizerEmail will be null in the case that this is an attendees-only 600 // change from the server 601 if (organizerEmail == null || 602 !organizerEmail.equalsIgnoreCase(attendeeEmail)) { 603 if (eventId < 0) { 604 ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, 605 Integer.toString(attendeeStatus)); 606 } else { 607 ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, 608 Integer.toString(attendeeStatus), eventId); 609 610 } 611 } 612 } 613 if (eventId < 0) { 614 ops.newAttendee(attendee); 615 } else { 616 ops.updatedAttendee(attendee, eventId); 617 } 618 } 619 if (eventId < 0) { 620 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString()); 621 ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0"); 622 ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0"); 623 } else { 624 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(), 625 eventId); 626 ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId); 627 ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId); 628 } 629 } 630 631 // Put the real event in the proper place in the ops ArrayList 632 if (eventOffset >= 0) { 633 // Store away the DTSTAMP here 634 if (dtStamp != null) { 635 ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp); 636 } 637 638 if (isValidEventValues(cv)) { 639 ops.set(eventOffset, 640 new Operation(ContentProviderOperation 641 .newInsert(mAsSyncAdapterEvents).withValues(cv))); 642 } else { 643 // If we can't add this event (it's invalid), remove all of the inserts 644 // we've built for it 645 int cnt = ops.mCount - eventOffset; 646 userLog(TAG, "Removing " + cnt + " inserts from mOps"); 647 for (int i = 0; i < cnt; i++) { 648 ops.remove(eventOffset); 649 } 650 ops.mCount = eventOffset; 651 // If this is a change, we need to also remove the deletion that comes 652 // before the addition 653 if (deleteOffset >= 0) { 654 // Remove the deletion 655 ops.remove(deleteOffset); 656 // And the deletion of exceptions 657 ops.remove(deleteOffset); 658 userLog(TAG, "Removing deletion ops from mOps"); 659 ops.mCount = deleteOffset; 660 } 661 } 662 } 663 // Mark the end of the event 664 addSeparatorOperation(ops, Events.CONTENT_URI); 665 } 666 667 private void logEventColumns(ContentValues cv, String reason) { 668 if (Eas.USER_LOG) { 669 StringBuilder sb = 670 new StringBuilder("Event invalid, " + reason + ", skipping: Columns = "); 671 for (Entry<String, Object> entry: cv.valueSet()) { 672 sb.append(entry.getKey()); 673 sb.append('/'); 674 } 675 userLog(TAG, sb.toString()); 676 } 677 } 678 679 /*package*/ boolean isValidEventValues(ContentValues cv) { 680 boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME); 681 // All events require DTSTART 682 if (!cv.containsKey(Events.DTSTART)) { 683 logEventColumns(cv, "DTSTART missing"); 684 return false; 685 // If we're a top-level event, we must have _SYNC_DATA (uid) 686 } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) { 687 logEventColumns(cv, "_SYNC_DATA missing"); 688 return false; 689 // We must also have DTEND or DURATION if we're not an exception 690 } else if (!isException && !cv.containsKey(Events.DTEND) && 691 !cv.containsKey(Events.DURATION)) { 692 logEventColumns(cv, "DTEND/DURATION missing"); 693 return false; 694 // Exceptions require DTEND 695 } else if (isException && !cv.containsKey(Events.DTEND)) { 696 logEventColumns(cv, "Exception missing DTEND"); 697 return false; 698 // If this is a recurrence, we need a DURATION (in days if an all-day event) 699 } else if (cv.containsKey(Events.RRULE)) { 700 String duration = cv.getAsString(Events.DURATION); 701 if (duration == null) return false; 702 if (cv.containsKey(Events.ALL_DAY)) { 703 Integer ade = cv.getAsInteger(Events.ALL_DAY); 704 if (ade != null && ade != 0 && !duration.endsWith("D")) { 705 return false; 706 } 707 } 708 } 709 return true; 710 } 711 712 public String recurrenceParser() throws IOException { 713 // Turn this information into an RRULE 714 int type = -1; 715 int occurrences = -1; 716 int interval = -1; 717 int dow = -1; 718 int dom = -1; 719 int wom = -1; 720 int moy = -1; 721 String until = null; 722 723 while (nextTag(Tags.CALENDAR_RECURRENCE) != END) { 724 switch (tag) { 725 case Tags.CALENDAR_RECURRENCE_TYPE: 726 type = getValueInt(); 727 break; 728 case Tags.CALENDAR_RECURRENCE_INTERVAL: 729 interval = getValueInt(); 730 break; 731 case Tags.CALENDAR_RECURRENCE_OCCURRENCES: 732 occurrences = getValueInt(); 733 break; 734 case Tags.CALENDAR_RECURRENCE_DAYOFWEEK: 735 dow = getValueInt(); 736 break; 737 case Tags.CALENDAR_RECURRENCE_DAYOFMONTH: 738 dom = getValueInt(); 739 break; 740 case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH: 741 wom = getValueInt(); 742 break; 743 case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR: 744 moy = getValueInt(); 745 break; 746 case Tags.CALENDAR_RECURRENCE_UNTIL: 747 until = getValue(); 748 break; 749 default: 750 skipTag(); 751 } 752 } 753 754 return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval, 755 dow, dom, wom, moy, until); 756 } 757 758 private void exceptionParser(CalendarOperations ops, ContentValues parentCv, 759 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, 760 long startTime, long endTime) throws IOException { 761 ContentValues cv = new ContentValues(); 762 cv.put(Events.CALENDAR_ID, mCalendarId); 763 764 // It appears that these values have to be copied from the parent if they are to appear 765 // Note that they can be overridden below 766 cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER)); 767 cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE)); 768 cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION)); 769 cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY)); 770 cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION)); 771 cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL)); 772 cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE)); 773 // Exceptions should always have this set to zero, since EAS has no concept of 774 // separate attendee lists for exceptions; if we fail to do this, then the UI will 775 // allow the user to change attendee data, and this change would never get reflected 776 // on the server. 777 cv.put(Events.HAS_ATTENDEE_DATA, 0); 778 779 int allDayEvent = 0; 780 781 // This column is the key that links the exception to the serverId 782 cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID)); 783 784 String exceptionStartTime = "_noStartTime"; 785 while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { 786 switch (tag) { 787 case Tags.CALENDAR_ATTACHMENTS: 788 attachmentsParser(); 789 break; 790 case Tags.CALENDAR_EXCEPTION_START_TIME: 791 exceptionStartTime = getValue(); 792 cv.put(Events.ORIGINAL_INSTANCE_TIME, 793 Utility.parseDateTimeToMillis(exceptionStartTime)); 794 break; 795 case Tags.CALENDAR_EXCEPTION_IS_DELETED: 796 if (getValueInt() == 1) { 797 cv.put(Events.STATUS, Events.STATUS_CANCELED); 798 } 799 break; 800 case Tags.CALENDAR_ALL_DAY_EVENT: 801 allDayEvent = getValueInt(); 802 cv.put(Events.ALL_DAY, allDayEvent); 803 break; 804 case Tags.BASE_BODY: 805 cv.put(Events.DESCRIPTION, bodyParser()); 806 break; 807 case Tags.CALENDAR_BODY: 808 cv.put(Events.DESCRIPTION, getValue()); 809 break; 810 case Tags.CALENDAR_START_TIME: 811 startTime = Utility.parseDateTimeToMillis(getValue()); 812 break; 813 case Tags.CALENDAR_END_TIME: 814 endTime = Utility.parseDateTimeToMillis(getValue()); 815 break; 816 case Tags.CALENDAR_LOCATION: 817 cv.put(Events.EVENT_LOCATION, getValue()); 818 break; 819 case Tags.CALENDAR_RECURRENCE: 820 String rrule = recurrenceParser(); 821 if (rrule != null) { 822 cv.put(Events.RRULE, rrule); 823 } 824 break; 825 case Tags.CALENDAR_SUBJECT: 826 cv.put(Events.TITLE, getValue()); 827 break; 828 case Tags.CALENDAR_SENSITIVITY: 829 cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); 830 break; 831 case Tags.CALENDAR_BUSY_STATUS: 832 busyStatus = getValueInt(); 833 // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate 834 // attendee! 835 break; 836 // TODO How to handle these items that are linked to event id! 837// case Tags.CALENDAR_DTSTAMP: 838// ops.newExtendedProperty("dtstamp", getValue()); 839// break; 840// case Tags.CALENDAR_REMINDER_MINS_BEFORE: 841// ops.newReminder(getValueInt()); 842// break; 843 default: 844 skipTag(); 845 } 846 } 847 848 // We need a _sync_id, but it can't be the parent's id, so we generate one 849 cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' + 850 exceptionStartTime); 851 852 // Enforce CalendarProvider required properties 853 setTimeRelatedValues(cv, startTime, endTime, allDayEvent); 854 855 // Don't insert an invalid exception event 856 if (!isValidEventValues(cv)) return; 857 858 // Add the exception insert 859 int exceptionStart = ops.mCount; 860 ops.newException(cv); 861 // Also add the attendees, because they need to be copied over from the parent event 862 boolean attendeesRedacted = false; 863 if (attendeeValues != null) { 864 for (ContentValues attValues: attendeeValues) { 865 // If this is the user, use his busy status for attendee status 866 String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL); 867 // Note that the exception at which we surpass the redaction limit might have 868 // any number of attendees shown; since this is an edge case and a workaround, 869 // it seems to be an acceptable implementation 870 if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { 871 attValues.put(Attendees.ATTENDEE_STATUS, 872 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus)); 873 ops.newAttendee(attValues, exceptionStart); 874 } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) { 875 ops.newAttendee(attValues, exceptionStart); 876 } else { 877 attendeesRedacted = true; 878 } 879 } 880 } 881 // And add the parent's reminder value 882 if (reminderMins > 0) { 883 ops.newReminder(reminderMins, exceptionStart); 884 } 885 if (attendeesRedacted) { 886 LogUtils.i(TAG, "Attendees redacted in this exception"); 887 } 888 } 889 890 private static int encodeVisibility(int easVisibility) { 891 int visibility = 0; 892 switch(easVisibility) { 893 case 0: 894 visibility = Events.ACCESS_DEFAULT; 895 break; 896 case 1: 897 visibility = Events.ACCESS_PUBLIC; 898 break; 899 case 2: 900 visibility = Events.ACCESS_PRIVATE; 901 break; 902 case 3: 903 visibility = Events.ACCESS_CONFIDENTIAL; 904 break; 905 } 906 return visibility; 907 } 908 909 private void exceptionsParser(CalendarOperations ops, ContentValues cv, 910 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, 911 long startTime, long endTime) throws IOException { 912 while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) { 913 switch (tag) { 914 case Tags.CALENDAR_EXCEPTION: 915 exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus, 916 startTime, endTime); 917 break; 918 default: 919 skipTag(); 920 } 921 } 922 } 923 924 private String categoriesParser() throws IOException { 925 StringBuilder categories = new StringBuilder(); 926 while (nextTag(Tags.CALENDAR_CATEGORIES) != END) { 927 switch (tag) { 928 case Tags.CALENDAR_CATEGORY: 929 // TODO Handle categories (there's no similar concept for gdata AFAIK) 930 // We need to save them and spit them back when we update the event 931 categories.append(getValue()); 932 categories.append(CATEGORY_TOKENIZER_DELIMITER); 933 break; 934 default: 935 skipTag(); 936 } 937 } 938 return categories.toString(); 939 } 940 941 /** 942 * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14 943 */ 944 private void attachmentsParser() throws IOException { 945 while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) { 946 switch (tag) { 947 case Tags.CALENDAR_ATTACHMENT: 948 skipParser(Tags.CALENDAR_ATTACHMENT); 949 break; 950 default: 951 skipTag(); 952 } 953 } 954 } 955 956 private ArrayList<ContentValues> attendeesParser() 957 throws IOException { 958 int attendeeCount = 0; 959 ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>(); 960 while (nextTag(Tags.CALENDAR_ATTENDEES) != END) { 961 switch (tag) { 962 case Tags.CALENDAR_ATTENDEE: 963 ContentValues cv = attendeeParser(); 964 // If we're going to redact these attendees anyway, let's avoid unnecessary 965 // memory pressure, and not keep them around 966 // We still need to parse them all, however 967 attendeeCount++; 968 // Allow one more than MAX_ATTENDEES, so that the check for "too many" will 969 // succeed in addEvent 970 if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) { 971 attendeeValues.add(cv); 972 } 973 break; 974 default: 975 skipTag(); 976 } 977 } 978 return attendeeValues; 979 } 980 981 private ContentValues attendeeParser() 982 throws IOException { 983 ContentValues cv = new ContentValues(); 984 while (nextTag(Tags.CALENDAR_ATTENDEE) != END) { 985 switch (tag) { 986 case Tags.CALENDAR_ATTENDEE_EMAIL: 987 cv.put(Attendees.ATTENDEE_EMAIL, getValue()); 988 break; 989 case Tags.CALENDAR_ATTENDEE_NAME: 990 cv.put(Attendees.ATTENDEE_NAME, getValue()); 991 break; 992 case Tags.CALENDAR_ATTENDEE_STATUS: 993 int status = getValueInt(); 994 cv.put(Attendees.ATTENDEE_STATUS, 995 (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE : 996 (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED : 997 (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED : 998 (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED : 999 Attendees.ATTENDEE_STATUS_NONE); 1000 break; 1001 case Tags.CALENDAR_ATTENDEE_TYPE: 1002 int type = Attendees.TYPE_NONE; 1003 // EAS types: 1 = req'd, 2 = opt, 3 = resource 1004 switch (getValueInt()) { 1005 case 1: 1006 type = Attendees.TYPE_REQUIRED; 1007 break; 1008 case 2: 1009 type = Attendees.TYPE_OPTIONAL; 1010 break; 1011 } 1012 cv.put(Attendees.ATTENDEE_TYPE, type); 1013 break; 1014 default: 1015 skipTag(); 1016 } 1017 } 1018 cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE); 1019 return cv; 1020 } 1021 1022 private String bodyParser() throws IOException { 1023 String body = null; 1024 while (nextTag(Tags.BASE_BODY) != END) { 1025 switch (tag) { 1026 case Tags.BASE_DATA: 1027 body = getValue(); 1028 break; 1029 default: 1030 skipTag(); 1031 } 1032 } 1033 1034 // Handle null data without error 1035 if (body == null) return ""; 1036 // Remove \r's from any body text 1037 return body.replace("\r\n", "\n"); 1038 } 1039 1040 public void addParser(CalendarOperations ops) throws IOException { 1041 String serverId = null; 1042 while (nextTag(Tags.SYNC_ADD) != END) { 1043 switch (tag) { 1044 case Tags.SYNC_SERVER_ID: // same as 1045 serverId = getValue(); 1046 break; 1047 case Tags.SYNC_APPLICATION_DATA: 1048 addEvent(ops, serverId, false); 1049 break; 1050 default: 1051 skipTag(); 1052 } 1053 } 1054 } 1055 1056 private Cursor getServerIdCursor(String serverId) { 1057 return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, 1058 SERVER_ID_AND_CALENDAR_ID, new String[] {serverId, Long.toString(mCalendarId)}, 1059 null); 1060 } 1061 1062 private Cursor getClientIdCursor(String clientId) { 1063 mBindArgument[0] = clientId; 1064 return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, CLIENT_ID_SELECTION, 1065 mBindArgument, null); 1066 } 1067 1068 public void deleteParser(CalendarOperations ops) throws IOException { 1069 while (nextTag(Tags.SYNC_DELETE) != END) { 1070 switch (tag) { 1071 case Tags.SYNC_SERVER_ID: 1072 String serverId = getValue(); 1073 // Find the event with the given serverId 1074 Cursor c = getServerIdCursor(serverId); 1075 try { 1076 if (c.moveToFirst()) { 1077 userLog("Deleting ", serverId); 1078 ops.delete(c.getLong(0), serverId); 1079 } 1080 } finally { 1081 c.close(); 1082 } 1083 break; 1084 default: 1085 skipTag(); 1086 } 1087 } 1088 } 1089 1090 /** 1091 * A change is handled as a delete (including all exceptions) and an add 1092 * This isn't as efficient as attempting to traverse the original and all of its exceptions, 1093 * but changes happen infrequently and this code is both simpler and easier to maintain 1094 * @param ops the array of pending ContactProviderOperations. 1095 * @throws IOException 1096 */ 1097 public void changeParser(CalendarOperations ops) throws IOException { 1098 String serverId = null; 1099 while (nextTag(Tags.SYNC_CHANGE) != END) { 1100 switch (tag) { 1101 case Tags.SYNC_SERVER_ID: 1102 serverId = getValue(); 1103 break; 1104 case Tags.SYNC_APPLICATION_DATA: 1105 userLog("Changing " + serverId); 1106 addEvent(ops, serverId, true); 1107 break; 1108 default: 1109 skipTag(); 1110 } 1111 } 1112 } 1113 1114 @Override 1115 public void commandsParser() throws IOException { 1116 while (nextTag(Tags.SYNC_COMMANDS) != END) { 1117 if (tag == Tags.SYNC_ADD) { 1118 addParser(mOps); 1119 } else if (tag == Tags.SYNC_DELETE) { 1120 deleteParser(mOps); 1121 } else if (tag == Tags.SYNC_CHANGE) { 1122 changeParser(mOps); 1123 } else 1124 skipTag(); 1125 } 1126 } 1127 1128 @Override 1129 public void commit() throws IOException { 1130 userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey); 1131 // Save the syncKey here, using the Helper provider by Calendar provider 1132 mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation( 1133 asSyncAdapter(SyncState.CONTENT_URI, mAccount.mEmailAddress, 1134 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 1135 mAccountManagerAccount, 1136 mMailbox.mSyncKey.getBytes()))); 1137 1138 // Execute our CPO's safely 1139 try { 1140 safeExecute(mContentResolver, CalendarContract.AUTHORITY, mOps); 1141 } catch (RemoteException e) { 1142 throw new IOException("Remote exception caught; will retry"); 1143 } 1144 } 1145 1146 public void addResponsesParser() throws IOException { 1147 String serverId = null; 1148 String clientId = null; 1149 int status = -1; 1150 ContentValues cv = new ContentValues(); 1151 while (nextTag(Tags.SYNC_ADD) != END) { 1152 switch (tag) { 1153 case Tags.SYNC_SERVER_ID: 1154 serverId = getValue(); 1155 break; 1156 case Tags.SYNC_CLIENT_ID: 1157 clientId = getValue(); 1158 break; 1159 case Tags.SYNC_STATUS: 1160 status = getValueInt(); 1161 if (status != 1) { 1162 userLog("Attempt to add event failed with status: " + status); 1163 } 1164 break; 1165 default: 1166 skipTag(); 1167 } 1168 } 1169 1170 if (clientId == null) return; 1171 if (serverId == null) { 1172 // TODO Reconsider how to handle this 1173 serverId = "FAIL:" + status; 1174 } 1175 1176 Cursor c = getClientIdCursor(clientId); 1177 try { 1178 if (c.moveToFirst()) { 1179 cv.put(Events._SYNC_ID, serverId); 1180 cv.put(Events.SYNC_DATA2, clientId); 1181 long id = c.getLong(0); 1182 // Write the serverId into the Event 1183 mOps.add(new Operation(ContentProviderOperation 1184 .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id)) 1185 .withValues(cv))); 1186 userLog("New event " + clientId + " was given serverId: " + serverId); 1187 } 1188 } finally { 1189 c.close(); 1190 } 1191 } 1192 1193 public void changeResponsesParser() throws IOException { 1194 String serverId = null; 1195 String status = null; 1196 while (nextTag(Tags.SYNC_CHANGE) != END) { 1197 switch (tag) { 1198 case Tags.SYNC_SERVER_ID: 1199 serverId = getValue(); 1200 break; 1201 case Tags.SYNC_STATUS: 1202 status = getValue(); 1203 break; 1204 default: 1205 skipTag(); 1206 } 1207 } 1208 if (serverId != null && status != null) { 1209 userLog("Changed event " + serverId + " failed with status: " + status); 1210 } 1211 } 1212 1213 1214 @Override 1215 public void responsesParser() throws IOException { 1216 // Handle server responses here (for Add and Change) 1217 while (nextTag(Tags.SYNC_RESPONSES) != END) { 1218 if (tag == Tags.SYNC_ADD) { 1219 addResponsesParser(); 1220 } else if (tag == Tags.SYNC_CHANGE) { 1221 changeResponsesParser(); 1222 } else 1223 skipTag(); 1224 } 1225 } 1226 1227 /** 1228 * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties, 1229 * and we just return quickly if the service has already been stopped. 1230 */ 1231 private static ContentProviderResult[] execute(final ContentResolver contentResolver, 1232 final String authority, final ArrayList<ContentProviderOperation> ops) 1233 throws RemoteException, OperationApplicationException { 1234 if (!ops.isEmpty()) { 1235 ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); 1236 //mService.userLog("Results: " + result.length); 1237 return result; 1238 } 1239 return new ContentProviderResult[0]; 1240 } 1241 1242 /** 1243 * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the 1244 * passed-in offset 1245 */ 1246 @VisibleForTesting 1247 static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) { 1248 if (op.mOp != null) { 1249 return op.mOp; 1250 } else if (op.mBuilder == null) { 1251 throw new IllegalArgumentException("Operation must have CPO.Builder"); 1252 } 1253 ContentProviderOperation.Builder builder = op.mBuilder; 1254 if (op.mColumnName != null) { 1255 builder.withValueBackReference(op.mColumnName, op.mOffset - offset); 1256 } 1257 return builder.build(); 1258 } 1259 1260 /** 1261 * Create a list of CPOs from a list of Operations, and then apply them in a batch 1262 */ 1263 private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, 1264 final String authority, final ArrayList<Operation> ops, final int offset) 1265 throws RemoteException, OperationApplicationException { 1266 // Handle the empty case 1267 if (ops.isEmpty()) { 1268 return new ContentProviderResult[0]; 1269 } 1270 ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>(); 1271 for (Operation op: ops) { 1272 cpos.add(operationToContentProviderOperation(op, offset)); 1273 } 1274 return execute(contentResolver, authority, cpos); 1275 } 1276 1277 /** 1278 * Apply the list of CPO's in the provider and copy the "mini" result into our full result array 1279 */ 1280 private static void applyAndCopyResults(final ContentResolver contentResolver, 1281 final String authority, final ArrayList<Operation> mini, 1282 final ContentProviderResult[] result, final int offset) throws RemoteException { 1283 // Empty lists are ok; we just ignore them 1284 if (mini.isEmpty()) return; 1285 try { 1286 ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini, 1287 offset); 1288 // Copy the results from this mini-batch into our results array 1289 System.arraycopy(miniResult, 0, result, offset, miniResult.length); 1290 } catch (OperationApplicationException e) { 1291 // Not possible since we're building the ops ourselves 1292 } 1293 } 1294 1295 /** 1296 * Called by a sync adapter to execute a list of Operations in the ContentProvider handling 1297 * the passed-in authority. If the attempt to apply the batch fails due to a too-large 1298 * binder transaction, we split the Operations as directed by separators. If any of the 1299 * "mini" batches fails due to a too-large transaction, we're screwed, but this would be 1300 * vanishingly rare. Other, possibly transient, errors are handled by throwing a 1301 * RemoteException, which the caller will likely re-throw as an IOException so that the sync 1302 * can be attempted again. 1303 * 1304 * Callers MAY leave a dangling separator at the end of the list; note that the separators 1305 * themselves are only markers and are not sent to the provider. 1306 */ 1307 protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, 1308 final String authority, final ArrayList<Operation> ops) throws RemoteException { 1309 //mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority); 1310 ContentProviderResult[] result = null; 1311 try { 1312 // Try to execute the whole thing 1313 return applyBatch(contentResolver, authority, ops, 0); 1314 } catch (TransactionTooLargeException e) { 1315 // Nope; split into smaller chunks, demarcated by the separator operation 1316 //mService.userLog("Transaction too large; spliting!"); 1317 ArrayList<Operation> mini = new ArrayList<Operation>(); 1318 // Build a result array with the total size we're sending 1319 result = new ContentProviderResult[ops.size()]; 1320 int count = 0; 1321 int offset = 0; 1322 for (Operation op: ops) { 1323 if (op.mSeparator) { 1324 try { 1325 //mService.userLog("Try mini-batch of ", mini.size(), " CPO's"); 1326 applyAndCopyResults(contentResolver, authority, mini, result, offset); 1327 mini.clear(); 1328 // Save away the offset here; this will need to be subtracted out of the 1329 // value originally set by the adapter 1330 offset = count + 1; // Remember to add 1 for the separator! 1331 } catch (TransactionTooLargeException e1) { 1332 throw new RuntimeException("Can't send transaction; sync stopped."); 1333 } catch (RemoteException e1) { 1334 throw e1; 1335 } 1336 } else { 1337 mini.add(op); 1338 } 1339 count++; 1340 } 1341 // Check out what's left; if it's more than just a separator, apply the batch 1342 int miniSize = mini.size(); 1343 if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) { 1344 applyAndCopyResults(contentResolver, authority, mini, result, offset); 1345 } 1346 } catch (RemoteException e) { 1347 throw e; 1348 } catch (OperationApplicationException e) { 1349 // Not possible since we're building the ops ourselves 1350 } 1351 return result; 1352 } 1353 1354 /** 1355 * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's 1356 */ 1357 protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { 1358 Operation op = new Operation( 1359 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); 1360 op.mSeparator = true; 1361 ops.add(op); 1362 } 1363 1364} 1365