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