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