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.ComponentName; 26 import android.content.ContentProviderClient; 27 import android.content.ContentResolver; 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.ServiceConnection; 32 import android.content.SyncResult; 33 import android.database.Cursor; 34 import android.net.Uri; 35 import android.os.AsyncTask; 36 import android.os.Bundle; 37 import android.os.IBinder; 38 import android.os.RemoteException; 39 import android.os.SystemClock; 40 import android.provider.CalendarContract; 41 import android.provider.ContactsContract; 42 import android.text.TextUtils; 43 import android.text.format.DateUtils; 44 import android.util.Log; 45 46 import com.android.emailcommon.TempDirectory; 47 import com.android.emailcommon.provider.Account; 48 import com.android.emailcommon.provider.EmailContent; 49 import com.android.emailcommon.provider.EmailContent.AccountColumns; 50 import com.android.emailcommon.provider.EmailContent.Message; 51 import com.android.emailcommon.provider.EmailContent.MessageColumns; 52 import com.android.emailcommon.provider.EmailContent.SyncColumns; 53 import com.android.emailcommon.provider.HostAuth; 54 import com.android.emailcommon.provider.Mailbox; 55 import com.android.emailcommon.service.EmailServiceStatus; 56 import com.android.emailcommon.service.IEmailService; 57 import com.android.emailcommon.service.IEmailServiceCallback; 58 import com.android.emailcommon.service.SearchParams; 59 import com.android.emailcommon.service.ServiceProxy; 60 import com.android.emailcommon.utility.IntentUtilities; 61 import com.android.emailcommon.utility.Utility; 62 import com.android.exchange.Eas; 63 import com.android.exchange.R.drawable; 64 import com.android.exchange.R.string; 65 import com.android.exchange.adapter.PingParser; 66 import com.android.exchange.eas.EasSyncContacts; 67 import com.android.exchange.eas.EasSyncCalendar; 68 import com.android.exchange.eas.EasFolderSync; 69 import com.android.exchange.eas.EasLoadAttachment; 70 import com.android.exchange.eas.EasMoveItems; 71 import com.android.exchange.eas.EasOperation; 72 import com.android.exchange.eas.EasOutboxSync; 73 import com.android.exchange.eas.EasPing; 74 import com.android.exchange.eas.EasSearch; 75 import com.android.exchange.eas.EasSync; 76 import com.android.exchange.eas.EasSyncBase; 77 import com.android.mail.providers.UIProvider; 78 import com.android.mail.utils.LogUtils; 79 80 import java.util.HashMap; 81 import java.util.HashSet; 82 83 /** 84 * Service for communicating with Exchange servers. There are three main parts of this class: 85 * TODO: Flesh out these comments. 86 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs. 87 * 2) Bookkeeping for running Ping requests, which handles push notifications. 88 * 3) An {@link IEmailService} Stub to handle RPC from the UI. 89 */ 90 public class EmailSyncAdapterService extends AbstractSyncAdapterService { 91 92 private static final String TAG = Eas.LOG_TAG; 93 94 /** 95 * Temporary while converting to EasService. Do not check in set to true. 96 * When true, delegates various operations to {@link EasService}, for use while developing the 97 * new service. 98 * The two following fields are used to support what happens when this is true. 99 */ 100 private static final boolean DELEGATE_TO_EAS_SERVICE = false; 101 private IEmailService mEasService; 102 private ServiceConnection mConnection; 103 104 private static final String EXTRA_START_PING = "START_PING"; 105 private static final String EXTRA_PING_ACCOUNT = "PING_ACCOUNT"; 106 private static final long SYNC_ERROR_BACKOFF_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS; 107 108 /** 109 * The amount of time between periodic syncs intended to ensure that push hasn't died. 110 */ 111 private static final long KICK_SYNC_INTERVAL = 112 DateUtils.HOUR_IN_MILLIS / DateUtils.SECOND_IN_MILLIS; 113 114 /** Controls whether we do a periodic "kick" to restart the ping. */ 115 private static final boolean SCHEDULE_KICK = true; 116 117 /** Projection used for getting email address for an account. */ 118 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 119 120 private static final Object sSyncAdapterLock = new Object(); 121 private static AbstractThreadedSyncAdapter sSyncAdapter = null; 122 123 // Value for a message's server id when sending fails. 124 public static final int SEND_FAILED = 1; 125 public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED = 126 MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " + 127 SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')'; 128 129 /** 130 * Bookkeeping for handling synchronization between pings and syncs. 131 * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is 132 * the term for the Exchange command, but this code should be generic enough to be easily 133 * extended to IMAP. 134 * "Sync" refers to an actual sync command to either fetch mail state, account state, or send 135 * mail (send is implemented as "sync the outbox"). 136 * TODO: Outbox sync probably need not stop a ping in progress. 137 * Basic rules of how these interact (note that all rules are per account): 138 * - Only one ping or sync may run at a time. 139 * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while 140 * a sync is in progress. 141 * - On the other hand, ping requests may come in while handling a ping. 142 * - "Ping request" is shorthand for "a request to change our ping parameters", which includes 143 * a request to stop receiving push notifications. 144 * - If neither a ping nor a sync is running, then a request for either will run it. 145 * - If a sync is running, new ping requests block until the sync completes. 146 * - If a ping is running, a new sync request stops the ping and creates a pending ping 147 * (which blocks until the sync completes). 148 * - If a ping is running, a new ping request stops the ping and either starts a new one or 149 * does nothing, as appopriate (since a ping request can be to stop pushing). 150 * - As an optimization, while a ping request is waiting to run, subsequent ping requests are 151 * ignored (the pending ping will pick up the latest ping parameters at the time it runs). 152 */ 153 public class SyncHandlerSynchronizer { 154 /** 155 * Map of account id -> ping handler. 156 * For a given account id, there are three possible states: 157 * 1) If no ping or sync is currently running, there is no entry in the map for the account. 158 * 2) If a ping is running, there is an entry with the appropriate ping handler. 159 * 3) If there is a sync running, there is an entry with null as the value. 160 * We cannot have more than one ping or sync running at a time. 161 */ 162 private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>(); 163 164 /** 165 * Wait until neither a sync nor a ping is running on this account, and then return. 166 * If there's a ping running, actively stop it. (For syncs, we have to just wait.) 167 * @param accountId The account we want to wait for. 168 */ 169 private synchronized void waitUntilNoActivity(final long accountId) { 170 while (mPingHandlers.containsKey(accountId)) { 171 final PingTask pingHandler = mPingHandlers.get(accountId); 172 if (pingHandler != null) { 173 pingHandler.stop(); 174 } 175 try { 176 wait(); 177 } catch (final InterruptedException e) { 178 // TODO: When would this happen, and how should I handle it? 179 } 180 } 181 } 182 183 /** 184 * Use this to see if we're currently syncing, as opposed to pinging or doing nothing. 185 * @param accountId The account to check. 186 * @return Whether that account is currently running a sync. 187 */ 188 private synchronized boolean isRunningSync(final long accountId) { 189 return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null); 190 } 191 192 /** 193 * If there are no running pings, stop the service. 194 */ 195 private void stopServiceIfNoPings() { 196 for (final PingTask pingHandler : mPingHandlers.values()) { 197 if (pingHandler != null) { 198 return; 199 } 200 } 201 EmailSyncAdapterService.this.stopSelf(); 202 } 203 204 /** 205 * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync 206 * here; the caller must do that. 207 * @param accountId The account on which we are running a sync. 208 */ 209 public synchronized void startSync(final long accountId) { 210 waitUntilNoActivity(accountId); 211 mPingHandlers.put(accountId, null); 212 } 213 214 /** 215 * Starts or restarts a ping for an account, if the current account state indicates that it 216 * wants to push. 217 * @param account The account whose ping is being modified. 218 */ 219 public synchronized void modifyPing(final boolean lastSyncHadError, 220 final Account account) { 221 // If a sync is currently running, it will start a ping when it's done, so there's no 222 // need to do anything right now. 223 if (isRunningSync(account.mId)) { 224 return; 225 } 226 227 // Don't ping if we're on security hold. 228 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 229 return; 230 } 231 232 // Don't ping for accounts that haven't performed initial sync. 233 if (EmailContent.isInitialSyncKey(account.mSyncKey)) { 234 return; 235 } 236 237 // Determine if this account needs pushes. All of the following must be true: 238 // - The account's sync interval must indicate that it wants push. 239 // - At least one content type must be sync-enabled in the account manager. 240 // - At least one mailbox of a sync-enabled type must have automatic sync enabled. 241 final EmailSyncAdapterService service = EmailSyncAdapterService.this; 242 final android.accounts.Account amAccount = new android.accounts.Account( 243 account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 244 boolean pushNeeded = false; 245 if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 246 final HashSet<String> authsToSync = getAuthsToSync(amAccount); 247 // If we have at least one sync-enabled content type, check for syncing mailboxes. 248 if (!authsToSync.isEmpty()) { 249 final Cursor c = Mailbox.getMailboxesForPush(service.getContentResolver(), 250 account.mId); 251 if (c != null) { 252 try { 253 while (c.moveToNext()) { 254 final int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 255 if (authsToSync.contains(Mailbox.getAuthority(mailboxType))) { 256 pushNeeded = true; 257 break; 258 } 259 } 260 } finally { 261 c.close(); 262 } 263 } 264 } 265 } 266 267 // Stop, start, or restart the ping as needed, as well as the ping kicker periodic sync. 268 final PingTask pingSyncHandler = mPingHandlers.get(account.mId); 269 final Bundle extras = new Bundle(1); 270 extras.putBoolean(Mailbox.SYNC_EXTRA_PUSH_ONLY, true); 271 if (pushNeeded) { 272 // First start or restart the ping as appropriate. 273 if (pingSyncHandler != null) { 274 pingSyncHandler.restart(); 275 } else { 276 if (lastSyncHadError) { 277 // Schedule an alarm to set up the ping in 5 minutes 278 scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS); 279 } else { 280 // Start a new ping. 281 // Note: unlike startSync, we CANNOT allow the caller to do the actual work. 282 // If we return before the ping starts, there's a race condition where 283 // another ping or sync might start first. It only works for startSync 284 // because sync is higher priority than ping (i.e. a ping can't start while 285 // a sync is pending) and only one sync can run at a time. 286 final PingTask pingHandler = new PingTask(service, account, amAccount, 287 this); 288 mPingHandlers.put(account.mId, pingHandler); 289 pingHandler.start(); 290 // Whenever we have a running ping, make sure this service stays running. 291 service.startService(new Intent(service, EmailSyncAdapterService.class)); 292 } 293 } 294 if (SCHEDULE_KICK) { 295 ContentResolver.addPeriodicSync(amAccount, EmailContent.AUTHORITY, extras, 296 KICK_SYNC_INTERVAL); 297 } 298 } else { 299 if (pingSyncHandler != null) { 300 pingSyncHandler.stop(); 301 } 302 if (SCHEDULE_KICK) { 303 ContentResolver.removePeriodicSync(amAccount, EmailContent.AUTHORITY, extras); 304 } 305 } 306 } 307 308 /** 309 * Updates the synchronization bookkeeping when a sync is done. 310 * @param account The account whose sync just finished. 311 */ 312 public synchronized void syncComplete(final boolean lastSyncHadError, 313 final Account account) { 314 LogUtils.d(TAG, "syncComplete, err: " + lastSyncHadError); 315 mPingHandlers.remove(account.mId); 316 // Syncs can interrupt pings, so we should check if we need to start one now. 317 // If the last sync had a fatal error, we will not immediately recreate the ping. 318 // Instead, we'll set an alarm that will restart them in a few minutes. This prevents 319 // a battery draining spin if there is some kind of protocol error or other 320 // non-transient failure. (Actually, immediately pinging even for a transient error 321 // isn't great) 322 modifyPing(lastSyncHadError, account); 323 stopServiceIfNoPings(); 324 notifyAll(); 325 } 326 327 /** 328 * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only 329 * sync if necessary. 330 * @param amAccount The {@link android.accounts.Account} for this account. 331 * @param accountId The account whose ping just finished. 332 * @param pingStatus The status value from {@link PingParser} for the last ping performed. 333 * This cannot be one of the values that results in another ping, so this 334 * function only needs to handle the terminal statuses. 335 */ 336 public synchronized void pingComplete(final android.accounts.Account amAccount, 337 final long accountId, final int pingStatus) { 338 mPingHandlers.remove(accountId); 339 340 // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI. 341 // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI. 342 343 if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE || 344 pingStatus == EasOperation.RESULT_OTHER_FAILURE) { 345 // TODO: Sticky problem here: we necessarily aren't in a sync, so it's impossible to 346 // signal the error to the SyncManager and take advantage of backoff there. Worse, 347 // the current mechanism for how we do this will just encourage spammy requests 348 // since the actual ping-only sync request ALWAYS succeeds. 349 // So for now, let's delay a bit before asking the SyncManager to perform the sync. 350 // Longer term, this should be incorporated into some form of backoff, either 351 // by integrating with the SyncManager more fully or by implementing a Ping-specific 352 // backoff mechanism (e.g. integrate this with the logic for ping duration). 353 LogUtils.e(TAG, "Ping for account %d completed with error %d, delaying next ping", 354 accountId, pingStatus); 355 scheduleDelayedPing(amAccount, SYNC_ERROR_BACKOFF_MILLIS); 356 } else { 357 stopServiceIfNoPings(); 358 } 359 360 // TODO: It might be the case that only STATUS_CHANGES_FOUND and 361 // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through. 362 notifyAll(); 363 } 364 365 } 366 private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer(); 367 368 /** 369 * The binder for IEmailService. 370 */ 371 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 372 373 private String getEmailAddressForAccount(final long accountId) { 374 final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this, 375 Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 376 new String[] {Long.toString(accountId)}, null, 0); 377 if (emailAddress == null) { 378 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 379 } 380 return emailAddress; 381 } 382 383 @Override 384 public Bundle validate(final HostAuth hostAuth) { 385 LogUtils.d(TAG, "IEmailService.validate"); 386 if (mEasService != null) { 387 try { 388 return mEasService.validate(hostAuth); 389 } catch (final RemoteException re) { 390 LogUtils.e(TAG, re, "While asking EasService to handle validate"); 391 } 392 } 393 return new EasFolderSync(EmailSyncAdapterService.this, hostAuth).doValidate(); 394 } 395 396 @Override 397 public Bundle autoDiscover(final String username, final String password) { 398 LogUtils.d(TAG, "IEmailService.autoDiscover"); 399 return new EasAutoDiscover(EmailSyncAdapterService.this, username, password) 400 .doAutodiscover(); 401 } 402 403 @Override 404 public void updateFolderList(final long accountId) { 405 LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId); 406 if (mEasService != null) { 407 try { 408 mEasService.updateFolderList(accountId); 409 return; 410 } catch (final RemoteException re) { 411 LogUtils.e(TAG, re, "While asking EasService to updateFolderList"); 412 } 413 } 414 final String emailAddress = getEmailAddressForAccount(accountId); 415 if (emailAddress != null) { 416 final Bundle extras = new Bundle(1); 417 extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); 418 ContentResolver.requestSync(new android.accounts.Account( 419 emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 420 EmailContent.AUTHORITY, extras); 421 } 422 } 423 424 @Override 425 public void setLogging(final int flags) { 426 // TODO: fix this? 427 // Protocol logging 428 Eas.setUserDebug(flags); 429 // Sync logging 430 //setUserDebug(flags); 431 } 432 433 @Override 434 public void loadAttachment(final IEmailServiceCallback callback, final long accountId, 435 final long attachmentId, final boolean background) { 436 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 437 // TODO: Prevent this from happening in parallel with a sync? 438 final EasLoadAttachment operation = new EasLoadAttachment(EmailSyncAdapterService.this, 439 accountId, attachmentId, callback); 440 operation.performOperation(); 441 } 442 443 @Override 444 public void sendMeetingResponse(final long messageId, final int response) { 445 LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response); 446 EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId, 447 response); 448 } 449 450 /** 451 * Delete PIM (calendar, contacts) data for the specified account 452 * 453 * @param emailAddress the email address for the account whose data should be deleted 454 */ 455 @Override 456 public void deleteAccountPIMData(final String emailAddress) { 457 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 458 if (emailAddress != null) { 459 final Context context = EmailSyncAdapterService.this; 460 EasSyncContacts.wipeAccountFromContentProvider(context, emailAddress); 461 EasSyncCalendar.wipeAccountFromContentProvider(context, emailAddress); 462 } 463 // TODO: Run account reconciler? 464 } 465 466 @Override 467 public int searchMessages(final long accountId, final SearchParams searchParams, 468 final long destMailboxId) { 469 LogUtils.d(TAG, "IEmailService.searchMessages"); 470 final EasSearch operation = new EasSearch(EmailSyncAdapterService.this, accountId, 471 searchParams, destMailboxId); 472 operation.performOperation(); 473 return operation.getTotalResults(); 474 // TODO: may need an explicit callback to replace the one to IEmailServiceCallback. 475 } 476 477 @Override 478 public void sendMail(final long accountId) {} 479 480 @Override 481 public void pushModify(final long accountId) { 482 LogUtils.d(TAG, "IEmailService.pushModify"); 483 if (mEasService != null) { 484 try { 485 mEasService.pushModify(accountId); 486 return; 487 } catch (final RemoteException re) { 488 LogUtils.e(TAG, re, "While asking EasService to handle pushModify"); 489 } 490 } 491 final Account account = Account.restoreAccountWithId(EmailSyncAdapterService.this, 492 accountId); 493 if (account != null) { 494 mSyncHandlerMap.modifyPing(false, account); 495 } 496 } 497 498 @Override 499 public void sync(final long accountId, final boolean updateFolderList, 500 final int mailboxType, final long[] folders) {} 501 }; 502 503 public EmailSyncAdapterService() { 504 super(); 505 } 506 507 /** 508 * {@link AsyncTask} for restarting pings for all accounts that need it. 509 */ 510 private static final String PUSH_ACCOUNTS_SELECTION = 511 AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 512 private class RestartPingsTask extends AsyncTask<Void, Void, Void> { 513 514 private final ContentResolver mContentResolver; 515 private final SyncHandlerSynchronizer mSyncHandlerMap; 516 private boolean mAnyAccounts; 517 518 public RestartPingsTask(final ContentResolver contentResolver, 519 final SyncHandlerSynchronizer syncHandlerMap) { 520 mContentResolver = contentResolver; 521 mSyncHandlerMap = syncHandlerMap; 522 } 523 524 @Override 525 protected Void doInBackground(Void... params) { 526 final Cursor c = mContentResolver.query(Account.CONTENT_URI, 527 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 528 if (c != null) { 529 try { 530 mAnyAccounts = (c.getCount() != 0); 531 while (c.moveToNext()) { 532 final Account account = new Account(); 533 account.restore(c); 534 mSyncHandlerMap.modifyPing(false, account); 535 } 536 } finally { 537 c.close(); 538 } 539 } else { 540 mAnyAccounts = false; 541 } 542 return null; 543 } 544 545 @Override 546 protected void onPostExecute(Void result) { 547 if (!mAnyAccounts) { 548 LogUtils.d(TAG, "stopping for no accounts"); 549 EmailSyncAdapterService.this.stopSelf(); 550 } 551 } 552 } 553 554 @Override 555 public void onCreate() { 556 LogUtils.v(TAG, "onCreate()"); 557 super.onCreate(); 558 startService(new Intent(this, EmailSyncAdapterService.class)); 559 // Restart push for all accounts that need it. 560 new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor( 561 AsyncTask.THREAD_POOL_EXECUTOR); 562 if (DELEGATE_TO_EAS_SERVICE) { 563 // TODO: This block is temporary to support the transition to EasService. 564 mConnection = new ServiceConnection() { 565 @Override 566 public void onServiceConnected(ComponentName name, IBinder binder) { 567 mEasService = IEmailService.Stub.asInterface(binder); 568 } 569 570 @Override 571 public void onServiceDisconnected(ComponentName name) { 572 mEasService = null; 573 } 574 }; 575 bindService(new Intent(this, EasService.class), mConnection, Context.BIND_AUTO_CREATE); 576 } 577 } 578 579 @Override 580 public void onDestroy() { 581 LogUtils.v(TAG, "onDestroy()"); 582 super.onDestroy(); 583 for (PingTask task : mSyncHandlerMap.mPingHandlers.values()) { 584 if (task != null) { 585 task.stop(); 586 } 587 } 588 if (DELEGATE_TO_EAS_SERVICE) { 589 // TODO: This block is temporary to support the transition to EasService. 590 unbindService(mConnection); 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 682 // Push only means this sync request should only refresh the ping (either because 683 // settings changed, or we need to restart it for some reason). 684 final boolean pushOnly = Mailbox.isPushOnlyExtras(extras); 685 // Account only means just do a FolderSync. 686 final boolean accountOnly = Mailbox.isAccountOnlyExtras(extras); 687 688 // A "full sync" means that we didn't request a more specific type of sync. 689 final boolean isFullSync = (!pushOnly && !accountOnly && mailboxIds == null && 690 mailboxType == Mailbox.TYPE_NONE); 691 692 // A FolderSync is necessary for full sync, initial sync, and account only sync. 693 final boolean isFolderSync = (isFullSync || isInitialSync || accountOnly); 694 695 // If we're just twiddling the push, we do the lightweight thing and bail early. 696 if (pushOnly && !isFolderSync) { 697 LogUtils.d(TAG, "onPerformSync: mailbox push only"); 698 if (mEasService != null) { 699 try { 700 mEasService.pushModify(account.mId); 701 return; 702 } catch (final RemoteException re) { 703 LogUtils.e(TAG, re, "While trying to pushModify within onPerformSync"); 704 } 705 } 706 mSyncHandlerMap.modifyPing(false, account); 707 return; 708 } 709 710 // Do the bookkeeping for starting a sync, including stopping a ping if necessary. 711 mSyncHandlerMap.startSync(account.mId); 712 int operationResult = 0; 713 try { 714 // Perform a FolderSync if necessary. 715 // TODO: We permit FolderSync even during security hold, because it's necessary to 716 // resolve some holds. Ideally we would only do it for the holds that require it. 717 if (isFolderSync) { 718 final EasFolderSync folderSync = new EasFolderSync(context, account); 719 operationResult = folderSync.doFolderSync(); 720 if (operationResult < 0) { 721 return; 722 } 723 } 724 725 // Do not permit further syncs if we're on security hold. 726 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 727 return; 728 } 729 730 // Perform email upsync for this account. Moves first, then state changes. 731 if (!isInitialSync) { 732 EasMoveItems move = new EasMoveItems(context, account); 733 operationResult = move.upsyncMovedMessages(); 734 if (operationResult < 0) { 735 return; 736 } 737 738 // TODO: EasSync should eventually handle both up and down; for now, it's used 739 // purely for upsync. 740 EasSync upsync = new EasSync(context, account); 741 operationResult = upsync.upsync(); 742 if (operationResult < 0) { 743 return; 744 } 745 } 746 747 if (mailboxIds != null) { 748 final boolean hasCallbackMethod = 749 extras.containsKey(EmailServiceStatus.SYNC_EXTRAS_CALLBACK_METHOD); 750 // Sync the mailbox that was explicitly requested. 751 for (final long mailboxId : mailboxIds) { 752 if (hasCallbackMethod) { 753 EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId, 754 EmailServiceStatus.IN_PROGRESS, 0, 755 UIProvider.LastSyncResult.SUCCESS); 756 } 757 operationResult = syncMailbox(context, cr, acct, account, mailboxId, 758 extras, syncResult, null, true); 759 if (hasCallbackMethod) { 760 EmailServiceStatus.syncMailboxStatus(cr, extras, 761 mailboxId,EmailServiceStatus.SUCCESS, 0, 762 EasOperation.translateSyncResultToUiResult(operationResult)); 763 } 764 765 if (operationResult < 0) { 766 break; 767 } 768 } 769 } else if (!accountOnly && !pushOnly) { 770 // We have to sync multiple folders. 771 final Cursor c; 772 if (isFullSync) { 773 // Full account sync includes all mailboxes that participate in system sync. 774 c = Mailbox.getMailboxIdsForSync(cr, account.mId); 775 } else { 776 // Type-filtered sync should only get the mailboxes of a specific type. 777 c = Mailbox.getMailboxIdsForSyncByType(cr, account.mId, mailboxType); 778 } 779 if (c != null) { 780 try { 781 final HashSet<String> authsToSync = getAuthsToSync(acct); 782 while (c.moveToNext()) { 783 operationResult = syncMailbox(context, cr, acct, account, 784 c.getLong(0), extras, syncResult, authsToSync, false); 785 if (operationResult < 0) { 786 break; 787 } 788 } 789 } finally { 790 c.close(); 791 } 792 } 793 } 794 } finally { 795 // Clean up the bookkeeping, including restarting ping if necessary. 796 mSyncHandlerMap.syncComplete(syncResult.hasError(), account); 797 798 if (operationResult < 0) { 799 EasFolderSync.writeResultToSyncResult(operationResult, syncResult); 800 // If any operations had an auth error, notify the user. 801 // Note that provisioning errors should have already triggered the policy 802 // notification, so suppress those from showing the auth notification. 803 if (syncResult.stats.numAuthExceptions > 0 && 804 operationResult != EasOperation.RESULT_PROVISIONING_ERROR) { 805 showAuthNotification(account.mId, account.mEmailAddress); 806 } 807 } 808 809 LogUtils.d(TAG, "onPerformSync: finished"); 810 } 811 } 812 813 /** 814 * Update the mailbox's sync status with the provider and, if we're finished with the sync, 815 * write the last sync time as well. 816 * @param context Our {@link Context}. 817 * @param mailbox The mailbox whose sync status to update. 818 * @param cv A {@link ContentValues} object to use for updating the provider. 819 * @param syncStatus The status for the current sync. 820 */ 821 private void updateMailbox(final Context context, final Mailbox mailbox, 822 final ContentValues cv, final int syncStatus) { 823 cv.put(Mailbox.UI_SYNC_STATUS, syncStatus); 824 if (syncStatus == EmailContent.SYNC_STATUS_NONE) { 825 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 826 } 827 mailbox.update(context, cv); 828 } 829 830 private int syncMailbox(final Context context, final ContentResolver cr, 831 final android.accounts.Account acct, final Account account, final long mailboxId, 832 final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync, 833 final boolean isMailboxSync) { 834 final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 835 if (mailbox == null) { 836 return EasSyncBase.RESULT_HARD_DATA_FAILURE; 837 } 838 839 if (mailbox.mAccountKey != account.mId) { 840 LogUtils.e(TAG, "Mailbox does not match account: %s, %s", acct.toString(), 841 extras.toString()); 842 return EasSyncBase.RESULT_HARD_DATA_FAILURE; 843 } 844 if (authsToSync != null && !authsToSync.contains(Mailbox.getAuthority(mailbox.mType))) { 845 // We are asking for an account sync, but this mailbox type is not configured for 846 // sync. Do NOT treat this as a sync error for ping backoff purposes. 847 return EasSyncBase.RESULT_DONE; 848 } 849 850 if (mailbox.mType == Mailbox.TYPE_DRAFTS) { 851 // TODO: Because we don't have bidirectional sync working, trying to downsync 852 // the drafts folder is confusing. b/11158759 853 // For now, just disable all syncing of DRAFTS type folders. 854 // Automatic syncing should always be disabled, but we also stop it here to ensure 855 // that we won't sync even if the user attempts to force a sync from the UI. 856 // Do NOT treat as a sync error for ping backoff purposes. 857 LogUtils.d(TAG, "Skipping sync of DRAFTS folder"); 858 return EasSyncBase.RESULT_DONE; 859 } 860 861 // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are 862 // treated as background syncs. 863 // TODO: Push will be treated as "user" syncs, and probably should be background. 864 if (mailbox.mType == Mailbox.TYPE_OUTBOX || mailbox.isSyncable()) { 865 final ContentValues cv = new ContentValues(2); 866 updateMailbox(context, mailbox, cv, isMailboxSync ? 867 EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND); 868 try { 869 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 870 return syncOutbox(context, cr, account, mailbox); 871 } 872 final EasSyncBase operation = new EasSyncBase(context, account, mailbox); 873 return operation.performOperation(); 874 } finally { 875 updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE); 876 } 877 } 878 879 return EasSyncBase.RESULT_DONE; 880 } 881 } 882 883 private int syncOutbox(Context context, ContentResolver cr, Account account, Mailbox mailbox) { 884 // Get a cursor to Outbox messages 885 final Cursor c = cr.query(Message.CONTENT_URI, 886 Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED, 887 new String[] {Long.toString(mailbox.mId)}, null); 888 try { 889 // Loop through the messages, sending each one 890 while (c.moveToNext()) { 891 final Message message = new Message(); 892 message.restore(c); 893 if (Utility.hasUnloadedAttachments(context, message.mId)) { 894 // We'll just have to wait on this... 895 continue; 896 } 897 898 // TODO: Fix -- how do we want to signal to UI that we started syncing? 899 // Note the entire callback mechanism here needs improving. 900 //sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0); 901 902 EasOperation op = new EasOutboxSync(context, account, message, true); 903 int result = op.performOperation(); 904 if (result == EasOutboxSync.RESULT_ITEM_NOT_FOUND) { 905 // This can happen if we are using smartReply, and the message we are referring 906 // to has disappeared from the server. Try again with smartReply disabled. 907 op = new EasOutboxSync(context, account, message, false); 908 result = op.performOperation(); 909 } 910 // If we got some connection error or other fatal error, terminate the sync. 911 if (result != EasOutboxSync.RESULT_OK && 912 result != EasOutboxSync.RESULT_NON_FATAL_ERROR && 913 result > EasOutboxSync.RESULT_OP_SPECIFIC_ERROR_RESULT) { 914 LogUtils.w(TAG, "Aborting outbox sync for error %d", result); 915 return result; 916 } 917 } 918 } finally { 919 // TODO: Some sort of sendMessageStatus() is needed here. 920 c.close(); 921 } 922 return EasOutboxSync.RESULT_OK; 923 } 924 925 private void showAuthNotification(long accountId, String accountName) { 926 final PendingIntent pendingIntent = PendingIntent.getActivity( 927 this, 928 0, 929 createAccountSettingsIntent(accountId, accountName), 930 0); 931 932 final Notification notification = new Builder(this) 933 .setContentTitle(this.getString(string.auth_error_notification_title)) 934 .setContentText(this.getString( 935 string.auth_error_notification_text, accountName)) 936 .setSmallIcon(drawable.stat_notify_auth) 937 .setContentIntent(pendingIntent) 938 .setAutoCancel(true) 939 .build(); 940 941 final NotificationManager nm = (NotificationManager) 942 this.getSystemService(Context.NOTIFICATION_SERVICE); 943 nm.notify("AuthError", 0, notification); 944 } 945 946 /** 947 * Create and return an intent to display (and edit) settings for a specific account, or -1 948 * for any/all accounts. If an account name string is provided, a warning dialog will be 949 * displayed as well. 950 */ 951 public static Intent createAccountSettingsIntent(long accountId, String accountName) { 952 final Uri.Builder builder = IntentUtilities.createActivityIntentUrlBuilder( 953 IntentUtilities.PATH_SETTINGS); 954 IntentUtilities.setAccountId(builder, accountId); 955 IntentUtilities.setAccountName(builder, accountName); 956 return new Intent(Intent.ACTION_EDIT, builder.build()); 957 } 958 959 /** 960 * Determine which content types are set to sync for an account. 961 * @param account The account whose sync settings we're looking for. 962 * @return The authorities for the content types we want to sync for account. 963 */ 964 private static HashSet<String> getAuthsToSync(final android.accounts.Account account) { 965 final HashSet<String> authsToSync = new HashSet(); 966 if (ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY)) { 967 authsToSync.add(EmailContent.AUTHORITY); 968 } 969 if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY)) { 970 authsToSync.add(CalendarContract.AUTHORITY); 971 } 972 if (ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) { 973 authsToSync.add(ContactsContract.AUTHORITY); 974 } 975 return authsToSync; 976 } 977 978 /** 979 * Schedule to have a ping start some time in the future. This is used when we encounter an 980 * error, and properly should be a more full featured back-off, but for the short run, just 981 * waiting a few minutes at least avoids burning battery. 982 * @param amAccount The account that needs to be pinged. 983 * @param delay The time in milliseconds to wait before requesting the ping-only sync. Note that 984 * it may take longer than this before the ping actually happens, since there's two 985 * layers of waiting ({@link AlarmManager} can choose to wait longer, as can the 986 * SyncManager). 987 */ 988 private void scheduleDelayedPing(final android.accounts.Account amAccount, final long delay) { 989 final Intent intent = new Intent(this, EmailSyncAdapterService.class); 990 intent.setAction(Eas.EXCHANGE_SERVICE_INTENT_ACTION); 991 intent.putExtra(EXTRA_START_PING, true); 992 intent.putExtra(EXTRA_PING_ACCOUNT, amAccount); 993 final PendingIntent pi = PendingIntent.getService(this, 0, intent, 994 PendingIntent.FLAG_ONE_SHOT); 995 final AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 996 final long atTime = SystemClock.elapsedRealtime() + delay; 997 am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, atTime, pi); 998 } 999 } 1000