| package com.android.exchange.adapter; |
| |
| import android.content.ContentProviderOperation; |
| import android.content.ContentProviderResult; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.OperationApplicationException; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.RemoteException; |
| import android.os.TransactionTooLargeException; |
| import android.provider.CalendarContract; |
| import android.provider.CalendarContract.Attendees; |
| import android.provider.CalendarContract.Calendars; |
| import android.provider.CalendarContract.Events; |
| import android.provider.CalendarContract.ExtendedProperties; |
| import android.provider.CalendarContract.Reminders; |
| import android.provider.CalendarContract.SyncState; |
| import android.provider.SyncStateContract; |
| import android.text.format.DateUtils; |
| |
| import com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.emailcommon.utility.Utility; |
| import com.android.exchange.Eas; |
| import com.android.exchange.adapter.AbstractSyncAdapter.Operation; |
| import com.android.exchange.eas.EasSyncCalendar; |
| import com.android.exchange.utility.CalendarUtilities; |
| import com.android.mail.utils.LogUtils; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.GregorianCalendar; |
| import java.util.Map.Entry; |
| import java.util.TimeZone; |
| |
| public class CalendarSyncParser extends AbstractSyncParser { |
| private static final String TAG = Eas.LOG_TAG; |
| |
| private final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); |
| private final TimeZone mLocalTimeZone = TimeZone.getDefault(); |
| |
| private final long mCalendarId; |
| private final android.accounts.Account mAccountManagerAccount; |
| private final Uri mAsSyncAdapterAttendees; |
| private final Uri mAsSyncAdapterEvents; |
| |
| private final String[] mBindArgument = new String[1]; |
| private final CalendarOperations mOps; |
| |
| |
| private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1; |
| // Since exceptions will have the same _SYNC_ID as the original event we have to check that |
| // there's no original event when finding an item by _SYNC_ID |
| private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " + |
| Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?"; |
| private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?"; |
| private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " + |
| Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER; |
| private static final String[] ID_PROJECTION = new String[] {Events._ID}; |
| private static final String EVENT_ID_AND_NAME = |
| ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?"; |
| |
| private static final String[] EXTENDED_PROPERTY_PROJECTION = |
| new String[] {ExtendedProperties._ID}; |
| private static final int EXTENDED_PROPERTY_ID = 0; |
| |
| private static final String CATEGORY_TOKENIZER_DELIMITER = "\\"; |
| private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER; |
| |
| private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus"; |
| private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees"; |
| private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp"; |
| private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status"; |
| private static final String EXTENDED_PROPERTY_CATEGORIES = "categories"; |
| // Used to indicate that we removed the attendee list because it was too large |
| private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted"; |
| // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges) |
| private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited"; |
| |
| private static final Operation PLACEHOLDER_OPERATION = |
| new Operation(ContentProviderOperation.newInsert(Uri.EMPTY)); |
| |
| private static final long SEPARATOR_ID = Long.MAX_VALUE; |
| |
| // Maximum number of allowed attendees; above this number, we mark the Event with the |
| // attendeesRedacted extended property and don't allow the event to be upsynced to the server |
| private static final int MAX_SYNCED_ATTENDEES = 50; |
| // We set the organizer to this when the user is the organizer and we've redacted the |
| // attendee list. By making the meeting organizer OTHER than the user, we cause the UI to |
| // prevent edits to this event (except local changes like reminder). |
| private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa"; |
| // Maximum number of CPO's before we start redacting attendees in exceptions |
| // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before |
| // binder failures occur, but we need room at any point for additional events/exceptions so |
| // we set our limit at 1/3 of the apparent maximum for extra safety |
| // TODO Find a better solution to this workaround |
| private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500; |
| |
| public CalendarSyncParser(final Context context, final ContentResolver resolver, |
| final InputStream in, final Mailbox mailbox, final Account account, |
| final android.accounts.Account accountManagerAccount, |
| final long calendarId) throws IOException { |
| super(context, resolver, in, mailbox, account); |
| mAccountManagerAccount = accountManagerAccount; |
| mCalendarId = calendarId; |
| mAsSyncAdapterAttendees = asSyncAdapter(Attendees.CONTENT_URI, |
| mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| mAsSyncAdapterEvents = asSyncAdapter(Events.CONTENT_URI, |
| mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| mOps = new CalendarOperations(resolver, mAsSyncAdapterAttendees, mAsSyncAdapterEvents, |
| asSyncAdapter(Reminders.CONTENT_URI, mAccount.mEmailAddress, |
| Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), |
| asSyncAdapter(ExtendedProperties.CONTENT_URI, mAccount.mEmailAddress, |
| Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)); |
| } |
| |
| protected static class CalendarOperations extends ArrayList<Operation> { |
| private static final long serialVersionUID = 1L; |
| public int mCount = 0; |
| private int mEventStart = 0; |
| private final ContentResolver mContentResolver; |
| private final Uri mAsSyncAdapterAttendees; |
| private final Uri mAsSyncAdapterEvents; |
| private final Uri mAsSyncAdapterReminders; |
| private final Uri mAsSyncAdapterExtendedProperties; |
| |
| public CalendarOperations(final ContentResolver contentResolver, |
| final Uri asSyncAdapterAttendees, final Uri asSyncAdapterEvents, |
| final Uri asSyncAdapterReminders, final Uri asSyncAdapterExtendedProperties) { |
| mContentResolver = contentResolver; |
| mAsSyncAdapterAttendees = asSyncAdapterAttendees; |
| mAsSyncAdapterEvents = asSyncAdapterEvents; |
| mAsSyncAdapterReminders = asSyncAdapterReminders; |
| mAsSyncAdapterExtendedProperties = asSyncAdapterExtendedProperties; |
| } |
| |
| @Override |
| public boolean add(Operation op) { |
| super.add(op); |
| mCount++; |
| return true; |
| } |
| |
| public int newEvent(Operation op) { |
| mEventStart = mCount; |
| add(op); |
| return mEventStart; |
| } |
| |
| public int newDelete(long id, String serverId) { |
| int offset = mCount; |
| delete(id, serverId); |
| return offset; |
| } |
| |
| public void newAttendee(ContentValues cv) { |
| newAttendee(cv, mEventStart); |
| } |
| |
| public void newAttendee(ContentValues cv, int eventStart) { |
| add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees) |
| .withValues(cv), |
| Attendees.EVENT_ID, |
| eventStart)); |
| } |
| |
| public void updatedAttendee(ContentValues cv, long id) { |
| cv.put(Attendees.EVENT_ID, id); |
| add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees) |
| .withValues(cv))); |
| } |
| |
| public void newException(ContentValues cv) { |
| add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents) |
| .withValues(cv))); |
| } |
| |
| public void newExtendedProperty(String name, String value) { |
| add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties) |
| .withValue(ExtendedProperties.NAME, name) |
| .withValue(ExtendedProperties.VALUE, value), |
| ExtendedProperties.EVENT_ID, |
| mEventStart)); |
| } |
| |
| public void updatedExtendedProperty(String name, String value, long id) { |
| // Find an existing ExtendedProperties row for this event and property name |
| Cursor c = mContentResolver.query(ExtendedProperties.CONTENT_URI, |
| EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME, |
| new String[] {Long.toString(id), name}, null); |
| long extendedPropertyId = -1; |
| // If there is one, capture its _id |
| if (c != null) { |
| try { |
| if (c.moveToFirst()) { |
| extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| // Either do an update or an insert, depending on whether one |
| // already exists |
| if (extendedPropertyId >= 0) { |
| add(new Operation(ContentProviderOperation |
| .newUpdate( |
| ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties, |
| extendedPropertyId)) |
| .withValue(ExtendedProperties.VALUE, value))); |
| } else { |
| newExtendedProperty(name, value); |
| } |
| } |
| |
| public void newReminder(int mins, int eventStart) { |
| add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders) |
| .withValue(Reminders.MINUTES, mins) |
| .withValue(Reminders.METHOD, Reminders.METHOD_ALERT), |
| ExtendedProperties.EVENT_ID, |
| eventStart)); |
| } |
| |
| public void newReminder(int mins) { |
| newReminder(mins, mEventStart); |
| } |
| |
| public void delete(long id, String syncId) { |
| add(new Operation(ContentProviderOperation.newDelete( |
| ContentUris.withAppendedId(mAsSyncAdapterEvents, id)))); |
| // Delete the exceptions for this Event (CalendarProvider doesn't do this) |
| add(new Operation(ContentProviderOperation |
| .newDelete(mAsSyncAdapterEvents) |
| .withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId}))); |
| } |
| } |
| |
| private static Uri asSyncAdapter(Uri uri, String account, String accountType) { |
| return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") |
| .appendQueryParameter(Calendars.ACCOUNT_NAME, account) |
| .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); |
| } |
| |
| private static void addOrganizerToAttendees(CalendarOperations ops, long eventId, |
| String organizerName, String organizerEmail) { |
| // Handle the organizer (who IS an attendee on device, but NOT in EAS) |
| if (organizerName != null || organizerEmail != null) { |
| ContentValues attendeeCv = new ContentValues(); |
| if (organizerName != null) { |
| attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName); |
| } |
| if (organizerEmail != null) { |
| attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail); |
| } |
| attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER); |
| attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED); |
| attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED); |
| if (eventId < 0) { |
| ops.newAttendee(attendeeCv); |
| } else { |
| ops.updatedAttendee(attendeeCv, eventId); |
| } |
| } |
| } |
| |
| /** |
| * Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event |
| * The follow rules are enforced by CalendarProvider2: |
| * Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION |
| * Recurring events (i.e. events with RRULE) must have a DURATION |
| * All-day recurring events MUST have a DURATION that is in the form P<n>D |
| * Other events MAY have a DURATION in any valid form (we use P<n>M) |
| * All-day events MUST have hour, minute, and second = 0; in addition, they must have |
| * the EVENT_TIMEZONE set to UTC |
| * Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has |
| * hour, minute, and second = 0 and be set in UTC |
| * @param cv the ContentValues for the Event |
| * @param startTime the start time for the Event |
| * @param endTime the end time for the Event |
| * @param allDayEvent whether this is an all day event (1) or not (0) |
| */ |
| /*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime, |
| int allDayEvent) { |
| // If there's no startTime, the event will be found to be invalid, so return |
| if (startTime < 0) return; |
| // EAS events can arrive without an end time, but CalendarProvider requires them |
| // so we'll default to 30 minutes; this will be superceded if this is an all-day event |
| if (endTime < 0) endTime = startTime + (30 * DateUtils.MINUTE_IN_MILLIS); |
| |
| // If this is an all-day event, set hour, minute, and second to zero, and use UTC |
| if (allDayEvent != 0) { |
| startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone); |
| endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone); |
| String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE); |
| cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone); |
| cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID()); |
| } |
| |
| // If this is an exception, and the original was an all-day event, make sure the |
| // original instance time has hour, minute, and second set to zero, and is in UTC |
| if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) && |
| cv.containsKey(Events.ORIGINAL_ALL_DAY)) { |
| Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY); |
| if (ade != null && ade != 0) { |
| long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME); |
| final GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE); |
| exceptionTime = CalendarUtilities.getUtcAllDayCalendarTime(exceptionTime, |
| mLocalTimeZone); |
| cal.setTimeInMillis(exceptionTime); |
| cal.set(GregorianCalendar.HOUR_OF_DAY, 0); |
| cal.set(GregorianCalendar.MINUTE, 0); |
| cal.set(GregorianCalendar.SECOND, 0); |
| cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis()); |
| } |
| } |
| |
| // Always set DTSTART |
| cv.put(Events.DTSTART, startTime); |
| // For recurring events, set DURATION. Use P<n>D format for all day events |
| if (cv.containsKey(Events.RRULE)) { |
| if (allDayEvent != 0) { |
| cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.DAY_IN_MILLIS) + "D"); |
| } |
| else { |
| cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.MINUTE_IN_MILLIS) + "M"); |
| } |
| // For other events, set DTEND and LAST_DATE |
| } else { |
| cv.put(Events.DTEND, endTime); |
| cv.put(Events.LAST_DATE, endTime); |
| } |
| } |
| |
| public void addEvent(CalendarOperations ops, String serverId, boolean update) |
| throws IOException { |
| ContentValues cv = new ContentValues(); |
| cv.put(Events.CALENDAR_ID, mCalendarId); |
| cv.put(Events._SYNC_ID, serverId); |
| cv.put(Events.HAS_ATTENDEE_DATA, 1); |
| cv.put(Events.SYNC_DATA2, "0"); |
| |
| int allDayEvent = 0; |
| String organizerName = null; |
| String organizerEmail = null; |
| int eventOffset = -1; |
| int deleteOffset = -1; |
| int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE; |
| int responseType = CalendarUtilities.RESPONSE_TYPE_NONE; |
| |
| boolean firstTag = true; |
| long eventId = -1; |
| long startTime = -1; |
| long endTime = -1; |
| TimeZone timeZone = null; |
| |
| // Keep track of the attendees; exceptions will need them |
| ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>(); |
| int reminderMins = -1; |
| String dtStamp = null; |
| boolean organizerAdded = false; |
| |
| while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) { |
| if (update && firstTag) { |
| // Find the event that's being updated |
| Cursor c = getServerIdCursor(serverId); |
| long id = -1; |
| try { |
| if (c != null && c.moveToFirst()) { |
| id = c.getLong(0); |
| } |
| } finally { |
| if (c != null) c.close(); |
| } |
| if (id > 0) { |
| // DTSTAMP can come first, and we simply need to track it |
| if (tag == Tags.CALENDAR_DTSTAMP) { |
| dtStamp = getValue(); |
| continue; |
| } else if (tag == Tags.CALENDAR_ATTENDEES) { |
| // This is an attendees-only update; just |
| // delete/re-add attendees |
| mBindArgument[0] = Long.toString(id); |
| ops.add(new Operation(ContentProviderOperation |
| .newDelete(mAsSyncAdapterAttendees) |
| .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument))); |
| eventId = id; |
| } else { |
| // Otherwise, delete the original event and recreate it |
| userLog("Changing (delete/add) event ", serverId); |
| deleteOffset = ops.newDelete(id, serverId); |
| // Add a placeholder event so that associated tables can reference |
| // this as a back reference. We add the event at the end of the method |
| eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); |
| } |
| } else { |
| // The changed item isn't found. We'll treat this as a new item |
| eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); |
| userLog(TAG, "Changed item not found; treating as new."); |
| } |
| } else if (firstTag) { |
| // Add a placeholder event so that associated tables can reference |
| // this as a back reference. We add the event at the end of the method |
| eventOffset = ops.newEvent(PLACEHOLDER_OPERATION); |
| } |
| firstTag = false; |
| switch (tag) { |
| case Tags.CALENDAR_ALL_DAY_EVENT: |
| allDayEvent = getValueInt(); |
| if (allDayEvent != 0 && timeZone != null) { |
| // If the event doesn't start at midnight local time, we won't consider |
| // this an all-day event in the local time zone (this is what OWA does) |
| GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone); |
| cal.setTimeInMillis(startTime); |
| userLog("All-day event arrived in: " + timeZone.getID()); |
| if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 || |
| cal.get(GregorianCalendar.MINUTE) != 0) { |
| allDayEvent = 0; |
| userLog("Not an all-day event locally: " + mLocalTimeZone.getID()); |
| } |
| } |
| cv.put(Events.ALL_DAY, allDayEvent); |
| break; |
| case Tags.CALENDAR_ATTACHMENTS: |
| attachmentsParser(); |
| break; |
| case Tags.CALENDAR_ATTENDEES: |
| // If eventId >= 0, this is an update; otherwise, a new Event |
| attendeeValues = attendeesParser(); |
| break; |
| case Tags.BASE_BODY: |
| cv.put(Events.DESCRIPTION, bodyParser()); |
| break; |
| case Tags.CALENDAR_BODY: |
| cv.put(Events.DESCRIPTION, getValue()); |
| break; |
| case Tags.CALENDAR_TIME_ZONE: |
| timeZone = CalendarUtilities.tziStringToTimeZone(getValue()); |
| if (timeZone == null) { |
| timeZone = mLocalTimeZone; |
| } |
| cv.put(Events.EVENT_TIMEZONE, timeZone.getID()); |
| break; |
| case Tags.CALENDAR_START_TIME: |
| try { |
| startTime = Utility.parseDateTimeToMillis(getValue()); |
| } catch (ParseException e) { |
| LogUtils.w(TAG, "Parse error for CALENDAR_START_TIME tag.", e); |
| } |
| break; |
| case Tags.CALENDAR_END_TIME: |
| try { |
| endTime = Utility.parseDateTimeToMillis(getValue()); |
| } catch (ParseException e) { |
| LogUtils.w(TAG, "Parse error for CALENDAR_END_TIME tag.", e); |
| } |
| break; |
| case Tags.CALENDAR_EXCEPTIONS: |
| // For exceptions to show the organizer, the organizer must be added before |
| // we call exceptionsParser |
| addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); |
| organizerAdded = true; |
| exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus, |
| startTime, endTime); |
| break; |
| case Tags.CALENDAR_LOCATION: |
| cv.put(Events.EVENT_LOCATION, getValue()); |
| break; |
| case Tags.CALENDAR_RECURRENCE: |
| String rrule = recurrenceParser(); |
| if (rrule != null) { |
| cv.put(Events.RRULE, rrule); |
| } |
| break; |
| case Tags.CALENDAR_ORGANIZER_EMAIL: |
| organizerEmail = getValue(); |
| cv.put(Events.ORGANIZER, organizerEmail); |
| break; |
| case Tags.CALENDAR_SUBJECT: |
| cv.put(Events.TITLE, getValue()); |
| break; |
| case Tags.CALENDAR_SENSITIVITY: |
| cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); |
| break; |
| case Tags.CALENDAR_ORGANIZER_NAME: |
| organizerName = getValue(); |
| break; |
| case Tags.CALENDAR_REMINDER_MINS_BEFORE: |
| // Save away whether this tag has content; Exchange 2010 sends an empty tag |
| // rather than not sending one (as with Ex07 and Ex03) |
| boolean hasContent = !noContent; |
| reminderMins = getValueInt(); |
| if (hasContent) { |
| ops.newReminder(reminderMins); |
| cv.put(Events.HAS_ALARM, 1); |
| } |
| break; |
| // The following are fields we should save (for changes), though they don't |
| // relate to data used by CalendarProvider at this point |
| case Tags.CALENDAR_UID: |
| cv.put(Events.SYNC_DATA2, getValue()); |
| break; |
| case Tags.CALENDAR_DTSTAMP: |
| dtStamp = getValue(); |
| break; |
| case Tags.CALENDAR_MEETING_STATUS: |
| ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue()); |
| break; |
| case Tags.CALENDAR_BUSY_STATUS: |
| // We'll set the user's status in the Attendees table below |
| // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate |
| // attendee! |
| busyStatus = getValueInt(); |
| break; |
| case Tags.CALENDAR_RESPONSE_TYPE: |
| // EAS 14+ uses this for the user's response status; we'll use this instead |
| // of busy status, if it appears |
| responseType = getValueInt(); |
| break; |
| case Tags.CALENDAR_CATEGORIES: |
| String categories = categoriesParser(); |
| if (categories.length() > 0) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| // Enforce CalendarProvider required properties |
| setTimeRelatedValues(cv, startTime, endTime, allDayEvent); |
| |
| // Set user's availability |
| cv.put(Events.AVAILABILITY, CalendarUtilities.availabilityFromBusyStatus(busyStatus)); |
| |
| // If we haven't added the organizer to attendees, do it now |
| if (!organizerAdded) { |
| addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail); |
| } |
| |
| // Note that organizerEmail can be null with a DTSTAMP only change from the server |
| boolean selfOrganizer = (mAccount.mEmailAddress.equals(organizerEmail)); |
| |
| // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties |
| // If the user is an attendee, set the attendee status using busyStatus (note that the |
| // busyStatus is inherited from the parent unless it's specified in the exception) |
| // Add the insert/update operation for each attendee (based on whether it's add/change) |
| int numAttendees = attendeeValues.size(); |
| if (numAttendees > MAX_SYNCED_ATTENDEES) { |
| // Indicate that we've redacted attendees. If we're the organizer, disable edit |
| // by setting organizerEmail to a bogus value and by setting the upsync prohibited |
| // extended properly. |
| // Note that we don't set ANY attendees if we're in this branch; however, the |
| // organizer has already been included above, and WILL show up (which is good) |
| if (eventId < 0) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1"); |
| if (selfOrganizer) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1"); |
| } |
| } else { |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId); |
| if (selfOrganizer) { |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1", |
| eventId); |
| } |
| } |
| if (selfOrganizer) { |
| organizerEmail = BOGUS_ORGANIZER_EMAIL; |
| cv.put(Events.ORGANIZER, organizerEmail); |
| } |
| // Tell UI that we don't have any attendees |
| cv.put(Events.HAS_ATTENDEE_DATA, "0"); |
| LogUtils.d(TAG, "Maximum number of attendees exceeded; redacting"); |
| } else if (numAttendees > 0) { |
| StringBuilder sb = new StringBuilder(); |
| for (ContentValues attendee: attendeeValues) { |
| String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL); |
| sb.append(attendeeEmail); |
| sb.append(ATTENDEE_TOKENIZER_DELIMITER); |
| if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { |
| int attendeeStatus; |
| // We'll use the response type (EAS 14), if we've got one; otherwise, we'll |
| // try to infer it from busy status |
| if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) { |
| attendeeStatus = |
| CalendarUtilities.attendeeStatusFromResponseType(responseType); |
| } else if (!update) { |
| // For new events in EAS < 14, we have no idea what the busy status |
| // means, so we show "none", allowing the user to select an option. |
| attendeeStatus = Attendees.ATTENDEE_STATUS_NONE; |
| } else { |
| // For updated events, we'll try to infer the attendee status from the |
| // busy status |
| attendeeStatus = |
| CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus); |
| } |
| attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus); |
| // If we're an attendee, save away our initial attendee status in the |
| // event's ExtendedProperties (we look for differences between this and |
| // the user's current attendee status to determine whether an email needs |
| // to be sent to the organizer) |
| // organizerEmail will be null in the case that this is an attendees-only |
| // change from the server |
| if (organizerEmail == null || |
| !organizerEmail.equalsIgnoreCase(attendeeEmail)) { |
| if (eventId < 0) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, |
| Integer.toString(attendeeStatus)); |
| } else { |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS, |
| Integer.toString(attendeeStatus), eventId); |
| |
| } |
| } |
| } |
| if (eventId < 0) { |
| ops.newAttendee(attendee); |
| } else { |
| ops.updatedAttendee(attendee, eventId); |
| } |
| } |
| if (eventId < 0) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString()); |
| ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0"); |
| ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0"); |
| } else { |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(), |
| eventId); |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId); |
| ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId); |
| } |
| } |
| |
| // Put the real event in the proper place in the ops ArrayList |
| if (eventOffset >= 0) { |
| // Store away the DTSTAMP here |
| if (dtStamp != null) { |
| ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp); |
| } |
| |
| if (isValidEventValues(cv)) { |
| ops.set(eventOffset, |
| new Operation(ContentProviderOperation |
| .newInsert(mAsSyncAdapterEvents).withValues(cv))); |
| } else { |
| // If we can't add this event (it's invalid), remove all of the inserts |
| // we've built for it |
| int cnt = ops.mCount - eventOffset; |
| userLog(TAG, "Removing " + cnt + " inserts from mOps"); |
| for (int i = 0; i < cnt; i++) { |
| ops.remove(eventOffset); |
| } |
| ops.mCount = eventOffset; |
| // If this is a change, we need to also remove the deletion that comes |
| // before the addition |
| if (deleteOffset >= 0) { |
| // Remove the deletion |
| ops.remove(deleteOffset); |
| // And the deletion of exceptions |
| ops.remove(deleteOffset); |
| userLog(TAG, "Removing deletion ops from mOps"); |
| ops.mCount = deleteOffset; |
| } |
| } |
| } |
| // Mark the end of the event |
| addSeparatorOperation(ops, Events.CONTENT_URI); |
| } |
| |
| private void logEventColumns(ContentValues cv, String reason) { |
| if (Eas.USER_LOG) { |
| StringBuilder sb = |
| new StringBuilder("Event invalid, " + reason + ", skipping: Columns = "); |
| for (Entry<String, Object> entry: cv.valueSet()) { |
| sb.append(entry.getKey()); |
| sb.append('/'); |
| } |
| userLog(TAG, sb.toString()); |
| } |
| } |
| |
| /*package*/ boolean isValidEventValues(ContentValues cv) { |
| boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME); |
| // All events require DTSTART |
| if (!cv.containsKey(Events.DTSTART)) { |
| logEventColumns(cv, "DTSTART missing"); |
| return false; |
| // If we're a top-level event, we must have _SYNC_DATA (uid) |
| } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) { |
| logEventColumns(cv, "_SYNC_DATA missing"); |
| return false; |
| // We must also have DTEND or DURATION if we're not an exception |
| } else if (!isException && !cv.containsKey(Events.DTEND) && |
| !cv.containsKey(Events.DURATION)) { |
| logEventColumns(cv, "DTEND/DURATION missing"); |
| return false; |
| // Exceptions require DTEND |
| } else if (isException && !cv.containsKey(Events.DTEND)) { |
| logEventColumns(cv, "Exception missing DTEND"); |
| return false; |
| // If this is a recurrence, we need a DURATION (in days if an all-day event) |
| } else if (cv.containsKey(Events.RRULE)) { |
| String duration = cv.getAsString(Events.DURATION); |
| if (duration == null) return false; |
| if (cv.containsKey(Events.ALL_DAY)) { |
| Integer ade = cv.getAsInteger(Events.ALL_DAY); |
| if (ade != null && ade != 0 && !duration.endsWith("D")) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| public String recurrenceParser() throws IOException { |
| // Turn this information into an RRULE |
| int type = -1; |
| int occurrences = -1; |
| int interval = -1; |
| int dow = -1; |
| int dom = -1; |
| int wom = -1; |
| int moy = -1; |
| String until = null; |
| |
| while (nextTag(Tags.CALENDAR_RECURRENCE) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_RECURRENCE_TYPE: |
| type = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_INTERVAL: |
| interval = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_OCCURRENCES: |
| occurrences = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_DAYOFWEEK: |
| dow = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_DAYOFMONTH: |
| dom = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH: |
| wom = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR: |
| moy = getValueInt(); |
| break; |
| case Tags.CALENDAR_RECURRENCE_UNTIL: |
| until = getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval, |
| dow, dom, wom, moy, until); |
| } |
| |
| private void exceptionParser(CalendarOperations ops, ContentValues parentCv, |
| ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, |
| long startTime, long endTime) throws IOException { |
| ContentValues cv = new ContentValues(); |
| cv.put(Events.CALENDAR_ID, mCalendarId); |
| |
| // It appears that these values have to be copied from the parent if they are to appear |
| // Note that they can be overridden below |
| cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER)); |
| cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE)); |
| cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION)); |
| cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY)); |
| cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION)); |
| cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL)); |
| cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE)); |
| // Exceptions should always have this set to zero, since EAS has no concept of |
| // separate attendee lists for exceptions; if we fail to do this, then the UI will |
| // allow the user to change attendee data, and this change would never get reflected |
| // on the server. |
| cv.put(Events.HAS_ATTENDEE_DATA, 0); |
| |
| int allDayEvent = 0; |
| |
| // This column is the key that links the exception to the serverId |
| cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID)); |
| |
| String exceptionStartTime = "_noStartTime"; |
| while (nextTag(Tags.CALENDAR_EXCEPTION) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_ATTACHMENTS: |
| attachmentsParser(); |
| break; |
| case Tags.CALENDAR_EXCEPTION_START_TIME: |
| final String valueStr = getValue(); |
| try { |
| cv.put(Events.ORIGINAL_INSTANCE_TIME, |
| Utility.parseDateTimeToMillis(valueStr)); |
| exceptionStartTime = valueStr; |
| } catch (ParseException e) { |
| LogUtils.w(TAG, "Parse error for CALENDAR_EXCEPTION_START_TIME tag.", e); |
| } |
| break; |
| case Tags.CALENDAR_EXCEPTION_IS_DELETED: |
| if (getValueInt() == 1) { |
| cv.put(Events.STATUS, Events.STATUS_CANCELED); |
| } |
| break; |
| case Tags.CALENDAR_ALL_DAY_EVENT: |
| allDayEvent = getValueInt(); |
| cv.put(Events.ALL_DAY, allDayEvent); |
| break; |
| case Tags.BASE_BODY: |
| cv.put(Events.DESCRIPTION, bodyParser()); |
| break; |
| case Tags.CALENDAR_BODY: |
| cv.put(Events.DESCRIPTION, getValue()); |
| break; |
| case Tags.CALENDAR_START_TIME: |
| try { |
| startTime = Utility.parseDateTimeToMillis(getValue()); |
| } catch (ParseException e) { |
| LogUtils.w(TAG, "Parse error for CALENDAR_START_TIME tag.", e); |
| } |
| break; |
| case Tags.CALENDAR_END_TIME: |
| try { |
| endTime = Utility.parseDateTimeToMillis(getValue()); |
| } catch (ParseException e) { |
| LogUtils.w(TAG, "Parse error for CALENDAR_END_TIME tag.", e); |
| } |
| break; |
| case Tags.CALENDAR_LOCATION: |
| cv.put(Events.EVENT_LOCATION, getValue()); |
| break; |
| case Tags.CALENDAR_RECURRENCE: |
| String rrule = recurrenceParser(); |
| if (rrule != null) { |
| cv.put(Events.RRULE, rrule); |
| } |
| break; |
| case Tags.CALENDAR_SUBJECT: |
| cv.put(Events.TITLE, getValue()); |
| break; |
| case Tags.CALENDAR_SENSITIVITY: |
| cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt())); |
| break; |
| case Tags.CALENDAR_BUSY_STATUS: |
| busyStatus = getValueInt(); |
| // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate |
| // attendee! |
| break; |
| // TODO How to handle these items that are linked to event id! |
| // case Tags.CALENDAR_DTSTAMP: |
| // ops.newExtendedProperty("dtstamp", getValue()); |
| // break; |
| // case Tags.CALENDAR_REMINDER_MINS_BEFORE: |
| // ops.newReminder(getValueInt()); |
| // break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| // We need a _sync_id, but it can't be the parent's id, so we generate one |
| cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' + |
| exceptionStartTime); |
| |
| // Enforce CalendarProvider required properties |
| setTimeRelatedValues(cv, startTime, endTime, allDayEvent); |
| |
| // Don't insert an invalid exception event |
| if (!isValidEventValues(cv)) return; |
| |
| // Add the exception insert |
| int exceptionStart = ops.mCount; |
| ops.newException(cv); |
| // Also add the attendees, because they need to be copied over from the parent event |
| boolean attendeesRedacted = false; |
| if (attendeeValues != null) { |
| for (ContentValues attValues: attendeeValues) { |
| // If this is the user, use his busy status for attendee status |
| String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL); |
| // Note that the exception at which we surpass the redaction limit might have |
| // any number of attendees shown; since this is an edge case and a workaround, |
| // it seems to be an acceptable implementation |
| if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) { |
| attValues.put(Attendees.ATTENDEE_STATUS, |
| CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus)); |
| ops.newAttendee(attValues, exceptionStart); |
| } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) { |
| ops.newAttendee(attValues, exceptionStart); |
| } else { |
| attendeesRedacted = true; |
| } |
| } |
| } |
| // And add the parent's reminder value |
| if (reminderMins > 0) { |
| ops.newReminder(reminderMins, exceptionStart); |
| } |
| if (attendeesRedacted) { |
| LogUtils.d(TAG, "Attendees redacted in this exception"); |
| } |
| } |
| |
| private static int encodeVisibility(int easVisibility) { |
| int visibility = 0; |
| switch(easVisibility) { |
| case 0: |
| visibility = Events.ACCESS_DEFAULT; |
| break; |
| case 1: |
| visibility = Events.ACCESS_PUBLIC; |
| break; |
| case 2: |
| visibility = Events.ACCESS_PRIVATE; |
| break; |
| case 3: |
| visibility = Events.ACCESS_CONFIDENTIAL; |
| break; |
| } |
| return visibility; |
| } |
| |
| private void exceptionsParser(CalendarOperations ops, ContentValues cv, |
| ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, |
| long startTime, long endTime) throws IOException { |
| while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_EXCEPTION: |
| exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus, |
| startTime, endTime); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private String categoriesParser() throws IOException { |
| StringBuilder categories = new StringBuilder(); |
| while (nextTag(Tags.CALENDAR_CATEGORIES) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_CATEGORY: |
| // TODO Handle categories (there's no similar concept for gdata AFAIK) |
| // We need to save them and spit them back when we update the event |
| categories.append(getValue()); |
| categories.append(CATEGORY_TOKENIZER_DELIMITER); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| return categories.toString(); |
| } |
| |
| /** |
| * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14 |
| */ |
| private void attachmentsParser() throws IOException { |
| while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_ATTACHMENT: |
| skipParser(Tags.CALENDAR_ATTACHMENT); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private ArrayList<ContentValues> attendeesParser() |
| throws IOException { |
| int attendeeCount = 0; |
| ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>(); |
| while (nextTag(Tags.CALENDAR_ATTENDEES) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_ATTENDEE: |
| ContentValues cv = attendeeParser(); |
| // If we're going to redact these attendees anyway, let's avoid unnecessary |
| // memory pressure, and not keep them around |
| // We still need to parse them all, however |
| attendeeCount++; |
| // Allow one more than MAX_ATTENDEES, so that the check for "too many" will |
| // succeed in addEvent |
| if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) { |
| attendeeValues.add(cv); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| return attendeeValues; |
| } |
| |
| private ContentValues attendeeParser() |
| throws IOException { |
| ContentValues cv = new ContentValues(); |
| while (nextTag(Tags.CALENDAR_ATTENDEE) != END) { |
| switch (tag) { |
| case Tags.CALENDAR_ATTENDEE_EMAIL: |
| cv.put(Attendees.ATTENDEE_EMAIL, getValue()); |
| break; |
| case Tags.CALENDAR_ATTENDEE_NAME: |
| cv.put(Attendees.ATTENDEE_NAME, getValue()); |
| break; |
| case Tags.CALENDAR_ATTENDEE_STATUS: |
| int status = getValueInt(); |
| cv.put(Attendees.ATTENDEE_STATUS, |
| (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE : |
| (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED : |
| (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED : |
| (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED : |
| Attendees.ATTENDEE_STATUS_NONE); |
| break; |
| case Tags.CALENDAR_ATTENDEE_TYPE: |
| int type = Attendees.TYPE_NONE; |
| // EAS types: 1 = req'd, 2 = opt, 3 = resource |
| switch (getValueInt()) { |
| case 1: |
| type = Attendees.TYPE_REQUIRED; |
| break; |
| case 2: |
| type = Attendees.TYPE_OPTIONAL; |
| break; |
| } |
| cv.put(Attendees.ATTENDEE_TYPE, type); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE); |
| return cv; |
| } |
| |
| private String bodyParser() throws IOException { |
| String body = null; |
| while (nextTag(Tags.BASE_BODY) != END) { |
| switch (tag) { |
| case Tags.BASE_DATA: |
| body = getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| // Handle null data without error |
| if (body == null) return ""; |
| // Remove \r's from any body text |
| return body.replace("\r\n", "\n"); |
| } |
| |
| public void addParser(CalendarOperations ops) throws IOException { |
| String serverId = null; |
| while (nextTag(Tags.SYNC_ADD) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: // same as |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_APPLICATION_DATA: |
| addEvent(ops, serverId, false); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| private Cursor getServerIdCursor(String serverId) { |
| return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, |
| SERVER_ID_AND_CALENDAR_ID, new String[] {serverId, Long.toString(mCalendarId)}, |
| null); |
| } |
| |
| private Cursor getClientIdCursor(String clientId) { |
| mBindArgument[0] = clientId; |
| return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, CLIENT_ID_SELECTION, |
| mBindArgument, null); |
| } |
| |
| public void deleteParser(CalendarOperations ops) throws IOException { |
| while (nextTag(Tags.SYNC_DELETE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| String serverId = getValue(); |
| // Find the event with the given serverId |
| Cursor c = getServerIdCursor(serverId); |
| try { |
| if (c.moveToFirst()) { |
| userLog("Deleting ", serverId); |
| ops.delete(c.getLong(0), serverId); |
| } |
| } finally { |
| c.close(); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| /** |
| * A change is handled as a delete (including all exceptions) and an add |
| * This isn't as efficient as attempting to traverse the original and all of its exceptions, |
| * but changes happen infrequently and this code is both simpler and easier to maintain |
| * @param ops the array of pending ContactProviderOperations. |
| * @throws IOException |
| */ |
| public void changeParser(CalendarOperations ops) throws IOException { |
| String serverId = null; |
| while (nextTag(Tags.SYNC_CHANGE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_APPLICATION_DATA: |
| userLog("Changing " + serverId); |
| addEvent(ops, serverId, true); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| } |
| |
| @Override |
| public void commandsParser() throws IOException { |
| while (nextTag(Tags.SYNC_COMMANDS) != END) { |
| if (tag == Tags.SYNC_ADD) { |
| addParser(mOps); |
| } else if (tag == Tags.SYNC_DELETE) { |
| deleteParser(mOps); |
| } else if (tag == Tags.SYNC_CHANGE) { |
| changeParser(mOps); |
| } else |
| skipTag(); |
| } |
| } |
| |
| @Override |
| public void commit() throws IOException { |
| userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey); |
| // Save the syncKey here, using the Helper provider by Calendar provider |
| mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation( |
| asSyncAdapter(SyncState.CONTENT_URI, mAccount.mEmailAddress, |
| Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), |
| mAccountManagerAccount, |
| mMailbox.mSyncKey.getBytes()))); |
| |
| // Execute our CPO's safely |
| try { |
| safeExecute(mContentResolver, CalendarContract.AUTHORITY, mOps); |
| } catch (RemoteException e) { |
| throw new IOException("Remote exception caught; will retry"); |
| } |
| } |
| |
| public void addResponsesParser() throws IOException { |
| String serverId = null; |
| String clientId = null; |
| int status = -1; |
| ContentValues cv = new ContentValues(); |
| while (nextTag(Tags.SYNC_ADD) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_CLIENT_ID: |
| clientId = getValue(); |
| break; |
| case Tags.SYNC_STATUS: |
| status = getValueInt(); |
| if (status != 1) { |
| userLog("Attempt to add event failed with status: " + status); |
| } |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| |
| if (clientId == null) return; |
| if (serverId == null) { |
| // TODO Reconsider how to handle this |
| serverId = "FAIL:" + status; |
| } |
| |
| Cursor c = getClientIdCursor(clientId); |
| try { |
| if (c.moveToFirst()) { |
| cv.put(Events._SYNC_ID, serverId); |
| cv.put(Events.SYNC_DATA2, clientId); |
| long id = c.getLong(0); |
| // Write the serverId into the Event |
| mOps.add(new Operation(ContentProviderOperation |
| .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id)) |
| .withValues(cv))); |
| userLog("New event " + clientId + " was given serverId: " + serverId); |
| } |
| } finally { |
| c.close(); |
| } |
| } |
| |
| public void changeResponsesParser() throws IOException { |
| String serverId = null; |
| String status = null; |
| while (nextTag(Tags.SYNC_CHANGE) != END) { |
| switch (tag) { |
| case Tags.SYNC_SERVER_ID: |
| serverId = getValue(); |
| break; |
| case Tags.SYNC_STATUS: |
| status = getValue(); |
| break; |
| default: |
| skipTag(); |
| } |
| } |
| if (serverId != null && status != null) { |
| userLog("Changed event " + serverId + " failed with status: " + status); |
| } |
| } |
| |
| |
| @Override |
| public void responsesParser() throws IOException { |
| // Handle server responses here (for Add and Change) |
| while (nextTag(Tags.SYNC_RESPONSES) != END) { |
| if (tag == Tags.SYNC_ADD) { |
| addResponsesParser(); |
| } else if (tag == Tags.SYNC_CHANGE) { |
| changeResponsesParser(); |
| } else |
| skipTag(); |
| } |
| } |
| |
| /** |
| * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties, |
| * and we just return quickly if the service has already been stopped. |
| */ |
| private static ContentProviderResult[] execute(final ContentResolver contentResolver, |
| final String authority, final ArrayList<ContentProviderOperation> ops) |
| throws RemoteException, OperationApplicationException { |
| if (!ops.isEmpty()) { |
| try { |
| ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); |
| //mService.userLog("Results: " + result.length); |
| return result; |
| } catch (IllegalArgumentException e) { |
| // Thrown when Calendar Provider is disabled |
| LogUtils.e(TAG, "Error executing operation; provider is disabled.", e); |
| } |
| } |
| return new ContentProviderResult[0]; |
| } |
| |
| /** |
| * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the |
| * passed-in offset |
| */ |
| @VisibleForTesting |
| static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) { |
| if (op.mOp != null) { |
| return op.mOp; |
| } else if (op.mBuilder == null) { |
| throw new IllegalArgumentException("Operation must have CPO.Builder"); |
| } |
| ContentProviderOperation.Builder builder = op.mBuilder; |
| if (op.mColumnName != null) { |
| builder.withValueBackReference(op.mColumnName, op.mOffset - offset); |
| } |
| return builder.build(); |
| } |
| |
| /** |
| * Create a list of CPOs from a list of Operations, and then apply them in a batch |
| */ |
| private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> ops, final int offset) |
| throws RemoteException, OperationApplicationException { |
| // Handle the empty case |
| if (ops.isEmpty()) { |
| return new ContentProviderResult[0]; |
| } |
| ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>(); |
| for (Operation op: ops) { |
| cpos.add(operationToContentProviderOperation(op, offset)); |
| } |
| return execute(contentResolver, authority, cpos); |
| } |
| |
| /** |
| * Apply the list of CPO's in the provider and copy the "mini" result into our full result array |
| */ |
| private static void applyAndCopyResults(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> mini, |
| final ContentProviderResult[] result, final int offset) throws RemoteException { |
| // Empty lists are ok; we just ignore them |
| if (mini.isEmpty()) return; |
| try { |
| ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini, |
| offset); |
| // Copy the results from this mini-batch into our results array |
| System.arraycopy(miniResult, 0, result, offset, miniResult.length); |
| } catch (OperationApplicationException e) { |
| // Not possible since we're building the ops ourselves |
| } |
| } |
| |
| /** |
| * Called by a sync adapter to execute a list of Operations in the ContentProvider handling |
| * the passed-in authority. If the attempt to apply the batch fails due to a too-large |
| * binder transaction, we split the Operations as directed by separators. If any of the |
| * "mini" batches fails due to a too-large transaction, we're screwed, but this would be |
| * vanishingly rare. Other, possibly transient, errors are handled by throwing a |
| * RemoteException, which the caller will likely re-throw as an IOException so that the sync |
| * can be attempted again. |
| * |
| * Callers MAY leave a dangling separator at the end of the list; note that the separators |
| * themselves are only markers and are not sent to the provider. |
| */ |
| protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> ops) throws RemoteException { |
| //mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority); |
| ContentProviderResult[] result = null; |
| try { |
| // Try to execute the whole thing |
| return applyBatch(contentResolver, authority, ops, 0); |
| } catch (TransactionTooLargeException e) { |
| // Nope; split into smaller chunks, demarcated by the separator operation |
| //mService.userLog("Transaction too large; spliting!"); |
| ArrayList<Operation> mini = new ArrayList<Operation>(); |
| // Build a result array with the total size we're sending |
| result = new ContentProviderResult[ops.size()]; |
| int count = 0; |
| int offset = 0; |
| for (Operation op: ops) { |
| if (op.mSeparator) { |
| //mService.userLog("Try mini-batch of ", mini.size(), " CPO's"); |
| applyAndCopyResults(contentResolver, authority, mini, result, offset); |
| mini.clear(); |
| // Save away the offset here; this will need to be subtracted out of the |
| // value originally set by the adapter |
| offset = count + 1; // Remember to add 1 for the separator! |
| } else { |
| mini.add(op); |
| } |
| count++; |
| } |
| // Check out what's left; if it's more than just a separator, apply the batch |
| int miniSize = mini.size(); |
| if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) { |
| applyAndCopyResults(contentResolver, authority, mini, result, offset); |
| } |
| } catch (RemoteException e) { |
| throw e; |
| } catch (OperationApplicationException e) { |
| // Not possible since we're building the ops ourselves |
| } |
| return result; |
| } |
| |
| /** |
| * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's |
| */ |
| protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { |
| Operation op = new Operation( |
| ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); |
| op.mSeparator = true; |
| ops.add(op); |
| } |
| |
| @Override |
| protected void wipe() { |
| LogUtils.w(TAG, "Wiping calendar for account %d", mAccount.mId); |
| EasSyncCalendar.wipeAccountFromContentProvider(mContext, |
| mAccount.mEmailAddress); |
| } |
| } |