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