1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.exchange.service; 18 19 import android.app.Notification; 20 import android.app.Notification.Builder; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.content.AbstractThreadedSyncAdapter; 24 import android.content.ContentProviderClient; 25 import android.content.ContentResolver; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SyncResult; 30 import android.database.Cursor; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.os.Bundle; 34 import android.os.IBinder; 35 import android.provider.CalendarContract; 36 import android.provider.ContactsContract; 37 import android.text.TextUtils; 38 import android.text.format.DateUtils; 39 import android.util.Log; 40 41 import com.android.emailcommon.Api; 42 import com.android.emailcommon.TempDirectory; 43 import com.android.emailcommon.provider.Account; 44 import com.android.emailcommon.provider.EmailContent; 45 import com.android.emailcommon.provider.EmailContent.AccountColumns; 46 import com.android.emailcommon.provider.HostAuth; 47 import com.android.emailcommon.provider.Mailbox; 48 import com.android.emailcommon.service.IEmailService; 49 import com.android.emailcommon.service.IEmailServiceCallback; 50 import com.android.emailcommon.service.SearchParams; 51 import com.android.emailcommon.service.ServiceProxy; 52 import com.android.emailcommon.utility.IntentUtilities; 53 import com.android.emailcommon.utility.Utility; 54 import com.android.exchange.Eas; 55 import com.android.exchange.R.drawable; 56 import com.android.exchange.R.string; 57 import com.android.exchange.adapter.PingParser; 58 import com.android.exchange.adapter.Search; 59 import com.android.exchange.eas.EasFolderSync; 60 import com.android.exchange.eas.EasMoveItems; 61 import com.android.exchange.eas.EasOperation; 62 import com.android.exchange.eas.EasPing; 63 import com.android.exchange.eas.EasSync; 64 import com.android.mail.providers.UIProvider.AccountCapabilities; 65 import com.android.mail.utils.LogUtils; 66 67 import java.util.HashMap; 68 import java.util.HashSet; 69 70 /** 71 * Service for communicating with Exchange servers. There are three main parts of this class: 72 * TODO: Flesh out these comments. 73 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs. 74 * 2) Bookkeeping for running Ping requests, which handles push notifications. 75 * 3) An {@link IEmailService} Stub to handle RPC from the UI. 76 */ 77 public class EmailSyncAdapterService extends AbstractSyncAdapterService { 78 79 private static final String TAG = Eas.LOG_TAG; 80 81 /** 82 * The amount of time between periodic syncs intended to ensure that push hasn't died. 83 */ 84 private static final long KICK_SYNC_INTERVAL = 85 DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS; 86 87 /** Controls whether we do a periodic "kick" to restart the ping. */ 88 private static final boolean SCHEDULE_KICK = true; 89 90 /** 91 * If sync extras do not include a mailbox id, then we want to perform a full sync. 92 */ 93 private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX; 94 95 /** Projection used for getting email address for an account. */ 96 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 97 98 private static final Object sSyncAdapterLock = new Object(); 99 private static AbstractThreadedSyncAdapter sSyncAdapter = null; 100 101 /** 102 * Bookkeeping for handling synchronization between pings and syncs. 103 * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is 104 * the term for the Exchange command, but this code should be generic enough to be easily 105 * extended to IMAP. 106 * "Sync" refers to an actual sync command to either fetch mail state, account state, or send 107 * mail (send is implemented as "sync the outbox"). 108 * TODO: Outbox sync probably need not stop a ping in progress. 109 * Basic rules of how these interact (note that all rules are per account): 110 * - Only one ping or sync may run at a time. 111 * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while 112 * a sync is in progress. 113 * - On the other hand, ping requests may come in while handling a ping. 114 * - "Ping request" is shorthand for "a request to change our ping parameters", which includes 115 * a request to stop receiving push notifications. 116 * - If neither a ping nor a sync is running, then a request for either will run it. 117 * - If a sync is running, new ping requests block until the sync completes. 118 * - If a ping is running, a new sync request stops the ping and creates a pending ping 119 * (which blocks until the sync completes). 120 * - If a ping is running, a new ping request stops the ping and either starts a new one or 121 * does nothing, as appopriate (since a ping request can be to stop pushing). 122 * - As an optimization, while a ping request is waiting to run, subsequent ping requests are 123 * ignored (the pending ping will pick up the latest ping parameters at the time it runs). 124 */ 125 public class SyncHandlerSynchronizer { 126 /** 127 * Map of account id -> ping handler. 128 * For a given account id, there are three possible states: 129 * 1) If no ping or sync is currently running, there is no entry in the map for the account. 130 * 2) If a ping is running, there is an entry with the appropriate ping handler. 131 * 3) If there is a sync running, there is an entry with null as the value. 132 * We cannot have more than one ping or sync running at a time. 133 */ 134 private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>(); 135 136 /** 137 * Wait until neither a sync nor a ping is running on this account, and then return. 138 * If there's a ping running, actively stop it. (For syncs, we have to just wait.) 139 * @param accountId The account we want to wait for. 140 */ 141 private synchronized void waitUntilNoActivity(final long accountId) { 142 while (mPingHandlers.containsKey(accountId)) { 143 final PingTask pingHandler = mPingHandlers.get(accountId); 144 if (pingHandler != null) { 145 pingHandler.stop(); 146 } 147 try { 148 wait(); 149 } catch (final InterruptedException e) { 150 // TODO: When would this happen, and how should I handle it? 151 } 152 } 153 } 154 155 /** 156 * Use this to see if we're currently syncing, as opposed to pinging or doing nothing. 157 * @param accountId The account to check. 158 * @return Whether that account is currently running a sync. 159 */ 160 private synchronized boolean isRunningSync(final long accountId) { 161 return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null); 162 } 163 164 /** 165 * If there are no running pings, stop the service. 166 */ 167 private void stopServiceIfNoPings() { 168 for (final PingTask pingHandler : mPingHandlers.values()) { 169 if (pingHandler != null) { 170 return; 171 } 172 } 173 EmailSyncAdapterService.this.stopSelf(); 174 } 175 176 /** 177 * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync 178 * here; the caller must do that. 179 * @param accountId The account on which we are running a sync. 180 */ 181 public synchronized void startSync(final long accountId) { 182 waitUntilNoActivity(accountId); 183 mPingHandlers.put(accountId, null); 184 } 185 186 /** 187 * Starts or restarts a ping for an account, if the current account state indicates that it 188 * wants to push. 189 * @param account The account whose ping is being modified. 190 */ 191 public synchronized void modifyPing(final Account account) { 192 // If a sync is currently running, it will start a ping when it's done, so there's no 193 // need to do anything right now. 194 if (isRunningSync(account.mId)) { 195 return; 196 } 197 198 // Don't ping for accounts that haven't performed initial sync. 199 if (EmailContent.isInitialSyncKey(account.mSyncKey)) { 200 return; 201 } 202 203 // Determine if this account needs pushes. All of the following must be true: 204 // - The account's sync interval must indicate that it wants push. 205 // - At least one content type must be sync-enabled in the account manager. 206 // - At least one mailbox of a sync-enabled type must have automatic sync enabled. 207 final EmailSyncAdapterService service = EmailSyncAdapterService.this; 208 final android.accounts.Account amAccount = new android.accounts.Account( 209 account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 210 boolean pushNeeded = false; 211 if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 212 final HashSet<String> authsToSync = getAuthsToSync(amAccount); 213 // If we have at least one sync-enabled content type, check for syncing mailboxes. 214 if (!authsToSync.isEmpty()) { 215 final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(), 216 account.mId); 217 if (c != null) { 218 try { 219 while (c.moveToNext()) { 220 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 221 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { 222 pushNeeded = true; 223 break; 224 } 225 } 226 } finally { 227 c.close(); 228 } 229 } 230 } 231 } 232 233 // Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync. 234 final PingTask pingSyncHandler = mPingHandlers.get(account.mId); 235 final Bundle extras = new Bundle(1); 236 extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true); 237 if (pushNeeded) { 238 // First start or restart the ping as appropriate. 239 if (pingSyncHandler != null) { 240 pingSyncHandler.restart(); 241 } else { 242 // Start a new ping. 243 // Note: unlike startSync, we CANNOT allow the caller to do the actual work. 244 // If we return before the ping starts, there's a race condition where another 245 // ping or sync might start first. It only works for startSync because sync is 246 // higher priority than ping (i.e. a ping can't start while a sync is pending) 247 // and only one sync can run at a time. 248 final PingTask pingHandler = new PingTask(service, account, amAccount, this); 249 mPingHandlers.put(account.mId, pingHandler); 250 pingHandler.start(); 251 // Whenever we have a running ping, make sure this service stays running. 252 service.startService(new Intent(service, EmailSyncAdapterService.class)); 253 } 254 if (SCHEDULE_KICK) { 255 ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras, 256 KICK_SYNC_INTERVAL); 257 } 258 } else { 259 if (pingSyncHandler != null) { 260 pingSyncHandler.stop(); 261 } 262 if (SCHEDULE_KICK) { 263 ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras); 264 } 265 } 266 } 267 268 /** 269 * Updates the synchronization bookkeeping when a sync is done. 270 * @param account The account whose sync just finished. 271 */ 272 public synchronized void syncComplete(final Account account) { 273 mPingHandlers.remove(account.mId); 274 // Syncs can interrupt pings, so we should check if we need to start one now. 275 modifyPing(account); 276 stopServiceIfNoPings(); 277 notifyAll(); 278 } 279 280 /** 281 * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only 282 * sync if necessary. 283 * @param amAccount The {@link android.accounts.Account} for this account. 284 * @param accountId The account whose ping just finished. 285 * @param pingStatus The status value from {@link PingParser} for the last ping performed. 286 * This cannot be one of the values that results in another ping, so this 287 * function only needs to handle the terminal statuses. 288 */ 289 public synchronized void pingComplete(final android.accounts.Account amAccount, 290 final long accountId, final int pingStatus) { 291 mPingHandlers.remove(accountId); 292 293 // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI. 294 // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI. 295 296 // TODO: Should this just re-request ping if status < 0? This would do the wrong thing 297 // for e.g. auth errors, though. 298 if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE || 299 pingStatus == EasOperation.RESULT_OTHER_FAILURE) { 300 // Request a new ping through the SyncManager. This will do the right thing if the 301 // exception was due to loss of network connectivity, etc. (i.e. it will wait for 302 // network to restore and then request it). 303 EasPing.requestPing(amAccount); 304 } else { 305 stopServiceIfNoPings(); 306 } 307 308 // TODO: It might be the case that only STATUS_CHANGES_FOUND and 309 // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through. 310 notifyAll(); 311 } 312 313 } 314 private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer(); 315 316 /** 317 * The binder for IEmailService. 318 */ 319 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 320 321 private String getEmailAddressForAccount(final long accountId) { 322 final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this, 323 Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 324 new String[] {Long.toString(accountId)}, null, 0); 325 if (emailAddress == null) { 326 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 327 } 328 return emailAddress; 329 } 330 331 @Override 332 public Bundle validate(final HostAuth hostAuth) { 333 LogUtils.d(TAG, "IEmailService.validate"); 334 return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).validate(); 335 } 336 337 @Override 338 public Bundle autoDiscover(final String username, final String password) { 339 LogUtils.d(TAG, "IEmailService.autoDiscover"); 340 return new EasAutoDiscover(EmailSyncAdapterService.this, username, password) 341 .doAutodiscover(); 342 } 343 344 @Override 345 public void updateFolderList(final long accountId) { 346 LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId); 347 final String emailAddress = getEmailAddressForAccount(accountId); 348 if (emailAddress != null) { 349 final Bundle extras = new Bundle(1); 350 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 351 ContentResolver.requestSync(new android.accounts.Account( 352 emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 353 EmailContent.AUTHORITY, extras); 354 } 355 } 356 357 @Override 358 public void setLogging(final int flags) { 359 // TODO: fix this? 360 // Protocol logging 361 Eas.setUserDebug(flags); 362 // Sync logging 363 //setUserDebug(flags); 364 } 365 366 @Override 367 public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId, 368 final boolean background) { 369 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 370 // TODO: Prevent this from happening in parallel with a sync? 371 EasAttachmentLoader.loadAttachment(EmailSyncAdapterService.this, attachmentId, 372 callback); 373 } 374 375 @Override 376 public void sendMeetingResponse(final long messageId, final int response) { 377 LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response); 378 EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId, 379 response); 380 } 381 382 /** 383 * Delete PIM (calendar, contacts) data for the specified account 384 * 385 * @param emailAddress the email address for the account whose data should be deleted 386 */ 387 @Override 388 public void deleteAccountPIMData(final String emailAddress) { 389 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 390 if (emailAddress != null) { 391 final Context context = EmailSyncAdapterService.this; 392 EasContactsSyncHandler.wipeAccountFromContentProvider(context, emailAddress); 393 EasCalendarSyncHandler.wipeAccountFromContentProvider(context, emailAddress); 394 } 395 // TODO: Run account reconciler? 396 } 397 398 @Override 399 public int searchMessages(final long accountId, final SearchParams searchParams, 400 final long destMailboxId) { 401 LogUtils.d(TAG, "IEmailService.searchMessages"); 402 return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams, 403 destMailboxId); 404 // TODO: may need an explicit callback to replace the one to IEmailServiceCallback. 405 } 406 407 @Override 408 public void sendMail(final long accountId) {} 409 410 @Override 411 public int getCapabilities(final Account acct) { 412 String easVersion = acct.mProtocolVersion; 413 Double easVersionDouble = 2.5D; 414 if (easVersion != null) { 415 try { 416 easVersionDouble = Double.parseDouble(easVersion); 417 } catch (NumberFormatException e) { 418 // Stick with 2.5 419 } 420 } 421 if (easVersionDouble >= 12.0D) { 422 return AccountCapabilities.SYNCABLE_FOLDERS | 423 AccountCapabilities.SERVER_SEARCH | 424 AccountCapabilities.FOLDER_SERVER_SEARCH | 425 AccountCapabilities.SMART_REPLY | 426 AccountCapabilities.UNDO | 427 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 428 } else { 429 return AccountCapabilities.SYNCABLE_FOLDERS | 430 AccountCapabilities.SMART_REPLY | 431 AccountCapabilities.UNDO | 432 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 433 } 434 } 435 436 @Override 437 public void serviceUpdated(final String emailAddress) { 438 // Not required for EAS 439 } 440 441 // All IEmailService messages below are UNCALLED in Email. 442 // TODO: Remove. 443 @Deprecated 444 @Override 445 public int getApiLevel() { 446 return Api.LEVEL; 447 } 448 449 @Deprecated 450 @Override 451 public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {} 452 453 @Deprecated 454 @Override 455 public void stopSync(long mailboxId) {} 456 457 @Deprecated 458 @Override 459 public void loadMore(long messageId) {} 460 461 @Deprecated 462 @Override 463 public boolean createFolder(long accountId, String name) { 464 return false; 465 } 466 467 @Deprecated 468 @Override 469 public boolean deleteFolder(long accountId, String name) { 470 return false; 471 } 472 473 @Deprecated 474 @Override 475 public boolean renameFolder(long accountId, String oldName, String newName) { 476 return false; 477 } 478 479 @Deprecated 480 @Override 481 public void hostChanged(long accountId) {} 482 }; 483 484 public EmailSyncAdapterService() { 485 super(); 486 } 487 488 /** 489 * {@link AsyncTask} for restarting pings for all accounts that need it. 490 */ 491 private static final String PUSH_ACCOUNTS_SELECTION = 492 AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 493 private class RestartPingsTask extends AsyncTask<Void, Void, Void> { 494 495 private final ContentResolver mContentResolver; 496 private final SyncHandlerSynchronizer mSyncHandlerMap; 497 private boolean mAnyAccounts; 498 499 public RestartPingsTask(final ContentResolver contentResolver, 500 final SyncHandlerSynchronizer syncHandlerMap) { 501 mContentResolver = contentResolver; 502 mSyncHandlerMap = syncHandlerMap; 503 } 504 505 @Override 506 protected Void doInBackground(Void... params) { 507 final Cursor c = mContentResolver.query(Account.CONTENT_URI, 508 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 509 if (c != null) { 510 try { 511 mAnyAccounts = (c.getCount() != 0); 512 while (c.moveToNext()) { 513 final Account account = new Account(); 514 account.restore(c); 515 mSyncHandlerMap.modifyPing(account); 516 } 517 } finally { 518 c.close(); 519 } 520 } else { 521 mAnyAccounts = false; 522 } 523 return null; 524 } 525 526 @Override 527 protected void onPostExecute(Void result) { 528 if (!mAnyAccounts) { 529 LogUtils.d(TAG, "stopping for no accounts"); 530 EmailSyncAdapterService.this.stopSelf(); 531 } 532 } 533 } 534 535 @Override 536 public void onCreate() { 537 LogUtils.v(TAG, "onCreate()"); 538 super.onCreate(); 539 startService(new Intent(this, EmailSyncAdapterService.class)); 540 // Restart push for all accounts that need it. 541 new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor( 542 AsyncTask.THREAD_POOL_EXECUTOR); 543 } 544 545 @Override 546 public void onDestroy() { 547 LogUtils.v(TAG, "onDestroy()"); 548 super.onDestroy(); 549 for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) { 550 if (task != null) { 551 task.stop(); 552 } 553 } 554 } 555 556 @Override 557 public IBinder onBind(Intent intent) { 558 if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) { 559 return mBinder; 560 } 561 return super.onBind(intent); 562 } 563 564 @Override 565 public int onStartCommand(Intent intent, int flags, int startId) { 566 if (intent != null && 567 TextUtils.equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION, intent.getAction())) { 568 if (intent.getBooleanExtra(ServiceProxy.EXTRA_FORCE_SHUTDOWN, false)) { 569 // We've been asked to forcibly shutdown. This happens if email accounts are 570 // deleted, otherwise we can get errors if services are still running for 571 // accounts that are now gone. 572 // TODO: This is kind of a hack, it would be nicer if we could handle it correctly 573 // if accounts disappear out from under us. 574 LogUtils.d(TAG, "Forced shutdown, killing process"); 575 System.exit(-1); 576 } 577 } 578 return super.onStartCommand(intent, flags, startId); 579 } 580 581 @Override 582 protected AbstractThreadedSyncAdapter getSyncAdapter() { 583 synchronized (sSyncAdapterLock) { 584 if (sSyncAdapter == null) { 585 sSyncAdapter = new SyncAdapterImpl(this); 586 } 587 return sSyncAdapter; 588 } 589 } 590 591 // TODO: Handle cancelSync() appropriately. 592 private class SyncAdapterImpl extends AbstractThreadedSyncAdapter { 593 public SyncAdapterImpl(Context context) { 594 super(context, true /* autoInitialize */); 595 } 596 597 @Override 598 public void onPerformSync(final android.accounts.Account acct, final Bundle extras, 599 final String authority, final ContentProviderClient provider, 600 final SyncResult syncResult) { 601 if (LogUtils.isLoggable(TAG, Log.DEBUG)) { 602 LogUtils.d(TAG, "onPerformSync: %s, %s", acct.toString(), extras.toString()); 603 } else { 604 LogUtils.i(TAG, "onPerformSync: %s", extras.toString()); 605 } 606 TempDirectory.setTempDirectory(EmailSyncAdapterService.this); 607 608 // TODO: Perform any connectivity checks, bail early if we don't have proper network 609 // for this sync operation. 610 611 final Context context = getContext(); 612 final ContentResolver cr = context.getContentResolver(); 613 614 // Get the EmailContent Account 615 final Account account; 616 final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, 617 AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null); 618 try { 619 if (!accountCursor.moveToFirst()) { 620 // Could not load account. 621 // TODO: improve error handling. 622 LogUtils.w(TAG, "onPerformSync: could not load account"); 623 return; 624 } 625 account = new Account(); 626 account.restore(accountCursor); 627 } finally { 628 accountCursor.close(); 629 } 630 631 // Figure out what we want to sync, based on the extras and our account sync status. 632 final boolean isInitialSync = EmailContent.isInitialSyncKey(account.mSyncKey); 633 final long[] mailboxIds = Mailbox.getMailboxIdsFromBundle(extras); 634 final int mailboxType = extras.getInt(Mailbox.SYNC_EXTRA_MAILBOX_TYPE, 635 Mailbox.TYPE_NONE); 636 637 // Push only means this sync request should only refresh the ping (either because 638 // settings changed, or we need to restart it for some reason). 639 final boolean pushOnly = Mailbox.isPushOnlyExtras(extras); 640 // Account only means just do a FolderSync. 641 final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras); 642 643 // A "full sync" means that we didn't request a more specific type of sync. 644 final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null && 645 mailboxType == Mailbox.TYPE_NONE); 646 647 // A FolderSync is necessary for full sync, initial sync, and account only sync. 648 final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly); 649 650 // If we're just twiddling the push, we do the lightweight thing and bail early. 651 if (pushOnly && !isFolderSync) { 652 mSyncHandlerMap.modifyPing(account); 653 LogUtils.d(TAG, "onPerformSync: mailbox push only"); 654 return; 655 } 656 657 // Do the bookkeeping for starting a sync, including stopping a ping if necessary. 658 mSyncHandlerMap.startSync(account.mId); 659 660 // Perform a FolderSync if necessary. 661 if (isFolderSync) { 662 final EasFolderSync folderSync = new EasFolderSync(context, account); 663 folderSync.doFolderSync(syncResult); 664 } 665 666 // Perform email upsync for this account. Moves first, then state changes. 667 if (!isInitialSync) { 668 EasMoveItems move = new EasMoveItems(context, account); 669 move.upsyncMovedMessages(syncResult); 670 // TODO: EasSync should eventually handle both up and down; for now, it's used 671 // purely for upsync. 672 EasSync upsync = new EasSync(context, account); 673 upsync.upsync(syncResult); 674 } 675 676 // TODO: Should we refresh account here? It may have changed while waiting for any 677 // pings to stop. It may not matter since the things that may have been twiddled might 678 // not affect syncing. 679 680 if (mailboxIds != null) { 681 // Sync the mailbox that was explicitly requested. 682 for (final long mailboxId : mailboxIds) { 683 syncMailbox(context, cr, acct, account, mailboxId, extras, syncResult, null, 684 true); 685 } 686 } else if (!accountOnly && !pushOnly) { 687 // We have to sync multiple folders. 688 final Cursor c; 689 if (isFullSync) { 690 // Full account sync includes all mailboxes that participate in system sync. 691 c = Mailbox.getMailboxIdsForSync(cr, account.mId); 692 } else { 693 // Type-filtered sync should only get the mailboxes of a specific type. 694 c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType); 695 } 696 if (c != null) { 697 try { 698 final HashSet<String> authsToSync = getAuthsToSync(acct); 699 while (c.moveToNext()) { 700 syncMailbox(context, cr, acct, account, c.getLong(0), extras, 701 syncResult, authsToSync, false); 702 } 703 } finally { 704 c.close(); 705 } 706 } 707 } 708 709 // Clean up the bookkeeping, including restarting ping if necessary. 710 mSyncHandlerMap.syncComplete(account); 711 712 // TODO: It may make sense to have common error handling here. Two possible mechanisms: 713 // 1) performSync return value can signal some useful info. 714 // 2) syncResult can contain useful info. 715 LogUtils.d(TAG, "onPerformSync: finished"); 716 } 717 718 /** 719 * Update the mailbox's sync status with the provider and, if we're finished with the sync, 720 * write the last sync time as well. 721 * @param context Our {@link Context}. 722 * @param mailbox The mailbox whose sync status to update. 723 * @param cv A {@link ContentValues} object to use for updating the provider. 724 * @param syncStatus The status for the current sync. 725 */ 726 private void updateMailbox(final Context context, final Mailbox mailbox, 727 final ContentValues cv, final int syncStatus) { 728 cv.put(Mailbox.UI_SYNC_STATUS, syncStatus); 729 if (syncStatus == EmailContent.SYNC_STATUS_NONE) { 730 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 731 } 732 mailbox.update(context, cv); 733 } 734 735 private boolean syncMailbox(final Context context, final ContentResolver cr, 736 final android.accounts.Account acct, final Account account, final long mailboxId, 737 final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync, 738 final boolean isMailboxSync) { 739 final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 740 if (mailbox == null) { 741 return false; 742 } 743 744 if (mailbox.mAccountKey != account.mId) { 745 LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(), 746 extras.toString()); 747 return false; 748 } 749 if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) { 750 // We are asking for an account sync, but this mailbox type is not configured for 751 // sync. 752 return false; 753 } 754 755 if (mailbox.mType == Mailbox.TYPE_DRAFTS) { 756 // TODO: Because we don't have bidirectional sync working, trying to downsync 757 // the drafts folder is confusing. b/11158759 758 // For now, just disable all syncing of DRAFTS type folders. 759 // Automatic syncing should always be disabled, but we also stop it here to ensure 760 // that we won't sync even if the user attempts to force a sync from the UI. 761 LogUtils.d(TAG, "Skipping sync of DRAFTS folder"); 762 return false; 763 } 764 final boolean success; 765 // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are 766 // treated as background syncs. 767 // TODO: Push will be treated as "user" syncs, and probably should be background. 768 final ContentValues cv = new ContentValues(2); 769 updateMailbox(context, mailbox, cv, isMailboxSync ? 770 EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND); 771 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 772 final EasOutboxSyncHandler outboxSyncHandler = 773 new EasOutboxSyncHandler(context, account, mailbox); 774 outboxSyncHandler.performSync(); 775 success = true; 776 } else if(mailbox.isSyncable()) { 777 final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr, 778 acct, account, mailbox, extras, syncResult); 779 success = (syncHandler != null); 780 if (syncHandler != null) { 781 syncHandler.performSync(syncResult); 782 } 783 } else { 784 success = false; 785 } 786 updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE); 787 788 if (syncResult.stats.numAuthExceptions > 0) { 789 showAuthNotification(account.mId, account.mEmailAddress); 790 } 791 return success; 792 } 793 } 794 private void showAuthNotification(long accountId, String accountName) { 795 final PendingIntent pendingIntent = PendingIntent.getActivity( 796 this, 797 0, 798 createAccountSettingsIntent(accountId, accountName), 799 0); 800 801 final Notification notification = new Builder(this) 802 .setContentTitle(this.getString(string.auth_error_notification_title)) 803 .setContentText(this.getString( 804 string.auth_error_notification_text, accountName)) 805 .setSmallIcon(drawable.stat_notify_auth) 806 .setContentIntent(pendingIntent) 807 .setAutoCancel(true) 808 .build(); 809 810 final NotificationManager nm = (NotificationManager) 811 this.getSystemService(Context.NOTIFICATION_SERVICE); 812 nm.notify("AuthError", 0, notification); 813 } 814 815 /** 816 * Create and return an intent to display (and edit) settings for a specific account, or -1 817 * for any/all accounts. If an account name string is provided, a warning dialog will be 818 * displayed as well. 819 */ 820 public static Intent createAccountSettingsIntent(long accountId, String accountName) { 821 final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder( 822 IntentUtilities.PATH_SETTINGS); 823 IntentUtilities.setAccountId(builder, accountId); 824 IntentUtilities.setAccountName(builder, accountName); 825 return new Intent(Intent.ACTION_EDIT, builder.build()); 826 } 827 828 /** 829 * Determine which content types are set to sync for an account. 830 * @param account The account whose sync settings we're looking for. 831 * @return The authorities for the content types we want to sync for account. 832 */ 833 private static HashSet<String> getAuthsToSync(final android.accounts.Account account) { 834 final HashSet<String> authsToSync = new HashSet(); 835 if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) { 836 authsToSync.add(EmailContent.AUTHORITY); 837 } 838 if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) { 839 authsToSync.add(CalendarContract.AUTHORITY); 840 } 841 if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) { 842 authsToSync.add(ContactsContract.AUTHORITY); 843 } 844 return authsToSync; 845 } 846 } 847