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