1 package com.android.exchange.eas;
2 
3 import android.content.ContentResolver;
4 import android.content.ContentUris;
5 import android.content.ContentValues;
6 import android.content.Context;
7 import android.content.Entity;
8 import android.content.EntityIterator;
9 import android.database.Cursor;
10 import android.database.DatabaseUtils;
11 import android.net.Uri;
12 import android.os.Bundle;
13 import android.provider.CalendarContract;
14 import android.provider.CalendarContract.Attendees;
15 import android.provider.CalendarContract.Calendars;
16 import android.provider.CalendarContract.Events;
17 import android.provider.CalendarContract.EventsEntity;
18 import android.provider.CalendarContract.ExtendedProperties;
19 import android.provider.CalendarContract.Reminders;
20 import android.text.TextUtils;
21 import android.text.format.DateUtils;
22 
23 import com.android.calendarcommon2.DateException;
24 import com.android.calendarcommon2.Duration;
25 import com.android.emailcommon.TrafficFlags;
26 import com.android.emailcommon.provider.Account;
27 import com.android.emailcommon.provider.EmailContent;
28 import com.android.emailcommon.provider.EmailContent.Message;
29 import com.android.emailcommon.provider.Mailbox;
30 import com.android.emailcommon.utility.Utility;
31 import com.android.exchange.Eas;
32 import com.android.exchange.R;
33 import com.android.exchange.adapter.AbstractSyncParser;
34 import com.android.exchange.adapter.CalendarSyncParser;
35 import com.android.exchange.adapter.Serializer;
36 import com.android.exchange.adapter.Tags;
37 import com.android.exchange.utility.CalendarUtilities;
38 import com.android.mail.utils.LogUtils;
39 import com.google.common.collect.Sets;
40 
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.util.ArrayList;
44 import java.util.Set;
45 import java.util.StringTokenizer;
46 import java.util.TimeZone;
47 import java.util.UUID;
48 
49 /**
50  * Performs an Exchange Sync for a Calendar collection.
51  */
52 public class EasSyncCalendar extends EasSyncCollectionTypeBase {
53     private static final String TAG = Eas.LOG_TAG;
54 
55     // TODO: Some constants are copied from CalendarSyncAdapter and are still used by the parser.
56     // These values need to stay in sync; when the parser is cleaned up, be sure to unify them.
57 
58     private static final int PIM_WINDOW_SIZE_CALENDAR = 10;
59 
60     /** Projection for getting a calendar id. */
61     private static final String[] CALENDAR_ID_PROJECTION = { Calendars._ID };
62     private static final int CALENDAR_ID_COLUMN = 0;
63 
64     /** Content selection for getting a calendar id for an account. */
65     private static final String CALENDAR_SELECTION_ACCOUNT_AND_SYNC_ID =
66             Calendars.ACCOUNT_NAME + "=? AND " +
67             Calendars.ACCOUNT_TYPE + "=? AND " +
68             Calendars._SYNC_ID + "=?";
69 
70     /** Content selection for getting a calendar id for an account. */
71     private static final String CALENDAR_SELECTION_ACCOUNT_AND_NO_SYNC =
72             Calendars.ACCOUNT_NAME + "=? AND " +
73             Calendars.ACCOUNT_TYPE + "=? AND " +
74             Calendars._SYNC_ID + " IS NULL";
75 
76     /** The column used to track the timezone of the event. */
77     private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
78 
79     /** Used to keep track of exception vs. parent event dirtiness. */
80     private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8;
81 
82     /** The column used to track the Event version sequence number. */
83     private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4;
84 
85     /** Projection for getting info about changed events. */
86     private static final String[] ORIGINAL_EVENT_PROJECTION = { Events.ORIGINAL_ID, Events._ID };
87     private static final int ORIGINAL_EVENT_ORIGINAL_ID_COLUMN = 0;
88     private static final int ORIGINAL_EVENT_ID_COLUMN = 1;
89 
90     /** Content selection for dirty calendar events. */
91     private static final String DIRTY_EXCEPTION_IN_CALENDAR = Events.DIRTY + "=1 AND " +
92             Events.ORIGINAL_ID + " NOTNULL AND " + Events.CALENDAR_ID + "=?";
93 
94     /** Where clause for updating dirty events. */
95     private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " +
96             Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
97 
98     /** Content selection for dirty or marked top level events. */
99     private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY +
100             "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " + Events.ORIGINAL_ID + " ISNULL AND " +
101             Events.CALENDAR_ID + "=?";
102 
103     /** Content selection for getting events when handling exceptions. */
104     private static final String ORIGINAL_EVENT_AND_CALENDAR = Events.ORIGINAL_SYNC_ID + "=? AND " +
105             Events.CALENDAR_ID + "=?";
106 
107     private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
108     private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
109 
110     /** Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges) */
111     private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited";
112 
113     private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
114     private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
115     private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
116 
117     private final android.accounts.Account mAndroidAccount;
118     private final long mCalendarId;
119 
120     // The following lists are populated as part of upsync, and handled during cleanup.
121     /** Ids of events that were deleted in this upsync. */
122     private final ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
123     /** Ids of events that were changed in this upsync. */
124     private final ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
125     /** Emails that need to be sent due to this upsync. */
126     private final ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
127 
EasSyncCalendar(final Context context, final Account account, final Mailbox mailbox)128     public EasSyncCalendar(final Context context, final Account account,
129             final Mailbox mailbox) {
130         super();
131         mAndroidAccount = new android.accounts.Account(account.mEmailAddress,
132             Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
133         final ContentResolver cr = context.getContentResolver();
134         final Cursor c = cr.query(Calendars.CONTENT_URI, CALENDAR_ID_PROJECTION,
135                 CALENDAR_SELECTION_ACCOUNT_AND_SYNC_ID,
136                 new String[] {
137                         account.mEmailAddress,
138                         Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE,
139                         mailbox.mServerId,
140                 }, null);
141         if (c == null) {
142             mCalendarId = -1;
143         } else {
144             try {
145                 if (c.moveToFirst()) {
146                     mCalendarId = c.getLong(CALENDAR_ID_COLUMN);
147                 } else {
148                     long id = -1;
149                     // Check if we have a calendar for this account with no server Id. If so, it was
150                     // synced with an older version of the sync adapter before serverId's were
151                     // supported.
152                     final Cursor c1 = cr.query(Calendars.CONTENT_URI,
153                             CALENDAR_ID_PROJECTION,
154                             CALENDAR_SELECTION_ACCOUNT_AND_NO_SYNC,
155                             new String[] {
156                                     account.mEmailAddress,
157                                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE,
158                             }, null);
159                     if (c1 != null) {
160                         try {
161                             if (c1.moveToFirst()) {
162                                 id = c1.getLong(CALENDAR_ID_COLUMN);
163                                 final ContentValues values = new ContentValues();
164                                 values.put(Calendars._SYNC_ID, mailbox.mServerId);
165                                 cr.update(
166                                         ContentUris.withAppendedId(
167                                                 asSyncAdapter(Calendars.CONTENT_URI, account), id),
168                                         values,
169                                         null, /* where */
170                                         null /* selectionArgs */);
171                             }
172                         } finally {
173                             c1.close();
174                         }
175                     }
176 
177                     if (id >= 0) {
178                         mCalendarId = id;
179                     } else {
180                         mCalendarId = CalendarUtilities.createCalendar(context, cr, account,
181                             mailbox);
182                     }
183                 }
184             } finally {
185                 c.close();
186             }
187         }
188     }
189 
190     @Override
setSyncOptions(final Context context, final Serializer s, final double protocolVersion, final Account account, final Mailbox mailbox, final boolean isInitialSync, final int numWindows)191     public void setSyncOptions(final Context context, final Serializer s,
192         final double protocolVersion, final Account account, final Mailbox mailbox,
193         final boolean isInitialSync, final int numWindows) throws IOException {
194         if (isInitialSync) {
195             setInitialSyncOptions(s);
196         } else {
197             setNonInitialSyncOptions(s, numWindows, protocolVersion);
198             setUpsyncCommands(context, account, protocolVersion, s);
199         }
200     }
201 
202 
203     @Override
getParser(final Context context, final Account account, final Mailbox mailbox, final InputStream is)204     public AbstractSyncParser getParser(final Context context, final Account account,
205         final Mailbox mailbox, final InputStream is) throws IOException {
206         return new CalendarSyncParser(context, context.getContentResolver(), is, mailbox, account,
207             mAndroidAccount, mCalendarId);
208     }
209 
210     @Override
getTrafficFlag()211     public int getTrafficFlag() {
212         return TrafficFlags.DATA_CALENDAR;
213     }
214 
215     /**
216      * Adds params to a {@link Uri} to indicate that the caller is a sync adapter, and to add the
217      * account info.
218      * @param uri The {@link Uri} to which to add params.
219      * @return The augmented {@link Uri}.
220      */
asSyncAdapter(final Uri uri, final String emailAddress)221     private static Uri asSyncAdapter(final Uri uri, final String emailAddress) {
222         return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
223                 .appendQueryParameter(Calendars.ACCOUNT_NAME, emailAddress)
224                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
225                 .build();
226     }
227 
228     /**
229      * Convenience wrapper to {@link #asSyncAdapter(android.net.Uri, String)}.
230      */
asSyncAdapter(final Uri uri, final Account account)231     private Uri asSyncAdapter(final Uri uri, final Account account) {
232         return asSyncAdapter(uri, account.mEmailAddress);
233     }
234 
getFolderClassName()235     protected String getFolderClassName() {
236         return "Calendar";
237     }
238 
setInitialSyncOptions(final Serializer s)239     protected void setInitialSyncOptions(final Serializer s) throws IOException {
240         // Nothing to do for Calendar.
241     }
242 
setNonInitialSyncOptions(final Serializer s, final int numWindows, final double protocolVersion)243     protected void setNonInitialSyncOptions(final Serializer s, final int numWindows,
244         final double protocolVersion) throws IOException {
245         final int windowSize = numWindows * PIM_WINDOW_SIZE_CALENDAR;
246         if (windowSize > MAX_WINDOW_SIZE  + PIM_WINDOW_SIZE_CALENDAR) {
247             throw new IOException("Max window size reached and still no data");
248         }
249         setPimSyncOptions(s, Eas.FILTER_2_WEEKS, protocolVersion,
250                 windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE);
251     }
252 
253     /**
254      * Find all dirty events for our calendar and mark their parents. Also delete any dirty events
255      * that have no parents.
256      * @param calendarIdString {@link #mCalendarId}, as a String.
257      * @param calendarIdArgument calendarIdString, in a String array.
258      */
markParentsOfDirtyEvents(final Context context, final Account account, final String calendarIdString, final String[] calendarIdArgument)259     private void markParentsOfDirtyEvents(final Context context, final Account account,
260             final String calendarIdString, final String[] calendarIdArgument) {
261         final ContentResolver cr = context.getContentResolver();
262         // We've got to handle exceptions as part of the parent when changes occur, so we need
263         // to find new/changed exceptions and mark the parent dirty
264         final ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
265         final Cursor c = cr.query(Events.CONTENT_URI,
266                 ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION_IN_CALENDAR, calendarIdArgument, null);
267         if (c != null) {
268             try {
269                 final ContentValues cv = new ContentValues(1);
270                 // We use _sync_mark here to distinguish dirty parents from parents with dirty
271                 // exceptions
272                 cv.put(EVENT_SYNC_MARK, "1");
273                 while (c.moveToNext()) {
274                     // Mark the parents of dirty exceptions
275                     final long parentId = c.getLong(ORIGINAL_EVENT_ORIGINAL_ID_COLUMN);
276                     final int cnt = cr.update(asSyncAdapter(Events.CONTENT_URI, account), cv,
277                             EVENT_ID_AND_CALENDAR_ID,
278                             new String[] { Long.toString(parentId), calendarIdString });
279                     // Keep track of any orphaned exceptions
280                     if (cnt == 0) {
281                         orphanedExceptions.add(c.getLong(ORIGINAL_EVENT_ID_COLUMN));
282                     }
283                 }
284             } finally {
285                 c.close();
286             }
287         }
288 
289         // Delete any orphaned exceptions
290         for (final long orphan : orphanedExceptions) {
291             LogUtils.d(TAG, "Deleted orphaned exception: %d", orphan);
292             cr.delete(asSyncAdapter(
293                     ContentUris.withAppendedId(Events.CONTENT_URI, orphan), account), null, null);
294         }
295     }
296 
297     /**
298      * Get the version number of the current event, incrementing it if it's already there.
299      * @param entityValues The {@link ContentValues} for this event.
300      * @return The new version number for this event (i.e. 0 if it's a new event, or the old version
301      *     number + 1).
302      */
getEntityVersion(final ContentValues entityValues)303     private static String getEntityVersion(final ContentValues entityValues) {
304         final String version = entityValues.getAsString(EVENT_SYNC_VERSION);
305         // This should never be null, but catch this error anyway
306         // Version should be "0" when we create the event, so use that
307         if (version != null) {
308             // Increment and save
309             try {
310                 return Integer.toString((Integer.parseInt(version) + 1));
311             } catch (final NumberFormatException e) {
312                 // Handle the case in which someone writes a non-integer here;
313                 // shouldn't happen, but we don't want to kill the sync for his
314             }
315         }
316         return "0";
317     }
318 
319     /**
320      * Convenience method for sending an email to the organizer declining the meeting.
321      * @param entity The {@link Entity} for this event.
322      * @param clientId The client id for this event.
323      */
sendDeclinedEmail(final Context context, final Account account, final Entity entity, final String clientId)324     private void sendDeclinedEmail(final Context context, final Account account,
325         final Entity entity, final String clientId) {
326         final Message msg =
327                 CalendarUtilities.createMessageForEntity(context, entity,
328                         Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, account);
329         if (msg != null) {
330             LogUtils.d(TAG, "Queueing declined response to %s", msg.mTo);
331             mOutgoingMailList.add(msg);
332         }
333     }
334 
335     /**
336      * Get an integer value from a {@link ContentValues}, or 0 if the value isn't there.
337      * @param cv The {@link ContentValues} to find the value in.
338      * @param column The name of the column in cv to get.
339      * @return The appropriate value as an integer, or 0 if it's not there.
340      */
getInt(final ContentValues cv, final String column)341     private static int getInt(final ContentValues cv, final String column) {
342         final Integer i = cv.getAsInteger(column);
343         if (i == null) return 0;
344         return i;
345     }
346 
347     /**
348      * Convert {@link Events} visibility values to EAS visibility values.
349      * @param visibility The {@link Events} visibility value.
350      * @return The corresponding EAS visibility value.
351      */
decodeVisibility(final int visibility)352     private static String decodeVisibility(final int visibility) {
353         final int easVisibility;
354         switch(visibility) {
355             case Events.ACCESS_DEFAULT:
356                 easVisibility = 0;
357                 break;
358             case Events.ACCESS_PUBLIC:
359                 easVisibility = 1;
360                 break;
361             case Events.ACCESS_PRIVATE:
362                 easVisibility = 2;
363                 break;
364             case Events.ACCESS_CONFIDENTIAL:
365                 easVisibility = 3;
366                 break;
367             default:
368                 easVisibility = 0;
369                 break;
370         }
371         return Integer.toString(easVisibility);
372     }
373 
374     /**
375      * Write an event to the {@link Serializer} for this upsync.
376      * @param entity The {@link Entity} for this event.
377      * @param clientId The client id for this event.
378      * @param s The {@link Serializer} for this Sync request.
379      * @throws IOException
380      * TODO: This can probably be refactored/cleaned up more.
381      */
sendEvent(final Context context, final Account account, final Entity entity, final String clientId, final double protocolVersion, final Serializer s)382     private void sendEvent(final Context context, final Account account, final Entity entity,
383         final String clientId, final double protocolVersion, final Serializer s)
384             throws IOException {
385         // Serialize for EAS here
386         // Set uid with the client id we created
387         // 1) Serialize the top-level event
388         // 2) Serialize attendees and reminders from subvalues
389         // 3) Look for exceptions and serialize with the top-level event
390         final ContentResolver cr = context.getContentResolver();
391         final ContentValues entityValues = entity.getEntityValues();
392         final boolean isException = (clientId == null);
393         boolean hasAttendees = false;
394         final boolean isChange = entityValues.containsKey(Events._SYNC_ID);
395         final boolean allDay =
396                 CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY);
397         final TimeZone localTimeZone = TimeZone.getDefault();
398 
399         // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception
400         // start time" data before other data in exceptions.  Failure to do so results in a
401         // status 6 error during sync
402         if (isException) {
403             // Send exception deleted flag if necessary
404             final Integer deleted = entityValues.getAsInteger(Events.DELETED);
405             final boolean isDeleted = deleted != null && deleted == 1;
406             final Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
407             final boolean isCanceled =
408                     eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
409             if (isDeleted || isCanceled) {
410                 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1");
411                 // If we're deleted, the UI will continue to show this exception until we mark
412                 // it canceled, so we'll do that here...
413                 if (isDeleted && !isCanceled) {
414                     final long eventId = entityValues.getAsLong(Events._ID);
415                     final ContentValues cv = new ContentValues(1);
416                     cv.put(Events.STATUS, Events.STATUS_CANCELED);
417                     cr.update(asSyncAdapter(
418                         ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account),
419                             cv, null, null);
420                 }
421             } else {
422                 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0");
423             }
424 
425             // TODO Add reminders to exceptions (allow them to be specified!)
426             Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
427             if (originalTime != null) {
428                 final boolean originalAllDay =
429                         CalendarUtilities.getIntegerValueAsBoolean(entityValues,
430                                 Events.ORIGINAL_ALL_DAY);
431                 if (originalAllDay) {
432                     // For all day events, we need our local all-day time
433                     originalTime =
434                         CalendarUtilities.getLocalAllDayCalendarTime(originalTime, localTimeZone);
435                 }
436                 s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
437                         CalendarUtilities.millisToEasDateTime(originalTime));
438             } else {
439                 // Illegal; what should we do?
440             }
441         }
442 
443         if (!isException) {
444             // A time zone is required in all EAS events; we'll use the default if none is set
445             // Exchange 2003 seems to require this first... :-)
446             String timeZoneName = entityValues.getAsString(
447                     allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE);
448             if (timeZoneName == null) {
449                 timeZoneName = localTimeZone.getID();
450             }
451             s.data(Tags.CALENDAR_TIME_ZONE,
452                     CalendarUtilities.timeZoneToTziString(TimeZone.getTimeZone(timeZoneName)));
453         }
454 
455         s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0");
456 
457         // DTSTART is always supplied
458         long startTime = entityValues.getAsLong(Events.DTSTART);
459         // Determine endTime; it's either provided as DTEND or we calculate using DURATION
460         // If no DURATION is provided, we default to one hour
461         long endTime;
462         if (entityValues.containsKey(Events.DTEND)) {
463             endTime = entityValues.getAsLong(Events.DTEND);
464         } else {
465             long durationMillis = DateUtils.HOUR_IN_MILLIS;
466             if (entityValues.containsKey(Events.DURATION)) {
467                 final Duration duration = new Duration();
468                 try {
469                     duration.parse(entityValues.getAsString(Events.DURATION));
470                     durationMillis = duration.getMillis();
471                 } catch (DateException e) {
472                     // Can't do much about this; use the default (1 hour)
473                 }
474             }
475             endTime = startTime + durationMillis;
476         }
477         if (allDay) {
478             startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, localTimeZone);
479             endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, localTimeZone);
480         }
481         s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime));
482         s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime));
483 
484         s.data(Tags.CALENDAR_DTSTAMP,
485                 CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
486 
487         String loc = entityValues.getAsString(Events.EVENT_LOCATION);
488         if (!TextUtils.isEmpty(loc)) {
489             if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
490                 // EAS 2.5 doesn't like bare line feeds
491                 loc = Utility.replaceBareLfWithCrlf(loc);
492             }
493             s.data(Tags.CALENDAR_LOCATION, loc);
494         }
495         s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
496 
497         if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
498             s.start(Tags.BASE_BODY);
499             s.data(Tags.BASE_TYPE, "1");
500             s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA);
501             s.end();
502         } else {
503             // EAS 2.5 doesn't like bare line feeds
504             s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY);
505         }
506 
507         if (!isException) {
508             // For Exchange 2003, only upsync if the event is new
509             if ((protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) {
510                 s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
511             }
512 
513             final String rrule = entityValues.getAsString(Events.RRULE);
514             if (rrule != null) {
515                 CalendarUtilities.recurrenceFromRrule(rrule, startTime, localTimeZone, s);
516             }
517         }
518         // Handle associated data EXCEPT for attendees, which have to be grouped
519         final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
520         // The earliest of the reminders for this Event; we can only send one reminder...
521         int earliestReminder = -1;
522         for (final Entity.NamedContentValues ncv: subValues) {
523             final Uri ncvUri = ncv.uri;
524             final ContentValues ncvValues = ncv.values;
525             if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
526                 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME);
527                 final String propertyValue = ncvValues.getAsString(ExtendedProperties.VALUE);
528                 if (TextUtils.isEmpty(propertyValue)) {
529                     continue;
530                 }
531                 if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) {
532                     // Send all the categories back to the server
533                     // We've saved them as a String of delimited tokens
534                     final StringTokenizer st =
535                             new StringTokenizer(propertyValue, CATEGORY_TOKENIZER_DELIMITER);
536                     if (st.countTokens() > 0) {
537                         s.start(Tags.CALENDAR_CATEGORIES);
538                         while (st.hasMoreTokens()) {
539                             s.data(Tags.CALENDAR_CATEGORY, st.nextToken());
540                         }
541                         s.end();
542                     }
543                 }
544             } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
545                 Integer mins = ncvValues.getAsInteger(Reminders.MINUTES);
546                 if (mins != null) {
547                     // -1 means "default", which for Exchange, is 30
548                     if (mins < 0) {
549                         mins = 30;
550                     }
551                     // Save this away if it's the earliest reminder (greatest minutes)
552                     if (mins > earliestReminder) {
553                         earliestReminder = mins;
554                     }
555                 }
556             }
557         }
558 
559         // If we have a reminder, send it to the server
560         if (earliestReminder >= 0) {
561             s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder));
562         }
563 
564         // We've got to send a UID, unless this is an exception.  If the event is new, we've
565         // generated one; if not, we should have gotten one from extended properties.
566         if (clientId != null) {
567             s.data(Tags.CALENDAR_UID, clientId);
568         }
569 
570         // Handle attendee data here; keep track of organizer and stream it afterward
571         String organizerName = null;
572         String organizerEmail = null;
573         for (final Entity.NamedContentValues ncv: subValues) {
574             final Uri ncvUri = ncv.uri;
575             final ContentValues ncvValues = ncv.values;
576             if (ncvUri.equals(Attendees.CONTENT_URI)) {
577                 final Integer relationship =
578                         ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
579                 final String attendeeEmail =
580                         ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
581                 // If there's no relationship, we can't create this for EAS
582                 // Similarly, we need an attendee email for each invitee
583                 if (relationship != null && !TextUtils.isEmpty(attendeeEmail)) {
584                     // Organizer isn't among attendees in EAS
585                     if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
586                         organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
587                         organizerEmail = attendeeEmail;
588                         continue;
589                     }
590                     if (!hasAttendees) {
591                         s.start(Tags.CALENDAR_ATTENDEES);
592                         hasAttendees = true;
593                     }
594                     s.start(Tags.CALENDAR_ATTENDEE);
595                     String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
596                     if (attendeeName == null) {
597                         attendeeName = attendeeEmail;
598                     }
599                     s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName);
600                     s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail);
601                     if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
602                         s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
603                     }
604                     s.end(); // Attendee
605                 }
606             }
607         }
608         if (hasAttendees) {
609             s.end();  // Attendees
610         }
611 
612         // Get busy status from availability
613         final int availability = entityValues.getAsInteger(Events.AVAILABILITY);
614         final int busyStatus = CalendarUtilities.busyStatusFromAvailability(availability);
615         s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
616 
617         // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
618         // In JB, organizer won't be an attendee
619         if (organizerEmail == null && entityValues.containsKey(Events.ORGANIZER)) {
620             organizerEmail = entityValues.getAsString(Events.ORGANIZER);
621         }
622         if (account.mEmailAddress.equalsIgnoreCase(organizerEmail)) {
623             s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
624         } else {
625             s.data(Tags.CALENDAR_MEETING_STATUS, "3");
626         }
627 
628         // For Exchange 2003, only upsync if the event is new
629         if (((protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) &&
630                 organizerName != null) {
631             s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
632         }
633 
634         // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003
635         // The result will be a status 6 failure during sync
636         final Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL);
637         if (visibility != null) {
638             s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
639         } else {
640             // Default to private if not set
641             s.data(Tags.CALENDAR_SENSITIVITY, "1");
642         }
643     }
644 
645     /**
646      * Handle exceptions to an event's recurrance pattern.
647      * @param s The {@link Serializer} for this upsync.
648      * @param entity The {@link Entity} for this event.
649      * @param entityValues The {@link ContentValues} for entity.
650      * @param serverId The server side id for this event.
651      * @param clientId The client side id for this event.
652      * @param calendarIdString The calendar id, as a {@link String}.
653      * @param selfOrganizer Whether the user is the organizer of this event.
654      * @throws IOException
655      */
handleExceptionsToRecurrenceRules(final Serializer s, final Context context, final Account account,final Entity entity, final ContentValues entityValues, final String serverId, final String clientId, final String calendarIdString, final boolean selfOrganizer, final double protocolVersion)656     private void handleExceptionsToRecurrenceRules(final Serializer s, final Context context,
657             final Account account,final Entity entity, final ContentValues entityValues,
658             final String serverId, final String clientId, final String calendarIdString,
659             final boolean selfOrganizer, final double protocolVersion) throws IOException {
660         final ContentResolver cr = context.getContentResolver();
661         final Cursor cursor = cr.query(asSyncAdapter(Events.CONTENT_URI, account), null,
662                 ORIGINAL_EVENT_AND_CALENDAR, new String[] { serverId, calendarIdString }, null);
663         if (cursor == null) {
664             return;
665         }
666         final EntityIterator exIterator = EventsEntity.newEntityIterator(cursor, cr);
667         boolean exFirst = true;
668         while (exIterator.hasNext()) {
669             final Entity exEntity = exIterator.next();
670             if (exFirst) {
671                 s.start(Tags.CALENDAR_EXCEPTIONS);
672                 exFirst = false;
673             }
674             s.start(Tags.CALENDAR_EXCEPTION);
675             sendEvent(context, account, exEntity, null, protocolVersion, s);
676             final ContentValues exValues = exEntity.getEntityValues();
677             if (getInt(exValues, Events.DIRTY) == 1) {
678                 // This is a new/updated exception, so we've got to notify our
679                 // attendees about it
680                 final long exEventId = exValues.getAsLong(Events._ID);
681 
682                 final int flag;
683                 if ((getInt(exValues, Events.DELETED) == 1) ||
684                         (getInt(exValues, Events.STATUS) == Events.STATUS_CANCELED)) {
685                     flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
686                     if (!selfOrganizer) {
687                         // Send a cancellation notice to the organizer
688                         // Since CalendarProvider2 sets the organizer of exceptions
689                         // to the user, we have to reset it first to the original
690                         // organizer
691                         exValues.put(Events.ORGANIZER, entityValues.getAsString(Events.ORGANIZER));
692                         sendDeclinedEmail(context, account, exEntity, clientId);
693                     }
694                 } else {
695                     flag = Message.FLAG_OUTGOING_MEETING_INVITE;
696                 }
697                 // Add the eventId of the exception to the uploaded id list, so that
698                 // the dirty/mark bits are cleared
699                 mUploadedIdList.add(exEventId);
700 
701                 // Copy version so the ics attachment shows the proper sequence #
702                 exValues.put(EVENT_SYNC_VERSION,
703                         entityValues.getAsString(EVENT_SYNC_VERSION));
704                 // Copy location so that it's included in the outgoing email
705                 if (entityValues.containsKey(Events.EVENT_LOCATION)) {
706                     exValues.put(Events.EVENT_LOCATION,
707                             entityValues.getAsString(Events.EVENT_LOCATION));
708                 }
709 
710                 if (selfOrganizer) {
711                     final Message msg = CalendarUtilities.createMessageForEntity(context, exEntity,
712                             flag, clientId, account);
713                     if (msg != null) {
714                         LogUtils.d(TAG, "Queueing exception update to %s", msg.mTo);
715                         mOutgoingMailList.add(msg);
716                     }
717 
718                     // Also send out a cancellation email to removed attendees
719                     final Entity removedEntity = new Entity(exValues);
720                     final Set<String> exAttendeeEmails = Sets.newHashSet();
721                     // Find all the attendees from the updated event
722                     for (final Entity.NamedContentValues ncv: exEntity.getSubValues()) {
723                         if (ncv.uri.equals(Attendees.CONTENT_URI)) {
724                             exAttendeeEmails.add(ncv.values.getAsString(Attendees.ATTENDEE_EMAIL));
725                         }
726                     }
727                     // Find the ones left out from the previous event and add them to the new entity
728                     for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
729                         if (ncv.uri.equals(Attendees.CONTENT_URI)) {
730                             final String attendeeEmail =
731                                     ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
732                             if (!exAttendeeEmails.contains(attendeeEmail)) {
733                                 removedEntity.addSubValue(ncv.uri, ncv.values);
734                             }
735                         }
736                     }
737 
738                     // Now send a cancellation email
739                     final Message removedMessage =
740                             CalendarUtilities.createMessageForEntity(context, removedEntity,
741                                     Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account);
742                     if (removedMessage != null) {
743                         LogUtils.d(TAG, "Queueing cancellation for removed attendees");
744                         mOutgoingMailList.add(removedMessage);
745                     }
746                 }
747             }
748             s.end(); // EXCEPTION
749         }
750         if (!exFirst) {
751             s.end(); // EXCEPTIONS
752         }
753     }
754 
755     /**
756      * Update the event properties with the attendee list, and send mail as appropriate.
757      * @param entity The {@link Entity} for this event.
758      * @param entityValues The {@link ContentValues} for entity.
759      * @param selfOrganizer Whether the user is the organizer of this event.
760      * @param eventId The id for this event.
761      * @param clientId The client side id for this event.
762      */
updateAttendeesAndSendMail(final Context context, final Account account, final Entity entity, final ContentValues entityValues, final boolean selfOrganizer, final long eventId, final String clientId)763     private void updateAttendeesAndSendMail(final Context context, final Account account,
764             final Entity entity, final ContentValues entityValues, final boolean selfOrganizer,
765             final long eventId, final String clientId) {
766         // Go through the extended properties of this Event and pull out our tokenized
767         // attendees list and the user attendee status; we will need them later
768         final ContentResolver cr = context.getContentResolver();
769         String attendeeString = null;
770         long attendeeStringId = -1;
771         String userAttendeeStatus = null;
772         long userAttendeeStatusId = -1;
773         for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
774             if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
775                 final ContentValues ncvValues = ncv.values;
776                 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME);
777                 if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
778                     attendeeString = ncvValues.getAsString(ExtendedProperties.VALUE);
779                     attendeeStringId = ncvValues.getAsLong(ExtendedProperties._ID);
780                 } else if (propertyName.equals(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
781                     userAttendeeStatus = ncvValues.getAsString(ExtendedProperties.VALUE);
782                     userAttendeeStatusId = ncvValues.getAsLong(ExtendedProperties._ID);
783                 }
784             }
785         }
786 
787         // Send the meeting invite if there are attendees and we're the organizer AND
788         // if the Event itself is dirty (we might be syncing only because an exception
789         // is dirty, in which case we DON'T send email about the Event)
790         if (selfOrganizer && (getInt(entityValues, Events.DIRTY) == 1)) {
791             final Message msg =
792                 CalendarUtilities.createMessageForEventId(context, eventId,
793                         Message.FLAG_OUTGOING_MEETING_INVITE, clientId, account);
794             if (msg != null) {
795                 LogUtils.d(TAG, "Queueing invitation to %s", msg.mTo);
796                 mOutgoingMailList.add(msg);
797             }
798             // Make a list out of our tokenized attendees, if we have any
799             final ArrayList<String> originalAttendeeList = new ArrayList<String>();
800             if (attendeeString != null) {
801                 final StringTokenizer st =
802                     new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
803                 while (st.hasMoreTokens()) {
804                     originalAttendeeList.add(st.nextToken());
805                 }
806             }
807             final StringBuilder newTokenizedAttendees = new StringBuilder();
808             // See if any attendees have been dropped and while we're at it, build
809             // an updated String with tokenized attendee addresses
810             for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
811                 if (ncv.uri.equals(Attendees.CONTENT_URI)) {
812                     final String attendeeEmail = ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
813                     // Remove all found attendees
814                     originalAttendeeList.remove(attendeeEmail);
815                     newTokenizedAttendees.append(attendeeEmail);
816                     newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
817                 }
818             }
819             // Update extended properties with the new attendee list, if we have one
820             // Otherwise, create one (this would be the case for Events created on
821             // device or "legacy" events (before this code was added)
822             final ContentValues cv = new ContentValues();
823             cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
824             if (attendeeString != null) {
825                 cr.update(asSyncAdapter(ContentUris.withAppendedId(
826                         ExtendedProperties.CONTENT_URI, attendeeStringId), account),
827                         cv, null, null);
828             } else {
829                 // If there wasn't an "attendees" property, insert one
830                 cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
831                 cv.put(ExtendedProperties.EVENT_ID, eventId);
832                 cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI, account), cv);
833             }
834             // Whoever is left has been removed from the attendee list; send them
835             // a cancellation
836             for (final String removedAttendee: originalAttendeeList) {
837                 // Send a cancellation message to each of them
838                 final Message cancelMsg = CalendarUtilities.createMessageForEventId(context,
839                         eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account,
840                         removedAttendee);
841                 if (cancelMsg != null) {
842                     // Just send it to the removed attendee
843                     LogUtils.d(TAG, "Queueing cancellation to removed attendee %s", cancelMsg.mTo);
844                     mOutgoingMailList.add(cancelMsg);
845                 }
846             }
847         } else if (!selfOrganizer) {
848             // If we're not the organizer, see if we've changed our attendee status
849             // Our last synced attendee status is in ExtendedProperties, and we've
850             // retrieved it above as userAttendeeStatus
851             final int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
852             int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
853             if (userAttendeeStatus != null) {
854                 try {
855                     syncStatus = Integer.parseInt(userAttendeeStatus);
856                 } catch (NumberFormatException e) {
857                     // Just in case somebody else mucked with this and it's not Integer
858                 }
859             }
860             if ((currentStatus != syncStatus) &&
861                     (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
862                 // If so, send a meeting reply
863                 final int messageFlag;
864                 switch (currentStatus) {
865                     case Attendees.ATTENDEE_STATUS_ACCEPTED:
866                         messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
867                         break;
868                     case Attendees.ATTENDEE_STATUS_DECLINED:
869                         messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
870                         break;
871                     case Attendees.ATTENDEE_STATUS_TENTATIVE:
872                         messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
873                         break;
874                     default:
875                         messageFlag = 0;
876                         break;
877                 }
878                 // Make sure we have a valid status (messageFlag should never be zero)
879                 if (messageFlag != 0 && userAttendeeStatusId >= 0) {
880                     // Save away the new status
881                     final ContentValues cv = new ContentValues(1);
882                     cv.put(ExtendedProperties.VALUE, Integer.toString(currentStatus));
883                     cr.update(asSyncAdapter(ContentUris.withAppendedId(
884                             ExtendedProperties.CONTENT_URI, userAttendeeStatusId), account),
885                             cv, null, null);
886                     // Send mail to the organizer advising of the new status
887                     final Message msg = CalendarUtilities.createMessageForEventId(context, eventId,
888                             messageFlag, clientId, account);
889                     if (msg != null) {
890                         LogUtils.d(TAG, "Queueing invitation reply to %s", msg.mTo);
891                         mOutgoingMailList.add(msg);
892                     }
893                 }
894             }
895         }
896     }
897 
898     /**
899      * Process a single event, adding to the {@link Serializer} as necessary.
900      * @param s The {@link Serializer} for this Sync request.
901      * @param entity The {@link Entity} for this event.
902      * @param calendarIdString The calendar's id, as a {@link String}.
903      * @param first Whether this would be the first event added to s.
904      * @return Whether this function added anything to s.
905      * @throws IOException
906      */
handleEntity(final Serializer s, final Context context, final Account account, final Entity entity, final String calendarIdString, final boolean first, final double protocolVersion)907     private boolean handleEntity(final Serializer s, final Context context, final Account account,
908             final Entity entity, final String calendarIdString, final boolean first,
909             final double protocolVersion) throws IOException {
910         // For each of these entities, create the change commands
911         final ContentResolver cr = context.getContentResolver();
912         final ContentValues entityValues = entity.getEntityValues();
913         // We first need to check whether we can upsync this event; our test for this
914         // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
915         // If this is set to "1", we can't upsync the event
916         for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
917             if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
918                 final ContentValues ncvValues = ncv.values;
919                 if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
920                         EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
921                     if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
922                         // Make sure we mark this to clear the dirty flag
923                         mUploadedIdList.add(entityValues.getAsLong(Events._ID));
924                         return false;
925                     }
926                 }
927             }
928         }
929 
930         // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
931         // We can generate all but what we're testing for below
932         final String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
933         if (organizerEmail == null || !entityValues.containsKey(Events.DTSTART) ||
934                 (!entityValues.containsKey(Events.DURATION)
935                         && !entityValues.containsKey(Events.DTEND))) {
936             return false;
937         }
938 
939         if (first) {
940             s.start(Tags.SYNC_COMMANDS);
941             LogUtils.d(TAG, "Sending Calendar changes to the server");
942         }
943 
944         final boolean selfOrganizer = organizerEmail.equalsIgnoreCase(account.mEmailAddress);
945         // Find our uid in the entity; otherwise create one
946         String clientId = entityValues.getAsString(Events.SYNC_DATA2);
947         if (clientId == null) {
948             clientId = UUID.randomUUID().toString();
949         }
950         final String serverId = entityValues.getAsString(Events._SYNC_ID);
951         final long eventId = entityValues.getAsLong(Events._ID);
952         if (serverId == null) {
953             // This is a new event; create a clientId
954             LogUtils.d(TAG, "Creating new event with clientId: %s", clientId);
955             s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
956             // And save it in the Event as the local id
957             final ContentValues cv = new ContentValues(2);
958             cv.put(Events.SYNC_DATA2, clientId);
959             cv.put(EVENT_SYNC_VERSION, "0");
960             cr.update(
961                     asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account),
962                     cv, null, null);
963         } else if (entityValues.getAsInteger(Events.DELETED) == 1) {
964             LogUtils.d(TAG, "Deleting event with serverId: %s", serverId);
965             s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
966             mDeletedIdList.add(eventId);
967             if (selfOrganizer) {
968                 final Message msg = CalendarUtilities.createMessageForEventId(context,
969                         eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, null, account);
970                 if (msg != null) {
971                     LogUtils.d(TAG, "Queueing cancellation to %s", msg.mTo);
972                     mOutgoingMailList.add(msg);
973                 }
974             } else {
975                 sendDeclinedEmail(context, account, entity, clientId);
976             }
977             // For deletions, we don't need to add application data, so just bail here.
978             return true;
979         } else {
980             LogUtils.d(TAG, "Upsync change to event with serverId: %s", serverId);
981             s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
982             // Save to the ContentResolver.
983             final String version = getEntityVersion(entityValues);
984             final ContentValues cv = new ContentValues(1);
985             cv.put(EVENT_SYNC_VERSION, version);
986             cr.update( asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
987                     account), cv, null, null);
988             // Also save in entityValues so that we send it this time around
989             entityValues.put(EVENT_SYNC_VERSION, version);
990         }
991         s.start(Tags.SYNC_APPLICATION_DATA);
992         sendEvent(context, account, entity, clientId, protocolVersion, s);
993 
994         // Now, the hard part; find exceptions for this event
995         if (serverId != null) {
996             handleExceptionsToRecurrenceRules(s, context, account, entity, entityValues, serverId,
997                     clientId, calendarIdString, selfOrganizer, protocolVersion);
998         }
999 
1000         s.end().end();  // ApplicationData & Add/Change
1001         mUploadedIdList.add(eventId);
1002         updateAttendeesAndSendMail(context, account, entity, entityValues, selfOrganizer, eventId,
1003             clientId);
1004         return true;
1005     }
1006 
setUpsyncCommands(Context context, final Account account, final double protocolVersion, final Serializer s)1007     protected void setUpsyncCommands(Context context, final Account account,
1008             final double protocolVersion, final Serializer s) throws IOException {
1009         final ContentResolver cr = context.getContentResolver();
1010         final String calendarIdString = Long.toString(mCalendarId);
1011         final String[] calendarIdArgument = { calendarIdString };
1012 
1013         markParentsOfDirtyEvents(context, account, calendarIdString, calendarIdArgument);
1014 
1015         // Now go through dirty/marked top-level events and send them back to the server
1016         final Cursor cursor = cr.query(asSyncAdapter(Events.CONTENT_URI, account), null,
1017                 DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, calendarIdArgument, null);
1018         if (cursor == null) {
1019             return;
1020         }
1021         final EntityIterator eventIterator = EventsEntity.newEntityIterator(cursor, cr);
1022 
1023         try {
1024             boolean first = true;
1025             while (eventIterator.hasNext()) {
1026                 final boolean addedCommand =
1027                         handleEntity(s, context, account, eventIterator.next(), calendarIdString,
1028                             first, protocolVersion);
1029                 if (addedCommand) {
1030                     first = false;
1031                 }
1032             }
1033             if (!first) {
1034                 s.end();  // Commands
1035             }
1036         } finally {
1037             eventIterator.close();
1038         }
1039     }
1040 
1041     @Override
cleanup(final Context context, final Account account)1042     public void cleanup(final Context context, final Account account) {
1043         final ContentResolver cr = context.getContentResolver();
1044         // Clear dirty and mark flags for updates sent to server
1045         if (!mUploadedIdList.isEmpty()) {
1046             final ContentValues cv = new ContentValues(2);
1047             cv.put(Events.DIRTY, 0);
1048             cv.put(EVENT_SYNC_MARK, "0");
1049             for (final long eventId : mUploadedIdList) {
1050                 cr.update(asSyncAdapter(ContentUris.withAppendedId(
1051                         Events.CONTENT_URI, eventId), account), cv, null, null);
1052             }
1053         }
1054         // Delete events marked for deletion
1055         if (!mDeletedIdList.isEmpty()) {
1056             for (final long eventId : mDeletedIdList) {
1057                 cr.delete(asSyncAdapter(ContentUris.withAppendedId(
1058                         Events.CONTENT_URI, eventId), account), null, null);
1059             }
1060         }
1061         // Send all messages that were created during this sync.
1062         for (final Message msg : mOutgoingMailList) {
1063             sendMessage(context, account, msg);
1064         }
1065 
1066         mDeletedIdList.clear();
1067         mUploadedIdList.clear();
1068         mOutgoingMailList.clear();
1069     }
1070 
1071     /**
1072      * Convenience method for adding a Message to an account's outbox
1073      * @param account The {@link Account} from which to send the message.
1074      * @param msg The message to send
1075      */
sendMessage(final Context context, final Account account, final EmailContent.Message msg)1076     protected void sendMessage(final Context context, final Account account,
1077         final EmailContent.Message msg) {
1078         long mailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
1079         // TODO: Improve system mailbox handling.
1080         if (mailboxId == Mailbox.NO_MAILBOX) {
1081             LogUtils.d(TAG, "No outbox for account %d, creating it", account.mId);
1082             final Mailbox outbox =
1083                     Mailbox.newSystemMailbox(context, account.mId, Mailbox.TYPE_OUTBOX);
1084             outbox.save(context);
1085             mailboxId = outbox.mId;
1086         }
1087         msg.mMailboxKey = mailboxId;
1088         msg.mAccountKey = account.mId;
1089         msg.save(context);
1090         requestSyncForMailbox(EmailContent.AUTHORITY, mailboxId);
1091     }
1092 
1093     /**
1094      * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
1095      * @param authority The authority for the mailbox that needs to sync.
1096      * @param mailboxId The id of the mailbox that needs to sync.
1097      */
requestSyncForMailbox(final String authority, final long mailboxId)1098     protected void requestSyncForMailbox(final String authority, final long mailboxId) {
1099         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
1100         ContentResolver.requestSync(mAndroidAccount, authority, extras);
1101         LogUtils.d(TAG, "requestSync EasServerConnection requestSyncForMailbox %s, %s",
1102                 mAndroidAccount.toString(), extras.toString());
1103     }
1104 
1105 
1106     /**
1107      * Delete an account from the Calendar provider.
1108      * @param context Our {@link Context}
1109      * @param emailAddress The email address of the account we wish to delete
1110      */
wipeAccountFromContentProvider(final Context context, final String emailAddress)1111     public static void wipeAccountFromContentProvider(final Context context,
1112             final String emailAddress) {
1113         try {
1114             context.getContentResolver().delete(asSyncAdapter(Calendars.CONTENT_URI, emailAddress),
1115                     Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(emailAddress)
1116                     + " AND " + Calendars.ACCOUNT_TYPE + "="+ DatabaseUtils.sqlEscapeString(
1117                             context.getString(R.string.account_manager_type_exchange)), null);
1118         } catch (IllegalArgumentException e) {
1119             LogUtils.e(TAG, "CalendarProvider disabled; unable to wipe account.");
1120         }
1121     }
1122 }
1123