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