• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange.adapter;
19 
20 import android.content.ContentProviderClient;
21 import android.content.ContentProviderOperation;
22 import android.content.ContentProviderResult;
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Entity;
27 import android.content.Entity.NamedContentValues;
28 import android.content.EntityIterator;
29 import android.database.Cursor;
30 import android.database.DatabaseUtils;
31 import android.net.Uri;
32 import android.os.RemoteException;
33 import android.provider.CalendarContract;
34 import android.provider.CalendarContract.Attendees;
35 import android.provider.CalendarContract.Calendars;
36 import android.provider.CalendarContract.Events;
37 import android.provider.CalendarContract.EventsEntity;
38 import android.provider.CalendarContract.ExtendedProperties;
39 import android.provider.CalendarContract.Reminders;
40 import android.provider.CalendarContract.SyncState;
41 import android.provider.ContactsContract.RawContacts;
42 import android.provider.SyncStateContract;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import com.android.calendarcommon2.DateException;
47 import com.android.calendarcommon2.Duration;
48 import com.android.emailcommon.AccountManagerTypes;
49 import com.android.emailcommon.provider.EmailContent;
50 import com.android.emailcommon.provider.EmailContent.Message;
51 import com.android.emailcommon.utility.Utility;
52 import com.android.exchange.CommandStatusException;
53 import com.android.exchange.Eas;
54 import com.android.exchange.EasOutboxService;
55 import com.android.exchange.EasSyncService;
56 import com.android.exchange.ExchangeService;
57 import com.android.exchange.utility.CalendarUtilities;
58 
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.util.ArrayList;
62 import java.util.GregorianCalendar;
63 import java.util.Map.Entry;
64 import java.util.StringTokenizer;
65 import java.util.TimeZone;
66 import java.util.UUID;
67 
68 /**
69  * Sync adapter class for EAS calendars
70  *
71  */
72 public class CalendarSyncAdapter extends AbstractSyncAdapter {
73 
74     private static final String TAG = "EasCalendarSyncAdapter";
75 
76     private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
77     /**
78      * Used to keep track of exception vs parent event dirtiness.
79      */
80     private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8;
81     private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4;
82     // Since exceptions will have the same _SYNC_ID as the original event we have to check that
83     // there's no original event when finding an item by _SYNC_ID
84     private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " +
85         Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
86     private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " +
87         Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
88     private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY
89             + "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " +
90         Events.ORIGINAL_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
91     private static final String DIRTY_EXCEPTION_IN_CALENDAR =
92         Events.DIRTY + "=1 AND " + Events.ORIGINAL_ID + " NOTNULL AND " +
93         Events.CALENDAR_ID + "=?";
94     private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?";
95     private static final String ORIGINAL_EVENT_AND_CALENDAR =
96         Events.ORIGINAL_SYNC_ID + "=? AND " + Events.CALENDAR_ID + "=?";
97     private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
98         Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
99     private static final String[] ID_PROJECTION = new String[] {Events._ID};
100     private static final String[] ORIGINAL_EVENT_PROJECTION =
101         new String[] {Events.ORIGINAL_ID, Events._ID};
102     private static final String EVENT_ID_AND_NAME =
103         ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?";
104 
105     // Note that we use LIKE below for its case insensitivity
106     private static final String EVENT_AND_EMAIL  =
107         Attendees.EVENT_ID + "=? AND "+ Attendees.ATTENDEE_EMAIL + " LIKE ?";
108     private static final int ATTENDEE_STATUS_COLUMN_STATUS = 0;
109     private static final String[] ATTENDEE_STATUS_PROJECTION =
110         new String[] {Attendees.ATTENDEE_STATUS};
111 
112     public static final String CALENDAR_SELECTION =
113         Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?";
114     private static final int CALENDAR_SELECTION_ID = 0;
115 
116     private static final String[] EXTENDED_PROPERTY_PROJECTION =
117         new String[] {ExtendedProperties._ID};
118     private static final int EXTENDED_PROPERTY_ID = 0;
119 
120     private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
121     private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
122 
123     private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
124     private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
125     private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp";
126     private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status";
127     private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
128     // Used to indicate that we removed the attendee list because it was too large
129     private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted";
130     // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges)
131     private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited";
132 
133     private static final Operation PLACEHOLDER_OPERATION =
134         new Operation(ContentProviderOperation.newInsert(Uri.EMPTY));
135 
136     private static final Object sSyncKeyLock = new Object();
137 
138     private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
139     private final TimeZone mLocalTimeZone = TimeZone.getDefault();
140 
141 
142     // Maximum number of allowed attendees; above this number, we mark the Event with the
143     // attendeesRedacted extended property and don't allow the event to be upsynced to the server
144     private static final int MAX_SYNCED_ATTENDEES = 50;
145     // We set the organizer to this when the user is the organizer and we've redacted the
146     // attendee list.  By making the meeting organizer OTHER than the user, we cause the UI to
147     // prevent edits to this event (except local changes like reminder).
148     private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa";
149     // Maximum number of CPO's before we start redacting attendees in exceptions
150     // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before
151     // binder failures occur, but we need room at any point for additional events/exceptions so
152     // we set our limit at 1/3 of the apparent maximum for extra safety
153     // TODO Find a better solution to this workaround
154     private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500;
155 
156     private long mCalendarId = -1;
157     private String mCalendarIdString;
158     private String[] mCalendarIdArgument;
159     /*package*/ String mEmailAddress;
160 
161     private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
162     private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
163     private ArrayList<Long> mSendCancelIdList = new ArrayList<Long>();
164     private ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
165 
166     private final Uri mAsSyncAdapterAttendees;
167     private final Uri mAsSyncAdapterEvents;
168     private final Uri mAsSyncAdapterReminders;
169     private final Uri mAsSyncAdapterExtendedProperties;
170 
CalendarSyncAdapter(EasSyncService service)171     public CalendarSyncAdapter(EasSyncService service) {
172         super(service);
173         mEmailAddress = mAccount.mEmailAddress;
174 
175         String amType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
176         mAsSyncAdapterAttendees =
177                 asSyncAdapter(Attendees.CONTENT_URI, mEmailAddress, amType);
178         mAsSyncAdapterEvents =
179                 asSyncAdapter(Events.CONTENT_URI, mEmailAddress, amType);
180         mAsSyncAdapterReminders =
181                 asSyncAdapter(Reminders.CONTENT_URI, mEmailAddress, amType);
182         mAsSyncAdapterExtendedProperties =
183                 asSyncAdapter(ExtendedProperties.CONTENT_URI, mEmailAddress, amType);
184 
185         Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
186                 new String[] {Calendars._ID}, CALENDAR_SELECTION,
187                 new String[] {mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
188         if (c == null) return;
189         try {
190             if (c.moveToFirst()) {
191                 mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
192             } else {
193                 mCalendarId = CalendarUtilities.createCalendar(mService, mAccount, mMailbox);
194             }
195             mCalendarIdString = Long.toString(mCalendarId);
196             mCalendarIdArgument = new String[] {mCalendarIdString};
197         } finally {
198             c.close();
199         }
200         }
201 
202     @Override
getCollectionName()203     public String getCollectionName() {
204         return "Calendar";
205     }
206 
207     @Override
cleanup()208     public void cleanup() {
209     }
210 
211     @Override
wipe()212     public void wipe() {
213         // Delete the calendar associated with this account
214         // CalendarProvider2 does NOT handle selection arguments in deletions
215         mContentResolver.delete(
216                 asSyncAdapter(Calendars.CONTENT_URI, mEmailAddress,
217                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
218                 Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(mEmailAddress)
219                         + " AND " + Calendars.ACCOUNT_TYPE + "="
220                         + DatabaseUtils.sqlEscapeString(AccountManagerTypes.TYPE_EXCHANGE), null);
221         // Invalidate our calendar observers
222         ExchangeService.unregisterCalendarObservers();
223     }
224 
225     @Override
sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)226     public void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)
227             throws IOException  {
228         if (!initialSync) {
229             setPimSyncOptions(protocolVersion, Eas.FILTER_2_WEEKS, s);
230         }
231     }
232 
233     @Override
isSyncable()234     public boolean isSyncable() {
235         return ContentResolver.getSyncAutomatically(mAccountManagerAccount,
236                 CalendarContract.AUTHORITY);
237     }
238 
239     @Override
parse(InputStream is)240     public boolean parse(InputStream is) throws IOException, CommandStatusException {
241         EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
242         return p.parse();
243     }
244 
asSyncAdapter(Uri uri, String account, String accountType)245     public static Uri asSyncAdapter(Uri uri, String account, String accountType) {
246         return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
247                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
248                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
249     }
250 
251     /**
252      * Generate the uri for the data row associated with this NamedContentValues object
253      * @param ncv the NamedContentValues object
254      * @return a uri that can be used to refer to this row
255      */
dataUriFromNamedContentValues(NamedContentValues ncv)256     public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
257         long id = ncv.values.getAsLong(RawContacts._ID);
258         Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
259         return dataUri;
260     }
261 
262     /**
263      * We get our SyncKey from CalendarProvider.  If there's not one, we set it to "0" (the reset
264      * state) and save that away.
265      */
266     @Override
getSyncKey()267     public String getSyncKey() throws IOException {
268         synchronized (sSyncKeyLock) {
269             ContentProviderClient client = mService.mContentResolver
270                     .acquireContentProviderClient(CalendarContract.CONTENT_URI);
271             try {
272                 byte[] data = SyncStateContract.Helpers.get(
273                         client,
274                         asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
275                                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount);
276                 if (data == null || data.length == 0) {
277                     // Initialize the SyncKey
278                     setSyncKey("0", false);
279                     return "0";
280                 } else {
281                     String syncKey = new String(data);
282                     userLog("SyncKey retrieved as ", syncKey, " from CalendarProvider");
283                     return syncKey;
284                 }
285             } catch (RemoteException e) {
286                 throw new IOException("Can't get SyncKey from CalendarProvider");
287             }
288         }
289     }
290 
291     /**
292      * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
293      * cases, the SyncKey is set within Calendar
294      */
295     @Override
setSyncKey(String syncKey, boolean inCommands)296     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
297         synchronized (sSyncKeyLock) {
298             if ("0".equals(syncKey) || !inCommands) {
299                 ContentProviderClient client = mService.mContentResolver
300                         .acquireContentProviderClient(CalendarContract.CONTENT_URI);
301                 try {
302                     SyncStateContract.Helpers.set(
303                             client,
304                             asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
305                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount,
306                             syncKey.getBytes());
307                     userLog("SyncKey set to ", syncKey, " in CalendarProvider");
308                 } catch (RemoteException e) {
309                     throw new IOException("Can't set SyncKey in CalendarProvider");
310                 }
311             }
312             mMailbox.mSyncKey = syncKey;
313         }
314     }
315 
316     public class EasCalendarSyncParser extends AbstractSyncParser {
317 
318         String[] mBindArgument = new String[1];
319         Uri mAccountUri;
320         CalendarOperations mOps = new CalendarOperations();
321 
EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)322         public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
323                 throws IOException {
324             super(in, adapter);
325             setLoggingTag("CalendarParser");
326             mAccountUri = Events.CONTENT_URI;
327         }
328 
addOrganizerToAttendees(CalendarOperations ops, long eventId, String organizerName, String organizerEmail)329         private void addOrganizerToAttendees(CalendarOperations ops, long eventId,
330                 String organizerName, String organizerEmail) {
331             // Handle the organizer (who IS an attendee on device, but NOT in EAS)
332             if (organizerName != null || organizerEmail != null) {
333                 ContentValues attendeeCv = new ContentValues();
334                 if (organizerName != null) {
335                     attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName);
336                 }
337                 if (organizerEmail != null) {
338                     attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
339                 }
340                 attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
341                 attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
342                 attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
343                 if (eventId < 0) {
344                     ops.newAttendee(attendeeCv);
345                 } else {
346                     ops.updatedAttendee(attendeeCv, eventId);
347                 }
348             }
349         }
350 
351         /**
352          * Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event
353          * The follow rules are enforced by CalendarProvider2:
354          *   Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION
355          *   Recurring events (i.e. events with RRULE) must have a DURATION
356          *   All-day recurring events MUST have a DURATION that is in the form P<n>D
357          *   Other events MAY have a DURATION in any valid form (we use P<n>M)
358          *   All-day events MUST have hour, minute, and second = 0; in addition, they must have
359          *   the EVENT_TIMEZONE set to UTC
360          *   Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has
361          *   hour, minute, and second = 0 and be set in UTC
362          * @param cv the ContentValues for the Event
363          * @param startTime the start time for the Event
364          * @param endTime the end time for the Event
365          * @param allDayEvent whether this is an all day event (1) or not (0)
366          */
setTimeRelatedValues(ContentValues cv, long startTime, long endTime, int allDayEvent)367         /*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime,
368                 int allDayEvent) {
369             // If there's no startTime, the event will be found to be invalid, so return
370             if (startTime < 0) return;
371             // EAS events can arrive without an end time, but CalendarProvider requires them
372             // so we'll default to 30 minutes; this will be superceded if this is an all-day event
373             if (endTime < 0) endTime = startTime + (30*MINUTES);
374 
375             // If this is an all-day event, set hour, minute, and second to zero, and use UTC
376             if (allDayEvent != 0) {
377                 startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone);
378                 endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone);
379                 String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE);
380                 cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone);
381                 cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
382             }
383 
384             // If this is an exception, and the original was an all-day event, make sure the
385             // original instance time has hour, minute, and second set to zero, and is in UTC
386             if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) &&
387                     cv.containsKey(Events.ORIGINAL_ALL_DAY)) {
388                 Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY);
389                 if (ade != null && ade != 0) {
390                     long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
391                     GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
392                     cal.setTimeInMillis(exceptionTime);
393                     cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
394                     cal.set(GregorianCalendar.MINUTE, 0);
395                     cal.set(GregorianCalendar.SECOND, 0);
396                     cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis());
397                 }
398             }
399 
400             // Always set DTSTART
401             cv.put(Events.DTSTART, startTime);
402             // For recurring events, set DURATION.  Use P<n>D format for all day events
403             if (cv.containsKey(Events.RRULE)) {
404                 if (allDayEvent != 0) {
405                     cv.put(Events.DURATION, "P" + ((endTime - startTime) / DAYS) + "D");
406                 }
407                 else {
408                     cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
409                 }
410             // For other events, set DTEND and LAST_DATE
411             } else {
412                 cv.put(Events.DTEND, endTime);
413                 cv.put(Events.LAST_DATE, endTime);
414             }
415         }
416 
addEvent(CalendarOperations ops, String serverId, boolean update)417         public void addEvent(CalendarOperations ops, String serverId, boolean update)
418                 throws IOException {
419             ContentValues cv = new ContentValues();
420             cv.put(Events.CALENDAR_ID, mCalendarId);
421             cv.put(Events._SYNC_ID, serverId);
422             cv.put(Events.HAS_ATTENDEE_DATA, 1);
423             cv.put(Events.SYNC_DATA2, "0");
424 
425             int allDayEvent = 0;
426             String organizerName = null;
427             String organizerEmail = null;
428             int eventOffset = -1;
429             int deleteOffset = -1;
430             int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
431             int responseType = CalendarUtilities.RESPONSE_TYPE_NONE;
432 
433             boolean firstTag = true;
434             long eventId = -1;
435             long startTime = -1;
436             long endTime = -1;
437             TimeZone timeZone = null;
438 
439             // Keep track of the attendees; exceptions will need them
440             ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
441             int reminderMins = -1;
442             String dtStamp = null;
443             boolean organizerAdded = false;
444 
445             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
446                 if (update && firstTag) {
447                     // Find the event that's being updated
448                     Cursor c = getServerIdCursor(serverId);
449                     long id = -1;
450                     try {
451                         if (c != null && c.moveToFirst()) {
452                             id = c.getLong(0);
453                         }
454                     } finally {
455                         if (c != null) c.close();
456                     }
457                     if (id > 0) {
458                         // DTSTAMP can come first, and we simply need to track it
459                         if (tag == Tags.CALENDAR_DTSTAMP) {
460                             dtStamp = getValue();
461                             continue;
462                         } else if (tag == Tags.CALENDAR_ATTENDEES) {
463                             // This is an attendees-only update; just
464                             // delete/re-add attendees
465                             mBindArgument[0] = Long.toString(id);
466                             ops.add(new Operation(ContentProviderOperation
467                                     .newDelete(mAsSyncAdapterAttendees)
468                                     .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)));
469                             eventId = id;
470                         } else {
471                             // Otherwise, delete the original event and recreate it
472                             userLog("Changing (delete/add) event ", serverId);
473                             deleteOffset = ops.newDelete(id, serverId);
474                             // Add a placeholder event so that associated tables can reference
475                             // this as a back reference.  We add the event at the end of the method
476                             eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
477                         }
478                     } else {
479                         // The changed item isn't found. We'll treat this as a new item
480                         eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
481                         userLog(TAG, "Changed item not found; treating as new.");
482                     }
483                 } else if (firstTag) {
484                     // Add a placeholder event so that associated tables can reference
485                     // this as a back reference.  We add the event at the end of the method
486                    eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
487                 }
488                 firstTag = false;
489                 switch (tag) {
490                     case Tags.CALENDAR_ALL_DAY_EVENT:
491                         allDayEvent = getValueInt();
492                         if (allDayEvent != 0 && timeZone != null) {
493                             // If the event doesn't start at midnight local time, we won't consider
494                             // this an all-day event in the local time zone (this is what OWA does)
495                             GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone);
496                             cal.setTimeInMillis(startTime);
497                             userLog("All-day event arrived in: " + timeZone.getID());
498                             if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 ||
499                                     cal.get(GregorianCalendar.MINUTE) != 0) {
500                                 allDayEvent = 0;
501                                 userLog("Not an all-day event locally: " + mLocalTimeZone.getID());
502                             }
503                         }
504                         cv.put(Events.ALL_DAY, allDayEvent);
505                         break;
506                     case Tags.CALENDAR_ATTACHMENTS:
507                         attachmentsParser();
508                         break;
509                     case Tags.CALENDAR_ATTENDEES:
510                         // If eventId >= 0, this is an update; otherwise, a new Event
511                         attendeeValues = attendeesParser(ops, eventId);
512                         break;
513                     case Tags.BASE_BODY:
514                         cv.put(Events.DESCRIPTION, bodyParser());
515                         break;
516                     case Tags.CALENDAR_BODY:
517                         cv.put(Events.DESCRIPTION, getValue());
518                         break;
519                     case Tags.CALENDAR_TIME_ZONE:
520                         timeZone = CalendarUtilities.tziStringToTimeZone(getValue());
521                         if (timeZone == null) {
522                             timeZone = mLocalTimeZone;
523                         }
524                         cv.put(Events.EVENT_TIMEZONE, timeZone.getID());
525                         break;
526                     case Tags.CALENDAR_START_TIME:
527                         startTime = Utility.parseDateTimeToMillis(getValue());
528                         break;
529                     case Tags.CALENDAR_END_TIME:
530                         endTime = Utility.parseDateTimeToMillis(getValue());
531                         break;
532                     case Tags.CALENDAR_EXCEPTIONS:
533                         // For exceptions to show the organizer, the organizer must be added before
534                         // we call exceptionsParser
535                         addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
536                         organizerAdded = true;
537                         exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus,
538                                 startTime, endTime);
539                         break;
540                     case Tags.CALENDAR_LOCATION:
541                         cv.put(Events.EVENT_LOCATION, getValue());
542                         break;
543                     case Tags.CALENDAR_RECURRENCE:
544                         String rrule = recurrenceParser();
545                         if (rrule != null) {
546                             cv.put(Events.RRULE, rrule);
547                         }
548                         break;
549                     case Tags.CALENDAR_ORGANIZER_EMAIL:
550                         organizerEmail = getValue();
551                         cv.put(Events.ORGANIZER, organizerEmail);
552                         break;
553                     case Tags.CALENDAR_SUBJECT:
554                         cv.put(Events.TITLE, getValue());
555                         break;
556                     case Tags.CALENDAR_SENSITIVITY:
557                         cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
558                         break;
559                     case Tags.CALENDAR_ORGANIZER_NAME:
560                         organizerName = getValue();
561                         break;
562                     case Tags.CALENDAR_REMINDER_MINS_BEFORE:
563                         // Save away whether this tag has content; Exchange 2010 sends an empty tag
564                         // rather than not sending one (as with Ex07 and Ex03)
565                         boolean hasContent = !noContent;
566                         reminderMins = getValueInt();
567                         if (hasContent) {
568                             ops.newReminder(reminderMins);
569                             cv.put(Events.HAS_ALARM, 1);
570                         }
571                         break;
572                     // The following are fields we should save (for changes), though they don't
573                     // relate to data used by CalendarProvider at this point
574                     case Tags.CALENDAR_UID:
575                         cv.put(Events.SYNC_DATA2, getValue());
576                         break;
577                     case Tags.CALENDAR_DTSTAMP:
578                         dtStamp = getValue();
579                         break;
580                     case Tags.CALENDAR_MEETING_STATUS:
581                         ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue());
582                         break;
583                     case Tags.CALENDAR_BUSY_STATUS:
584                         // We'll set the user's status in the Attendees table below
585                         // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
586                         // attendee!
587                         busyStatus = getValueInt();
588                         break;
589                     case Tags.CALENDAR_RESPONSE_TYPE:
590                         // EAS 14+ uses this for the user's response status; we'll use this instead
591                         // of busy status, if it appears
592                         responseType = getValueInt();
593                         break;
594                     case Tags.CALENDAR_CATEGORIES:
595                         String categories = categoriesParser(ops);
596                         if (categories.length() > 0) {
597                             ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories);
598                         }
599                         break;
600                     default:
601                         skipTag();
602                 }
603             }
604 
605             // Enforce CalendarProvider required properties
606             setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
607 
608             // Set user's availability
609             cv.put(Events.AVAILABILITY, CalendarUtilities.availabilityFromBusyStatus(busyStatus));
610 
611             // If we haven't added the organizer to attendees, do it now
612             if (!organizerAdded) {
613                 addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
614             }
615 
616             // Note that organizerEmail can be null with a DTSTAMP only change from the server
617             boolean selfOrganizer = (mEmailAddress.equals(organizerEmail));
618 
619             // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties
620             // If the user is an attendee, set the attendee status using busyStatus (note that the
621             // busyStatus is inherited from the parent unless it's specified in the exception)
622             // Add the insert/update operation for each attendee (based on whether it's add/change)
623             int numAttendees = attendeeValues.size();
624             if (numAttendees > MAX_SYNCED_ATTENDEES) {
625                 // Indicate that we've redacted attendees.  If we're the organizer, disable edit
626                 // by setting organizerEmail to a bogus value and by setting the upsync prohibited
627                 // extended properly.
628                 // Note that we don't set ANY attendees if we're in this branch; however, the
629                 // organizer has already been included above, and WILL show up (which is good)
630                 if (eventId < 0) {
631                     ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1");
632                     if (selfOrganizer) {
633                         ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1");
634                     }
635                 } else {
636                     ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId);
637                     if (selfOrganizer) {
638                         ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1",
639                                 eventId);
640                     }
641                 }
642                 if (selfOrganizer) {
643                     organizerEmail = BOGUS_ORGANIZER_EMAIL;
644                     cv.put(Events.ORGANIZER, organizerEmail);
645                 }
646                 // Tell UI that we don't have any attendees
647                 cv.put(Events.HAS_ATTENDEE_DATA, "0");
648                 mService.userLog("Maximum number of attendees exceeded; redacting");
649             } else if (numAttendees > 0) {
650                 StringBuilder sb = new StringBuilder();
651                 for (ContentValues attendee: attendeeValues) {
652                     String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL);
653                     sb.append(attendeeEmail);
654                     sb.append(ATTENDEE_TOKENIZER_DELIMITER);
655                     if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
656                         int attendeeStatus;
657                         // We'll use the response type (EAS 14), if we've got one; otherwise, we'll
658                         // try to infer it from busy status
659                         if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) {
660                             attendeeStatus =
661                                 CalendarUtilities.attendeeStatusFromResponseType(responseType);
662                         } else if (!update) {
663                             // For new events in EAS < 14, we have no idea what the busy status
664                             // means, so we show "none", allowing the user to select an option.
665                             attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
666                         } else {
667                             // For updated events, we'll try to infer the attendee status from the
668                             // busy status
669                             attendeeStatus =
670                                 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus);
671                         }
672                         attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus);
673                         // If we're an attendee, save away our initial attendee status in the
674                         // event's ExtendedProperties (we look for differences between this and
675                         // the user's current attendee status to determine whether an email needs
676                         // to be sent to the organizer)
677                         // organizerEmail will be null in the case that this is an attendees-only
678                         // change from the server
679                         if (organizerEmail == null ||
680                                 !organizerEmail.equalsIgnoreCase(attendeeEmail)) {
681                             if (eventId < 0) {
682                                 ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
683                                         Integer.toString(attendeeStatus));
684                             } else {
685                                 ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
686                                         Integer.toString(attendeeStatus), eventId);
687 
688                             }
689                         }
690                     }
691                     if (eventId < 0) {
692                         ops.newAttendee(attendee);
693                     } else {
694                         ops.updatedAttendee(attendee, eventId);
695                     }
696                 }
697                 if (eventId < 0) {
698                     ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString());
699                     ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0");
700                     ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0");
701                 } else {
702                     ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(),
703                             eventId);
704                     ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId);
705                     ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId);
706                 }
707             }
708 
709             // Put the real event in the proper place in the ops ArrayList
710             if (eventOffset >= 0) {
711                 // Store away the DTSTAMP here
712                 if (dtStamp != null) {
713                     ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp);
714                 }
715 
716                 if (isValidEventValues(cv)) {
717                     ops.set(eventOffset,
718                             new Operation(ContentProviderOperation
719                                     .newInsert(mAsSyncAdapterEvents).withValues(cv)));
720                 } else {
721                     // If we can't add this event (it's invalid), remove all of the inserts
722                     // we've built for it
723                     int cnt = ops.mCount - eventOffset;
724                     userLog(TAG, "Removing " + cnt + " inserts from mOps");
725                     for (int i = 0; i < cnt; i++) {
726                         ops.remove(eventOffset);
727                     }
728                     ops.mCount = eventOffset;
729                     // If this is a change, we need to also remove the deletion that comes
730                     // before the addition
731                     if (deleteOffset >= 0) {
732                         // Remove the deletion
733                         ops.remove(deleteOffset);
734                         // And the deletion of exceptions
735                         ops.remove(deleteOffset);
736                         userLog(TAG, "Removing deletion ops from mOps");
737                         ops.mCount = deleteOffset;
738                     }
739                 }
740             }
741             // Mark the end of the event
742             addSeparatorOperation(ops, Events.CONTENT_URI);
743         }
744 
logEventColumns(ContentValues cv, String reason)745         private void logEventColumns(ContentValues cv, String reason) {
746             if (Eas.USER_LOG) {
747                 StringBuilder sb =
748                     new StringBuilder("Event invalid, " + reason + ", skipping: Columns = ");
749                 for (Entry<String, Object> entry: cv.valueSet()) {
750                     sb.append(entry.getKey());
751                     sb.append('/');
752                 }
753                 userLog(TAG, sb.toString());
754             }
755         }
756 
isValidEventValues(ContentValues cv)757         /*package*/ boolean isValidEventValues(ContentValues cv) {
758             boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME);
759             // All events require DTSTART
760             if (!cv.containsKey(Events.DTSTART)) {
761                 logEventColumns(cv, "DTSTART missing");
762                 return false;
763             // If we're a top-level event, we must have _SYNC_DATA (uid)
764             } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) {
765                 logEventColumns(cv, "_SYNC_DATA missing");
766                 return false;
767             // We must also have DTEND or DURATION if we're not an exception
768             } else if (!isException && !cv.containsKey(Events.DTEND) &&
769                     !cv.containsKey(Events.DURATION)) {
770                 logEventColumns(cv, "DTEND/DURATION missing");
771                 return false;
772             // Exceptions require DTEND
773             } else if (isException && !cv.containsKey(Events.DTEND)) {
774                 logEventColumns(cv, "Exception missing DTEND");
775                 return false;
776             // If this is a recurrence, we need a DURATION (in days if an all-day event)
777             } else if (cv.containsKey(Events.RRULE)) {
778                 String duration = cv.getAsString(Events.DURATION);
779                 if (duration == null) return false;
780                 if (cv.containsKey(Events.ALL_DAY)) {
781                     Integer ade = cv.getAsInteger(Events.ALL_DAY);
782                     if (ade != null && ade != 0 && !duration.endsWith("D")) {
783                         return false;
784                     }
785                 }
786             }
787             return true;
788         }
789 
recurrenceParser()790         public String recurrenceParser() throws IOException {
791             // Turn this information into an RRULE
792             int type = -1;
793             int occurrences = -1;
794             int interval = -1;
795             int dow = -1;
796             int dom = -1;
797             int wom = -1;
798             int moy = -1;
799             String until = null;
800 
801             while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
802                 switch (tag) {
803                     case Tags.CALENDAR_RECURRENCE_TYPE:
804                         type = getValueInt();
805                         break;
806                     case Tags.CALENDAR_RECURRENCE_INTERVAL:
807                         interval = getValueInt();
808                         break;
809                     case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
810                         occurrences = getValueInt();
811                         break;
812                     case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
813                         dow = getValueInt();
814                         break;
815                     case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
816                         dom = getValueInt();
817                         break;
818                     case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
819                         wom = getValueInt();
820                         break;
821                     case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
822                         moy = getValueInt();
823                         break;
824                     case Tags.CALENDAR_RECURRENCE_UNTIL:
825                         until = getValue();
826                         break;
827                     default:
828                        skipTag();
829                 }
830             }
831 
832             return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
833                     dow, dom, wom, moy, until);
834         }
835 
exceptionParser(CalendarOperations ops, ContentValues parentCv, ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, long startTime, long endTime)836         private void exceptionParser(CalendarOperations ops, ContentValues parentCv,
837                 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
838                 long startTime, long endTime) throws IOException {
839             ContentValues cv = new ContentValues();
840             cv.put(Events.CALENDAR_ID, mCalendarId);
841 
842             // It appears that these values have to be copied from the parent if they are to appear
843             // Note that they can be overridden below
844             cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
845             cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
846             cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION));
847             cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
848             cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION));
849             cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL));
850             cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
851             // Exceptions should always have this set to zero, since EAS has no concept of
852             // separate attendee lists for exceptions; if we fail to do this, then the UI will
853             // allow the user to change attendee data, and this change would never get reflected
854             // on the server.
855             cv.put(Events.HAS_ATTENDEE_DATA, 0);
856 
857             int allDayEvent = 0;
858 
859             // This column is the key that links the exception to the serverId
860             cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID));
861 
862             String exceptionStartTime = "_noStartTime";
863             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
864                 switch (tag) {
865                     case Tags.CALENDAR_ATTACHMENTS:
866                         attachmentsParser();
867                         break;
868                     case Tags.CALENDAR_EXCEPTION_START_TIME:
869                         exceptionStartTime = getValue();
870                         cv.put(Events.ORIGINAL_INSTANCE_TIME,
871                                 Utility.parseDateTimeToMillis(exceptionStartTime));
872                         break;
873                     case Tags.CALENDAR_EXCEPTION_IS_DELETED:
874                         if (getValueInt() == 1) {
875                             cv.put(Events.STATUS, Events.STATUS_CANCELED);
876                         }
877                         break;
878                     case Tags.CALENDAR_ALL_DAY_EVENT:
879                         allDayEvent = getValueInt();
880                         cv.put(Events.ALL_DAY, allDayEvent);
881                         break;
882                     case Tags.BASE_BODY:
883                         cv.put(Events.DESCRIPTION, bodyParser());
884                         break;
885                     case Tags.CALENDAR_BODY:
886                         cv.put(Events.DESCRIPTION, getValue());
887                         break;
888                     case Tags.CALENDAR_START_TIME:
889                         startTime = Utility.parseDateTimeToMillis(getValue());
890                         break;
891                     case Tags.CALENDAR_END_TIME:
892                         endTime = Utility.parseDateTimeToMillis(getValue());
893                         break;
894                     case Tags.CALENDAR_LOCATION:
895                         cv.put(Events.EVENT_LOCATION, getValue());
896                         break;
897                     case Tags.CALENDAR_RECURRENCE:
898                         String rrule = recurrenceParser();
899                         if (rrule != null) {
900                             cv.put(Events.RRULE, rrule);
901                         }
902                         break;
903                     case Tags.CALENDAR_SUBJECT:
904                         cv.put(Events.TITLE, getValue());
905                         break;
906                     case Tags.CALENDAR_SENSITIVITY:
907                         cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
908                         break;
909                     case Tags.CALENDAR_BUSY_STATUS:
910                         busyStatus = getValueInt();
911                         // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
912                         // attendee!
913                         break;
914                         // TODO How to handle these items that are linked to event id!
915 //                    case Tags.CALENDAR_DTSTAMP:
916 //                        ops.newExtendedProperty("dtstamp", getValue());
917 //                        break;
918 //                    case Tags.CALENDAR_REMINDER_MINS_BEFORE:
919 //                        ops.newReminder(getValueInt());
920 //                        break;
921                     default:
922                         skipTag();
923                 }
924             }
925 
926             // We need a _sync_id, but it can't be the parent's id, so we generate one
927             cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
928                     exceptionStartTime);
929 
930             // Enforce CalendarProvider required properties
931             setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
932 
933             // Don't insert an invalid exception event
934             if (!isValidEventValues(cv)) return;
935 
936             // Add the exception insert
937             int exceptionStart = ops.mCount;
938             ops.newException(cv);
939             // Also add the attendees, because they need to be copied over from the parent event
940             boolean attendeesRedacted = false;
941             if (attendeeValues != null) {
942                 for (ContentValues attValues: attendeeValues) {
943                     // If this is the user, use his busy status for attendee status
944                     String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL);
945                     // Note that the exception at which we surpass the redaction limit might have
946                     // any number of attendees shown; since this is an edge case and a workaround,
947                     // it seems to be an acceptable implementation
948                     if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
949                         attValues.put(Attendees.ATTENDEE_STATUS,
950                                 CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
951                         ops.newAttendee(attValues, exceptionStart);
952                     } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) {
953                         ops.newAttendee(attValues, exceptionStart);
954                     } else {
955                         attendeesRedacted = true;
956                     }
957                 }
958             }
959             // And add the parent's reminder value
960             if (reminderMins > 0) {
961                 ops.newReminder(reminderMins, exceptionStart);
962             }
963             if (attendeesRedacted) {
964                 mService.userLog("Attendees redacted in this exception");
965             }
966         }
967 
encodeVisibility(int easVisibility)968         private int encodeVisibility(int easVisibility) {
969             int visibility = 0;
970             switch(easVisibility) {
971                 case 0:
972                     visibility = Events.ACCESS_DEFAULT;
973                     break;
974                 case 1:
975                     visibility = Events.ACCESS_PUBLIC;
976                     break;
977                 case 2:
978                     visibility = Events.ACCESS_PRIVATE;
979                     break;
980                 case 3:
981                     visibility = Events.ACCESS_CONFIDENTIAL;
982                     break;
983             }
984             return visibility;
985         }
986 
exceptionsParser(CalendarOperations ops, ContentValues cv, ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus, long startTime, long endTime)987         private void exceptionsParser(CalendarOperations ops, ContentValues cv,
988                 ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
989                 long startTime, long endTime) throws IOException {
990             while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
991                 switch (tag) {
992                     case Tags.CALENDAR_EXCEPTION:
993                         exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus,
994                                 startTime, endTime);
995                         break;
996                     default:
997                         skipTag();
998                 }
999             }
1000         }
1001 
categoriesParser(CalendarOperations ops)1002         private String categoriesParser(CalendarOperations ops) throws IOException {
1003             StringBuilder categories = new StringBuilder();
1004             while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
1005                 switch (tag) {
1006                     case Tags.CALENDAR_CATEGORY:
1007                         // TODO Handle categories (there's no similar concept for gdata AFAIK)
1008                         // We need to save them and spit them back when we update the event
1009                         categories.append(getValue());
1010                         categories.append(CATEGORY_TOKENIZER_DELIMITER);
1011                         break;
1012                     default:
1013                         skipTag();
1014                 }
1015             }
1016             return categories.toString();
1017         }
1018 
1019         /**
1020          * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14
1021          */
attachmentsParser()1022         private void attachmentsParser() throws IOException {
1023             while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) {
1024                 switch (tag) {
1025                     case Tags.CALENDAR_ATTACHMENT:
1026                         skipParser(Tags.CALENDAR_ATTACHMENT);
1027                         break;
1028                     default:
1029                         skipTag();
1030                 }
1031             }
1032         }
1033 
attendeesParser(CalendarOperations ops, long eventId)1034         private ArrayList<ContentValues> attendeesParser(CalendarOperations ops, long eventId)
1035                 throws IOException {
1036             int attendeeCount = 0;
1037             ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
1038             while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
1039                 switch (tag) {
1040                     case Tags.CALENDAR_ATTENDEE:
1041                         ContentValues cv = attendeeParser(ops, eventId);
1042                         // If we're going to redact these attendees anyway, let's avoid unnecessary
1043                         // memory pressure, and not keep them around
1044                         // We still need to parse them all, however
1045                         attendeeCount++;
1046                         // Allow one more than MAX_ATTENDEES, so that the check for "too many" will
1047                         // succeed in addEvent
1048                         if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) {
1049                             attendeeValues.add(cv);
1050                         }
1051                         break;
1052                     default:
1053                         skipTag();
1054                 }
1055             }
1056             return attendeeValues;
1057         }
1058 
attendeeParser(CalendarOperations ops, long eventId)1059         private ContentValues attendeeParser(CalendarOperations ops, long eventId)
1060                 throws IOException {
1061             ContentValues cv = new ContentValues();
1062             while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
1063                 switch (tag) {
1064                     case Tags.CALENDAR_ATTENDEE_EMAIL:
1065                         cv.put(Attendees.ATTENDEE_EMAIL, getValue());
1066                         break;
1067                     case Tags.CALENDAR_ATTENDEE_NAME:
1068                         cv.put(Attendees.ATTENDEE_NAME, getValue());
1069                         break;
1070                     case Tags.CALENDAR_ATTENDEE_STATUS:
1071                         int status = getValueInt();
1072                         cv.put(Attendees.ATTENDEE_STATUS,
1073                                 (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
1074                                 (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
1075                                 (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
1076                                 (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
1077                                     Attendees.ATTENDEE_STATUS_NONE);
1078                         break;
1079                     case Tags.CALENDAR_ATTENDEE_TYPE:
1080                         int type = Attendees.TYPE_NONE;
1081                         // EAS types: 1 = req'd, 2 = opt, 3 = resource
1082                         switch (getValueInt()) {
1083                             case 1:
1084                                 type = Attendees.TYPE_REQUIRED;
1085                                 break;
1086                             case 2:
1087                                 type = Attendees.TYPE_OPTIONAL;
1088                                 break;
1089                         }
1090                         cv.put(Attendees.ATTENDEE_TYPE, type);
1091                         break;
1092                     default:
1093                         skipTag();
1094                 }
1095             }
1096             cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1097             return cv;
1098         }
1099 
bodyParser()1100         private String bodyParser() throws IOException {
1101             String body = null;
1102             while (nextTag(Tags.BASE_BODY) != END) {
1103                 switch (tag) {
1104                     case Tags.BASE_DATA:
1105                         body = getValue();
1106                         break;
1107                     default:
1108                         skipTag();
1109                 }
1110             }
1111 
1112             // Handle null data without error
1113             if (body == null) return "";
1114             // Remove \r's from any body text
1115             return body.replace("\r\n", "\n");
1116         }
1117 
addParser(CalendarOperations ops)1118         public void addParser(CalendarOperations ops) throws IOException {
1119             String serverId = null;
1120             while (nextTag(Tags.SYNC_ADD) != END) {
1121                 switch (tag) {
1122                     case Tags.SYNC_SERVER_ID: // same as
1123                         serverId = getValue();
1124                         break;
1125                     case Tags.SYNC_APPLICATION_DATA:
1126                         addEvent(ops, serverId, false);
1127                         break;
1128                     default:
1129                         skipTag();
1130                 }
1131             }
1132         }
1133 
getServerIdCursor(String serverId)1134         private Cursor getServerIdCursor(String serverId) {
1135             return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_AND_CALENDAR_ID,
1136                     new String[] {serverId, mCalendarIdString}, null);
1137         }
1138 
getClientIdCursor(String clientId)1139         private Cursor getClientIdCursor(String clientId) {
1140             mBindArgument[0] = clientId;
1141             return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
1142                     mBindArgument, null);
1143         }
1144 
deleteParser(CalendarOperations ops)1145         public void deleteParser(CalendarOperations ops) throws IOException {
1146             while (nextTag(Tags.SYNC_DELETE) != END) {
1147                 switch (tag) {
1148                     case Tags.SYNC_SERVER_ID:
1149                         String serverId = getValue();
1150                         // Find the event with the given serverId
1151                         Cursor c = getServerIdCursor(serverId);
1152                         try {
1153                             if (c.moveToFirst()) {
1154                                 userLog("Deleting ", serverId);
1155                                 ops.delete(c.getLong(0), serverId);
1156                             }
1157                         } finally {
1158                             c.close();
1159                         }
1160                         break;
1161                     default:
1162                         skipTag();
1163                 }
1164             }
1165         }
1166 
1167         /**
1168          * A change is handled as a delete (including all exceptions) and an add
1169          * This isn't as efficient as attempting to traverse the original and all of its exceptions,
1170          * but changes happen infrequently and this code is both simpler and easier to maintain
1171          * @param ops the array of pending ContactProviderOperations.
1172          * @throws IOException
1173          */
changeParser(CalendarOperations ops)1174         public void changeParser(CalendarOperations ops) throws IOException {
1175             String serverId = null;
1176             while (nextTag(Tags.SYNC_CHANGE) != END) {
1177                 switch (tag) {
1178                     case Tags.SYNC_SERVER_ID:
1179                         serverId = getValue();
1180                         break;
1181                     case Tags.SYNC_APPLICATION_DATA:
1182                         userLog("Changing " + serverId);
1183                         addEvent(ops, serverId, true);
1184                         break;
1185                     default:
1186                         skipTag();
1187                 }
1188             }
1189         }
1190 
1191         @Override
commandsParser()1192         public void commandsParser() throws IOException {
1193             while (nextTag(Tags.SYNC_COMMANDS) != END) {
1194                 if (tag == Tags.SYNC_ADD) {
1195                     addParser(mOps);
1196                     incrementChangeCount();
1197                 } else if (tag == Tags.SYNC_DELETE) {
1198                     deleteParser(mOps);
1199                     incrementChangeCount();
1200                 } else if (tag == Tags.SYNC_CHANGE) {
1201                     changeParser(mOps);
1202                     incrementChangeCount();
1203                 } else
1204                     skipTag();
1205             }
1206         }
1207 
1208         @Override
commit()1209         public void commit() throws IOException {
1210             userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
1211             // Save the syncKey here, using the Helper provider by Calendar provider
1212             mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation(
1213                     asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
1214                             Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
1215                     mAccountManagerAccount,
1216                     mMailbox.mSyncKey.getBytes())));
1217 
1218             // We need to send cancellations now, because the Event won't exist after the commit
1219             for (long eventId: mSendCancelIdList) {
1220                 EmailContent.Message msg;
1221                 try {
1222                     msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
1223                             EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL, null,
1224                             mAccount);
1225                 } catch (RemoteException e) {
1226                     // Nothing to do here; the Event may no longer exist
1227                     continue;
1228                 }
1229                 if (msg != null) {
1230                     EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
1231                 }
1232             }
1233 
1234             // Execute our CPO's safely
1235             try {
1236                 mOps.mResults = safeExecute(CalendarContract.AUTHORITY, mOps);
1237             } catch (RemoteException e) {
1238                 throw new IOException("Remote exception caught; will retry");
1239             }
1240 
1241             if (mOps.mResults != null) {
1242                 // Clear dirty and mark flags for updates sent to server
1243                 if (!mUploadedIdList.isEmpty())  {
1244                     ContentValues cv = new ContentValues();
1245                     cv.put(Events.DIRTY, 0);
1246                     cv.put(EVENT_SYNC_MARK, "0");
1247                     for (long eventId : mUploadedIdList) {
1248                         mContentResolver.update(
1249                                 asSyncAdapter(
1250                                         ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
1251                                         mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
1252                                 null, null);
1253                     }
1254                 }
1255                 // Delete events marked for deletion
1256                 if (!mDeletedIdList.isEmpty()) {
1257                     for (long eventId : mDeletedIdList) {
1258                         mContentResolver.delete(
1259                                 asSyncAdapter(
1260                                         ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
1261                                         mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
1262                                 null);
1263                     }
1264                 }
1265                 // Send any queued up email (invitations replies, etc.)
1266                 for (Message msg: mOutgoingMailList) {
1267                     EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
1268                 }
1269             }
1270         }
1271 
addResponsesParser()1272         public void addResponsesParser() throws IOException {
1273             String serverId = null;
1274             String clientId = null;
1275             int status = -1;
1276             ContentValues cv = new ContentValues();
1277             while (nextTag(Tags.SYNC_ADD) != END) {
1278                 switch (tag) {
1279                     case Tags.SYNC_SERVER_ID:
1280                         serverId = getValue();
1281                         break;
1282                     case Tags.SYNC_CLIENT_ID:
1283                         clientId = getValue();
1284                         break;
1285                     case Tags.SYNC_STATUS:
1286                         status = getValueInt();
1287                         if (status != 1) {
1288                             userLog("Attempt to add event failed with status: " + status);
1289                         }
1290                         break;
1291                     default:
1292                         skipTag();
1293                 }
1294             }
1295 
1296             if (clientId == null) return;
1297             if (serverId == null) {
1298                 // TODO Reconsider how to handle this
1299                 serverId = "FAIL:" + status;
1300             }
1301 
1302             Cursor c = getClientIdCursor(clientId);
1303             try {
1304                 if (c.moveToFirst()) {
1305                     cv.put(Events._SYNC_ID, serverId);
1306                     cv.put(Events.SYNC_DATA2, clientId);
1307                     long id = c.getLong(0);
1308                     // Write the serverId into the Event
1309                     mOps.add(new Operation(ContentProviderOperation
1310                             .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id))
1311                             .withValues(cv)));
1312                     userLog("New event " + clientId + " was given serverId: " + serverId);
1313                 }
1314             } finally {
1315                 c.close();
1316             }
1317         }
1318 
changeResponsesParser()1319         public void changeResponsesParser() throws IOException {
1320             String serverId = null;
1321             String status = null;
1322             while (nextTag(Tags.SYNC_CHANGE) != END) {
1323                 switch (tag) {
1324                     case Tags.SYNC_SERVER_ID:
1325                         serverId = getValue();
1326                         break;
1327                     case Tags.SYNC_STATUS:
1328                         status = getValue();
1329                         break;
1330                     default:
1331                         skipTag();
1332                 }
1333             }
1334             if (serverId != null && status != null) {
1335                 userLog("Changed event " + serverId + " failed with status: " + status);
1336             }
1337         }
1338 
1339 
1340         @Override
responsesParser()1341         public void responsesParser() throws IOException {
1342             // Handle server responses here (for Add and Change)
1343             while (nextTag(Tags.SYNC_RESPONSES) != END) {
1344                 if (tag == Tags.SYNC_ADD) {
1345                     addResponsesParser();
1346                 } else if (tag == Tags.SYNC_CHANGE) {
1347                     changeResponsesParser();
1348                 } else
1349                     skipTag();
1350             }
1351         }
1352     }
1353 
1354     protected class CalendarOperations extends ArrayList<Operation> {
1355         private static final long serialVersionUID = 1L;
1356         public int mCount = 0;
1357         private ContentProviderResult[] mResults = null;
1358         private int mEventStart = 0;
1359 
1360         @Override
add(Operation op)1361         public boolean add(Operation op) {
1362             super.add(op);
1363             mCount++;
1364             return true;
1365         }
1366 
newEvent(Operation op)1367         public int newEvent(Operation op) {
1368             mEventStart = mCount;
1369             add(op);
1370             return mEventStart;
1371         }
1372 
newDelete(long id, String serverId)1373         public int newDelete(long id, String serverId) {
1374             int offset = mCount;
1375             delete(id, serverId);
1376             return offset;
1377         }
1378 
newAttendee(ContentValues cv)1379         public void newAttendee(ContentValues cv) {
1380             newAttendee(cv, mEventStart);
1381         }
1382 
newAttendee(ContentValues cv, int eventStart)1383         public void newAttendee(ContentValues cv, int eventStart) {
1384             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
1385                     .withValues(cv),
1386                     Attendees.EVENT_ID,
1387                     eventStart));
1388         }
1389 
updatedAttendee(ContentValues cv, long id)1390         public void updatedAttendee(ContentValues cv, long id) {
1391             cv.put(Attendees.EVENT_ID, id);
1392             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
1393                     .withValues(cv)));
1394         }
1395 
newException(ContentValues cv)1396         public void newException(ContentValues cv) {
1397             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents)
1398                     .withValues(cv)));
1399         }
1400 
newExtendedProperty(String name, String value)1401         public void newExtendedProperty(String name, String value) {
1402             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties)
1403                     .withValue(ExtendedProperties.NAME, name)
1404                     .withValue(ExtendedProperties.VALUE, value),
1405                     ExtendedProperties.EVENT_ID,
1406                     mEventStart));
1407         }
1408 
updatedExtendedProperty(String name, String value, long id)1409         public void updatedExtendedProperty(String name, String value, long id) {
1410             // Find an existing ExtendedProperties row for this event and property name
1411             Cursor c = mService.mContentResolver.query(ExtendedProperties.CONTENT_URI,
1412                     EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME,
1413                     new String[] {Long.toString(id), name}, null);
1414             long extendedPropertyId = -1;
1415             // If there is one, capture its _id
1416             if (c != null) {
1417                 try {
1418                     if (c.moveToFirst()) {
1419                         extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID);
1420                     }
1421                 } finally {
1422                     c.close();
1423                 }
1424             }
1425             // Either do an update or an insert, depending on whether one
1426             // already exists
1427             if (extendedPropertyId >= 0) {
1428                 add(new Operation(ContentProviderOperation
1429                         .newUpdate(
1430                                 ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties,
1431                                         extendedPropertyId))
1432                         .withValue(ExtendedProperties.VALUE, value)));
1433             } else {
1434                 newExtendedProperty(name, value);
1435             }
1436         }
1437 
newReminder(int mins, int eventStart)1438         public void newReminder(int mins, int eventStart) {
1439             add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders)
1440                     .withValue(Reminders.MINUTES, mins)
1441                     .withValue(Reminders.METHOD, Reminders.METHOD_ALERT),
1442                     ExtendedProperties.EVENT_ID,
1443                     eventStart));
1444         }
1445 
newReminder(int mins)1446         public void newReminder(int mins) {
1447             newReminder(mins, mEventStart);
1448         }
1449 
delete(long id, String syncId)1450         public void delete(long id, String syncId) {
1451             add(new Operation(ContentProviderOperation.newDelete(
1452                     ContentUris.withAppendedId(mAsSyncAdapterEvents, id))));
1453             // Delete the exceptions for this Event (CalendarProvider doesn't do this)
1454             add(new Operation(ContentProviderOperation
1455                     .newDelete(mAsSyncAdapterEvents)
1456                     .withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId})));
1457         }
1458     }
1459 
decodeVisibility(int visibility)1460     private String decodeVisibility(int visibility) {
1461         int easVisibility = 0;
1462         switch(visibility) {
1463             case Events.ACCESS_DEFAULT:
1464                 easVisibility = 0;
1465                 break;
1466             case Events.ACCESS_PUBLIC:
1467                 easVisibility = 1;
1468                 break;
1469             case Events.ACCESS_PRIVATE:
1470                 easVisibility = 2;
1471                 break;
1472             case Events.ACCESS_CONFIDENTIAL:
1473                 easVisibility = 3;
1474                 break;
1475         }
1476         return Integer.toString(easVisibility);
1477     }
1478 
getInt(ContentValues cv, String column)1479     private int getInt(ContentValues cv, String column) {
1480         Integer i = cv.getAsInteger(column);
1481         if (i == null) return 0;
1482         return i;
1483     }
1484 
sendEvent(Entity entity, String clientId, Serializer s)1485     private void sendEvent(Entity entity, String clientId, Serializer s)
1486             throws IOException {
1487         // Serialize for EAS here
1488         // Set uid with the client id we created
1489         // 1) Serialize the top-level event
1490         // 2) Serialize attendees and reminders from subvalues
1491         // 3) Look for exceptions and serialize with the top-level event
1492         ContentValues entityValues = entity.getEntityValues();
1493         final boolean isException = (clientId == null);
1494         boolean hasAttendees = false;
1495         final boolean isChange = entityValues.containsKey(Events._SYNC_ID);
1496         final Double version = mService.mProtocolVersionDouble;
1497         final boolean allDay =
1498             CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY);
1499 
1500         // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception
1501         // start time" data before other data in exceptions.  Failure to do so results in a
1502         // status 6 error during sync
1503         if (isException) {
1504            // Send exception deleted flag if necessary
1505             Integer deleted = entityValues.getAsInteger(Events.DELETED);
1506             boolean isDeleted = deleted != null && deleted == 1;
1507             Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
1508             boolean isCanceled = eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
1509             if (isDeleted || isCanceled) {
1510                 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1");
1511                 // If we're deleted, the UI will continue to show this exception until we mark
1512                 // it canceled, so we'll do that here...
1513                 if (isDeleted && !isCanceled) {
1514                     final long eventId = entityValues.getAsLong(Events._ID);
1515                     ContentValues cv = new ContentValues();
1516                     cv.put(Events.STATUS, Events.STATUS_CANCELED);
1517                     mService.mContentResolver.update(
1518                             asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
1519                                     mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv, null,
1520                             null);
1521                 }
1522             } else {
1523                 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0");
1524             }
1525 
1526             // TODO Add reminders to exceptions (allow them to be specified!)
1527             Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1528             if (originalTime != null) {
1529                 final boolean originalAllDay =
1530                     CalendarUtilities.getIntegerValueAsBoolean(entityValues,
1531                             Events.ORIGINAL_ALL_DAY);
1532                 if (originalAllDay) {
1533                     // For all day events, we need our local all-day time
1534                     originalTime =
1535                         CalendarUtilities.getLocalAllDayCalendarTime(originalTime, mLocalTimeZone);
1536                 }
1537                 s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
1538                         CalendarUtilities.millisToEasDateTime(originalTime));
1539             } else {
1540                 // Illegal; what should we do?
1541             }
1542         }
1543 
1544         // Get the event's time zone
1545         String timeZoneName =
1546             entityValues.getAsString(allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE);
1547         if (timeZoneName == null) {
1548             timeZoneName = mLocalTimeZone.getID();
1549         }
1550         TimeZone eventTimeZone = TimeZone.getTimeZone(timeZoneName);
1551 
1552         if (!isException) {
1553             // A time zone is required in all EAS events; we'll use the default if none is set
1554             // Exchange 2003 seems to require this first... :-)
1555             String timeZone = CalendarUtilities.timeZoneToTziString(eventTimeZone);
1556             s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
1557         }
1558 
1559         s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0");
1560 
1561         // DTSTART is always supplied
1562         long startTime = entityValues.getAsLong(Events.DTSTART);
1563         // Determine endTime; it's either provided as DTEND or we calculate using DURATION
1564         // If no DURATION is provided, we default to one hour
1565         long endTime;
1566         if (entityValues.containsKey(Events.DTEND)) {
1567             endTime = entityValues.getAsLong(Events.DTEND);
1568         } else {
1569             long durationMillis = HOURS;
1570             if (entityValues.containsKey(Events.DURATION)) {
1571                 Duration duration = new Duration();
1572                 try {
1573                     duration.parse(entityValues.getAsString(Events.DURATION));
1574                     durationMillis = duration.getMillis();
1575                 } catch (DateException e) {
1576                     // Can't do much about this; use the default (1 hour)
1577                 }
1578             }
1579             endTime = startTime + durationMillis;
1580         }
1581         if (allDay) {
1582             TimeZone tz = mLocalTimeZone;
1583             startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, tz);
1584             endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, tz);
1585         }
1586         s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime));
1587         s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime));
1588 
1589         s.data(Tags.CALENDAR_DTSTAMP,
1590                 CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
1591 
1592         String loc = entityValues.getAsString(Events.EVENT_LOCATION);
1593         if (!TextUtils.isEmpty(loc)) {
1594             if (version < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1595                 // EAS 2.5 doesn't like bare line feeds
1596                 loc = Utility.replaceBareLfWithCrlf(loc);
1597             }
1598             s.data(Tags.CALENDAR_LOCATION, loc);
1599         }
1600         s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
1601 
1602         if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1603             s.start(Tags.BASE_BODY);
1604             s.data(Tags.BASE_TYPE, "1");
1605             s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA);
1606             s.end();
1607         } else {
1608             // EAS 2.5 doesn't like bare line feeds
1609             s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY);
1610         }
1611 
1612         if (!isException) {
1613             // For Exchange 2003, only upsync if the event is new
1614             if ((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) {
1615                 s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
1616             }
1617 
1618             String rrule = entityValues.getAsString(Events.RRULE);
1619             if (rrule != null) {
1620                 CalendarUtilities.recurrenceFromRrule(rrule, startTime, s);
1621             }
1622 
1623             // Handle associated data EXCEPT for attendees, which have to be grouped
1624             ArrayList<NamedContentValues> subValues = entity.getSubValues();
1625             // The earliest of the reminders for this Event; we can only send one reminder...
1626             int earliestReminder = -1;
1627             for (NamedContentValues ncv: subValues) {
1628                 Uri ncvUri = ncv.uri;
1629                 ContentValues ncvValues = ncv.values;
1630                 if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
1631                     String propertyName =
1632                         ncvValues.getAsString(ExtendedProperties.NAME);
1633                     String propertyValue =
1634                         ncvValues.getAsString(ExtendedProperties.VALUE);
1635                     if (TextUtils.isEmpty(propertyValue)) {
1636                         continue;
1637                     }
1638                     if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) {
1639                         // Send all the categories back to the server
1640                         // We've saved them as a String of delimited tokens
1641                         StringTokenizer st =
1642                             new StringTokenizer(propertyValue, CATEGORY_TOKENIZER_DELIMITER);
1643                         if (st.countTokens() > 0) {
1644                             s.start(Tags.CALENDAR_CATEGORIES);
1645                             while (st.hasMoreTokens()) {
1646                                 String category = st.nextToken();
1647                                 s.data(Tags.CALENDAR_CATEGORY, category);
1648                             }
1649                             s.end();
1650                         }
1651                     }
1652                 } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
1653                     Integer mins = ncvValues.getAsInteger(Reminders.MINUTES);
1654                     if (mins != null) {
1655                         // -1 means "default", which for Exchange, is 30
1656                         if (mins < 0) {
1657                             mins = 30;
1658                         }
1659                         // Save this away if it's the earliest reminder (greatest minutes)
1660                         if (mins > earliestReminder) {
1661                             earliestReminder = mins;
1662                         }
1663                     }
1664                 }
1665             }
1666 
1667             // If we have a reminder, send it to the server
1668             if (earliestReminder >= 0) {
1669                 s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder));
1670             }
1671 
1672             // We've got to send a UID, unless this is an exception.  If the event is new, we've
1673             // generated one; if not, we should have gotten one from extended properties.
1674             if (clientId != null) {
1675                 s.data(Tags.CALENDAR_UID, clientId);
1676             }
1677 
1678             // Handle attendee data here; keep track of organizer and stream it afterward
1679             String organizerName = null;
1680             String organizerEmail = null;
1681             for (NamedContentValues ncv: subValues) {
1682                 Uri ncvUri = ncv.uri;
1683                 ContentValues ncvValues = ncv.values;
1684                 if (ncvUri.equals(Attendees.CONTENT_URI)) {
1685                     Integer relationship = ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
1686                     // If there's no relationship, we can't create this for EAS
1687                     // Similarly, we need an attendee email for each invitee
1688                     if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
1689                         // Organizer isn't among attendees in EAS
1690                         if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
1691                             organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
1692                             organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
1693                             continue;
1694                         }
1695                         if (!hasAttendees) {
1696                             s.start(Tags.CALENDAR_ATTENDEES);
1697                             hasAttendees = true;
1698                         }
1699                         s.start(Tags.CALENDAR_ATTENDEE);
1700                         String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
1701                         String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
1702                         if (attendeeName == null) {
1703                             attendeeName = attendeeEmail;
1704                         }
1705                         s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName);
1706                         s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail);
1707                         if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1708                             s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
1709                         }
1710                         s.end(); // Attendee
1711                      }
1712                 }
1713             }
1714             if (hasAttendees) {
1715                 s.end();  // Attendees
1716             }
1717 
1718             // Get busy status from availability
1719             int availability = entityValues.getAsInteger(Events.AVAILABILITY);
1720             int busyStatus = CalendarUtilities.busyStatusFromAvailability(availability);
1721             s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
1722 
1723             // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
1724             // In JB, organizer won't be an attendee
1725             if (organizerEmail == null && entityValues.containsKey(Events.ORGANIZER)) {
1726                 organizerEmail = entityValues.getAsString(Events.ORGANIZER);
1727             }
1728             if (mEmailAddress.equalsIgnoreCase(organizerEmail)) {
1729                 s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
1730             } else {
1731                 s.data(Tags.CALENDAR_MEETING_STATUS, "3");
1732             }
1733 
1734             // For Exchange 2003, only upsync if the event is new
1735             if (((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) &&
1736                     organizerName != null) {
1737                 s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
1738             }
1739 
1740             // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003
1741             // The result will be a status 6 failure during sync
1742             Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL);
1743             if (visibility != null) {
1744                 s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
1745             } else {
1746                 // Default to private if not set
1747                 s.data(Tags.CALENDAR_SENSITIVITY, "1");
1748             }
1749         }
1750     }
1751 
1752     /**
1753      * Convenience method for sending an email to the organizer declining the meeting
1754      * @param entity
1755      * @param clientId
1756      */
sendDeclinedEmail(Entity entity, String clientId)1757     private void sendDeclinedEmail(Entity entity, String clientId) {
1758         Message msg =
1759             CalendarUtilities.createMessageForEntity(mContext, entity,
1760                     Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, mAccount);
1761         if (msg != null) {
1762             userLog("Queueing declined response to " + msg.mTo);
1763             mOutgoingMailList.add(msg);
1764         }
1765     }
1766 
1767     @Override
sendLocalChanges(Serializer s)1768     public boolean sendLocalChanges(Serializer s) throws IOException {
1769         ContentResolver cr = mService.mContentResolver;
1770 
1771         if (getSyncKey().equals("0")) {
1772             return false;
1773         }
1774 
1775         try {
1776             // We've got to handle exceptions as part of the parent when changes occur, so we need
1777             // to find new/changed exceptions and mark the parent dirty
1778             ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
1779             Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION,
1780                     DIRTY_EXCEPTION_IN_CALENDAR, mCalendarIdArgument, null);
1781             try {
1782                 ContentValues cv = new ContentValues();
1783                 // We use _sync_mark here to distinguish dirty parents from parents with dirty
1784                 // exceptions
1785                 cv.put(EVENT_SYNC_MARK, "1");
1786                 while (c.moveToNext()) {
1787                     // Mark the parents of dirty exceptions
1788                     long parentId = c.getLong(0);
1789                     int cnt = cr.update(
1790                             asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
1791                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
1792                             EVENT_ID_AND_CALENDAR_ID, new String[] {
1793                                     Long.toString(parentId), mCalendarIdString
1794                             });
1795                     // Keep track of any orphaned exceptions
1796                     if (cnt == 0) {
1797                         orphanedExceptions.add(c.getLong(1));
1798                     }
1799                 }
1800             } finally {
1801                 c.close();
1802             }
1803 
1804             // Delete any orphaned exceptions
1805             for (long orphan : orphanedExceptions) {
1806                 userLog(TAG, "Deleted orphaned exception: " + orphan);
1807                 cr.delete(
1808                         asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, orphan),
1809                                 mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null, null);
1810             }
1811             orphanedExceptions.clear();
1812 
1813             // Now we can go through dirty/marked top-level events and send them
1814             // back to the server
1815             EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
1816                     asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
1817                             Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
1818                     DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, mCalendarIdArgument, null), cr);
1819             ContentValues cidValues = new ContentValues();
1820 
1821             try {
1822                 boolean first = true;
1823                 while (eventIterator.hasNext()) {
1824                     Entity entity = eventIterator.next();
1825 
1826                     // For each of these entities, create the change commands
1827                     ContentValues entityValues = entity.getEntityValues();
1828                     String serverId = entityValues.getAsString(Events._SYNC_ID);
1829 
1830                     // We first need to check whether we can upsync this event; our test for this
1831                     // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
1832                     // If this is set to "1", we can't upsync the event
1833                     for (NamedContentValues ncv: entity.getSubValues()) {
1834                         if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
1835                             ContentValues ncvValues = ncv.values;
1836                             if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
1837                                     EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
1838                                 if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
1839                                     // Make sure we mark this to clear the dirty flag
1840                                     mUploadedIdList.add(entityValues.getAsLong(Events._ID));
1841                                     continue;
1842                                 }
1843                             }
1844                         }
1845                     }
1846 
1847                     // Find our uid in the entity; otherwise create one
1848                     String clientId = entityValues.getAsString(Events.SYNC_DATA2);
1849                     if (clientId == null) {
1850                         clientId = UUID.randomUUID().toString();
1851                     }
1852 
1853                     // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
1854                     // We can generate all but what we're testing for below
1855                     String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
1856                     boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
1857 
1858                     if (!entityValues.containsKey(Events.DTSTART)
1859                             || (!entityValues.containsKey(Events.DURATION) &&
1860                                     !entityValues.containsKey(Events.DTEND))
1861                                     || organizerEmail == null) {
1862                         continue;
1863                     }
1864 
1865                     if (first) {
1866                         s.start(Tags.SYNC_COMMANDS);
1867                         userLog("Sending Calendar changes to the server");
1868                         first = false;
1869                     }
1870                     long eventId = entityValues.getAsLong(Events._ID);
1871                     if (serverId == null) {
1872                         // This is a new event; create a clientId
1873                         userLog("Creating new event with clientId: ", clientId);
1874                         s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
1875                         // And save it in the Event as the local id
1876                         cidValues.put(Events.SYNC_DATA2, clientId);
1877                         cidValues.put(EVENT_SYNC_VERSION, "0");
1878                         cr.update(
1879                                 asSyncAdapter(
1880                                         ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
1881                                         mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
1882                                 cidValues, null, null);
1883                     } else {
1884                         if (entityValues.getAsInteger(Events.DELETED) == 1) {
1885                             userLog("Deleting event with serverId: ", serverId);
1886                             s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
1887                             mDeletedIdList.add(eventId);
1888                             if (selfOrganizer) {
1889                                 mSendCancelIdList.add(eventId);
1890                             } else {
1891                                 sendDeclinedEmail(entity, clientId);
1892                             }
1893                             continue;
1894                         }
1895                         userLog("Upsync change to event with serverId: " + serverId);
1896                         // Get the current version
1897                         String version = entityValues.getAsString(EVENT_SYNC_VERSION);
1898                         // This should never be null, but catch this error anyway
1899                         // Version should be "0" when we create the event, so use that
1900                         if (version == null) {
1901                             version = "0";
1902                         } else {
1903                             // Increment and save
1904                             try {
1905                                 version = Integer.toString((Integer.parseInt(version) + 1));
1906                             } catch (Exception e) {
1907                                 // Handle the case in which someone writes a non-integer here;
1908                                 // shouldn't happen, but we don't want to kill the sync for his
1909                                 version = "0";
1910                             }
1911                         }
1912                         cidValues.put(EVENT_SYNC_VERSION, version);
1913                         // Also save in entityValues so that we send it this time around
1914                         entityValues.put(EVENT_SYNC_VERSION, version);
1915                         cr.update(
1916                                 asSyncAdapter(
1917                                         ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
1918                                         mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
1919                                 cidValues, null, null);
1920                         s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
1921                     }
1922                     s.start(Tags.SYNC_APPLICATION_DATA);
1923 
1924                     sendEvent(entity, clientId, s);
1925 
1926                     // Now, the hard part; find exceptions for this event
1927                     if (serverId != null) {
1928                         EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query(
1929                                 asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
1930                                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
1931                                 ORIGINAL_EVENT_AND_CALENDAR, new String[] {
1932                                         serverId, mCalendarIdString
1933                                 }, null), cr);
1934                         boolean exFirst = true;
1935                         while (exIterator.hasNext()) {
1936                             Entity exEntity = exIterator.next();
1937                             if (exFirst) {
1938                                 s.start(Tags.CALENDAR_EXCEPTIONS);
1939                                 exFirst = false;
1940                             }
1941                             s.start(Tags.CALENDAR_EXCEPTION);
1942                             sendEvent(exEntity, null, s);
1943                             ContentValues exValues = exEntity.getEntityValues();
1944                             if (getInt(exValues, Events.DIRTY) == 1) {
1945                                 // This is a new/updated exception, so we've got to notify our
1946                                 // attendees about it
1947                                 long exEventId = exValues.getAsLong(Events._ID);
1948                                 int flag;
1949 
1950                                 // Copy subvalues into the exception; otherwise, we won't see the
1951                                 // attendees when preparing the message
1952                                 for (NamedContentValues ncv: entity.getSubValues()) {
1953                                     exEntity.addSubValue(ncv.uri, ncv.values);
1954                                 }
1955 
1956                                 if ((getInt(exValues, Events.DELETED) == 1) ||
1957                                         (getInt(exValues, Events.STATUS) ==
1958                                             Events.STATUS_CANCELED)) {
1959                                     flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
1960                                     if (!selfOrganizer) {
1961                                         // Send a cancellation notice to the organizer
1962                                         // Since CalendarProvider2 sets the organizer of exceptions
1963                                         // to the user, we have to reset it first to the original
1964                                         // organizer
1965                                         exValues.put(Events.ORGANIZER,
1966                                                 entityValues.getAsString(Events.ORGANIZER));
1967                                         sendDeclinedEmail(exEntity, clientId);
1968                                     }
1969                                 } else {
1970                                     flag = Message.FLAG_OUTGOING_MEETING_INVITE;
1971                                 }
1972                                 // Add the eventId of the exception to the uploaded id list, so that
1973                                 // the dirty/mark bits are cleared
1974                                 mUploadedIdList.add(exEventId);
1975 
1976                                 // Copy version so the ics attachment shows the proper sequence #
1977                                 exValues.put(EVENT_SYNC_VERSION,
1978                                         entityValues.getAsString(EVENT_SYNC_VERSION));
1979                                 // Copy location so that it's included in the outgoing email
1980                                 if (entityValues.containsKey(Events.EVENT_LOCATION)) {
1981                                     exValues.put(Events.EVENT_LOCATION,
1982                                             entityValues.getAsString(Events.EVENT_LOCATION));
1983                                 }
1984 
1985                                 if (selfOrganizer) {
1986                                     Message msg =
1987                                         CalendarUtilities.createMessageForEntity(mContext,
1988                                                 exEntity, flag, clientId, mAccount);
1989                                     if (msg != null) {
1990                                         userLog("Queueing exception update to " + msg.mTo);
1991                                         mOutgoingMailList.add(msg);
1992                                     }
1993                                 }
1994                             }
1995                             s.end(); // EXCEPTION
1996                         }
1997                         if (!exFirst) {
1998                             s.end(); // EXCEPTIONS
1999                         }
2000                     }
2001 
2002                     s.end().end(); // ApplicationData & Change
2003                     mUploadedIdList.add(eventId);
2004 
2005                     // Go through the extended properties of this Event and pull out our tokenized
2006                     // attendees list and the user attendee status; we will need them later
2007                     String attendeeString = null;
2008                     long attendeeStringId = -1;
2009                     String userAttendeeStatus = null;
2010                     long userAttendeeStatusId = -1;
2011                     for (NamedContentValues ncv: entity.getSubValues()) {
2012                         if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
2013                             ContentValues ncvValues = ncv.values;
2014                             String propertyName =
2015                                 ncvValues.getAsString(ExtendedProperties.NAME);
2016                             if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
2017                                 attendeeString =
2018                                     ncvValues.getAsString(ExtendedProperties.VALUE);
2019                                 attendeeStringId =
2020                                     ncvValues.getAsLong(ExtendedProperties._ID);
2021                             } else if (propertyName.equals(
2022                                     EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
2023                                 userAttendeeStatus =
2024                                     ncvValues.getAsString(ExtendedProperties.VALUE);
2025                                 userAttendeeStatusId =
2026                                     ncvValues.getAsLong(ExtendedProperties._ID);
2027                             }
2028                         }
2029                     }
2030 
2031                     // Send the meeting invite if there are attendees and we're the organizer AND
2032                     // if the Event itself is dirty (we might be syncing only because an exception
2033                     // is dirty, in which case we DON'T send email about the Event)
2034                     if (selfOrganizer &&
2035                             (getInt(entityValues, Events.DIRTY) == 1)) {
2036                         EmailContent.Message msg =
2037                             CalendarUtilities.createMessageForEventId(mContext, eventId,
2038                                     EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE, clientId,
2039                                     mAccount);
2040                         if (msg != null) {
2041                             userLog("Queueing invitation to ", msg.mTo);
2042                             mOutgoingMailList.add(msg);
2043                         }
2044                         // Make a list out of our tokenized attendees, if we have any
2045                         ArrayList<String> originalAttendeeList = new ArrayList<String>();
2046                         if (attendeeString != null) {
2047                             StringTokenizer st =
2048                                 new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
2049                             while (st.hasMoreTokens()) {
2050                                 originalAttendeeList.add(st.nextToken());
2051                             }
2052                         }
2053                         StringBuilder newTokenizedAttendees = new StringBuilder();
2054                         // See if any attendees have been dropped and while we're at it, build
2055                         // an updated String with tokenized attendee addresses
2056                         for (NamedContentValues ncv: entity.getSubValues()) {
2057                             if (ncv.uri.equals(Attendees.CONTENT_URI)) {
2058                                 String attendeeEmail =
2059                                     ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
2060                                 // Remove all found attendees
2061                                 originalAttendeeList.remove(attendeeEmail);
2062                                 newTokenizedAttendees.append(attendeeEmail);
2063                                 newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
2064                             }
2065                         }
2066                         // Update extended properties with the new attendee list, if we have one
2067                         // Otherwise, create one (this would be the case for Events created on
2068                         // device or "legacy" events (before this code was added)
2069                         ContentValues cv = new ContentValues();
2070                         cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
2071                         if (attendeeString != null) {
2072                             cr.update(asSyncAdapter(ContentUris.withAppendedId(
2073                                     ExtendedProperties.CONTENT_URI, attendeeStringId),
2074                                     mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
2075                                     cv, null, null);
2076                         } else {
2077                             // If there wasn't an "attendees" property, insert one
2078                             cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
2079                             cv.put(ExtendedProperties.EVENT_ID, eventId);
2080                             cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI,
2081                                     mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
2082                         }
2083                         // Whoever is left has been removed from the attendee list; send them
2084                         // a cancellation
2085                         for (String removedAttendee: originalAttendeeList) {
2086                             // Send a cancellation message to each of them
2087                             msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
2088                                     Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
2089                                     removedAttendee);
2090                             if (msg != null) {
2091                                 // Just send it to the removed attendee
2092                                 userLog("Queueing cancellation to removed attendee " + msg.mTo);
2093                                 mOutgoingMailList.add(msg);
2094                             }
2095                         }
2096                     } else if (!selfOrganizer) {
2097                         // If we're not the organizer, see if we've changed our attendee status
2098                         // Our last synced attendee status is in ExtendedProperties, and we've
2099                         // retrieved it above as userAttendeeStatus
2100                         int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
2101                         int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
2102                         if (userAttendeeStatus != null) {
2103                             try {
2104                                 syncStatus = Integer.parseInt(userAttendeeStatus);
2105                             } catch (NumberFormatException e) {
2106                                 // Just in case somebody else mucked with this and it's not Integer
2107                             }
2108                         }
2109                         if ((currentStatus != syncStatus) &&
2110                                 (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
2111                             // If so, send a meeting reply
2112                             int messageFlag = 0;
2113                             switch (currentStatus) {
2114                                 case Attendees.ATTENDEE_STATUS_ACCEPTED:
2115                                     messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
2116                                     break;
2117                                 case Attendees.ATTENDEE_STATUS_DECLINED:
2118                                     messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
2119                                     break;
2120                                 case Attendees.ATTENDEE_STATUS_TENTATIVE:
2121                                     messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
2122                                     break;
2123                             }
2124                             // Make sure we have a valid status (messageFlag should never be zero)
2125                             if (messageFlag != 0 && userAttendeeStatusId >= 0) {
2126                                 // Save away the new status
2127                                 cidValues.clear();
2128                                 cidValues.put(ExtendedProperties.VALUE,
2129                                         Integer.toString(currentStatus));
2130                                 cr.update(asSyncAdapter(ContentUris.withAppendedId(
2131                                         ExtendedProperties.CONTENT_URI, userAttendeeStatusId),
2132                                         mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
2133                                         cidValues, null, null);
2134                                 // Send mail to the organizer advising of the new status
2135                                 EmailContent.Message msg =
2136                                     CalendarUtilities.createMessageForEventId(mContext, eventId,
2137                                             messageFlag, clientId, mAccount);
2138                                 if (msg != null) {
2139                                     userLog("Queueing invitation reply to " + msg.mTo);
2140                                     mOutgoingMailList.add(msg);
2141                                 }
2142                             }
2143                         }
2144                     }
2145                 }
2146                 if (!first) {
2147                     s.end(); // Commands
2148                 }
2149             } finally {
2150                 eventIterator.close();
2151             }
2152         } catch (RemoteException e) {
2153             Log.e(TAG, "Could not read dirty events.");
2154         }
2155 
2156         return false;
2157     }
2158 }
2159