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