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