• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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 
CalendarSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Mailbox mailbox, final Account account, final android.accounts.Account accountManagerAccount, final long calendarId)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 
CalendarOperations(final ContentResolver contentResolver, final Uri asSyncAdapterAttendees, final Uri asSyncAdapterEvents, final Uri asSyncAdapterReminders, final Uri asSyncAdapterExtendedProperties)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
add(Operation op)144         public boolean add(Operation op) {
145             super.add(op);
146             mCount++;
147             return true;
148         }
149 
newEvent(Operation op)150         public int newEvent(Operation op) {
151             mEventStart = mCount;
152             add(op);
153             return mEventStart;
154         }
155 
newDelete(long id, String serverId)156         public int newDelete(long id, String serverId) {
157             int offset = mCount;
158             delete(id, serverId);
159             return offset;
160         }
161 
newAttendee(ContentValues cv)162         public void newAttendee(ContentValues cv) {
163             newAttendee(cv, mEventStart);
164         }
165 
newAttendee(ContentValues cv, int eventStart)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 
updatedAttendee(ContentValues cv, long id)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 
newException(ContentValues cv)179         public void newException(ContentValues cv) {
180             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents)
181                     .withValues(cv)));
182         }
183 
newExtendedProperty(String name, String value)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 
updatedExtendedProperty(String name, String value, long id)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 
newReminder(int mins, int eventStart)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 
newReminder(int mins)229         public void newReminder(int mins) {
230             newReminder(mins, mEventStart);
231         }
232 
delete(long id, String syncId)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 
asSyncAdapter(Uri uri, String account, String accountType)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 
addOrganizerToAttendees(CalendarOperations ops, long eventId, String organizerName, String organizerEmail)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      */
setTimeRelatedValues(ContentValues cv, long startTime, long endTime, int allDayEvent)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 
addEvent(CalendarOperations ops, String serverId, boolean update)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 
logEventColumns(ContentValues cv, String reason)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 
isValidEventValues(ContentValues cv)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 
recurrenceParser()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 
exceptionParser(CalendarOperations ops, ContentValues parentCv, ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, long startTime, long endTime)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 
encodeVisibility(int easVisibility)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 
exceptionsParser(CalendarOperations ops, ContentValues cv, ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, long startTime, long endTime)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 
categoriesParser()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      */
attachmentsParser()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 
attendeesParser()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 
attendeeParser()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 
bodyParser()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 
addParser(CalendarOperations ops)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 
getServerIdCursor(String serverId)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 
getClientIdCursor(String clientId)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 
deleteParser(CalendarOperations ops)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      */
changeParser(CalendarOperations ops)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
commandsParser()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
commit()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 
addResponsesParser()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 
changeResponsesParser()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
responsesParser()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      */
execute(final ContentResolver contentResolver, final String authority, final ArrayList<ContentProviderOperation> ops)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
operationToContentProviderOperation(Operation op, int offset)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      */
applyBatch(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> ops, final int offset)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      */
applyAndCopyResults(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> mini, final ContentProviderResult[] result, final int offset)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      */
safeExecute(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> ops)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      */
addSeparatorOperation(ArrayList<Operation> ops, Uri uri)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
wipe()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