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