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