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