1 package com.android.exchange.eas; 2 3 import android.content.ContentResolver; 4 import android.content.ContentUris; 5 import android.content.ContentValues; 6 import android.content.Context; 7 import android.content.Entity; 8 import android.content.EntityIterator; 9 import android.database.Cursor; 10 import android.database.DatabaseUtils; 11 import android.net.Uri; 12 import android.os.Bundle; 13 import android.provider.CalendarContract; 14 import android.provider.CalendarContract.Attendees; 15 import android.provider.CalendarContract.Calendars; 16 import android.provider.CalendarContract.Events; 17 import android.provider.CalendarContract.EventsEntity; 18 import android.provider.CalendarContract.ExtendedProperties; 19 import android.provider.CalendarContract.Reminders; 20 import android.text.TextUtils; 21 import android.text.format.DateUtils; 22 23 import com.android.calendarcommon2.DateException; 24 import com.android.calendarcommon2.Duration; 25 import com.android.emailcommon.TrafficFlags; 26 import com.android.emailcommon.provider.Account; 27 import com.android.emailcommon.provider.EmailContent; 28 import com.android.emailcommon.provider.EmailContent.Message; 29 import com.android.emailcommon.provider.Mailbox; 30 import com.android.emailcommon.utility.Utility; 31 import com.android.exchange.Eas; 32 import com.android.exchange.R; 33 import com.android.exchange.adapter.AbstractSyncParser; 34 import com.android.exchange.adapter.CalendarSyncParser; 35 import com.android.exchange.adapter.Serializer; 36 import com.android.exchange.adapter.Tags; 37 import com.android.exchange.utility.CalendarUtilities; 38 import com.android.mail.utils.LogUtils; 39 import com.google.common.collect.Sets; 40 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.util.ArrayList; 44 import java.util.Set; 45 import java.util.StringTokenizer; 46 import java.util.TimeZone; 47 import java.util.UUID; 48 49 /** 50 * Performs an Exchange Sync for a Calendar collection. 51 */ 52 public class EasSyncCalendar extends EasSyncCollectionTypeBase { 53 private static final String TAG = Eas.LOG_TAG; 54 55 // TODO: Some constants are copied from CalendarSyncAdapter and are still used by the parser. 56 // These values need to stay in sync; when the parser is cleaned up, be sure to unify them. 57 58 private static final int PIM_WINDOW_SIZE_CALENDAR = 10; 59 60 /** Projection for getting a calendar id. */ 61 private static final String[] CALENDAR_ID_PROJECTION = { Calendars._ID }; 62 private static final int CALENDAR_ID_COLUMN = 0; 63 64 /** Content selection for getting a calendar id for an account. */ 65 private static final String CALENDAR_SELECTION_ACCOUNT_AND_SYNC_ID = 66 Calendars.ACCOUNT_NAME + "=? AND " + 67 Calendars.ACCOUNT_TYPE + "=? AND " + 68 Calendars._SYNC_ID + "=?"; 69 70 /** Content selection for getting a calendar id for an account. */ 71 private static final String CALENDAR_SELECTION_ACCOUNT_AND_NO_SYNC = 72 Calendars.ACCOUNT_NAME + "=? AND " + 73 Calendars.ACCOUNT_TYPE + "=? AND " + 74 Calendars._SYNC_ID + " IS NULL"; 75 76 /** The column used to track the timezone of the event. */ 77 private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1; 78 79 /** Used to keep track of exception vs. parent event dirtiness. */ 80 private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8; 81 82 /** The column used to track the Event version sequence number. */ 83 private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4; 84 85 /** Projection for getting info about changed events. */ 86 private static final String[] ORIGINAL_EVENT_PROJECTION = { Events.ORIGINAL_ID, Events._ID }; 87 private static final int ORIGINAL_EVENT_ORIGINAL_ID_COLUMN = 0; 88 private static final int ORIGINAL_EVENT_ID_COLUMN = 1; 89 90 /** Content selection for dirty calendar events. */ 91 private static final String DIRTY_EXCEPTION_IN_CALENDAR = Events.DIRTY + "=1 AND " + 92 Events.ORIGINAL_ID + " NOTNULL AND " + Events.CALENDAR_ID + "=?"; 93 94 /** Where clause for updating dirty events. */ 95 private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " + 96 Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?"; 97 98 /** Content selection for dirty or marked top level events. */ 99 private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY + 100 "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " + Events.ORIGINAL_ID + " ISNULL AND " + 101 Events.CALENDAR_ID + "=?"; 102 103 /** Content selection for getting events when handling exceptions. */ 104 private static final String ORIGINAL_EVENT_AND_CALENDAR = Events.ORIGINAL_SYNC_ID + "=? AND " + 105 Events.CALENDAR_ID + "=?"; 106 107 private static final String CATEGORY_TOKENIZER_DELIMITER = "\\"; 108 private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER; 109 110 /** Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges) */ 111 private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited"; 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_CATEGORIES = "categories"; 116 117 private final android.accounts.Account mAndroidAccount; 118 private final long mCalendarId; 119 120 // The following lists are populated as part of upsync, and handled during cleanup. 121 /** Ids of events that were deleted in this upsync. */ 122 private final ArrayList<Long> mDeletedIdList = new ArrayList<Long>(); 123 /** Ids of events that were changed in this upsync. */ 124 private final ArrayList<Long> mUploadedIdList = new ArrayList<Long>(); 125 /** Emails that need to be sent due to this upsync. */ 126 private final ArrayList<Message> mOutgoingMailList = new ArrayList<Message>(); 127 128 public EasSyncCalendar(final Context context, final Account account, 129 final Mailbox mailbox) { 130 super(); 131 mAndroidAccount = new android.accounts.Account(account.mEmailAddress, 132 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 133 final ContentResolver cr = context.getContentResolver(); 134 final Cursor c = cr.query(Calendars.CONTENT_URI, CALENDAR_ID_PROJECTION, 135 CALENDAR_SELECTION_ACCOUNT_AND_SYNC_ID, 136 new String[] { 137 account.mEmailAddress, 138 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE, 139 mailbox.mServerId, 140 }, null); 141 if (c == null) { 142 mCalendarId = -1; 143 } else { 144 try { 145 if (c.moveToFirst()) { 146 mCalendarId = c.getLong(CALENDAR_ID_COLUMN); 147 } else { 148 long id = -1; 149 // Check if we have a calendar for this account with no server Id. If so, it was 150 // synced with an older version of the sync adapter before serverId's were 151 // supported. 152 final Cursor c1 = cr.query(Calendars.CONTENT_URI, 153 CALENDAR_ID_PROJECTION, 154 CALENDAR_SELECTION_ACCOUNT_AND_NO_SYNC, 155 new String[] { 156 account.mEmailAddress, 157 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE, 158 }, null); 159 if (c1 != null) { 160 try { 161 if (c1.moveToFirst()) { 162 id = c1.getLong(CALENDAR_ID_COLUMN); 163 final ContentValues values = new ContentValues(); 164 values.put(Calendars._SYNC_ID, mailbox.mServerId); 165 cr.update( 166 ContentUris.withAppendedId( 167 asSyncAdapter(Calendars.CONTENT_URI, account), id), 168 values, 169 null, /* where */ 170 null /* selectionArgs */); 171 } 172 } finally { 173 c1.close(); 174 } 175 } 176 177 if (id >= 0) { 178 mCalendarId = id; 179 } else { 180 mCalendarId = CalendarUtilities.createCalendar(context, cr, account, 181 mailbox); 182 } 183 } 184 } finally { 185 c.close(); 186 } 187 } 188 } 189 190 @Override 191 public void setSyncOptions(final Context context, final Serializer s, 192 final double protocolVersion, final Account account, final Mailbox mailbox, 193 final boolean isInitialSync, final int numWindows) throws IOException { 194 if (isInitialSync) { 195 setInitialSyncOptions(s); 196 } else { 197 setNonInitialSyncOptions(s, numWindows, protocolVersion); 198 setUpsyncCommands(context, account, protocolVersion, s); 199 } 200 } 201 202 203 @Override 204 public AbstractSyncParser getParser(final Context context, final Account account, 205 final Mailbox mailbox, final InputStream is) throws IOException { 206 return new CalendarSyncParser(context, context.getContentResolver(), is, mailbox, account, 207 mAndroidAccount, mCalendarId); 208 } 209 210 @Override 211 public int getTrafficFlag() { 212 return TrafficFlags.DATA_CALENDAR; 213 } 214 215 /** 216 * Adds params to a {@link Uri} to indicate that the caller is a sync adapter, and to add the 217 * account info. 218 * @param uri The {@link Uri} to which to add params. 219 * @return The augmented {@link Uri}. 220 */ 221 private static Uri asSyncAdapter(final Uri uri, final String emailAddress) { 222 return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 223 .appendQueryParameter(Calendars.ACCOUNT_NAME, emailAddress) 224 .appendQueryParameter(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE) 225 .build(); 226 } 227 228 /** 229 * Convenience wrapper to {@link #asSyncAdapter(android.net.Uri, String)}. 230 */ 231 private Uri asSyncAdapter(final Uri uri, final Account account) { 232 return asSyncAdapter(uri, account.mEmailAddress); 233 } 234 235 protected String getFolderClassName() { 236 return "Calendar"; 237 } 238 239 protected void setInitialSyncOptions(final Serializer s) throws IOException { 240 // Nothing to do for Calendar. 241 } 242 243 protected void setNonInitialSyncOptions(final Serializer s, final int numWindows, 244 final double protocolVersion) throws IOException { 245 final int windowSize = numWindows * PIM_WINDOW_SIZE_CALENDAR; 246 if (windowSize > MAX_WINDOW_SIZE + PIM_WINDOW_SIZE_CALENDAR) { 247 throw new IOException("Max window size reached and still no data"); 248 } 249 setPimSyncOptions(s, Eas.FILTER_2_WEEKS, protocolVersion, 250 windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE); 251 } 252 253 /** 254 * Find all dirty events for our calendar and mark their parents. Also delete any dirty events 255 * that have no parents. 256 * @param calendarIdString {@link #mCalendarId}, as a String. 257 * @param calendarIdArgument calendarIdString, in a String array. 258 */ 259 private void markParentsOfDirtyEvents(final Context context, final Account account, 260 final String calendarIdString, final String[] calendarIdArgument) { 261 final ContentResolver cr = context.getContentResolver(); 262 // We've got to handle exceptions as part of the parent when changes occur, so we need 263 // to find new/changed exceptions and mark the parent dirty 264 final ArrayList<Long> orphanedExceptions = new ArrayList<Long>(); 265 final Cursor c = cr.query(Events.CONTENT_URI, 266 ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION_IN_CALENDAR, calendarIdArgument, null); 267 if (c != null) { 268 try { 269 final ContentValues cv = new ContentValues(1); 270 // We use _sync_mark here to distinguish dirty parents from parents with dirty 271 // exceptions 272 cv.put(EVENT_SYNC_MARK, "1"); 273 while (c.moveToNext()) { 274 // Mark the parents of dirty exceptions 275 final long parentId = c.getLong(ORIGINAL_EVENT_ORIGINAL_ID_COLUMN); 276 final int cnt = cr.update(asSyncAdapter(Events.CONTENT_URI, account), cv, 277 EVENT_ID_AND_CALENDAR_ID, 278 new String[] { Long.toString(parentId), calendarIdString }); 279 // Keep track of any orphaned exceptions 280 if (cnt == 0) { 281 orphanedExceptions.add(c.getLong(ORIGINAL_EVENT_ID_COLUMN)); 282 } 283 } 284 } finally { 285 c.close(); 286 } 287 } 288 289 // Delete any orphaned exceptions 290 for (final long orphan : orphanedExceptions) { 291 LogUtils.d(TAG, "Deleted orphaned exception: %d", orphan); 292 cr.delete(asSyncAdapter( 293 ContentUris.withAppendedId(Events.CONTENT_URI, orphan), account), null, null); 294 } 295 } 296 297 /** 298 * Get the version number of the current event, incrementing it if it's already there. 299 * @param entityValues The {@link ContentValues} for this event. 300 * @return The new version number for this event (i.e. 0 if it's a new event, or the old version 301 * number + 1). 302 */ 303 private static String getEntityVersion(final ContentValues entityValues) { 304 final String version = entityValues.getAsString(EVENT_SYNC_VERSION); 305 // This should never be null, but catch this error anyway 306 // Version should be "0" when we create the event, so use that 307 if (version != null) { 308 // Increment and save 309 try { 310 return Integer.toString((Integer.parseInt(version) + 1)); 311 } catch (final NumberFormatException e) { 312 // Handle the case in which someone writes a non-integer here; 313 // shouldn't happen, but we don't want to kill the sync for his 314 } 315 } 316 return "0"; 317 } 318 319 /** 320 * Convenience method for sending an email to the organizer declining the meeting. 321 * @param entity The {@link Entity} for this event. 322 * @param clientId The client id for this event. 323 */ 324 private void sendDeclinedEmail(final Context context, final Account account, 325 final Entity entity, final String clientId) { 326 final Message msg = 327 CalendarUtilities.createMessageForEntity(context, entity, 328 Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, account); 329 if (msg != null) { 330 LogUtils.d(TAG, "Queueing declined response to %s", msg.mTo); 331 mOutgoingMailList.add(msg); 332 } 333 } 334 335 /** 336 * Get an integer value from a {@link ContentValues}, or 0 if the value isn't there. 337 * @param cv The {@link ContentValues} to find the value in. 338 * @param column The name of the column in cv to get. 339 * @return The appropriate value as an integer, or 0 if it's not there. 340 */ 341 private static int getInt(final ContentValues cv, final String column) { 342 final Integer i = cv.getAsInteger(column); 343 if (i == null) return 0; 344 return i; 345 } 346 347 /** 348 * Convert {@link Events} visibility values to EAS visibility values. 349 * @param visibility The {@link Events} visibility value. 350 * @return The corresponding EAS visibility value. 351 */ 352 private static String decodeVisibility(final int visibility) { 353 final int easVisibility; 354 switch(visibility) { 355 case Events.ACCESS_DEFAULT: 356 easVisibility = 0; 357 break; 358 case Events.ACCESS_PUBLIC: 359 easVisibility = 1; 360 break; 361 case Events.ACCESS_PRIVATE: 362 easVisibility = 2; 363 break; 364 case Events.ACCESS_CONFIDENTIAL: 365 easVisibility = 3; 366 break; 367 default: 368 easVisibility = 0; 369 break; 370 } 371 return Integer.toString(easVisibility); 372 } 373 374 /** 375 * Write an event to the {@link Serializer} for this upsync. 376 * @param entity The {@link Entity} for this event. 377 * @param clientId The client id for this event. 378 * @param s The {@link Serializer} for this Sync request. 379 * @throws IOException 380 * TODO: This can probably be refactored/cleaned up more. 381 */ 382 private void sendEvent(final Context context, final Account account, final Entity entity, 383 final String clientId, final double protocolVersion, final Serializer s) 384 throws IOException { 385 // Serialize for EAS here 386 // Set uid with the client id we created 387 // 1) Serialize the top-level event 388 // 2) Serialize attendees and reminders from subvalues 389 // 3) Look for exceptions and serialize with the top-level event 390 final ContentResolver cr = context.getContentResolver(); 391 final ContentValues entityValues = entity.getEntityValues(); 392 final boolean isException = (clientId == null); 393 boolean hasAttendees = false; 394 final boolean isChange = entityValues.containsKey(Events._SYNC_ID); 395 final boolean allDay = 396 CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY); 397 final TimeZone localTimeZone = TimeZone.getDefault(); 398 399 // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception 400 // start time" data before other data in exceptions. Failure to do so results in a 401 // status 6 error during sync 402 if (isException) { 403 // Send exception deleted flag if necessary 404 final Integer deleted = entityValues.getAsInteger(Events.DELETED); 405 final boolean isDeleted = deleted != null && deleted == 1; 406 final Integer eventStatus = entityValues.getAsInteger(Events.STATUS); 407 final boolean isCanceled = 408 eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED); 409 if (isDeleted || isCanceled) { 410 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1"); 411 // If we're deleted, the UI will continue to show this exception until we mark 412 // it canceled, so we'll do that here... 413 if (isDeleted && !isCanceled) { 414 final long eventId = entityValues.getAsLong(Events._ID); 415 final ContentValues cv = new ContentValues(1); 416 cv.put(Events.STATUS, Events.STATUS_CANCELED); 417 cr.update(asSyncAdapter( 418 ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account), 419 cv, null, null); 420 } 421 } else { 422 s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0"); 423 } 424 425 // TODO Add reminders to exceptions (allow them to be specified!) 426 Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME); 427 if (originalTime != null) { 428 final boolean originalAllDay = 429 CalendarUtilities.getIntegerValueAsBoolean(entityValues, 430 Events.ORIGINAL_ALL_DAY); 431 if (originalAllDay) { 432 // For all day events, we need our local all-day time 433 originalTime = 434 CalendarUtilities.getLocalAllDayCalendarTime(originalTime, localTimeZone); 435 } 436 s.data(Tags.CALENDAR_EXCEPTION_START_TIME, 437 CalendarUtilities.millisToEasDateTime(originalTime)); 438 } else { 439 // Illegal; what should we do? 440 } 441 } 442 443 if (!isException) { 444 // A time zone is required in all EAS events; we'll use the default if none is set 445 // Exchange 2003 seems to require this first... :-) 446 String timeZoneName = entityValues.getAsString( 447 allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE); 448 if (timeZoneName == null) { 449 timeZoneName = localTimeZone.getID(); 450 } 451 s.data(Tags.CALENDAR_TIME_ZONE, 452 CalendarUtilities.timeZoneToTziString(TimeZone.getTimeZone(timeZoneName))); 453 } 454 455 s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0"); 456 457 // DTSTART is always supplied 458 long startTime = entityValues.getAsLong(Events.DTSTART); 459 // Determine endTime; it's either provided as DTEND or we calculate using DURATION 460 // If no DURATION is provided, we default to one hour 461 long endTime; 462 if (entityValues.containsKey(Events.DTEND)) { 463 endTime = entityValues.getAsLong(Events.DTEND); 464 } else { 465 long durationMillis = DateUtils.HOUR_IN_MILLIS; 466 if (entityValues.containsKey(Events.DURATION)) { 467 final Duration duration = new Duration(); 468 try { 469 duration.parse(entityValues.getAsString(Events.DURATION)); 470 durationMillis = duration.getMillis(); 471 } catch (DateException e) { 472 // Can't do much about this; use the default (1 hour) 473 } 474 } 475 endTime = startTime + durationMillis; 476 } 477 if (allDay) { 478 startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, localTimeZone); 479 endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, localTimeZone); 480 } 481 s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime)); 482 s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime)); 483 484 s.data(Tags.CALENDAR_DTSTAMP, 485 CalendarUtilities.millisToEasDateTime(System.currentTimeMillis())); 486 487 String loc = entityValues.getAsString(Events.EVENT_LOCATION); 488 if (!TextUtils.isEmpty(loc)) { 489 if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 490 // EAS 2.5 doesn't like bare line feeds 491 loc = Utility.replaceBareLfWithCrlf(loc); 492 } 493 s.data(Tags.CALENDAR_LOCATION, loc); 494 } 495 s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT); 496 497 if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 498 s.start(Tags.BASE_BODY); 499 s.data(Tags.BASE_TYPE, "1"); 500 s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA); 501 s.end(); 502 } else { 503 // EAS 2.5 doesn't like bare line feeds 504 s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY); 505 } 506 507 if (!isException) { 508 // For Exchange 2003, only upsync if the event is new 509 if ((protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) { 510 s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL); 511 } 512 513 final String rrule = entityValues.getAsString(Events.RRULE); 514 if (rrule != null) { 515 CalendarUtilities.recurrenceFromRrule(rrule, startTime, localTimeZone, s); 516 } 517 } 518 // Handle associated data EXCEPT for attendees, which have to be grouped 519 final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues(); 520 // The earliest of the reminders for this Event; we can only send one reminder... 521 int earliestReminder = -1; 522 for (final Entity.NamedContentValues ncv: subValues) { 523 final Uri ncvUri = ncv.uri; 524 final ContentValues ncvValues = ncv.values; 525 if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) { 526 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME); 527 final String propertyValue = ncvValues.getAsString(ExtendedProperties.VALUE); 528 if (TextUtils.isEmpty(propertyValue)) { 529 continue; 530 } 531 if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) { 532 // Send all the categories back to the server 533 // We've saved them as a String of delimited tokens 534 final StringTokenizer st = 535 new StringTokenizer(propertyValue, CATEGORY_TOKENIZER_DELIMITER); 536 if (st.countTokens() > 0) { 537 s.start(Tags.CALENDAR_CATEGORIES); 538 while (st.hasMoreTokens()) { 539 s.data(Tags.CALENDAR_CATEGORY, st.nextToken()); 540 } 541 s.end(); 542 } 543 } 544 } else if (ncvUri.equals(Reminders.CONTENT_URI)) { 545 Integer mins = ncvValues.getAsInteger(Reminders.MINUTES); 546 if (mins != null) { 547 // -1 means "default", which for Exchange, is 30 548 if (mins < 0) { 549 mins = 30; 550 } 551 // Save this away if it's the earliest reminder (greatest minutes) 552 if (mins > earliestReminder) { 553 earliestReminder = mins; 554 } 555 } 556 } 557 } 558 559 // If we have a reminder, send it to the server 560 if (earliestReminder >= 0) { 561 s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder)); 562 } 563 564 // We've got to send a UID, unless this is an exception. If the event is new, we've 565 // generated one; if not, we should have gotten one from extended properties. 566 if (clientId != null) { 567 s.data(Tags.CALENDAR_UID, clientId); 568 } 569 570 // Handle attendee data here; keep track of organizer and stream it afterward 571 String organizerName = null; 572 String organizerEmail = null; 573 for (final Entity.NamedContentValues ncv: subValues) { 574 final Uri ncvUri = ncv.uri; 575 final ContentValues ncvValues = ncv.values; 576 if (ncvUri.equals(Attendees.CONTENT_URI)) { 577 final Integer relationship = 578 ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); 579 final String attendeeEmail = 580 ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 581 // If there's no relationship, we can't create this for EAS 582 // Similarly, we need an attendee email for each invitee 583 if (relationship != null && !TextUtils.isEmpty(attendeeEmail)) { 584 // Organizer isn't among attendees in EAS 585 if (relationship == Attendees.RELATIONSHIP_ORGANIZER) { 586 organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 587 organizerEmail = attendeeEmail; 588 continue; 589 } 590 if (!hasAttendees) { 591 s.start(Tags.CALENDAR_ATTENDEES); 592 hasAttendees = true; 593 } 594 s.start(Tags.CALENDAR_ATTENDEE); 595 String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 596 if (attendeeName == null) { 597 attendeeName = attendeeEmail; 598 } 599 s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName); 600 s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail); 601 if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 602 s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required 603 } 604 s.end(); // Attendee 605 } 606 } 607 } 608 if (hasAttendees) { 609 s.end(); // Attendees 610 } 611 612 // Get busy status from availability 613 final int availability = entityValues.getAsInteger(Events.AVAILABILITY); 614 final int busyStatus = CalendarUtilities.busyStatusFromAvailability(availability); 615 s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus)); 616 617 // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee 618 // In JB, organizer won't be an attendee 619 if (organizerEmail == null && entityValues.containsKey(Events.ORGANIZER)) { 620 organizerEmail = entityValues.getAsString(Events.ORGANIZER); 621 } 622 if (account.mEmailAddress.equalsIgnoreCase(organizerEmail)) { 623 s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0"); 624 } else { 625 s.data(Tags.CALENDAR_MEETING_STATUS, "3"); 626 } 627 628 // For Exchange 2003, only upsync if the event is new 629 if (((protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) && 630 organizerName != null) { 631 s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName); 632 } 633 634 // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003 635 // The result will be a status 6 failure during sync 636 final Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL); 637 if (visibility != null) { 638 s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility)); 639 } else { 640 // Default to private if not set 641 s.data(Tags.CALENDAR_SENSITIVITY, "1"); 642 } 643 } 644 645 /** 646 * Handle exceptions to an event's recurrance pattern. 647 * @param s The {@link Serializer} for this upsync. 648 * @param entity The {@link Entity} for this event. 649 * @param entityValues The {@link ContentValues} for entity. 650 * @param serverId The server side id for this event. 651 * @param clientId The client side id for this event. 652 * @param calendarIdString The calendar id, as a {@link String}. 653 * @param selfOrganizer Whether the user is the organizer of this event. 654 * @throws IOException 655 */ 656 private void handleExceptionsToRecurrenceRules(final Serializer s, final Context context, 657 final Account account,final Entity entity, final ContentValues entityValues, 658 final String serverId, final String clientId, final String calendarIdString, 659 final boolean selfOrganizer, final double protocolVersion) throws IOException { 660 final ContentResolver cr = context.getContentResolver(); 661 final Cursor cursor = cr.query(asSyncAdapter(Events.CONTENT_URI, account), null, 662 ORIGINAL_EVENT_AND_CALENDAR, new String[] { serverId, calendarIdString }, null); 663 if (cursor == null) { 664 return; 665 } 666 final EntityIterator exIterator = EventsEntity.newEntityIterator(cursor, cr); 667 boolean exFirst = true; 668 while (exIterator.hasNext()) { 669 final Entity exEntity = exIterator.next(); 670 if (exFirst) { 671 s.start(Tags.CALENDAR_EXCEPTIONS); 672 exFirst = false; 673 } 674 s.start(Tags.CALENDAR_EXCEPTION); 675 sendEvent(context, account, exEntity, null, protocolVersion, s); 676 final ContentValues exValues = exEntity.getEntityValues(); 677 if (getInt(exValues, Events.DIRTY) == 1) { 678 // This is a new/updated exception, so we've got to notify our 679 // attendees about it 680 final long exEventId = exValues.getAsLong(Events._ID); 681 682 final int flag; 683 if ((getInt(exValues, Events.DELETED) == 1) || 684 (getInt(exValues, Events.STATUS) == Events.STATUS_CANCELED)) { 685 flag = Message.FLAG_OUTGOING_MEETING_CANCEL; 686 if (!selfOrganizer) { 687 // Send a cancellation notice to the organizer 688 // Since CalendarProvider2 sets the organizer of exceptions 689 // to the user, we have to reset it first to the original 690 // organizer 691 exValues.put(Events.ORGANIZER, entityValues.getAsString(Events.ORGANIZER)); 692 sendDeclinedEmail(context, account, exEntity, clientId); 693 } 694 } else { 695 flag = Message.FLAG_OUTGOING_MEETING_INVITE; 696 } 697 // Add the eventId of the exception to the uploaded id list, so that 698 // the dirty/mark bits are cleared 699 mUploadedIdList.add(exEventId); 700 701 // Copy version so the ics attachment shows the proper sequence # 702 exValues.put(EVENT_SYNC_VERSION, 703 entityValues.getAsString(EVENT_SYNC_VERSION)); 704 // Copy location so that it's included in the outgoing email 705 if (entityValues.containsKey(Events.EVENT_LOCATION)) { 706 exValues.put(Events.EVENT_LOCATION, 707 entityValues.getAsString(Events.EVENT_LOCATION)); 708 } 709 710 if (selfOrganizer) { 711 final Message msg = CalendarUtilities.createMessageForEntity(context, exEntity, 712 flag, clientId, account); 713 if (msg != null) { 714 LogUtils.d(TAG, "Queueing exception update to %s", msg.mTo); 715 mOutgoingMailList.add(msg); 716 } 717 718 // Also send out a cancellation email to removed attendees 719 final Entity removedEntity = new Entity(exValues); 720 final Set<String> exAttendeeEmails = Sets.newHashSet(); 721 // Find all the attendees from the updated event 722 for (final Entity.NamedContentValues ncv: exEntity.getSubValues()) { 723 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 724 exAttendeeEmails.add(ncv.values.getAsString(Attendees.ATTENDEE_EMAIL)); 725 } 726 } 727 // Find the ones left out from the previous event and add them to the new entity 728 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 729 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 730 final String attendeeEmail = 731 ncv.values.getAsString(Attendees.ATTENDEE_EMAIL); 732 if (!exAttendeeEmails.contains(attendeeEmail)) { 733 removedEntity.addSubValue(ncv.uri, ncv.values); 734 } 735 } 736 } 737 738 // Now send a cancellation email 739 final Message removedMessage = 740 CalendarUtilities.createMessageForEntity(context, removedEntity, 741 Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account); 742 if (removedMessage != null) { 743 LogUtils.d(TAG, "Queueing cancellation for removed attendees"); 744 mOutgoingMailList.add(removedMessage); 745 } 746 } 747 } 748 s.end(); // EXCEPTION 749 } 750 if (!exFirst) { 751 s.end(); // EXCEPTIONS 752 } 753 } 754 755 /** 756 * Update the event properties with the attendee list, and send mail as appropriate. 757 * @param entity The {@link Entity} for this event. 758 * @param entityValues The {@link ContentValues} for entity. 759 * @param selfOrganizer Whether the user is the organizer of this event. 760 * @param eventId The id for this event. 761 * @param clientId The client side id for this event. 762 */ 763 private void updateAttendeesAndSendMail(final Context context, final Account account, 764 final Entity entity, final ContentValues entityValues, final boolean selfOrganizer, 765 final long eventId, final String clientId) { 766 // Go through the extended properties of this Event and pull out our tokenized 767 // attendees list and the user attendee status; we will need them later 768 final ContentResolver cr = context.getContentResolver(); 769 String attendeeString = null; 770 long attendeeStringId = -1; 771 String userAttendeeStatus = null; 772 long userAttendeeStatusId = -1; 773 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 774 if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) { 775 final ContentValues ncvValues = ncv.values; 776 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME); 777 if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) { 778 attendeeString = ncvValues.getAsString(ExtendedProperties.VALUE); 779 attendeeStringId = ncvValues.getAsLong(ExtendedProperties._ID); 780 } else if (propertyName.equals(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) { 781 userAttendeeStatus = ncvValues.getAsString(ExtendedProperties.VALUE); 782 userAttendeeStatusId = ncvValues.getAsLong(ExtendedProperties._ID); 783 } 784 } 785 } 786 787 // Send the meeting invite if there are attendees and we're the organizer AND 788 // if the Event itself is dirty (we might be syncing only because an exception 789 // is dirty, in which case we DON'T send email about the Event) 790 if (selfOrganizer && (getInt(entityValues, Events.DIRTY) == 1)) { 791 final Message msg = 792 CalendarUtilities.createMessageForEventId(context, eventId, 793 Message.FLAG_OUTGOING_MEETING_INVITE, clientId, account); 794 if (msg != null) { 795 LogUtils.d(TAG, "Queueing invitation to %s", msg.mTo); 796 mOutgoingMailList.add(msg); 797 } 798 // Make a list out of our tokenized attendees, if we have any 799 final ArrayList<String> originalAttendeeList = new ArrayList<String>(); 800 if (attendeeString != null) { 801 final StringTokenizer st = 802 new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER); 803 while (st.hasMoreTokens()) { 804 originalAttendeeList.add(st.nextToken()); 805 } 806 } 807 final StringBuilder newTokenizedAttendees = new StringBuilder(); 808 // See if any attendees have been dropped and while we're at it, build 809 // an updated String with tokenized attendee addresses 810 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 811 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 812 final String attendeeEmail = ncv.values.getAsString(Attendees.ATTENDEE_EMAIL); 813 // Remove all found attendees 814 originalAttendeeList.remove(attendeeEmail); 815 newTokenizedAttendees.append(attendeeEmail); 816 newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER); 817 } 818 } 819 // Update extended properties with the new attendee list, if we have one 820 // Otherwise, create one (this would be the case for Events created on 821 // device or "legacy" events (before this code was added) 822 final ContentValues cv = new ContentValues(); 823 cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString()); 824 if (attendeeString != null) { 825 cr.update(asSyncAdapter(ContentUris.withAppendedId( 826 ExtendedProperties.CONTENT_URI, attendeeStringId), account), 827 cv, null, null); 828 } else { 829 // If there wasn't an "attendees" property, insert one 830 cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES); 831 cv.put(ExtendedProperties.EVENT_ID, eventId); 832 cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI, account), cv); 833 } 834 // Whoever is left has been removed from the attendee list; send them 835 // a cancellation 836 for (final String removedAttendee: originalAttendeeList) { 837 // Send a cancellation message to each of them 838 final Message cancelMsg = CalendarUtilities.createMessageForEventId(context, 839 eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account, 840 removedAttendee); 841 if (cancelMsg != null) { 842 // Just send it to the removed attendee 843 LogUtils.d(TAG, "Queueing cancellation to removed attendee %s", cancelMsg.mTo); 844 mOutgoingMailList.add(cancelMsg); 845 } 846 } 847 } else if (!selfOrganizer) { 848 // If we're not the organizer, see if we've changed our attendee status 849 // Our last synced attendee status is in ExtendedProperties, and we've 850 // retrieved it above as userAttendeeStatus 851 final int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS); 852 int syncStatus = Attendees.ATTENDEE_STATUS_NONE; 853 if (userAttendeeStatus != null) { 854 try { 855 syncStatus = Integer.parseInt(userAttendeeStatus); 856 } catch (NumberFormatException e) { 857 // Just in case somebody else mucked with this and it's not Integer 858 } 859 } 860 if ((currentStatus != syncStatus) && 861 (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) { 862 // If so, send a meeting reply 863 final int messageFlag; 864 switch (currentStatus) { 865 case Attendees.ATTENDEE_STATUS_ACCEPTED: 866 messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT; 867 break; 868 case Attendees.ATTENDEE_STATUS_DECLINED: 869 messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE; 870 break; 871 case Attendees.ATTENDEE_STATUS_TENTATIVE: 872 messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE; 873 break; 874 default: 875 messageFlag = 0; 876 break; 877 } 878 // Make sure we have a valid status (messageFlag should never be zero) 879 if (messageFlag != 0 && userAttendeeStatusId >= 0) { 880 // Save away the new status 881 final ContentValues cv = new ContentValues(1); 882 cv.put(ExtendedProperties.VALUE, Integer.toString(currentStatus)); 883 cr.update(asSyncAdapter(ContentUris.withAppendedId( 884 ExtendedProperties.CONTENT_URI, userAttendeeStatusId), account), 885 cv, null, null); 886 // Send mail to the organizer advising of the new status 887 final Message msg = CalendarUtilities.createMessageForEventId(context, eventId, 888 messageFlag, clientId, account); 889 if (msg != null) { 890 LogUtils.d(TAG, "Queueing invitation reply to %s", msg.mTo); 891 mOutgoingMailList.add(msg); 892 } 893 } 894 } 895 } 896 } 897 898 /** 899 * Process a single event, adding to the {@link Serializer} as necessary. 900 * @param s The {@link Serializer} for this Sync request. 901 * @param entity The {@link Entity} for this event. 902 * @param calendarIdString The calendar's id, as a {@link String}. 903 * @param first Whether this would be the first event added to s. 904 * @return Whether this function added anything to s. 905 * @throws IOException 906 */ 907 private boolean handleEntity(final Serializer s, final Context context, final Account account, 908 final Entity entity, final String calendarIdString, final boolean first, 909 final double protocolVersion) throws IOException { 910 // For each of these entities, create the change commands 911 final ContentResolver cr = context.getContentResolver(); 912 final ContentValues entityValues = entity.getEntityValues(); 913 // We first need to check whether we can upsync this event; our test for this 914 // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED 915 // If this is set to "1", we can't upsync the event 916 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 917 if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) { 918 final ContentValues ncvValues = ncv.values; 919 if (ncvValues.getAsString(ExtendedProperties.NAME).equals( 920 EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) { 921 if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) { 922 // Make sure we mark this to clear the dirty flag 923 mUploadedIdList.add(entityValues.getAsLong(Events._ID)); 924 return false; 925 } 926 } 927 } 928 } 929 930 // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID 931 // We can generate all but what we're testing for below 932 final String organizerEmail = entityValues.getAsString(Events.ORGANIZER); 933 if (organizerEmail == null || !entityValues.containsKey(Events.DTSTART) || 934 (!entityValues.containsKey(Events.DURATION) 935 && !entityValues.containsKey(Events.DTEND))) { 936 return false; 937 } 938 939 if (first) { 940 s.start(Tags.SYNC_COMMANDS); 941 LogUtils.d(TAG, "Sending Calendar changes to the server"); 942 } 943 944 final boolean selfOrganizer = organizerEmail.equalsIgnoreCase(account.mEmailAddress); 945 // Find our uid in the entity; otherwise create one 946 String clientId = entityValues.getAsString(Events.SYNC_DATA2); 947 if (clientId == null) { 948 clientId = UUID.randomUUID().toString(); 949 } 950 final String serverId = entityValues.getAsString(Events._SYNC_ID); 951 final long eventId = entityValues.getAsLong(Events._ID); 952 if (serverId == null) { 953 // This is a new event; create a clientId 954 LogUtils.d(TAG, "Creating new event with clientId: %s", clientId); 955 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 956 // And save it in the Event as the local id 957 final ContentValues cv = new ContentValues(2); 958 cv.put(Events.SYNC_DATA2, clientId); 959 cv.put(EVENT_SYNC_VERSION, "0"); 960 cr.update( 961 asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account), 962 cv, null, null); 963 } else if (entityValues.getAsInteger(Events.DELETED) == 1) { 964 LogUtils.d(TAG, "Deleting event with serverId: %s", serverId); 965 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 966 mDeletedIdList.add(eventId); 967 if (selfOrganizer) { 968 final Message msg = CalendarUtilities.createMessageForEventId(context, 969 eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, null, account); 970 if (msg != null) { 971 LogUtils.d(TAG, "Queueing cancellation to %s", msg.mTo); 972 mOutgoingMailList.add(msg); 973 } 974 } else { 975 sendDeclinedEmail(context, account, entity, clientId); 976 } 977 // For deletions, we don't need to add application data, so just bail here. 978 return true; 979 } else { 980 LogUtils.d(TAG, "Upsync change to event with serverId: %s", serverId); 981 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 982 // Save to the ContentResolver. 983 final String version = getEntityVersion(entityValues); 984 final ContentValues cv = new ContentValues(1); 985 cv.put(EVENT_SYNC_VERSION, version); 986 cr.update( asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 987 account), cv, null, null); 988 // Also save in entityValues so that we send it this time around 989 entityValues.put(EVENT_SYNC_VERSION, version); 990 } 991 s.start(Tags.SYNC_APPLICATION_DATA); 992 sendEvent(context, account, entity, clientId, protocolVersion, s); 993 994 // Now, the hard part; find exceptions for this event 995 if (serverId != null) { 996 handleExceptionsToRecurrenceRules(s, context, account, entity, entityValues, serverId, 997 clientId, calendarIdString, selfOrganizer, protocolVersion); 998 } 999 1000 s.end().end(); // ApplicationData & Add/Change 1001 mUploadedIdList.add(eventId); 1002 updateAttendeesAndSendMail(context, account, entity, entityValues, selfOrganizer, eventId, 1003 clientId); 1004 return true; 1005 } 1006 1007 protected void setUpsyncCommands(Context context, final Account account, 1008 final double protocolVersion, final Serializer s) throws IOException { 1009 final ContentResolver cr = context.getContentResolver(); 1010 final String calendarIdString = Long.toString(mCalendarId); 1011 final String[] calendarIdArgument = { calendarIdString }; 1012 1013 markParentsOfDirtyEvents(context, account, calendarIdString, calendarIdArgument); 1014 1015 // Now go through dirty/marked top-level events and send them back to the server 1016 final Cursor cursor = cr.query(asSyncAdapter(Events.CONTENT_URI, account), null, 1017 DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, calendarIdArgument, null); 1018 if (cursor == null) { 1019 return; 1020 } 1021 final EntityIterator eventIterator = EventsEntity.newEntityIterator(cursor, cr); 1022 1023 try { 1024 boolean first = true; 1025 while (eventIterator.hasNext()) { 1026 final boolean addedCommand = 1027 handleEntity(s, context, account, eventIterator.next(), calendarIdString, 1028 first, protocolVersion); 1029 if (addedCommand) { 1030 first = false; 1031 } 1032 } 1033 if (!first) { 1034 s.end(); // Commands 1035 } 1036 } finally { 1037 eventIterator.close(); 1038 } 1039 } 1040 1041 @Override 1042 public void cleanup(final Context context, final Account account) { 1043 final ContentResolver cr = context.getContentResolver(); 1044 // Clear dirty and mark flags for updates sent to server 1045 if (!mUploadedIdList.isEmpty()) { 1046 final ContentValues cv = new ContentValues(2); 1047 cv.put(Events.DIRTY, 0); 1048 cv.put(EVENT_SYNC_MARK, "0"); 1049 for (final long eventId : mUploadedIdList) { 1050 cr.update(asSyncAdapter(ContentUris.withAppendedId( 1051 Events.CONTENT_URI, eventId), account), cv, null, null); 1052 } 1053 } 1054 // Delete events marked for deletion 1055 if (!mDeletedIdList.isEmpty()) { 1056 for (final long eventId : mDeletedIdList) { 1057 cr.delete(asSyncAdapter(ContentUris.withAppendedId( 1058 Events.CONTENT_URI, eventId), account), null, null); 1059 } 1060 } 1061 // Send all messages that were created during this sync. 1062 for (final Message msg : mOutgoingMailList) { 1063 sendMessage(context, account, msg); 1064 } 1065 1066 mDeletedIdList.clear(); 1067 mUploadedIdList.clear(); 1068 mOutgoingMailList.clear(); 1069 } 1070 1071 /** 1072 * Convenience method for adding a Message to an account's outbox 1073 * @param account The {@link Account} from which to send the message. 1074 * @param msg The message to send 1075 */ 1076 protected void sendMessage(final Context context, final Account account, 1077 final EmailContent.Message msg) { 1078 long mailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); 1079 // TODO: Improve system mailbox handling. 1080 if (mailboxId == Mailbox.NO_MAILBOX) { 1081 LogUtils.d(TAG, "No outbox for account %d, creating it", account.mId); 1082 final Mailbox outbox = 1083 Mailbox.newSystemMailbox(context, account.mId, Mailbox.TYPE_OUTBOX); 1084 outbox.save(context); 1085 mailboxId = outbox.mId; 1086 } 1087 msg.mMailboxKey = mailboxId; 1088 msg.mAccountKey = account.mId; 1089 msg.save(context); 1090 requestSyncForMailbox(EmailContent.AUTHORITY, mailboxId); 1091 } 1092 1093 /** 1094 * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox. 1095 * @param authority The authority for the mailbox that needs to sync. 1096 * @param mailboxId The id of the mailbox that needs to sync. 1097 */ 1098 protected void requestSyncForMailbox(final String authority, final long mailboxId) { 1099 final Bundle extras = Mailbox.createSyncBundle(mailboxId); 1100 ContentResolver.requestSync(mAndroidAccount, authority, extras); 1101 LogUtils.d(TAG, "requestSync EasServerConnection requestSyncForMailbox %s, %s", 1102 mAndroidAccount.toString(), extras.toString()); 1103 } 1104 1105 1106 /** 1107 * Delete an account from the Calendar provider. 1108 * @param context Our {@link Context} 1109 * @param emailAddress The email address of the account we wish to delete 1110 */ 1111 public static void wipeAccountFromContentProvider(final Context context, 1112 final String emailAddress) { 1113 try { 1114 context.getContentResolver().delete(asSyncAdapter(Calendars.CONTENT_URI, emailAddress), 1115 Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(emailAddress) 1116 + " AND " + Calendars.ACCOUNT_TYPE + "="+ DatabaseUtils.sqlEscapeString( 1117 context.getString(R.string.account_manager_type_exchange)), null); 1118 } catch (IllegalArgumentException e) { 1119 LogUtils.e(TAG, "CalendarProvider disabled; unable to wipe account."); 1120 } 1121 } 1122 } 1123