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