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