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 // If there's no relationship, we can't create this for EAS 580 // Similarly, we need an attendee email for each invitee 581 if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) { 582 // Organizer isn't among attendees in EAS 583 if (relationship == Attendees.RELATIONSHIP_ORGANIZER) { 584 organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME); 585 organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 586 continue; 587 } 588 if (!hasAttendees) { 589 s.start(Tags.CALENDAR_ATTENDEES); 590 hasAttendees = true; 591 } 592 s.start(Tags.CALENDAR_ATTENDEE); 593 final String attendeeEmail = 594 ncvValues.getAsString(Attendees.ATTENDEE_EMAIL); 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 EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query( 662 asSyncAdapter(Events.CONTENT_URI, account), null, ORIGINAL_EVENT_AND_CALENDAR, 663 new String[] { serverId, calendarIdString }, null), cr); 664 boolean exFirst = true; 665 while (exIterator.hasNext()) { 666 final Entity exEntity = exIterator.next(); 667 if (exFirst) { 668 s.start(Tags.CALENDAR_EXCEPTIONS); 669 exFirst = false; 670 } 671 s.start(Tags.CALENDAR_EXCEPTION); 672 sendEvent(context, account, exEntity, null, protocolVersion, s); 673 final ContentValues exValues = exEntity.getEntityValues(); 674 if (getInt(exValues, Events.DIRTY) == 1) { 675 // This is a new/updated exception, so we've got to notify our 676 // attendees about it 677 final long exEventId = exValues.getAsLong(Events._ID); 678 679 final int flag; 680 if ((getInt(exValues, Events.DELETED) == 1) || 681 (getInt(exValues, Events.STATUS) == Events.STATUS_CANCELED)) { 682 flag = Message.FLAG_OUTGOING_MEETING_CANCEL; 683 if (!selfOrganizer) { 684 // Send a cancellation notice to the organizer 685 // Since CalendarProvider2 sets the organizer of exceptions 686 // to the user, we have to reset it first to the original 687 // organizer 688 exValues.put(Events.ORGANIZER, entityValues.getAsString(Events.ORGANIZER)); 689 sendDeclinedEmail(context, account, exEntity, clientId); 690 } 691 } else { 692 flag = Message.FLAG_OUTGOING_MEETING_INVITE; 693 } 694 // Add the eventId of the exception to the uploaded id list, so that 695 // the dirty/mark bits are cleared 696 mUploadedIdList.add(exEventId); 697 698 // Copy version so the ics attachment shows the proper sequence # 699 exValues.put(EVENT_SYNC_VERSION, 700 entityValues.getAsString(EVENT_SYNC_VERSION)); 701 // Copy location so that it's included in the outgoing email 702 if (entityValues.containsKey(Events.EVENT_LOCATION)) { 703 exValues.put(Events.EVENT_LOCATION, 704 entityValues.getAsString(Events.EVENT_LOCATION)); 705 } 706 707 if (selfOrganizer) { 708 final Message msg = CalendarUtilities.createMessageForEntity(context, exEntity, 709 flag, clientId, account); 710 if (msg != null) { 711 LogUtils.d(TAG, "Queueing exception update to %s", msg.mTo); 712 mOutgoingMailList.add(msg); 713 } 714 715 // Also send out a cancellation email to removed attendees 716 final Entity removedEntity = new Entity(exValues); 717 final Set<String> exAttendeeEmails = Sets.newHashSet(); 718 // Find all the attendees from the updated event 719 for (final Entity.NamedContentValues ncv: exEntity.getSubValues()) { 720 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 721 exAttendeeEmails.add(ncv.values.getAsString(Attendees.ATTENDEE_EMAIL)); 722 } 723 } 724 // Find the ones left out from the previous event and add them to the new entity 725 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 726 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 727 final String attendeeEmail = 728 ncv.values.getAsString(Attendees.ATTENDEE_EMAIL); 729 if (!exAttendeeEmails.contains(attendeeEmail)) { 730 removedEntity.addSubValue(ncv.uri, ncv.values); 731 } 732 } 733 } 734 735 // Now send a cancellation email 736 final Message removedMessage = 737 CalendarUtilities.createMessageForEntity(context, removedEntity, 738 Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account); 739 if (removedMessage != null) { 740 LogUtils.d(TAG, "Queueing cancellation for removed attendees"); 741 mOutgoingMailList.add(removedMessage); 742 } 743 } 744 } 745 s.end(); // EXCEPTION 746 } 747 if (!exFirst) { 748 s.end(); // EXCEPTIONS 749 } 750 } 751 752 /** 753 * Update the event properties with the attendee list, and send mail as appropriate. 754 * @param entity The {@link Entity} for this event. 755 * @param entityValues The {@link ContentValues} for entity. 756 * @param selfOrganizer Whether the user is the organizer of this event. 757 * @param eventId The id for this event. 758 * @param clientId The client side id for this event. 759 */ 760 private void updateAttendeesAndSendMail(final Context context, final Account account, 761 final Entity entity, final ContentValues entityValues, final boolean selfOrganizer, 762 final long eventId, final String clientId) { 763 // Go through the extended properties of this Event and pull out our tokenized 764 // attendees list and the user attendee status; we will need them later 765 final ContentResolver cr = context.getContentResolver(); 766 String attendeeString = null; 767 long attendeeStringId = -1; 768 String userAttendeeStatus = null; 769 long userAttendeeStatusId = -1; 770 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 771 if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) { 772 final ContentValues ncvValues = ncv.values; 773 final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME); 774 if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) { 775 attendeeString = ncvValues.getAsString(ExtendedProperties.VALUE); 776 attendeeStringId = ncvValues.getAsLong(ExtendedProperties._ID); 777 } else if (propertyName.equals(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) { 778 userAttendeeStatus = ncvValues.getAsString(ExtendedProperties.VALUE); 779 userAttendeeStatusId = ncvValues.getAsLong(ExtendedProperties._ID); 780 } 781 } 782 } 783 784 // Send the meeting invite if there are attendees and we're the organizer AND 785 // if the Event itself is dirty (we might be syncing only because an exception 786 // is dirty, in which case we DON'T send email about the Event) 787 if (selfOrganizer && (getInt(entityValues, Events.DIRTY) == 1)) { 788 final Message msg = 789 CalendarUtilities.createMessageForEventId(context, eventId, 790 Message.FLAG_OUTGOING_MEETING_INVITE, clientId, account); 791 if (msg != null) { 792 LogUtils.d(TAG, "Queueing invitation to %s", msg.mTo); 793 mOutgoingMailList.add(msg); 794 } 795 // Make a list out of our tokenized attendees, if we have any 796 final ArrayList<String> originalAttendeeList = new ArrayList<String>(); 797 if (attendeeString != null) { 798 final StringTokenizer st = 799 new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER); 800 while (st.hasMoreTokens()) { 801 originalAttendeeList.add(st.nextToken()); 802 } 803 } 804 final StringBuilder newTokenizedAttendees = new StringBuilder(); 805 // See if any attendees have been dropped and while we're at it, build 806 // an updated String with tokenized attendee addresses 807 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 808 if (ncv.uri.equals(Attendees.CONTENT_URI)) { 809 final String attendeeEmail = ncv.values.getAsString(Attendees.ATTENDEE_EMAIL); 810 // Remove all found attendees 811 originalAttendeeList.remove(attendeeEmail); 812 newTokenizedAttendees.append(attendeeEmail); 813 newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER); 814 } 815 } 816 // Update extended properties with the new attendee list, if we have one 817 // Otherwise, create one (this would be the case for Events created on 818 // device or "legacy" events (before this code was added) 819 final ContentValues cv = new ContentValues(); 820 cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString()); 821 if (attendeeString != null) { 822 cr.update(asSyncAdapter(ContentUris.withAppendedId( 823 ExtendedProperties.CONTENT_URI, attendeeStringId), account), 824 cv, null, null); 825 } else { 826 // If there wasn't an "attendees" property, insert one 827 cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES); 828 cv.put(ExtendedProperties.EVENT_ID, eventId); 829 cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI, account), cv); 830 } 831 // Whoever is left has been removed from the attendee list; send them 832 // a cancellation 833 for (final String removedAttendee: originalAttendeeList) { 834 // Send a cancellation message to each of them 835 final Message cancelMsg = CalendarUtilities.createMessageForEventId(context, 836 eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, account, 837 removedAttendee); 838 if (cancelMsg != null) { 839 // Just send it to the removed attendee 840 LogUtils.d(TAG, "Queueing cancellation to removed attendee %s", cancelMsg.mTo); 841 mOutgoingMailList.add(cancelMsg); 842 } 843 } 844 } else if (!selfOrganizer) { 845 // If we're not the organizer, see if we've changed our attendee status 846 // Our last synced attendee status is in ExtendedProperties, and we've 847 // retrieved it above as userAttendeeStatus 848 final int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS); 849 int syncStatus = Attendees.ATTENDEE_STATUS_NONE; 850 if (userAttendeeStatus != null) { 851 try { 852 syncStatus = Integer.parseInt(userAttendeeStatus); 853 } catch (NumberFormatException e) { 854 // Just in case somebody else mucked with this and it's not Integer 855 } 856 } 857 if ((currentStatus != syncStatus) && 858 (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) { 859 // If so, send a meeting reply 860 final int messageFlag; 861 switch (currentStatus) { 862 case Attendees.ATTENDEE_STATUS_ACCEPTED: 863 messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT; 864 break; 865 case Attendees.ATTENDEE_STATUS_DECLINED: 866 messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE; 867 break; 868 case Attendees.ATTENDEE_STATUS_TENTATIVE: 869 messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE; 870 break; 871 default: 872 messageFlag = 0; 873 break; 874 } 875 // Make sure we have a valid status (messageFlag should never be zero) 876 if (messageFlag != 0 && userAttendeeStatusId >= 0) { 877 // Save away the new status 878 final ContentValues cv = new ContentValues(1); 879 cv.put(ExtendedProperties.VALUE, Integer.toString(currentStatus)); 880 cr.update(asSyncAdapter(ContentUris.withAppendedId( 881 ExtendedProperties.CONTENT_URI, userAttendeeStatusId), account), 882 cv, null, null); 883 // Send mail to the organizer advising of the new status 884 final Message msg = CalendarUtilities.createMessageForEventId(context, eventId, 885 messageFlag, clientId, account); 886 if (msg != null) { 887 LogUtils.d(TAG, "Queueing invitation reply to %s", msg.mTo); 888 mOutgoingMailList.add(msg); 889 } 890 } 891 } 892 } 893 } 894 895 /** 896 * Process a single event, adding to the {@link Serializer} as necessary. 897 * @param s The {@link Serializer} for this Sync request. 898 * @param entity The {@link Entity} for this event. 899 * @param calendarIdString The calendar's id, as a {@link String}. 900 * @param first Whether this would be the first event added to s. 901 * @return Whether this function added anything to s. 902 * @throws IOException 903 */ 904 private boolean handleEntity(final Serializer s, final Context context, final Account account, 905 final Entity entity, final String calendarIdString, final boolean first, 906 final double protocolVersion) throws IOException { 907 // For each of these entities, create the change commands 908 final ContentResolver cr = context.getContentResolver(); 909 final ContentValues entityValues = entity.getEntityValues(); 910 // We first need to check whether we can upsync this event; our test for this 911 // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED 912 // If this is set to "1", we can't upsync the event 913 for (final Entity.NamedContentValues ncv: entity.getSubValues()) { 914 if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) { 915 final ContentValues ncvValues = ncv.values; 916 if (ncvValues.getAsString(ExtendedProperties.NAME).equals( 917 EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) { 918 if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) { 919 // Make sure we mark this to clear the dirty flag 920 mUploadedIdList.add(entityValues.getAsLong(Events._ID)); 921 return false; 922 } 923 } 924 } 925 } 926 927 // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID 928 // We can generate all but what we're testing for below 929 final String organizerEmail = entityValues.getAsString(Events.ORGANIZER); 930 if (organizerEmail == null || !entityValues.containsKey(Events.DTSTART) || 931 (!entityValues.containsKey(Events.DURATION) 932 && !entityValues.containsKey(Events.DTEND))) { 933 return false; 934 } 935 936 if (first) { 937 s.start(Tags.SYNC_COMMANDS); 938 LogUtils.d(TAG, "Sending Calendar changes to the server"); 939 } 940 941 final boolean selfOrganizer = organizerEmail.equalsIgnoreCase(account.mEmailAddress); 942 // Find our uid in the entity; otherwise create one 943 String clientId = entityValues.getAsString(Events.SYNC_DATA2); 944 if (clientId == null) { 945 clientId = UUID.randomUUID().toString(); 946 } 947 final String serverId = entityValues.getAsString(Events._SYNC_ID); 948 final long eventId = entityValues.getAsLong(Events._ID); 949 if (serverId == null) { 950 // This is a new event; create a clientId 951 LogUtils.d(TAG, "Creating new event with clientId: %s", clientId); 952 s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId); 953 // And save it in the Event as the local id 954 final ContentValues cv = new ContentValues(2); 955 cv.put(Events.SYNC_DATA2, clientId); 956 cv.put(EVENT_SYNC_VERSION, "0"); 957 cr.update( 958 asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account), 959 cv, null, null); 960 } else if (entityValues.getAsInteger(Events.DELETED) == 1) { 961 LogUtils.d(TAG, "Deleting event with serverId: %s", serverId); 962 s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end(); 963 mDeletedIdList.add(eventId); 964 if (selfOrganizer) { 965 final Message msg = CalendarUtilities.createMessageForEventId(context, 966 eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, null, account); 967 if (msg != null) { 968 LogUtils.d(TAG, "Queueing cancellation to %s", msg.mTo); 969 mOutgoingMailList.add(msg); 970 } 971 } else { 972 sendDeclinedEmail(context, account, entity, clientId); 973 } 974 // For deletions, we don't need to add application data, so just bail here. 975 return true; 976 } else { 977 LogUtils.d(TAG, "Upsync change to event with serverId: %s", serverId); 978 s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId); 979 // Save to the ContentResolver. 980 final String version = getEntityVersion(entityValues); 981 final ContentValues cv = new ContentValues(1); 982 cv.put(EVENT_SYNC_VERSION, version); 983 cr.update( asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), 984 account), cv, null, null); 985 // Also save in entityValues so that we send it this time around 986 entityValues.put(EVENT_SYNC_VERSION, version); 987 } 988 s.start(Tags.SYNC_APPLICATION_DATA); 989 sendEvent(context, account, entity, clientId, protocolVersion, s); 990 991 // Now, the hard part; find exceptions for this event 992 if (serverId != null) { 993 handleExceptionsToRecurrenceRules(s, context, account, entity, entityValues, serverId, 994 clientId, calendarIdString, selfOrganizer, protocolVersion); 995 } 996 997 s.end().end(); // ApplicationData & Add/Change 998 mUploadedIdList.add(eventId); 999 updateAttendeesAndSendMail(context, account, entity, entityValues, selfOrganizer, eventId, 1000 clientId); 1001 return true; 1002 } 1003 1004 protected void setUpsyncCommands(Context context, final Account account, 1005 final double protocolVersion, final Serializer s) throws IOException { 1006 final ContentResolver cr = context.getContentResolver(); 1007 final String calendarIdString = Long.toString(mCalendarId); 1008 final String[] calendarIdArgument = { calendarIdString }; 1009 1010 markParentsOfDirtyEvents(context, account, calendarIdString, calendarIdArgument); 1011 1012 // Now go through dirty/marked top-level events and send them back to the server 1013 final EntityIterator eventIterator = EventsEntity.newEntityIterator( 1014 cr.query(asSyncAdapter(Events.CONTENT_URI, account), null, 1015 DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, calendarIdArgument, null), cr); 1016 1017 try { 1018 boolean first = true; 1019 while (eventIterator.hasNext()) { 1020 final boolean addedCommand = 1021 handleEntity(s, context, account, eventIterator.next(), calendarIdString, 1022 first, protocolVersion); 1023 if (addedCommand) { 1024 first = false; 1025 } 1026 } 1027 if (!first) { 1028 s.end(); // Commands 1029 } 1030 } finally { 1031 eventIterator.close(); 1032 } 1033 } 1034 1035 @Override 1036 public void cleanup(final Context context, final Account account) { 1037 final ContentResolver cr = context.getContentResolver(); 1038 // Clear dirty and mark flags for updates sent to server 1039 if (!mUploadedIdList.isEmpty()) { 1040 final ContentValues cv = new ContentValues(2); 1041 cv.put(Events.DIRTY, 0); 1042 cv.put(EVENT_SYNC_MARK, "0"); 1043 for (final long eventId : mUploadedIdList) { 1044 cr.update(asSyncAdapter(ContentUris.withAppendedId( 1045 Events.CONTENT_URI, eventId), account), cv, null, null); 1046 } 1047 } 1048 // Delete events marked for deletion 1049 if (!mDeletedIdList.isEmpty()) { 1050 for (final long eventId : mDeletedIdList) { 1051 cr.delete(asSyncAdapter(ContentUris.withAppendedId( 1052 Events.CONTENT_URI, eventId), account), null, null); 1053 } 1054 } 1055 // Send all messages that were created during this sync. 1056 for (final Message msg : mOutgoingMailList) { 1057 sendMessage(context, account, msg); 1058 } 1059 1060 mDeletedIdList.clear(); 1061 mUploadedIdList.clear(); 1062 mOutgoingMailList.clear(); 1063 } 1064 1065 /** 1066 * Convenience method for adding a Message to an account's outbox 1067 * @param account The {@link Account} from which to send the message. 1068 * @param msg The message to send 1069 */ 1070 protected void sendMessage(final Context context, final Account account, 1071 final EmailContent.Message msg) { 1072 long mailboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); 1073 // TODO: Improve system mailbox handling. 1074 if (mailboxId == Mailbox.NO_MAILBOX) { 1075 LogUtils.d(TAG, "No outbox for account %d, creating it", account.mId); 1076 final Mailbox outbox = 1077 Mailbox.newSystemMailbox(context, account.mId, Mailbox.TYPE_OUTBOX); 1078 outbox.save(context); 1079 mailboxId = outbox.mId; 1080 } 1081 msg.mMailboxKey = mailboxId; 1082 msg.mAccountKey = account.mId; 1083 msg.save(context); 1084 requestSyncForMailbox(EmailContent.AUTHORITY, mailboxId); 1085 } 1086 1087 /** 1088 * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox. 1089 * @param authority The authority for the mailbox that needs to sync. 1090 * @param mailboxId The id of the mailbox that needs to sync. 1091 */ 1092 protected void requestSyncForMailbox(final String authority, final long mailboxId) { 1093 final Bundle extras = Mailbox.createSyncBundle(mailboxId); 1094 ContentResolver.requestSync(mAndroidAccount, authority, extras); 1095 LogUtils.d(TAG, "requestSync EasServerConnection requestSyncForMailbox %s, %s", 1096 mAndroidAccount.toString(), extras.toString()); 1097 } 1098 1099 1100 /** 1101 * Delete an account from the Calendar provider. 1102 * @param context Our {@link Context} 1103 * @param emailAddress The email address of the account we wish to delete 1104 */ 1105 public static void wipeAccountFromContentProvider(final Context context, 1106 final String emailAddress) { 1107 context.getContentResolver().delete(asSyncAdapter(Calendars.CONTENT_URI, emailAddress), 1108 Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(emailAddress) 1109 + " AND " + Calendars.ACCOUNT_TYPE + "="+ DatabaseUtils.sqlEscapeString( 1110 context.getString(R.string.account_manager_type_exchange)), null); 1111 } 1112 } 1113