Home | History | Annotate | Download | only in eas
      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 
    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
    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
    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
    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      */
    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      */
    231     private Uri asSyncAdapter(final Uri uri, final Account account) {
    232         return asSyncAdapter(uri, account.mEmailAddress);
    233     }
    234 
    235     protected String getFolderClassName() {
    236         return "Calendar";
    237     }
    238 
    239     protected void setInitialSyncOptions(final Serializer s) throws IOException {
    240         // Nothing to do for Calendar.
    241     }
    242 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
   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
   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      */
   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      */
   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      */
   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