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