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