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