EmailSyncAdapterService.java revision 456a6e3e01205e8c779930d8c7533b1c7467df5e
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.Context; 23import android.content.Intent; 24import android.content.SyncResult; 25import android.database.Cursor; 26import android.os.Bundle; 27import android.os.IBinder; 28 29import com.android.emailcommon.Api; 30import com.android.emailcommon.TempDirectory; 31import com.android.emailcommon.provider.Account; 32import com.android.emailcommon.provider.EmailContent; 33import com.android.emailcommon.provider.EmailContent.AccountColumns; 34import com.android.emailcommon.provider.HostAuth; 35import com.android.emailcommon.provider.Mailbox; 36import com.android.emailcommon.service.EmailServiceStatus; 37import com.android.emailcommon.service.IEmailService; 38import com.android.emailcommon.service.IEmailServiceCallback; 39import com.android.emailcommon.service.SearchParams; 40import com.android.emailcommon.utility.Utility; 41import com.android.exchange.Eas; 42import com.android.exchange.adapter.Search; 43import com.android.mail.providers.UIProvider.AccountCapabilities; 44import com.android.mail.utils.LogUtils; 45 46import java.util.HashMap; 47import java.util.HashSet; 48 49/** 50 * Service for communicating with Exchange servers. There are three main parts of this class: 51 * TODO: Flesh out these comments. 52 * 1) An {@link AbstractThreadedSyncAdapter} to handle actually performing syncs. 53 * 2) Bookkeeping for running Ping requests, which handles push notifications. 54 * 3) An {@link IEmailService} Stub to handle RPC from the UI. 55 */ 56public class EmailSyncAdapterService extends AbstractSyncAdapterService { 57 58 private static final String TAG = "EAS EmailSyncAdaptSvc"; 59 60 /** 61 * If sync extras do not include a mailbox id, then we want to perform a full sync. 62 */ 63 private static final long FULL_ACCOUNT_SYNC = Mailbox.NO_MAILBOX; 64 65 /** 66 * Bookkeeping for handling synchronization between pings and syncs. 67 * "Ping" refers to a hanging POST or GET that is used to receive push notifications. Ping is 68 * the term for the Exchange command, but this code should be generic enough to be easily 69 * extended to IMAP. 70 * "Sync" refers to an actual sync command to either fetch mail state, account state, or send 71 * mail (send is implemented as "sync the outbox"). 72 * TODO: Outbox sync probably need not stop a ping in progress. 73 * Basic rules of how these interact (note that all rules are per account): 74 * - Only one ping or sync may run at a time. 75 * - Due to how {@link AbstractThreadedSyncAdapter} works, sync requests will not occur while 76 * a sync is in progress. 77 * - On the other hand, ping requests may come in while handling a ping. 78 * - "Ping request" is shorthand for "a request to change our ping parameters", which includes 79 * a request to stop receiving push notifications. 80 * - If neither a ping nor a sync is running, then a request for either will run it. 81 * - If a sync is running, new ping requests block until the sync completes. 82 * - If a ping is running, a new sync request stops the ping and creates a pending ping 83 * (which blocks until the sync completes). 84 * - If a ping is running, a new ping request stops the ping and either starts a new one or 85 * does nothing, as appopriate (since a ping request can be to stop pushing). 86 * - As an optimization, while a ping request is waiting to run, subsequent ping requests are 87 * ignored (the pending ping will pick up the latest ping parameters at the time it runs). 88 */ 89 public class SyncHandlerSynchronizer { 90 /** 91 * Map of account id -> ping handler. 92 * For a given account id, there are three possible states: 93 * 1) If no ping or sync is currently running, there is no entry in the map for the account. 94 * 2) If a ping is running, there is an entry with the appropriate ping handler. 95 * 3) If there is a sync running, there is an entry with null as the value. 96 * We cannot have more than one ping or sync running at a time. 97 */ 98 private final HashMap<Long, EasPingSyncHandler> mPingHandlers = 99 new HashMap<Long, EasPingSyncHandler>(); 100 101 /** 102 * Set of all accounts that are in the middle of processing a ping modification. This is 103 * used to ignore duplicate modification requests. 104 */ 105 private final HashSet<Long> mPendingPings = new HashSet<Long>(); 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 EasPingSyncHandler pingHandler = mPingHandlers.get(accountId); 115 if (pingHandler != null) { 116 pingHandler.stop(); 117 } 118 try { 119 wait(); 120 } catch (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 private void stopServiceIfNoPings() { 136 for (final EasPingSyncHandler pingHandler : mPingHandlers.values()) { 137 if (pingHandler != null) { 138 return; 139 } 140 } 141 EmailSyncAdapterService.this.stopSelf(); 142 } 143 144 /** 145 * Called prior to starting a sync to update our state. 146 * @param accountId The account on which we are running a sync. 147 */ 148 public synchronized void startSync(final long accountId) { 149 waitUntilNoActivity(accountId); 150 mPingHandlers.put(accountId, null); 151 } 152 153 /** 154 * Called prior to starting, stopping, or changing a ping for reasons other than a sync 155 * request (e.g. new account added, settings change, or app startup). This is currently 156 * implemented as shutting down any running ping and starting a new one if needed. It might 157 * be better to signal any running ping to reload itself, but this is simpler for now. 158 * @param accountId The account whose ping is being modified. 159 */ 160 public synchronized void modifyPing(final long accountId) { 161 // If a sync is currently running, we'd have to wait for it complete, but it'll call 162 // modifyPing at that point anyway. Therefore we can ignore this request. 163 if (isRunningSync(accountId)) { 164 return; 165 } 166 167 // Similarly, if multiple ping requests happen while a ping is running, we can ignore 168 // all but one of them -- by the time the first one is done waiting, it'll pick up the 169 // latest account settings anyway. 170 if (mPendingPings.contains(accountId)) { 171 return; 172 } 173 mPendingPings.add(accountId); 174 175 try { 176 // TODO: If a ping is running, it'd be better to just tell it to reload its state 177 // rather than kill and restart it. 178 waitUntilNoActivity(accountId); 179 final Context context = EmailSyncAdapterService.this; 180 // No ping or sync running. Figure out whether a ping is needed, and if so with 181 // what params. 182 final Account account = Account.restoreAccountWithId(context, accountId); 183 if (account == null || account.mSyncInterval != Account.CHECK_INTERVAL_PUSH) { 184 // A ping that was running is no longer running, or something happened to the 185 // account. 186 stopServiceIfNoPings(); 187 } else { 188 // Note: unlike startSync, we CANNOT allow the caller to do the actual work. 189 // If we return before the ping starts, there's a race condition where another 190 // ping or sync might start first. It only works for startSync because sync is 191 // higher priority than ping (i.e. a ping can't start while a sync is pending) 192 // and only one ping can run at a time. 193 final EasPingSyncHandler pingHandler = 194 new EasPingSyncHandler(context, account, this); 195 mPingHandlers.put(accountId, pingHandler); 196 // Whenever we have a running ping, make sure this service stays running. 197 final EmailSyncAdapterService service = EmailSyncAdapterService.this; 198 service.startService(new Intent(service, EmailSyncAdapterService.class)); 199 } 200 } finally { 201 mPendingPings.remove(accountId); 202 } 203 } 204 205 /** 206 * All operations must call this when they complete to update the synchronization 207 * bookkeeping. 208 * @param accountId The account whose ping or sync just completed. 209 * @param wasSync Whether the operation that's completing was a sync. 210 * @param notify Whether to notify all threads waiting on this object. This should be true 211 * for all sync operations, and for any pings that were interrupted. Pings that complete 212 * naturally possibly don't need to wake up anyone else. 213 * TODO: is this optimization worth any possible problem? For example, the syncs started 214 * by a ping may need to be signaled here. 215 */ 216 public synchronized void signalDone(final long accountId, final boolean wasSync, 217 final boolean notify) { 218 mPingHandlers.remove(accountId); 219 // If this was a sync, we may have killed a ping that now needs to be restarted. 220 // modifyPing will do the appropriate checks. 221 // We do this here rather than at the caller because at this point, we are guaranteed 222 // that there is no entry for this account in mPingHandlers, and therefore we cannot 223 // block. 224 if (wasSync) { 225 modifyPing(accountId); 226 } else { 227 // A ping stopped, so check if we should stop the service. 228 stopServiceIfNoPings(); 229 } 230 231 // Similarly, it's ok to notify after we restart the ping, because we know the ping 232 // can't possibly be waiting. 233 if (notify) { 234 notifyAll(); 235 } 236 } 237 } 238 private final SyncHandlerSynchronizer mSyncHandlerMap = new SyncHandlerSynchronizer(); 239 240 /** 241 * The binder for IEmailService. 242 */ 243 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 244 @Override 245 public Bundle validate(final HostAuth hostAuth) { 246 LogUtils.d(TAG, "IEmailService.validate"); 247 return new EasAccountValidator(EmailSyncAdapterService.this, hostAuth).validate(); 248 } 249 250 @Override 251 public Bundle autoDiscover(final String username, final String password) { 252 LogUtils.d(TAG, "IEmailService.autoDiscover"); 253 return new EasAutoDiscover(EmailSyncAdapterService.this, username, password) 254 .doAutodiscover(); 255 } 256 257 @Override 258 public void updateFolderList(final long accountId) { 259 LogUtils.d(TAG, "IEmailService.updateFolderList"); 260 final String emailAddress = Utility.getFirstRowString(EmailSyncAdapterService.this, 261 Account.CONTENT_URI, new String[] {AccountColumns.EMAIL_ADDRESS}, 262 Account.ID_SELECTION, new String[] {Long.toString(accountId)}, null, 0); 263 if (emailAddress != null) { 264 ContentResolver.requestSync(new android.accounts.Account( 265 emailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), 266 EmailContent.AUTHORITY, new Bundle()); 267 } 268 } 269 270 @Override 271 public void setCallback(final IEmailServiceCallback cb) { 272 // TODO: Determine if this is ever called in practice. 273 //mCallbackList.register(cb); 274 } 275 276 @Override 277 public void setLogging(final int flags) { 278 // TODO: fix this? 279 // Protocol logging 280 Eas.setUserDebug(flags); 281 // Sync logging 282 //setUserDebug(flags); 283 } 284 285 @Override 286 public void loadAttachment(final long attachmentId, final boolean background) { 287 LogUtils.d(TAG, "IEmailService.loadAttachment"); 288 // TODO: Implement. 289 /* 290 Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId); 291 log("loadAttachment " + attachmentId + ": " + att.mFileName); 292 sendMessageRequest(new PartRequest(att, null, null)); 293 */ 294 } 295 296 @Override 297 public void sendMeetingResponse(final long messageId, final int response) { 298 LogUtils.d(TAG, "IEmailService.sendMeetingResponse"); 299 // TODO: Implement. 300 //sendMessageRequest(new MeetingResponseRequest(messageId, response)); 301 } 302 303 /** 304 * Delete PIM (calendar, contacts) data for the specified account 305 * 306 * @param accountId the account whose data should be deleted 307 */ 308 @Override 309 public void deleteAccountPIMData(final long accountId) { 310 LogUtils.d(TAG, "IEmailService.deleteAccountPIMData"); 311 // TODO: Implement 312 /* 313 SyncManager exchangeService = INSTANCE; 314 if (exchangeService == null) return; 315 // Stop any running syncs 316 ExchangeService.stopAccountSyncs(accountId); 317 // Delete the data 318 ExchangeService.deleteAccountPIMData(ExchangeService.this, accountId); 319 long accountMailboxId = Mailbox.findMailboxOfType(exchangeService, accountId, 320 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX); 321 if (accountMailboxId != Mailbox.NO_MAILBOX) { 322 // Make sure the account mailbox is held due to security 323 synchronized(sSyncLock) { 324 mSyncErrorMap.put(accountMailboxId, exchangeService.new SyncError( 325 AbstractSyncService.EXIT_SECURITY_FAILURE, false)); 326 327 } 328 } 329 // Make sure the reconciler runs 330 runAccountReconcilerSync(ExchangeService.this); 331 */ 332 } 333 334 @Override 335 public int searchMessages(final long accountId, final SearchParams searchParams, 336 final long destMailboxId) { 337 LogUtils.d(TAG, "IEmailService.searchMessages"); 338 return Search.searchMessages(EmailSyncAdapterService.this, accountId, searchParams, 339 destMailboxId); 340 341 } 342 343 @Override 344 public void sendMail(final long accountId) {} 345 346 @Override 347 public int getCapabilities(final Account acct) { 348 String easVersion = acct.mProtocolVersion; 349 Double easVersionDouble = 2.5D; 350 if (easVersion != null) { 351 try { 352 easVersionDouble = Double.parseDouble(easVersion); 353 } catch (NumberFormatException e) { 354 // Stick with 2.5 355 } 356 } 357 if (easVersionDouble >= 12.0D) { 358 return AccountCapabilities.SYNCABLE_FOLDERS | 359 AccountCapabilities.SERVER_SEARCH | 360 AccountCapabilities.FOLDER_SERVER_SEARCH | 361 AccountCapabilities.SANITIZED_HTML | 362 AccountCapabilities.SMART_REPLY | 363 AccountCapabilities.SERVER_SEARCH | 364 AccountCapabilities.UNDO; 365 } else { 366 return AccountCapabilities.SYNCABLE_FOLDERS | 367 AccountCapabilities.SANITIZED_HTML | 368 AccountCapabilities.SMART_REPLY | 369 AccountCapabilities.UNDO; 370 } 371 } 372 373 @Override 374 public void serviceUpdated(final String emailAddress) { 375 // Not required for EAS 376 } 377 378 // All IEmailService messages below are UNCALLED in Email. 379 // TODO: Remove. 380 @Deprecated 381 @Override 382 public int getApiLevel() { 383 return Api.LEVEL; 384 } 385 386 @Deprecated 387 @Override 388 public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount) {} 389 390 @Deprecated 391 @Override 392 public void stopSync(long mailboxId) {} 393 394 @Deprecated 395 @Override 396 public void loadMore(long messageId) {} 397 398 @Deprecated 399 @Override 400 public boolean createFolder(long accountId, String name) { 401 return false; 402 } 403 404 @Deprecated 405 @Override 406 public boolean deleteFolder(long accountId, String name) { 407 return false; 408 } 409 410 @Deprecated 411 @Override 412 public boolean renameFolder(long accountId, String oldName, String newName) { 413 return false; 414 } 415 416 @Deprecated 417 @Override 418 public void hostChanged(long accountId) {} 419 }; 420 421 public EmailSyncAdapterService() { 422 super(); 423 } 424 425 @Override 426 public IBinder onBind(Intent intent) { 427 if (intent.getAction().equals(Eas.EXCHANGE_SERVICE_INTENT_ACTION)) { 428 return mBinder; 429 } 430 return super.onBind(intent); 431 } 432 433 @Override 434 protected AbstractThreadedSyncAdapter newSyncAdapter() { 435 return new SyncAdapterImpl(this); 436 } 437 438 // TODO: Handle cancelSync() appropriately. 439 private class SyncAdapterImpl extends AbstractThreadedSyncAdapter { 440 public SyncAdapterImpl(Context context) { 441 super(context, true /* autoInitialize */); 442 } 443 444 @Override 445 public void onPerformSync(final android.accounts.Account acct, final Bundle extras, 446 final String authority, final ContentProviderClient provider, 447 final SyncResult syncResult) { 448 LogUtils.i(TAG, "performSync: extras = %s", extras.toString()); 449 TempDirectory.setTempDirectory(EmailSyncAdapterService.this); 450 451 // TODO: Perform any connectivity checks, bail early if we don't have proper network 452 // for this sync operation. 453 454 final Context context = getContext(); 455 final ContentResolver cr = context.getContentResolver(); 456 457 // Get the EmailContent Account 458 final Account account; 459 final Cursor accountCursor = cr.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, 460 AccountColumns.EMAIL_ADDRESS + "=?", new String[] {acct.name}, null); 461 try { 462 if (!accountCursor.moveToFirst()) { 463 // Could not load account. 464 // TODO: improve error handling. 465 return; 466 } 467 account = new Account(); 468 account.restore(accountCursor); 469 } finally { 470 accountCursor.close(); 471 } 472 473 // Do the bookkeeping for starting a sync, including stopping a ping if necessary. 474 mSyncHandlerMap.startSync(account.mId); 475 476 // TODO: Should we refresh the account here? It may have changed while waiting for any 477 // pings to stop. It may not matter since the things that may have been twiddled might 478 // not affect syncing. 479 480 // There are three possibilities for Mailbox.SYNC_EXTRA_MAILBOX_ID: 481 // 1) It's Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY. Sync only the account data. 482 // 2) It's not present. Perform a full account sync. 483 // 3) It's a mailbox id (non-negative value). Sync that mailbox only. 484 final long mailboxId = extras.getLong(Mailbox.SYNC_EXTRA_MAILBOX_ID, FULL_ACCOUNT_SYNC); 485 if (mailboxId == FULL_ACCOUNT_SYNC || 486 mailboxId == Mailbox.SYNC_EXTRA_MAILBOX_ID_ACCOUNT_ONLY) { 487 final EasAccountSyncHandler accountSyncHandler = 488 new EasAccountSyncHandler(context, account); 489 accountSyncHandler.performSync(); 490 491 if (mailboxId == Mailbox.NO_MAILBOX) { 492 // Full account sync includes all mailboxes that participate in system sync. 493 final Cursor c = Mailbox.getMailboxIdsForSync(cr, account.mId); 494 if (c != null) { 495 try { 496 while (c.moveToNext()) { 497 syncMailbox(context, cr, acct, account, c.getLong(0), extras, 498 syncResult); 499 } 500 } finally { 501 c.close(); 502 } 503 } 504 } 505 } else { 506 // Sync the mailbox that was explicitly requested. 507 if (!syncMailbox(context, cr, acct, account, mailboxId, extras, syncResult)) { 508 // We can't sync this mailbox, so just send the expected UI callbacks. 509 EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId, 510 EmailServiceStatus.IN_PROGRESS, 0); 511 EmailServiceStatus.syncMailboxStatus(cr, extras, mailboxId, 512 EmailServiceStatus.SUCCESS, 0); 513 } 514 } 515 516 // Signal any waiting ping that it's good to go now. 517 mSyncHandlerMap.signalDone(account.mId, true, true); 518 519 // TODO: It may make sense to have common error handling here. Two possible mechanisms: 520 // 1) performSync return value can signal some useful info. 521 // 2) syncResult can contain useful info. 522 } 523 524 private boolean syncMailbox(final Context context, final ContentResolver cr, 525 final android.accounts.Account acct, final Account account, final long mailboxId, 526 final Bundle extras, final SyncResult syncResult) { 527 final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); 528 if (mailbox == null) { 529 return false; 530 } 531 532 if (mailbox.mType == Mailbox.TYPE_OUTBOX) { 533 final EasOutboxSyncHandler outboxSyncHandler = 534 new EasOutboxSyncHandler(context, account, mailbox); 535 outboxSyncHandler.performSync(); 536 } else { 537 final EasSyncHandler syncHandler = EasSyncHandler.getEasSyncHandler(context, cr, 538 acct, account, mailbox, extras, syncResult); 539 if (syncHandler == null) { 540 return false; 541 } 542 syncHandler.performSync(); 543 } 544 return true; 545 } 546 } 547} 548