EmailSyncAdapterService.java revision 328ca0d959f7e729e96f19538c8f3af8fe782a09
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 17package com.android.exchange.service; 18 19import android.content.AbstractThreadedSyncAdapter; 20import android.content.ContentProviderClient; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Intent; 25import android.content.SyncResult; 26import android.database.Cursor; 27import android.os.AsyncTask; 28import android.os.Bundle; 29import android.os.IBinder; 30 31import com.android.emailcommon.Api; 32import com.android.emailcommon.TempDirectory; 33import com.android.emailcommon.provider.Account; 34import com.android.emailcommon.provider.EmailContent; 35import com.android.emailcommon.provider.EmailContent.AccountColumns; 36import com.android.emailcommon.provider.HostAuth; 37import com.android.emailcommon.provider.Mailbox; 38import com.android.emailcommon.service.IEmailService; 39import com.android.emailcommon.service.IEmailServiceCallback; 40import com.android.emailcommon.service.SearchParams; 41import com.android.emailcommon.utility.Utility; 42import com.android.exchange.Eas; 43import com.android.exchange.adapter.PingParser; 44import com.android.exchange.adapter.Search; 45import com.android.exchange.eas.EasFolderSync; 46import com.android.exchange.eas.EasOperation; 47import com.android.exchange.eas.EasPing; 48import com.android.mail.providers.UIProvider.AccountCapabilities; 49import com.android.mail.utils.LogUtils; 50 51import java.util.HashMap; 52 53/** 54 * Service for communicating with Exchange servers. There are three main parts of this class: 55 * TODO: Flesh out these comments. 56 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs. 57 * 2) Bookkeeping for running Ping requests, which handles push notifications. 58 * 3) An {@link IEmailService} Stub to handle RPC from the UI. 59 */ 60public class EmailSyncAdapterService extends AbstractSyncAdapterService { 61 62 private static final String TAG = "EASEmailSyncAdaptSvc"; 63 64 /** 65 * If sync extras do not include a mailbox id, then we want to perform a full sync. 66 */ 67 private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX; 68 69 /** Projection used for getting email address for an account. */ 70 private static final String[] ACCOUNT_EMAIL_PROJECTION = { AccountColumns.EMAIL_ADDRESS }; 71 72 /** 73 * Bookkeeping for handling synchronization between pings and syncs. 74 * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is 75 * the term for the Exchange command, but this code should be generic enough to be easily 76 * extended to IMAP. 77 * "Sync" refers to an actual sync command to either fetch mail state, account state, or send 78 * mail (send is implemented as "sync the outbox"). 79 * TODO: Outbox sync probably need not stop a ping in progress. 80 * Basic rules of how these interact (note that all rules are per account): 81 * - Only one ping or sync may run at a time. 82 * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while 83 * a sync is in progress. 84 * - On the other hand, ping requests may come in while handling a ping. 85 * - "Ping request" is shorthand for "a request to change our ping parameters", which includes 86 * a request to stop receiving push notifications. 87 * - If neither a ping nor a sync is running, then a request for either will run it. 88 * - If a sync is running, new ping requests block until the sync completes. 89 * - If a ping is running, a new sync request stops the ping and creates a pending ping 90 * (which blocks until the sync completes). 91 * - If a ping is running, a new ping request stops the ping and either starts a new one or 92 * does nothing, as appopriate (since a ping request can be to stop pushing). 93 * - As an optimization, while a ping request is waiting to run, subsequent ping requests are 94 * ignored (the pending ping will pick up the latest ping parameters at the time it runs). 95 */ 96 public class SyncHandlerSynchronizer { 97 /** 98 * Map of account id -> ping handler. 99 * For a given account id, there are three possible states: 100 * 1) If no ping or sync is currently running, there is no entry in the map for the account. 101 * 2) If a ping is running, there is an entry with the appropriate ping handler. 102 * 3) If there is a sync running, there is an entry with null as the value. 103 * We cannot have more than one ping or sync running at a time. 104 */ 105 private final HashMap<Long, PingTask> mPingHandlers = new HashMap<Long, PingTask>(); 106 107 /** 108 * Wait until neither a sync nor a ping is running on this account, and then return. 109 * If there's a ping running, actively stop it. (For syncs, we have to just wait.) 110 * @param accountId The account we want to wait for. 111 */ 112 private synchronized void waitUntilNoActivity(final long accountId) { 113 while (mPingHandlers.containsKey(accountId)) { 114 final PingTask pingHandler = mPingHandlers.get(accountId); 115 if (pingHandler != null) { 116 pingHandler.stop(); 117 } 118 try { 119 wait(); 120 } catch (final InterruptedException e) { 121 // TODO: When would this happen, and how should I handle it? 122 } 123 } 124 } 125 126 /** 127 * Use this to see if we're currently syncing, as opposed to pinging or doing nothing. 128 * @param accountId The account to check. 129 * @return Whether that account is currently running a sync. 130 */ 131 private synchronized boolean isRunningSync(final long accountId) { 132 return (mPingHandlers.containsKey(accountId) && mPingHandlers.get(accountId) == null); 133 } 134 135 /** 136 * If there are no running pings, stop the service. 137 */ 138 private void stopServiceIfNoPings() { 139 for (final PingTask pingHandler : mPingHandlers.values()) { 140 if (pingHandler != null) { 141 return; 142 } 143 } 144 EmailSyncAdapterService.this.stopSelf(); 145 } 146 147 /** 148 * Called prior to starting a sync to update our bookkeeping. We don't actually run the sync 149 * here; the caller must do that. 150 * @param accountId The account on which we are running a sync. 151 */ 152 public synchronized void startSync(final long accountId) { 153 waitUntilNoActivity(accountId); 154 mPingHandlers.put(accountId, null); 155 } 156 157 /** 158 * Starts or restarts a ping for an account, if the current account state indicates that it 159 * wants to push. 160 * @param account The account whose ping is being modified. 161 */ 162 public synchronized void modifyPing(final Account account) { 163 // If a sync is currently running, it will start a ping when it's done, so there's no 164 // need to do anything right now. 165 if (isRunningSync(account.mId)) { 166 return; 167 } 168 169 // If a ping is currently running, tell it to restart to pick up new params. 170 final PingTask pingSyncHandler = mPingHandlers.get(account.mId); 171 if (pingSyncHandler != null) { 172 pingSyncHandler.restart(); 173 return; 174 } 175 176 // If we're here, then there's neither a sync nor a ping running. Start a new ping. 177 final EmailSyncAdapterService service = EmailSyncAdapterService.this; 178 if (account.mSyncInterval == Account.CHECK_INTERVAL_PUSH) { 179 // TODO: Also check if we have any mailboxes that WANT push. 180 // This account needs to ping. 181 // Note: unlike startSync, we CANNOT allow the caller to do the actual work. 182 // If we return before the ping starts, there's a race condition where another 183 // ping or sync might start first. It only works for startSync because sync is 184 // higher priority than ping (i.e. a ping can't start while a sync is pending) 185 // and only one sync can run at a time. 186 final PingTask pingHandler = new PingTask(service, account, this); 187 mPingHandlers.put(account.mId, pingHandler); 188 pingHandler.start(); 189 // Whenever we have a running ping, make sure this service stays running. 190 service.startService(new Intent(service, EmailSyncAdapterService.class)); 191 } 192 } 193 194 /** 195 * Updates the synchronization bookkeeping when a sync is done. 196 * @param account The account whose sync just finished. 197 */ 198 public synchronized void syncComplete(final Account account) { 199 mPingHandlers.remove(account.mId); 200 // Syncs can interrupt pings, so we should check if we need to start one now. 201 modifyPing(account); 202 stopServiceIfNoPings(); 203 notifyAll(); 204 } 205 206 /** 207 * Updates the synchronization bookkeeping when a ping is done. Also requests a ping-only 208 * sync if necessary. 209 * @param amAccount The {@link android.accounts.Account} for this account. 210 * @param accountId The account whose ping just finished. 211 * @param pingStatus The status value from {@link PingParser} for the last ping performed. 212 * This cannot be one of the values that results in another ping, so this 213 * function only needs to handle the terminal statuses. 214 */ 215 public synchronized void pingComplete(final android.accounts.Account amAccount, 216 final long accountId, final int pingStatus) { 217 mPingHandlers.remove(accountId); 218 219 // TODO: if (pingStatus == PingParser.STATUS_FAILED), notify UI. 220 // TODO: if (pingStatus == PingParser.STATUS_REQUEST_TOO_MANY_FOLDERS), notify UI. 221 222 if (pingStatus == EasOperation.RESULT_REQUEST_FAILURE) { 223 // Request a new ping through the SyncManager. This will do the right thing if the 224 // exception was due to loss of network connectivity, etc. (i.e. it will wait for 225 // network to restore and then request it). 226 EasPing.requestPing(amAccount); 227 } else { 228 stopServiceIfNoPings(); 229 } 230 231 // TODO: It might be the case that only STATUS_CHANGES_FOUND and 232 // STATUS_FOLDER_REFRESH_NEEDED need to notifyAll(). Think this through. 233 notifyAll(); 234 } 235 236 } 237 private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer(); 238 239 /** 240 * The binder for IEmailService. 241 */ 242 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 243 244 private String getEmailAddressForAccount(final long accountId) { 245 final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this, 246 Account.CONTENT_URI, ACCOUNT_EMAIL_PROJECTION, Account.ID_SELECTION, 247 new String[] {Long.toString(accountId)}, null, 0); 248 if (emailAddress == null) { 249 LogUtils.e(TAG, "Could not find email address for account %d", accountId); 250 } 251 return emailAddress; 252 } 253 254 @Override 255 public Bundle validate(final HostAuth hostAuth) { 256 LogUtils.d(TAG, "IEmailService.validate"); 257 return new EasAccountValidator(EmailSyncAdapterService.this, hostAuth).validate(); 258 } 259 260 @Override 261 public Bundle autoDiscover(final String username, final String password) { 262 LogUtils.d(TAG, "IEmailService.autoDiscover"); 263 return new EasAutoDiscover(EmailSyncAdapterService.this, username, password) 264 .doAutodiscover(); 265 } 266 267 @Override 268 public void updateFolderList(final long accountId) { 269 LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId); 270 final String emailAddress = getEmailAddressForAccount(accountId); 271 if (emailAddress != null) { 272 ContentResolver.requestSync(new android.accounts.Account( 273 emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 274 EmailContent.AUTHORITY, new Bundle()); 275 } 276 } 277 278 @Override 279 public void setLogging(final int flags) { 280 // TODO: fix this? 281 // Protocol logging 282 Eas.setUserDebug(flags); 283 // Sync logging 284 //setUserDebug(flags); 285 } 286 287 @Override 288 public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId, 289 final boolean background) { 290 LogUtils.d(TAG, "IEmailService.loadAttachment: %d", attachmentId); 291 // TODO: Prevent this from happening in parallel with a sync? 292 EasAttachmentLoader.loadAttachment(EmailSyncAdapterService.this, attachmentId, 293 callback); 294 } 295 296 @Override 297 public void sendMeetingResponse(final long messageId, final int response) { 298 LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response); 299 EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId, 300 response); 301 } 302 303 /** 304 * Delete PIM (calendar, contacts) data for the specified account 305 * 306 * @param emailAddress the email address for the account whose data should be deleted 307 */ 308 @Override 309 public void deleteAccountPIMData(final String emailAddress) { 310 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 311 if (emailAddress != null) { 312 final Context context = EmailSyncAdapterService.this; 313 EasContactsSyncHandler.wipeAccountFromContentProvider(context, emailAddress); 314 EasCalendarSyncHandler.wipeAccountFromContentProvider(context, emailAddress); 315 } 316 // TODO: Run account reconciler? 317 } 318 319 @Override 320 public int searchMessages(final long accountId, final SearchParams searchParams, 321 final long destMailboxId) { 322 LogUtils.d(TAG, "IEmailService.searchMessages"); 323 return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams, 324 destMailboxId); 325 // TODO: may need an explicit callback to replace the one to IEmailServiceCallback. 326 } 327 328 @Override 329 public void sendMail(final long accountId) {} 330 331 @Override 332 public int getCapabilities(final Account acct) { 333 String easVersion = acct.mProtocolVersion; 334 Double easVersionDouble = 2.5D; 335 if (easVersion != null) { 336 try { 337 easVersionDouble = Double.parseDouble(easVersion); 338 } catch (NumberFormatException e) { 339 // Stick with 2.5 340 } 341 } 342 if (easVersionDouble >= 12.0D) { 343 return AccountCapabilities.SYNCABLE_FOLDERS | 344 AccountCapabilities.SERVER_SEARCH | 345 AccountCapabilities.FOLDER_SERVER_SEARCH | 346 AccountCapabilities.SANITIZED_HTML | 347 AccountCapabilities.SMART_REPLY | 348 AccountCapabilities.SERVER_SEARCH | 349 AccountCapabilities.UNDO | 350 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 351 } else { 352 return AccountCapabilities.SYNCABLE_FOLDERS | 353 AccountCapabilities.SANITIZED_HTML | 354 AccountCapabilities.SMART_REPLY | 355 AccountCapabilities.UNDO | 356 AccountCapabilities.DISCARD_CONVERSATION_DRAFTS; 357 } 358 } 359 360 @Override 361 public void serviceUpdated(final String emailAddress) { 362 // Not required for EAS 363 } 364 365 // All IEmailService messages below are UNCALLED in Email. 366 // TODO: Remove. 367 @Deprecated 368 @Override 369 public int getApiLevel() { 370 return Api.LEVEL; 371 } 372 373 @Deprecated 374 @Override 375 public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {} 376 377 @Deprecated 378 @Override 379 public void stopSync(long mailboxId) {} 380 381 @Deprecated 382 @Override 383 public void loadMore(long messageId) {} 384 385 @Deprecated 386 @Override 387 public boolean createFolder(long accountId, String name) { 388 return false; 389 } 390 391 @Deprecated 392 @Override 393 public boolean deleteFolder(long accountId, String name) { 394 return false; 395 } 396 397 @Deprecated 398 @Override 399 public boolean renameFolder(long accountId, String oldName, String newName) { 400 return false; 401 } 402 403 @Deprecated 404 @Override 405 public void hostChanged(long accountId) {} 406 }; 407 408 public EmailSyncAdapterService() { 409 super(); 410 } 411 412 /** 413 * {@link AsyncTask} for restarting pings for all accounts that need it. 414 */ 415 private static class RestartPingsTask extends AsyncTask<Void, Void, Void> { 416 private static final String PUSH_ACCOUNTS_SELECTION = 417 AccountColumns.SYNC_INTERVAL + "=" + Integer.toString(Account.CHECK_INTERVAL_PUSH); 418 419 private final ContentResolver mContentResolver; 420 private final SyncHandlerSynchronizer mSyncHandlerMap; 421 422 public RestartPingsTask(final ContentResolver contentResolver, 423 final SyncHandlerSynchronizer syncHandlerMap) { 424 mContentResolver = contentResolver; 425 mSyncHandlerMap = syncHandlerMap; 426 } 427 428 @Override 429 protected Void doInBackground(Void... params) { 430 final Cursor c = mContentResolver.query(Account.CONTENT_URI, 431 Account.CONTENT_PROJECTION, PUSH_ACCOUNTS_SELECTION, null, null); 432 if (c != null) { 433 try { 434 while (c.moveToNext()) { 435 final Account account = new Account(); 436 account.restore(c); 437 mSyncHandlerMap.modifyPing(account); 438 } 439 } finally { 440 c.close(); 441 } 442 } 443 return null; 444 } 445 } 446 447 @Override 448 public void onCreate() { 449 super.onCreate(); 450 // Restart push for all accounts that need it. 451 new RestartPingsTask(getContentResolver(), mSyncHandlerMap).executeOnExecutor( 452 AsyncTask.THREAD_POOL_EXECUTOR); 453 } 454 455 @Override 456 public IBinder onBind(Intent intent) { 457 if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) { 458 return mBinder; 459 } 460 return super.onBind(intent); 461 } 462 463 @Override 464 protected AbstractThreadedSyncAdapter newSyncAdapter() { 465 return new SyncAdapterImpl(this); 466 } 467 468 // TODO: Handle cancelSync() appropriately. 469 private class SyncAdapterImpl extends AbstractThreadedSyncAdapter { 470 public SyncAdapterImpl(Context context) { 471 super(context, true /* autoInitialize */); 472 } 473 474 @Override 475 public void onPerformSync(final android.accounts.Account acct, final Bundle extras, 476 final String authority, final ContentProviderClient provider, 477 final SyncResult syncResult) { 478 LogUtils.i(TAG, "performSync: extras = %s", extras.toString()); 479 TempDirectory.setTempDirectory(EmailSyncAdapterService.this); 480 481 // TODO: Perform any connectivity checks, bail early if we don't have proper network 482 // for this sync operation. 483 484 final Context context = getContext(); 485 final ContentResolver cr = context.getContentResolver(); 486 487 // Get the EmailContent Account 488 final Account account; 489 final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, 490 AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null); 491 try { 492 if (!accountCursor.moveToFirst()) { 493 // Could not load account. 494 // TODO: improve error handling. 495 return; 496 } 497 account = new Account(); 498 account.restore(accountCursor); 499 } finally { 500 accountCursor.close(); 501 } 502 // Get the mailbox that we want to sync. 503 // There are four possibilities for Mailbox.SYNC_EXTRA_MAILBOX_ID: 504 // 1) Mailbox.SYNC_EXTRA_MAILBOX_ID_PUSH_ONLY: Restart push if appropriate. 505 // 2) Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY: Sync only the account data. 506 // 3) Not present: Perform a full account sync. 507 // 4) Non-negative value: It's an actual mailbox id, sync that mailbox only. 508 final long mailboxId = extras.getLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, FULL_ACCOUNT_SYNC); 509 510 // If we're just twiddling the push, we do the lightweight thing and just bail. 511 if (mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_PUSH_ONLY) { 512 mSyncHandlerMap.modifyPing(account); 513 return; 514 } 515 516 // Do the bookkeeping for starting a sync, including stopping a ping if necessary. 517 mSyncHandlerMap.startSync(account.mId); 518 519 // TODO: Should we refresh the account here? It may have changed while waiting for any 520 // pings to stop. It may not matter since the things that may have been twiddled might 521 // not affect syncing. 522 523 if (mailboxId == FULL_ACCOUNT_SYNC || 524 mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY) { 525 final EasFolderSync folderSync = new EasFolderSync(context, account); 526 folderSync.doFolderSync(syncResult); 527 528 if (mailboxId == FULL_ACCOUNT_SYNC) { 529 // Full account sync includes all mailboxes that participate in system sync. 530 final Cursor c = Mailbox.getMailboxIdsForSync(cr, account.mId); 531 if (c != null) { 532 try { 533 while (c.moveToNext()) { 534 syncMailbox(context, cr, acct, account, c.getLong(0), extras, 535 syncResult, false); 536 } 537 } finally { 538 c.close(); 539 } 540 } 541 } 542 } else { 543 // Sync the mailbox that was explicitly requested. 544 syncMailbox(context, cr, acct, account, mailboxId, extras, syncResult, true); 545 } 546 547 // Clean up the bookkeeping, including restarting ping if necessary. 548 mSyncHandlerMap.syncComplete(account); 549 550 // TODO: It may make sense to have common error handling here. Two possible mechanisms: 551 // 1) performSync return value can signal some useful info. 552 // 2) syncResult can contain useful info. 553 } 554 555 /** 556 * Update the mailbox's sync status with the provider and, if we're finished with the sync, 557 * write the last sync time as well. 558 * @param context Our {@link Context}. 559 * @param mailbox The mailbox whose sync status to update. 560 * @param cv A {@link ContentValues} object to use for updating the provider. 561 * @param syncStatus The status for the current sync. 562 */ 563 private void updateMailbox(final Context context, final Mailbox mailbox, 564 final ContentValues cv, final int syncStatus) { 565 cv.put(Mailbox.UI_SYNC_STATUS, syncStatus); 566 if (syncStatus == EmailContent.SYNC_STATUS_NONE) { 567 cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis()); 568 } 569 mailbox.update(context, cv); 570 } 571 572 private boolean syncMailbox(final Context context, final ContentResolver cr, 573 final android.accounts.Account acct, final Account account, final long mailboxId, 574 final Bundle extras, final SyncResult syncResult, final boolean isMailboxSync) { 575 final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 576 if (mailbox == null) { 577 return false; 578 } 579 580 final boolean success; 581 // Non-mailbox syncs are whole account syncs initiated by the AccountManager and are 582 // treated as background syncs. 583 // TODO: Push will be treated as "user" syncs, and probably should be background. 584 final ContentValues cv = new ContentValues(2); 585 updateMailbox(context, mailbox, cv, isMailboxSync ? 586 EmailContent.SYNC_STATUS_USER : EmailContent.SYNC_STATUS_BACKGROUND); 587 588 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 589 final EasOutboxSyncHandler outboxSyncHandler = 590 new EasOutboxSyncHandler(context, account, mailbox); 591 outboxSyncHandler.performSync(); 592 success = true; 593 } else if(mailbox.isSyncable()) { 594 final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr, 595 acct, account, mailbox, extras, syncResult); 596 success = (syncHandler != null); 597 if (syncHandler != null) { 598 syncHandler.performSync(); 599 } 600 } else { 601 success = false; 602 } 603 updateMailbox(context, mailbox, cv, EmailContent.SYNC_STATUS_NONE); 604 return success; 605 } 606 } 607} 608