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 */ waitUntilNoActivity(final long accountId)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 */ isRunningSync(final long accountId)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 */ stopServiceIfNoPings()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 */ startSync(final long accountId)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 */ modifyPing(final boolean lastSyncHadError, final Account account)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 */ syncComplete(final boolean lastSyncHadError, final Account account)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 */ pingComplete(final android.accounts.Account amAccount, final long accountId, final int pingStatus)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 EmailSyncAdapterService()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 RestartPingsTask(final ContentResolver contentResolver, final SyncHandlerSynchronizer syncHandlerMap)518 public RestartPingsTask(final ContentResolver contentResolver, 519 final SyncHandlerSynchronizer syncHandlerMap) { 520 mContentResolver = contentResolver; 521 mSyncHandlerMap = syncHandlerMap; 522 } 523 524 @Override doInBackground(Void... params)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 onPostExecute(Void result)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 onCreate()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 onDestroy()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 onBind(Intent intent)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 onStartCommand(Intent intent, int flags, int startId)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 getSyncAdapter()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 { SyncAdapterImpl(Context context)638 public SyncAdapterImpl(Context context) { 639 super(context, true /* autoInitialize */); 640 } 641 642 @Override onPerformSync(final android.accounts.Account acct, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult)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 */ updateMailbox(final Context context, final Mailbox mailbox, final ContentValues cv, final int syncStatus)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 syncMailbox(final Context context, final ContentResolver cr, final android.accounts.Account acct, final Account account, final long mailboxId, final Bundle extras, final SyncResult syncResult, final HashSet<String> authsToSync, final boolean isMailboxSync)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 syncOutbox(Context context, ContentResolver cr, Account account, Mailbox mailbox)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 showAuthNotification(long accountId, String accountName)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 */ createAccountSettingsIntent(long accountId, String accountName)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 */ getAuthsToSync(final android.accounts.Account account)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 */ scheduleDelayedPing(final android.accounts.Account amAccount, final long delay)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