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