• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                 // If there's no relationship, we can't create this for EAS
580                 // Similarly, we need an attendee email for each invitee
581                 if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
582                     // Organizer isn't among attendees in EAS
583                     if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
584                         organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
585                         organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
586                         continue;
587                     }
588                     if (!hasAttendees) {
589                         s.start(Tags.CALENDAR_ATTENDEES);
590                         hasAttendees = true;
591                     }
592                     s.start(Tags.CALENDAR_ATTENDEE);
593                     final String attendeeEmail =
594                             ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
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 EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query(
662                 asSyncAdapter(Events.CONTENT_URI, account), null, ORIGINAL_EVENT_AND_CALENDAR,
663                 new String[] { serverId, calendarIdString }, null), cr);
664         boolean exFirst = true;
665         while (exIterator.hasNext()) {
666             final Entity exEntity = exIterator.next();
667             if (exFirst) {
668                 s.start(Tags.CALENDAR_EXCEPTIONS);
669                 exFirst = false;
670             }
671             s.start(Tags.CALENDAR_EXCEPTION);
672             sendEvent(context, account, exEntity, null, protocolVersion, s);
673             final ContentValues exValues = exEntity.getEntityValues();
674             if (getInt(exValues, Events.DIRTY) == 1) {
675                 // This is a new/updated exception, so we've got to notify our
676                 // attendees about it
677                 final long exEventId = exValues.getAsLong(Events._ID);
678 
679                 final int flag;
680                 if ((getInt(exValues, Events.DELETED) == 1) ||
681                         (getInt(exValues, Events.STATUS) == Events.STATUS_CANCELED)) {
682                     flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
683                     if (!selfOrganizer) {
684                         // Send a cancellation notice to the organizer
685                         // Since CalendarProvider2 sets the organizer of exceptions
686                         // to the user, we have to reset it first to the original
687                         // organizer
688                         exValues.put(Events.ORGANIZER, entityValues.getAsString(Events.ORGANIZER));
689                         sendDeclinedEmail(context, account, exEntity, clientId);
690                     }
691                 } else {
692                     flag = Message.FLAG_OUTGOING_MEETING_INVITE;
693                 }
694                 // Add the eventId of the exception to the uploaded id list, so that
695                 // the dirty/mark bits are cleared
696                 mUploadedIdList.add(exEventId);
697 
698                 // Copy version so the ics attachment shows the proper sequence #
699                 exValues.put(EVENT_SYNC_VERSION,
700                         entityValues.getAsString(EVENT_SYNC_VERSION));
701                 // Copy location so that it's included in the outgoing email
702                 if (entityValues.containsKey(Events.EVENT_LOCATION)) {
703                     exValues.put(Events.EVENT_LOCATION,
704                             entityValues.getAsString(Events.EVENT_LOCATION));
705                 }
706 
707                 if (selfOrganizer) {
708                     final Message msg = CalendarUtilities.createMessageForEntity(context, exEntity,
709                             flag, clientId, account);
710                     if (msg != null) {
711                         LogUtils.d(TAG, "Queueing exception update to %s", msg.mTo);
712                         mOutgoingMailList.add(msg);
713                     }
714 
715                     // Also send out a cancellation email to removed attendees
716                     final Entity removedEntity = new Entity(exValues);
717                     final Set<String> exAttendeeEmails = Sets.newHashSet();
718                     // Find all the attendees from the updated event
719                     for (final Entity.NamedContentValues ncv: exEntity.getSubValues()) {
720                         if (ncv.uri.equals(Attendees.CONTENT_URI)) {
721                             exAttendeeEmails.add(ncv.values.getAsString(Attendees.ATTENDEE_EMAIL));
722                         }
723                     }
724                     // Find the ones left out from the previous event and add them to the new entity
725                     for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
726                         if (ncv.uri.equals(Attendees.CONTENT_URI)) {
727                             final String attendeeEmail =
728                                     ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
729                             if (!exAttendeeEmails.contains(attendeeEmail)) {
730                                 removedEntity.addSubValue(ncv.uri, ncv.values);
731                             }
732                         }
733                     }
734 
735                     // Now send a cancellation email
736                     final Message removedMessage =
737                             CalendarUtilities.createMessageForEntity(context, removedEntity,
738                                     Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account);
739                     if (removedMessage != null) {
740                         LogUtils.d(TAG, "Queueing cancellation for removed attendees");
741                         mOutgoingMailList.add(removedMessage);
742                     }
743                 }
744             }
745             s.end(); // EXCEPTION
746         }
747         if (!exFirst) {
748             s.end(); // EXCEPTIONS
749         }
750     }
751 
752     /**
753      * Update the event properties with the attendee list, and send mail as appropriate.
754      * @param entity The {@link Entity} for this event.
755      * @param entityValues The {@link ContentValues} for entity.
756      * @param selfOrganizer Whether the user is the organizer of this event.
757      * @param eventId The id for this event.
758      * @param clientId The client side id for this event.
759      */
updateAttendeesAndSendMail(final Context context, final Account account, final Entity entity, final ContentValues entityValues, final boolean selfOrganizer, final long eventId, final String clientId)760     private void updateAttendeesAndSendMail(final Context context, final Account account,
761             final Entity entity, final ContentValues entityValues, final boolean selfOrganizer,
762             final long eventId, final String clientId) {
763         // Go through the extended properties of this Event and pull out our tokenized
764         // attendees list and the user attendee status; we will need them later
765         final ContentResolver cr = context.getContentResolver();
766         String attendeeString = null;
767         long attendeeStringId = -1;
768         String userAttendeeStatus = null;
769         long userAttendeeStatusId = -1;
770         for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
771             if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
772                 final ContentValues ncvValues = ncv.values;
773                 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME);
774                 if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
775                     attendeeString = ncvValues.getAsString(ExtendedProperties.VALUE);
776                     attendeeStringId = ncvValues.getAsLong(ExtendedProperties._ID);
777                 } else if (propertyName.equals(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
778                     userAttendeeStatus = ncvValues.getAsString(ExtendedProperties.VALUE);
779                     userAttendeeStatusId = ncvValues.getAsLong(ExtendedProperties._ID);
780                 }
781             }
782         }
783 
784         // Send the meeting invite if there are attendees and we're the organizer AND
785         // if the Event itself is dirty (we might be syncing only because an exception
786         // is dirty, in which case we DON'T send email about the Event)
787         if (selfOrganizer && (getInt(entityValues, Events.DIRTY) == 1)) {
788             final Message msg =
789                 CalendarUtilities.createMessageForEventId(context, eventId,
790                         Message.FLAG_OUTGOING_MEETING_INVITE, clientId, account);
791             if (msg != null) {
792                 LogUtils.d(TAG, "Queueing invitation to %s", msg.mTo);
793                 mOutgoingMailList.add(msg);
794             }
795             // Make a list out of our tokenized attendees, if we have any
796             final ArrayList<String> originalAttendeeList = new ArrayList<String>();
797             if (attendeeString != null) {
798                 final StringTokenizer st =
799                     new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
800                 while (st.hasMoreTokens()) {
801                     originalAttendeeList.add(st.nextToken());
802                 }
803             }
804             final StringBuilder newTokenizedAttendees = new StringBuilder();
805             // See if any attendees have been dropped and while we're at it, build
806             // an updated String with tokenized attendee addresses
807             for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
808                 if (ncv.uri.equals(Attendees.CONTENT_URI)) {
809                     final String attendeeEmail = ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
810                     // Remove all found attendees
811                     originalAttendeeList.remove(attendeeEmail);
812                     newTokenizedAttendees.append(attendeeEmail);
813                     newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
814                 }
815             }
816             // Update extended properties with the new attendee list, if we have one
817             // Otherwise, create one (this would be the case for Events created on
818             // device or "legacy" events (before this code was added)
819             final ContentValues cv = new ContentValues();
820             cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
821             if (attendeeString != null) {
822                 cr.update(asSyncAdapter(ContentUris.withAppendedId(
823                         ExtendedProperties.CONTENT_URI, attendeeStringId), account),
824                         cv, null, null);
825             } else {
826                 // If there wasn't an "attendees" property, insert one
827                 cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
828                 cv.put(ExtendedProperties.EVENT_ID, eventId);
829                 cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI, account), cv);
830             }
831             // Whoever is left has been removed from the attendee list; send them
832             // a cancellation
833             for (final String removedAttendee: originalAttendeeList) {
834                 // Send a cancellation message to each of them
835                 final Message cancelMsg = CalendarUtilities.createMessageForEventId(context,
836                         eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account,
837                         removedAttendee);
838                 if (cancelMsg != null) {
839                     // Just send it to the removed attendee
840                     LogUtils.d(TAG, "Queueing cancellation to removed attendee %s", cancelMsg.mTo);
841                     mOutgoingMailList.add(cancelMsg);
842                 }
843             }
844         } else if (!selfOrganizer) {
845             // If we're not the organizer, see if we've changed our attendee status
846             // Our last synced attendee status is in ExtendedProperties, and we've
847             // retrieved it above as userAttendeeStatus
848             final int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
849             int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
850             if (userAttendeeStatus != null) {
851                 try {
852                     syncStatus = Integer.parseInt(userAttendeeStatus);
853                 } catch (NumberFormatException e) {
854                     // Just in case somebody else mucked with this and it's not Integer
855                 }
856             }
857             if ((currentStatus != syncStatus) &&
858                     (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
859                 // If so, send a meeting reply
860                 final int messageFlag;
861                 switch (currentStatus) {
862                     case Attendees.ATTENDEE_STATUS_ACCEPTED:
863                         messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
864                         break;
865                     case Attendees.ATTENDEE_STATUS_DECLINED:
866                         messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
867                         break;
868                     case Attendees.ATTENDEE_STATUS_TENTATIVE:
869                         messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
870                         break;
871                     default:
872                         messageFlag = 0;
873                         break;
874                 }
875                 // Make sure we have a valid status (messageFlag should never be zero)
876                 if (messageFlag != 0 && userAttendeeStatusId >= 0) {
877                     // Save away the new status
878                     final ContentValues cv = new ContentValues(1);
879                     cv.put(ExtendedProperties.VALUE, Integer.toString(currentStatus));
880                     cr.update(asSyncAdapter(ContentUris.withAppendedId(
881                             ExtendedProperties.CONTENT_URI, userAttendeeStatusId), account),
882                             cv, null, null);
883                     // Send mail to the organizer advising of the new status
884                     final Message msg = CalendarUtilities.createMessageForEventId(context, eventId,
885                             messageFlag, clientId, account);
886                     if (msg != null) {
887                         LogUtils.d(TAG, "Queueing invitation reply to %s", msg.mTo);
888                         mOutgoingMailList.add(msg);
889                     }
890                 }
891             }
892         }
893     }
894 
895     /**
896      * Process a single event, adding to the {@link Serializer} as necessary.
897      * @param s The {@link Serializer} for this Sync request.
898      * @param entity The {@link Entity} for this event.
899      * @param calendarIdString The calendar's id, as a {@link String}.
900      * @param first Whether this would be the first event added to s.
901      * @return Whether this function added anything to s.
902      * @throws IOException
903      */
handleEntity(final Serializer s, final Context context, final Account account, final Entity entity, final String calendarIdString, final boolean first, final double protocolVersion)904     private boolean handleEntity(final Serializer s, final Context context, final Account account,
905             final Entity entity, final String calendarIdString, final boolean first,
906             final double protocolVersion) throws IOException {
907         // For each of these entities, create the change commands
908         final ContentResolver cr = context.getContentResolver();
909         final ContentValues entityValues = entity.getEntityValues();
910         // We first need to check whether we can upsync this event; our test for this
911         // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
912         // If this is set to "1", we can't upsync the event
913         for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
914             if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
915                 final ContentValues ncvValues = ncv.values;
916                 if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
917                         EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
918                     if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
919                         // Make sure we mark this to clear the dirty flag
920                         mUploadedIdList.add(entityValues.getAsLong(Events._ID));
921                         return false;
922                     }
923                 }
924             }
925         }
926 
927         // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
928         // We can generate all but what we're testing for below
929         final String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
930         if (organizerEmail == null || !entityValues.containsKey(Events.DTSTART) ||
931                 (!entityValues.containsKey(Events.DURATION)
932                         && !entityValues.containsKey(Events.DTEND))) {
933             return false;
934         }
935 
936         if (first) {
937             s.start(Tags.SYNC_COMMANDS);
938             LogUtils.d(TAG, "Sending Calendar changes to the server");
939         }
940 
941         final boolean selfOrganizer = organizerEmail.equalsIgnoreCase(account.mEmailAddress);
942         // Find our uid in the entity; otherwise create one
943         String clientId = entityValues.getAsString(Events.SYNC_DATA2);
944         if (clientId == null) {
945             clientId = UUID.randomUUID().toString();
946         }
947         final String serverId = entityValues.getAsString(Events._SYNC_ID);
948         final long eventId = entityValues.getAsLong(Events._ID);
949         if (serverId == null) {
950             // This is a new event; create a clientId
951             LogUtils.d(TAG, "Creating new event with clientId: %s", clientId);
952             s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
953             // And save it in the Event as the local id
954             final ContentValues cv = new ContentValues(2);
955             cv.put(Events.SYNC_DATA2, clientId);
956             cv.put(EVENT_SYNC_VERSION, "0");
957             cr.update(
958                     asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account),
959                     cv, null, null);
960         } else if (entityValues.getAsInteger(Events.DELETED) == 1) {
961             LogUtils.d(TAG, "Deleting event with serverId: %s", serverId);
962             s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
963             mDeletedIdList.add(eventId);
964             if (selfOrganizer) {
965                 final Message msg = CalendarUtilities.createMessageForEventId(context,
966                         eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, null, account);
967                 if (msg != null) {
968                     LogUtils.d(TAG, "Queueing cancellation to %s", msg.mTo);
969                     mOutgoingMailList.add(msg);
970                 }
971             } else {
972                 sendDeclinedEmail(context, account, entity, clientId);
973             }
974             // For deletions, we don't need to add application data, so just bail here.
975             return true;
976         } else {
977             LogUtils.d(TAG, "Upsync change to event with serverId: %s", serverId);
978             s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
979             // Save to the ContentResolver.
980             final String version = getEntityVersion(entityValues);
981             final ContentValues cv = new ContentValues(1);
982             cv.put(EVENT_SYNC_VERSION, version);
983             cr.update( asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
984                     account), cv, null, null);
985             // Also save in entityValues so that we send it this time around
986             entityValues.put(EVENT_SYNC_VERSION, version);
987         }
988         s.start(Tags.SYNC_APPLICATION_DATA);
989         sendEvent(context, account, entity, clientId, protocolVersion, s);
990 
991         // Now, the hard part; find exceptions for this event
992         if (serverId != null) {
993             handleExceptionsToRecurrenceRules(s, context, account, entity, entityValues, serverId,
994                     clientId, calendarIdString, selfOrganizer, protocolVersion);
995         }
996 
997         s.end().end();  // ApplicationData & Add/Change
998         mUploadedIdList.add(eventId);
999         updateAttendeesAndSendMail(context, account, entity, entityValues, selfOrganizer, eventId,
1000             clientId);
1001         return true;
1002     }
1003 
setUpsyncCommands(Context context, final Account account, final double protocolVersion, final Serializer s)1004     protected void setUpsyncCommands(Context context, final Account account,
1005             final double protocolVersion, final Serializer s) throws IOException {
1006         final ContentResolver cr = context.getContentResolver();
1007         final String calendarIdString = Long.toString(mCalendarId);
1008         final String[] calendarIdArgument = { calendarIdString };
1009 
1010         markParentsOfDirtyEvents(context, account, calendarIdString, calendarIdArgument);
1011 
1012         // Now go through dirty/marked top-level events and send them back to the server
1013         final EntityIterator eventIterator = EventsEntity.newEntityIterator(
1014                 cr.query(asSyncAdapter(Events.CONTENT_URI, account), null,
1015                 DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, calendarIdArgument, null), cr);
1016 
1017         try {
1018             boolean first = true;
1019             while (eventIterator.hasNext()) {
1020                 final boolean addedCommand =
1021                         handleEntity(s, context, account, eventIterator.next(), calendarIdString,
1022                             first, protocolVersion);
1023                 if (addedCommand) {
1024                     first = false;
1025                 }
1026             }
1027             if (!first) {
1028                 s.end();  // Commands
1029             }
1030         } finally {
1031             eventIterator.close();
1032         }
1033     }
1034 
1035     @Override
cleanup(final Context context, final Account account)1036     public void cleanup(final Context context, final Account account) {
1037         final ContentResolver cr = context.getContentResolver();
1038         // Clear dirty and mark flags for updates sent to server
1039         if (!mUploadedIdList.isEmpty()) {
1040             final ContentValues cv = new ContentValues(2);
1041             cv.put(Events.DIRTY, 0);
1042             cv.put(EVENT_SYNC_MARK, "0");
1043             for (final long eventId : mUploadedIdList) {
1044                 cr.update(asSyncAdapter(ContentUris.withAppendedId(
1045                         Events.CONTENT_URI, eventId), account), cv, null, null);
1046             }
1047         }
1048         // Delete events marked for deletion
1049         if (!mDeletedIdList.isEmpty()) {
1050             for (final long eventId : mDeletedIdList) {
1051                 cr.delete(asSyncAdapter(ContentUris.withAppendedId(
1052                         Events.CONTENT_URI, eventId), account), null, null);
1053             }
1054         }
1055         // Send all messages that were created during this sync.
1056         for (final Message msg : mOutgoingMailList) {
1057             sendMessage(context, account, msg);
1058         }
1059 
1060         mDeletedIdList.clear();
1061         mUploadedIdList.clear();
1062         mOutgoingMailList.clear();
1063     }
1064 
1065     /**
1066      * Convenience method for adding a Message to an account's outbox
1067      * @param account The {@link Account} from which to send the message.
1068      * @param msg The message to send
1069      */
sendMessage(final Context context, final Account account, final EmailContent.Message msg)1070     protected void sendMessage(final Context context, final Account account,
1071         final EmailContent.Message msg) {
1072         long mailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX);
1073         // TODO: Improve system mailbox handling.
1074         if (mailboxId == Mailbox.NO_MAILBOX) {
1075             LogUtils.d(TAG, "No outbox for account %d, creating it", account.mId);
1076             final Mailbox outbox =
1077                     Mailbox.newSystemMailbox(context, account.mId, Mailbox.TYPE_OUTBOX);
1078             outbox.save(context);
1079             mailboxId = outbox.mId;
1080         }
1081         msg.mMailboxKey = mailboxId;
1082         msg.mAccountKey = account.mId;
1083         msg.save(context);
1084         requestSyncForMailbox(EmailContent.AUTHORITY, mailboxId);
1085     }
1086 
1087     /**
1088      * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
1089      * @param authority The authority for the mailbox that needs to sync.
1090      * @param mailboxId The id of the mailbox that needs to sync.
1091      */
requestSyncForMailbox(final String authority, final long mailboxId)1092     protected void requestSyncForMailbox(final String authority, final long mailboxId) {
1093         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
1094         ContentResolver.requestSync(mAndroidAccount, authority, extras);
1095         LogUtils.d(TAG, "requestSync EasServerConnection requestSyncForMailbox %s, %s",
1096                 mAndroidAccount.toString(), extras.toString());
1097     }
1098 
1099 
1100     /**
1101      * Delete an account from the Calendar provider.
1102      * @param context Our {@link Context}
1103      * @param emailAddress The email address of the account we wish to delete
1104      */
wipeAccountFromContentProvider(final Context context, final String emailAddress)1105     public static void wipeAccountFromContentProvider(final Context context,
1106             final String emailAddress) {
1107         context.getContentResolver().delete(asSyncAdapter(Calendars.CONTENT_URI, emailAddress),
1108                 Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(emailAddress)
1109                         + " AND " + Calendars.ACCOUNT_TYPE + "="+ DatabaseUtils.sqlEscapeString(
1110                         context.getString(R.string.account_manager_type_exchange)), null);
1111     }
1112 }
1113