1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange; 19 20import android.app.AlarmManager; 21import android.app.PendingIntent; 22import android.app.Service; 23import android.content.BroadcastReceiver; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.database.ContentObserver; 31import android.database.Cursor; 32import android.net.ConnectivityManager; 33import android.net.NetworkInfo; 34import android.net.NetworkInfo.State; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.Debug; 38import android.os.Handler; 39import android.os.IBinder; 40import android.os.PowerManager; 41import android.os.PowerManager.WakeLock; 42import android.os.Process; 43import android.os.RemoteCallbackList; 44import android.os.RemoteException; 45import android.provider.CalendarContract; 46import android.provider.CalendarContract.Calendars; 47import android.provider.CalendarContract.Events; 48import android.provider.ContactsContract; 49import android.util.Log; 50 51import com.android.emailcommon.Api; 52import com.android.emailcommon.TempDirectory; 53import com.android.emailcommon.provider.Account; 54import com.android.emailcommon.provider.EmailContent; 55import com.android.emailcommon.provider.EmailContent.Attachment; 56import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 57import com.android.emailcommon.provider.EmailContent.MailboxColumns; 58import com.android.emailcommon.provider.EmailContent.Message; 59import com.android.emailcommon.provider.EmailContent.SyncColumns; 60import com.android.emailcommon.provider.HostAuth; 61import com.android.emailcommon.provider.Mailbox; 62import com.android.emailcommon.provider.Policy; 63import com.android.emailcommon.provider.ProviderUnavailableException; 64import com.android.emailcommon.service.AccountServiceProxy; 65import com.android.emailcommon.service.EmailServiceProxy; 66import com.android.emailcommon.service.EmailServiceStatus; 67import com.android.emailcommon.service.IEmailService; 68import com.android.emailcommon.service.IEmailServiceCallback; 69import com.android.emailcommon.service.PolicyServiceProxy; 70import com.android.emailcommon.service.SearchParams; 71import com.android.emailcommon.utility.EmailAsyncTask; 72import com.android.emailcommon.utility.EmailClientConnectionManager; 73import com.android.emailcommon.utility.Utility; 74import com.android.exchange.adapter.CalendarSyncAdapter; 75import com.android.exchange.adapter.ContactsSyncAdapter; 76import com.android.exchange.adapter.Search; 77import com.android.exchange.provider.MailboxUtilities; 78import com.android.exchange.utility.FileLogger; 79 80import org.apache.http.conn.params.ConnManagerPNames; 81import org.apache.http.conn.params.ConnPerRoute; 82import org.apache.http.conn.routing.HttpRoute; 83import org.apache.http.params.BasicHttpParams; 84import org.apache.http.params.HttpParams; 85 86import java.io.IOException; 87import java.util.ArrayList; 88import java.util.HashMap; 89import java.util.List; 90import java.util.concurrent.ConcurrentHashMap; 91 92/** 93 * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync 94 * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it 95 * would be appropriate to use for IMAP push, when that functionality is added to the Email 96 * application. 97 * 98 * The Email application communicates with EAS sync adapters via ExchangeService's binder interface, 99 * which exposes UI-related functionality to the application (see the definitions below) 100 * 101 * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in 102 * order to maintain proper 2-way syncing of data. (More documentation to follow) 103 * 104 */ 105public class ExchangeService extends Service implements Runnable { 106 107 private static final String TAG = "ExchangeService"; 108 109 // The ExchangeService's mailbox "id" 110 public static final int EXTRA_MAILBOX_ID = -1; 111 public static final int EXCHANGE_SERVICE_MAILBOX_ID = 0; 112 113 private static final int SECONDS = 1000; 114 private static final int MINUTES = 60*SECONDS; 115 private static final int ONE_DAY_MINUTES = 1440; 116 117 private static final int EXCHANGE_SERVICE_HEARTBEAT_TIME = 15*MINUTES; 118 private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES; 119 120 // Sync hold constants for services with transient errors 121 private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES; 122 123 // Reason codes when ExchangeService.kick is called (mainly for debugging) 124 // UI has changed data, requiring an upsync of changes 125 public static final int SYNC_UPSYNC = 0; 126 // A scheduled sync (when not using push) 127 public static final int SYNC_SCHEDULED = 1; 128 // Mailbox was marked push 129 public static final int SYNC_PUSH = 2; 130 // A ping (EAS push signal) was received 131 public static final int SYNC_PING = 3; 132 // Misc. 133 public static final int SYNC_KICK = 4; 134 // A part request (attachment load, for now) was sent to ExchangeService 135 public static final int SYNC_SERVICE_PART_REQUEST = 5; 136 137 // Requests >= SYNC_CALLBACK_START generate callbacks to the UI 138 public static final int SYNC_CALLBACK_START = 6; 139 // startSync was requested of ExchangeService (other than due to user request) 140 public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0; 141 // startSync was requested of ExchangeService (due to user request) 142 public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1; 143 144 private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX = 145 MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" + 146 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL + 147 " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')'; 148 protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE = 149 MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ',' 150 + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ',' 151 + Mailbox.TYPE_CALENDAR + ')'; 152 protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX = 153 MailboxColumns.ACCOUNT_KEY + "=? and type = " + Mailbox.TYPE_INBOX ; 154 private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?"; 155 private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" + 156 AbstractSyncService.EAS_PROTOCOL + "\""; 157 private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN = 158 "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX 159 + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')' 160 + " and " + MailboxColumns.ACCOUNT_KEY + " in ("; 161 private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in ("; 162 private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?"; 163 164 // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count 165 // The format is S<type_char>:<exit_char>:<change_count> 166 public static final int STATUS_TYPE_CHAR = 1; 167 public static final int STATUS_EXIT_CHAR = 3; 168 public static final int STATUS_CHANGE_COUNT_OFFSET = 5; 169 170 // Ready for ping 171 public static final int PING_STATUS_OK = 0; 172 // Service already running (can't ping) 173 public static final int PING_STATUS_RUNNING = 1; 174 // Service waiting after I/O error (can't ping) 175 public static final int PING_STATUS_WAITING = 2; 176 // Service had a fatal error; can't run 177 public static final int PING_STATUS_UNABLE = 3; 178 179 private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1; 180 181 // We synchronize on this for all actions affecting the service and error maps 182 private static final Object sSyncLock = new Object(); 183 // All threads can use this lock to wait for connectivity 184 public static final Object sConnectivityLock = new Object(); 185 public static boolean sConnectivityHold = false; 186 187 // Keeps track of running services (by mailbox id) 188 private final HashMap<Long, AbstractSyncService> mServiceMap = 189 new HashMap<Long, AbstractSyncService>(); 190 // Keeps track of services whose last sync ended with an error (by mailbox id) 191 /*package*/ ConcurrentHashMap<Long, SyncError> mSyncErrorMap = 192 new ConcurrentHashMap<Long, SyncError>(); 193 // Keeps track of which services require a wake lock (by mailbox id) 194 private final HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>(); 195 // Keeps track of PendingIntents for mailbox alarms (by mailbox id) 196 private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>(); 197 // The actual WakeLock obtained by ExchangeService 198 private WakeLock mWakeLock = null; 199 // Keep our cached list of active Accounts here 200 public final AccountList mAccountList = new AccountList(); 201 202 // Observers that we use to look for changed mail-related data 203 private final Handler mHandler = new Handler(); 204 private AccountObserver mAccountObserver; 205 private MailboxObserver mMailboxObserver; 206 private SyncedMessageObserver mSyncedMessageObserver; 207 208 // Concurrent because CalendarSyncAdapter can modify the map during a wipe 209 private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers = 210 new ConcurrentHashMap<Long, CalendarObserver>(); 211 212 private ContentResolver mResolver; 213 214 // The singleton ExchangeService object, with its thread and stop flag 215 protected static ExchangeService INSTANCE; 216 private static Thread sServiceThread = null; 217 // Cached unique device id 218 private static String sDeviceId = null; 219 // ConnectionManager that all EAS threads can use 220 private static EmailClientConnectionManager sClientConnectionManager = null; 221 // Count of ClientConnectionManager shutdowns 222 private static volatile int sClientConnectionManagerShutdownCount = 0; 223 224 private static volatile boolean sStartingUp = false; 225 private static volatile boolean sStop = false; 226 227 // The reason for ExchangeService's next wakeup call 228 private String mNextWaitReason; 229 // Whether we have an unsatisfied "kick" pending 230 private boolean mKicked = false; 231 232 // Receiver of connectivity broadcasts 233 private ConnectivityReceiver mConnectivityReceiver = null; 234 private ConnectivityReceiver mBackgroundDataSettingReceiver = null; 235 private volatile boolean mBackgroundData = true; 236 // The most current NetworkInfo (from ConnectivityManager) 237 private NetworkInfo mNetworkInfo; 238 239 // Callbacks as set up via setCallback 240 private final RemoteCallbackList<IEmailServiceCallback> mCallbackList = 241 new RemoteCallbackList<IEmailServiceCallback>(); 242 243 private interface ServiceCallbackWrapper { 244 public void call(IEmailServiceCallback cb) throws RemoteException; 245 } 246 247 /** 248 * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system 249 * Used this way: ExchangeService.callback().callbackMethod(args...); 250 * The proxy wraps checking for existence of a ExchangeService instance 251 * Failures of these callbacks can be safely ignored. 252 */ 253 static private final IEmailServiceCallback.Stub sCallbackProxy = 254 new IEmailServiceCallback.Stub() { 255 256 /** 257 * Broadcast a callback to the everyone that's registered 258 * 259 * @param wrapper the ServiceCallbackWrapper used in the broadcast 260 */ 261 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 262 RemoteCallbackList<IEmailServiceCallback> callbackList = 263 (INSTANCE == null) ? null: INSTANCE.mCallbackList; 264 if (callbackList != null) { 265 // Call everyone on our callback list 266 int count = callbackList.beginBroadcast(); 267 try { 268 for (int i = 0; i < count; i++) { 269 try { 270 wrapper.call(callbackList.getBroadcastItem(i)); 271 } catch (RemoteException e) { 272 // Safe to ignore 273 } catch (RuntimeException e) { 274 // We don't want an exception in one call to prevent other calls, so 275 // we'll just log this and continue 276 Log.e(TAG, "Caught RuntimeException in broadcast", e); 277 } 278 } 279 } finally { 280 // No matter what, we need to finish the broadcast 281 callbackList.finishBroadcast(); 282 } 283 } 284 } 285 286 public void loadAttachmentStatus(final long messageId, final long attachmentId, 287 final int status, final int progress) { 288 broadcastCallback(new ServiceCallbackWrapper() { 289 @Override 290 public void call(IEmailServiceCallback cb) throws RemoteException { 291 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 292 } 293 }); 294 } 295 296 public void sendMessageStatus(final long messageId, final String subject, final int status, 297 final int progress) { 298 broadcastCallback(new ServiceCallbackWrapper() { 299 @Override 300 public void call(IEmailServiceCallback cb) throws RemoteException { 301 cb.sendMessageStatus(messageId, subject, status, progress); 302 } 303 }); 304 } 305 306 public void syncMailboxListStatus(final long accountId, final int status, 307 final int progress) { 308 broadcastCallback(new ServiceCallbackWrapper() { 309 @Override 310 public void call(IEmailServiceCallback cb) throws RemoteException { 311 cb.syncMailboxListStatus(accountId, status, progress); 312 } 313 }); 314 } 315 316 public void syncMailboxStatus(final long mailboxId, final int status, 317 final int progress) { 318 broadcastCallback(new ServiceCallbackWrapper() { 319 @Override 320 public void call(IEmailServiceCallback cb) throws RemoteException { 321 cb.syncMailboxStatus(mailboxId, status, progress); 322 } 323 }); 324 } 325 }; 326 327 /** 328 * Create our EmailService implementation here. 329 */ 330 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 331 332 public int getApiLevel() { 333 return Api.LEVEL; 334 } 335 336 public Bundle validate(HostAuth hostAuth) throws RemoteException { 337 return AbstractSyncService.validate(EasSyncService.class, 338 hostAuth, ExchangeService.this); 339 } 340 341 public Bundle autoDiscover(String userName, String password) throws RemoteException { 342 return new EasSyncService().tryAutodiscover(userName, password); 343 } 344 345 public void startSync(long mailboxId, boolean userRequest) throws RemoteException { 346 ExchangeService exchangeService = INSTANCE; 347 if (exchangeService == null) return; 348 checkExchangeServiceServiceRunning(); 349 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 350 if (m == null) return; 351 Account acct = Account.restoreAccountWithId(exchangeService, m.mAccountKey); 352 if (acct == null) return; 353 // If this is a user request and we're being held, release the hold; this allows us to 354 // try again (the hold might have been specific to this account and released already) 355 if (userRequest) { 356 if (onSyncDisabledHold(acct)) { 357 releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_ACCESS_DENIED, acct); 358 log("User requested sync of account in sync disabled hold; releasing"); 359 } else if (onSecurityHold(acct)) { 360 releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_SECURITY_FAILURE, 361 acct); 362 log("User requested sync of account in security hold; releasing"); 363 } 364 if (sConnectivityHold) { 365 try { 366 // UI is expecting the callbacks.... 367 sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 368 0); 369 sCallbackProxy.syncMailboxStatus(mailboxId, 370 EmailServiceStatus.CONNECTION_ERROR, 0); 371 } catch (RemoteException ignore) { 372 } 373 return; 374 } 375 } 376 if (m.mType == Mailbox.TYPE_OUTBOX) { 377 // We're using SERVER_ID to indicate an error condition (it has no other use for 378 // sent mail) Upon request to sync the Outbox, we clear this so that all messages 379 // are candidates for sending. 380 ContentValues cv = new ContentValues(); 381 cv.put(SyncColumns.SERVER_ID, 0); 382 exchangeService.getContentResolver().update(Message.CONTENT_URI, 383 cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)}); 384 // Clear the error state; the Outbox sync will be started from checkMailboxes 385 exchangeService.mSyncErrorMap.remove(mailboxId); 386 kick("start outbox"); 387 // Outbox can't be synced in EAS 388 return; 389 } else if (!isSyncable(m)) { 390 try { 391 // UI may be expecting the callbacks, so send them 392 sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 0); 393 sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.SUCCESS, 0); 394 } catch (RemoteException ignore) { 395 // We tried 396 } 397 return; 398 } 399 startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST : 400 ExchangeService.SYNC_SERVICE_START_SYNC, null); 401 } 402 403 public void stopSync(long mailboxId) throws RemoteException { 404 stopManualSync(mailboxId); 405 } 406 407 public void loadAttachment(long attachmentId, boolean background) throws RemoteException { 408 Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId); 409 log("loadAttachment " + attachmentId + ": " + att.mFileName); 410 sendMessageRequest(new PartRequest(att, null, null)); 411 } 412 413 public void updateFolderList(long accountId) throws RemoteException { 414 reloadFolderList(ExchangeService.this, accountId, false); 415 } 416 417 public void hostChanged(long accountId) throws RemoteException { 418 ExchangeService exchangeService = INSTANCE; 419 if (exchangeService == null) return; 420 ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap; 421 // Go through the various error mailboxes 422 for (long mailboxId: syncErrorMap.keySet()) { 423 SyncError error = syncErrorMap.get(mailboxId); 424 // If it's a login failure, look a little harder 425 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 426 // If it's for the account whose host has changed, clear the error 427 // If the mailbox is no longer around, remove the entry in the map 428 if (m == null) { 429 syncErrorMap.remove(mailboxId); 430 } else if (error != null && m.mAccountKey == accountId) { 431 error.fatal = false; 432 error.holdEndTime = 0; 433 } 434 } 435 // Stop any running syncs 436 exchangeService.stopAccountSyncs(accountId, true); 437 // Kick ExchangeService 438 kick("host changed"); 439 } 440 441 public void setLogging(int flags) throws RemoteException { 442 Eas.setUserDebug(flags); 443 } 444 445 public void sendMeetingResponse(long messageId, int response) throws RemoteException { 446 sendMessageRequest(new MeetingResponseRequest(messageId, response)); 447 } 448 449 public void loadMore(long messageId) throws RemoteException { 450 } 451 452 // The following three methods are not implemented in this version 453 public boolean createFolder(long accountId, String name) throws RemoteException { 454 return false; 455 } 456 457 public boolean deleteFolder(long accountId, String name) throws RemoteException { 458 return false; 459 } 460 461 public boolean renameFolder(long accountId, String oldName, String newName) 462 throws RemoteException { 463 return false; 464 } 465 466 public void setCallback(IEmailServiceCallback cb) throws RemoteException { 467 mCallbackList.register(cb); 468 } 469 470 /** 471 * Delete PIM (calendar, contacts) data for the specified account 472 * 473 * @param accountId the account whose data should be deleted 474 * @throws RemoteException 475 */ 476 public void deleteAccountPIMData(long accountId) throws RemoteException { 477 // Stop any running syncs 478 ExchangeService.stopAccountSyncs(accountId); 479 // Delete the data 480 ExchangeService.deleteAccountPIMData(accountId); 481 } 482 483 public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { 484 ExchangeService exchangeService = INSTANCE; 485 if (exchangeService == null) return 0; 486 return Search.searchMessages(exchangeService, accountId, searchParams, 487 destMailboxId); 488 } 489 }; 490 491 /** 492 * Return a list of all Accounts in EmailProvider. Because the result of this call may be used 493 * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate 494 * @param context the caller's context 495 * @param accounts a list that Accounts will be added into 496 * @return the list of Accounts 497 * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid 498 */ 499 private static AccountList collectEasAccounts(Context context, AccountList accounts) { 500 ContentResolver resolver = context.getContentResolver(); 501 Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, 502 null); 503 // We must throw here; callers might use the information we provide for reconciliation, etc. 504 if (c == null) throw new ProviderUnavailableException(); 505 try { 506 ContentValues cv = new ContentValues(); 507 while (c.moveToNext()) { 508 long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN); 509 if (hostAuthId > 0) { 510 HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId); 511 if (ha != null && ha.mProtocol.equals("eas")) { 512 Account account = new Account(); 513 account.restore(c); 514 // Cache the HostAuth 515 account.mHostAuthRecv = ha; 516 accounts.add(account); 517 // Fixup flags for inbox (should accept moved mail) 518 Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId, 519 Mailbox.TYPE_INBOX); 520 if (inbox != null && 521 ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) { 522 cv.put(MailboxColumns.FLAGS, 523 inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL); 524 resolver.update( 525 ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv, 526 null, null); 527 } 528 } 529 } 530 } 531 } finally { 532 c.close(); 533 } 534 return accounts; 535 } 536 537 static class AccountList extends ArrayList<Account> { 538 private static final long serialVersionUID = 1L; 539 540 @Override 541 public boolean add(Account account) { 542 // Cache the account manager account 543 account.mAmAccount = new android.accounts.Account(account.mEmailAddress, 544 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 545 super.add(account); 546 return true; 547 } 548 549 public boolean contains(long id) { 550 for (Account account : this) { 551 if (account.mId == id) { 552 return true; 553 } 554 } 555 return false; 556 } 557 558 public Account getById(long id) { 559 for (Account account : this) { 560 if (account.mId == id) { 561 return account; 562 } 563 } 564 return null; 565 } 566 567 public Account getByName(String accountName) { 568 for (Account account : this) { 569 if (account.mEmailAddress.equalsIgnoreCase(accountName)) { 570 return account; 571 } 572 } 573 return null; 574 } 575 } 576 577 public static void deleteAccountPIMData(long accountId) { 578 ExchangeService exchangeService = INSTANCE; 579 if (exchangeService == null) return; 580 Mailbox mailbox = 581 Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS); 582 if (mailbox != null) { 583 EasSyncService service = new EasSyncService(exchangeService, mailbox); 584 ContactsSyncAdapter adapter = new ContactsSyncAdapter(service); 585 adapter.wipe(); 586 } 587 mailbox = 588 Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR); 589 if (mailbox != null) { 590 EasSyncService service = new EasSyncService(exchangeService, mailbox); 591 CalendarSyncAdapter adapter = new CalendarSyncAdapter(service); 592 adapter.wipe(); 593 } 594 } 595 596 private boolean onSecurityHold(Account account) { 597 return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0; 598 } 599 600 private boolean onSyncDisabledHold(Account account) { 601 return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0; 602 } 603 604 class AccountObserver extends ContentObserver { 605 String mSyncableEasMailboxSelector = null; 606 String mEasAccountSelector = null; 607 608 // Runs when ExchangeService first starts 609 public AccountObserver(Handler handler) { 610 super(handler); 611 // At startup, we want to see what EAS accounts exist and cache them 612 // TODO: Move database work out of UI thread 613 Context context = getContext(); 614 synchronized (mAccountList) { 615 try { 616 collectEasAccounts(context, mAccountList); 617 } catch (ProviderUnavailableException e) { 618 // Just leave if EmailProvider is unavailable 619 return; 620 } 621 // Create an account mailbox for any account without one 622 for (Account account : mAccountList) { 623 int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" 624 + account.mId, null); 625 if (cnt == 0) { 626 // This case handles a newly created account 627 addAccountMailbox(account.mId); 628 } 629 } 630 } 631 // Run through accounts and update account hold information 632 Utility.runAsync(new Runnable() { 633 @Override 634 public void run() { 635 synchronized (mAccountList) { 636 for (Account account : mAccountList) { 637 if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) { 638 // If we're in a security hold, and our policies are active, release 639 // the hold; otherwise, ping PolicyService that this account's 640 // policies are required 641 if (PolicyServiceProxy.isActive(ExchangeService.this, null)) { 642 PolicyServiceProxy.setAccountHoldFlag(ExchangeService.this, 643 account, false); 644 log("isActive true; release hold for " + account.mDisplayName); 645 } else { 646 PolicyServiceProxy.policiesRequired(ExchangeService.this, 647 account.mId); 648 } 649 } 650 } 651 } 652 }}); 653 } 654 655 /** 656 * Returns a String suitable for appending to a where clause that selects for all syncable 657 * mailboxes in all eas accounts 658 * @return a complex selection string that is not to be cached 659 */ 660 public String getSyncableEasMailboxWhere() { 661 if (mSyncableEasMailboxSelector == null) { 662 StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN); 663 boolean first = true; 664 synchronized (mAccountList) { 665 for (Account account : mAccountList) { 666 if (!first) { 667 sb.append(','); 668 } else { 669 first = false; 670 } 671 sb.append(account.mId); 672 } 673 } 674 sb.append(')'); 675 mSyncableEasMailboxSelector = sb.toString(); 676 } 677 return mSyncableEasMailboxSelector; 678 } 679 680 /** 681 * Returns a String suitable for appending to a where clause that selects for all eas 682 * accounts. 683 * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached 684 */ 685 public String getAccountKeyWhere() { 686 if (mEasAccountSelector == null) { 687 StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN); 688 boolean first = true; 689 synchronized (mAccountList) { 690 for (Account account : mAccountList) { 691 if (!first) { 692 sb.append(','); 693 } else { 694 first = false; 695 } 696 sb.append(account.mId); 697 } 698 } 699 sb.append(')'); 700 mEasAccountSelector = sb.toString(); 701 } 702 return mEasAccountSelector; 703 } 704 705 private void onAccountChanged() { 706 try { 707 maybeStartExchangeServiceThread(); 708 Context context = getContext(); 709 710 // A change to the list requires us to scan for deletions (stop running syncs) 711 // At startup, we want to see what accounts exist and cache them 712 AccountList currentAccounts = new AccountList(); 713 try { 714 collectEasAccounts(context, currentAccounts); 715 } catch (ProviderUnavailableException e) { 716 // Just leave if EmailProvider is unavailable 717 return; 718 } 719 synchronized (mAccountList) { 720 for (Account account : mAccountList) { 721 boolean accountIncomplete = 722 (account.mFlags & Account.FLAGS_INCOMPLETE) != 0; 723 // If the current list doesn't include this account and the account wasn't 724 // incomplete, then this is a deletion 725 if (!currentAccounts.contains(account.mId) && !accountIncomplete) { 726 // The implication is that the account has been deleted; let's find out 727 alwaysLog("Observer found deleted account: " + account.mDisplayName); 728 // Run the reconciler (the reconciliation itself runs in the Email app) 729 runAccountReconcilerSync(ExchangeService.this); 730 // See if the account is still around 731 Account deletedAccount = 732 Account.restoreAccountWithId(context, account.mId); 733 if (deletedAccount != null) { 734 // It is; add it to our account list 735 alwaysLog("Account still in provider: " + account.mDisplayName); 736 currentAccounts.add(account); 737 } else { 738 // It isn't; stop syncs and clear our selectors 739 alwaysLog("Account deletion confirmed: " + account.mDisplayName); 740 stopAccountSyncs(account.mId, true); 741 mSyncableEasMailboxSelector = null; 742 mEasAccountSelector = null; 743 } 744 } else { 745 // Get the newest version of this account 746 Account updatedAccount = 747 Account.restoreAccountWithId(context, account.mId); 748 if (updatedAccount == null) continue; 749 if (account.mSyncInterval != updatedAccount.mSyncInterval 750 || account.mSyncLookback != updatedAccount.mSyncLookback) { 751 // Set the inbox interval to the interval of the Account 752 // This setting should NOT affect other boxes 753 ContentValues cv = new ContentValues(); 754 cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval); 755 getContentResolver().update(Mailbox.CONTENT_URI, cv, 756 WHERE_IN_ACCOUNT_AND_TYPE_INBOX, new String[] { 757 Long.toString(account.mId) 758 }); 759 // Stop all current syncs; the appropriate ones will restart 760 log("Account " + account.mDisplayName + " changed; stop syncs"); 761 stopAccountSyncs(account.mId, true); 762 } 763 764 // See if this account is no longer on security hold 765 if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) { 766 releaseSyncHolds(ExchangeService.this, 767 AbstractSyncService.EXIT_SECURITY_FAILURE, account); 768 } 769 770 // Put current values into our cached account 771 account.mSyncInterval = updatedAccount.mSyncInterval; 772 account.mSyncLookback = updatedAccount.mSyncLookback; 773 account.mFlags = updatedAccount.mFlags; 774 } 775 } 776 // Look for new accounts 777 for (Account account : currentAccounts) { 778 if (!mAccountList.contains(account.mId)) { 779 // Don't forget to cache the HostAuth 780 HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(), 781 account.mHostAuthKeyRecv); 782 if (ha == null) continue; 783 account.mHostAuthRecv = ha; 784 // This is an addition; create our magic hidden mailbox... 785 log("Account observer found new account: " + account.mDisplayName); 786 addAccountMailbox(account.mId); 787 mAccountList.add(account); 788 mSyncableEasMailboxSelector = null; 789 mEasAccountSelector = null; 790 } 791 } 792 // Finally, make sure our account list is up to date 793 mAccountList.clear(); 794 mAccountList.addAll(currentAccounts); 795 } 796 797 // See if there's anything to do... 798 kick("account changed"); 799 } catch (ProviderUnavailableException e) { 800 alwaysLog("Observer failed; provider unavailable"); 801 } 802 } 803 804 @Override 805 public void onChange(boolean selfChange) { 806 new Thread(new Runnable() { 807 public void run() { 808 onAccountChanged(); 809 }}, "Account Observer").start(); 810 } 811 812 private void addAccountMailbox(long acctId) { 813 Account acct = Account.restoreAccountWithId(getContext(), acctId); 814 Mailbox main = new Mailbox(); 815 main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX; 816 main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime(); 817 main.mAccountKey = acct.mId; 818 main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 819 main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH; 820 main.mFlagVisible = false; 821 main.save(getContext()); 822 log("Initializing account: " + acct.mDisplayName); 823 } 824 825 } 826 827 /** 828 * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS 829 * column has changed (when sync has turned off or on) 830 * @param account the Account whose Calendar we're observing 831 */ 832 private void registerCalendarObserver(Account account) { 833 // Get a new observer 834 CalendarObserver observer = new CalendarObserver(mHandler, account); 835 if (observer.mCalendarId != 0) { 836 // If we find the Calendar (and we'd better) register it and store it in the map 837 mCalendarObservers.put(account.mId, observer); 838 mResolver.registerContentObserver( 839 ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false, 840 observer); 841 } 842 } 843 844 /** 845 * Unregister all CalendarObserver's 846 */ 847 static public void unregisterCalendarObservers() { 848 ExchangeService exchangeService = INSTANCE; 849 if (exchangeService == null) return; 850 ContentResolver resolver = exchangeService.mResolver; 851 for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) { 852 resolver.unregisterContentObserver(observer); 853 } 854 exchangeService.mCalendarObservers.clear(); 855 } 856 857 /** 858 * Return the syncable state of an account's calendar, as determined by the sync_events column 859 * of our Calendar (from CalendarProvider2) 860 * Note that the current state of sync_events is cached in our CalendarObserver 861 * @param accountId the id of the account whose calendar we are checking 862 * @return whether or not syncing of events is enabled 863 */ 864 private boolean isCalendarEnabled(long accountId) { 865 CalendarObserver observer = mCalendarObservers.get(accountId); 866 if (observer != null) { 867 return (observer.mSyncEvents == 1); 868 } 869 // If there's no observer, there's no Calendar in CalendarProvider2, so we return true 870 // to allow Calendar creation 871 return true; 872 } 873 874 private class CalendarObserver extends ContentObserver { 875 long mAccountId; 876 long mCalendarId; 877 long mSyncEvents; 878 String mAccountName; 879 880 public CalendarObserver(Handler handler, Account account) { 881 super(handler); 882 mAccountId = account.mId; 883 mAccountName = account.mEmailAddress; 884 885 // Find the Calendar for this account 886 Cursor c = mResolver.query(Calendars.CONTENT_URI, 887 new String[] {Calendars._ID, Calendars.SYNC_EVENTS}, 888 CalendarSyncAdapter.CALENDAR_SELECTION, 889 new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, 890 null); 891 if (c != null) { 892 // Save its id and its sync events status 893 try { 894 if (c.moveToFirst()) { 895 mCalendarId = c.getLong(0); 896 mSyncEvents = c.getLong(1); 897 } 898 } finally { 899 c.close(); 900 } 901 } 902 } 903 904 @Override 905 public synchronized void onChange(boolean selfChange) { 906 // See if the user has changed syncing of our calendar 907 if (!selfChange) { 908 new Thread(new Runnable() { 909 public void run() { 910 try { 911 Cursor c = mResolver.query(Calendars.CONTENT_URI, 912 new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?", 913 new String[] {Long.toString(mCalendarId)}, null); 914 if (c == null) return; 915 // Get its sync events; if it's changed, we've got work to do 916 try { 917 if (c.moveToFirst()) { 918 long newSyncEvents = c.getLong(0); 919 if (newSyncEvents != mSyncEvents) { 920 log("_sync_events changed for calendar in " + mAccountName); 921 Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE, 922 mAccountId, Mailbox.TYPE_CALENDAR); 923 // Sanity check for mailbox deletion 924 if (mailbox == null) return; 925 ContentValues cv = new ContentValues(); 926 if (newSyncEvents == 0) { 927 // When sync is disabled, we're supposed to delete 928 // all events in the calendar 929 log("Deleting events and setting syncKey to 0 for " + 930 mAccountName); 931 // First, stop any sync that's ongoing 932 stopManualSync(mailbox.mId); 933 // Set the syncKey to 0 (reset) 934 EasSyncService service = 935 new EasSyncService(INSTANCE, mailbox); 936 CalendarSyncAdapter adapter = 937 new CalendarSyncAdapter(service); 938 try { 939 adapter.setSyncKey("0", false); 940 } catch (IOException e) { 941 // The provider can't be reached; nothing to be done 942 } 943 // Reset the sync key locally and stop syncing 944 cv.put(Mailbox.SYNC_KEY, "0"); 945 cv.put(Mailbox.SYNC_INTERVAL, 946 Mailbox.CHECK_INTERVAL_NEVER); 947 mResolver.update(ContentUris.withAppendedId( 948 Mailbox.CONTENT_URI, mailbox.mId), cv, null, 949 null); 950 // Delete all events using the sync adapter 951 // parameter so that the deletion is only local 952 Uri eventsAsSyncAdapter = 953 CalendarSyncAdapter.asSyncAdapter( 954 Events.CONTENT_URI, 955 mAccountName, 956 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 957 mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID, 958 new String[] {Long.toString(mCalendarId)}); 959 } else { 960 // Make this a push mailbox and kick; this will start 961 // a resync of the Calendar; the account mailbox will 962 // ping on this during the next cycle of the ping loop 963 cv.put(Mailbox.SYNC_INTERVAL, 964 Mailbox.CHECK_INTERVAL_PUSH); 965 mResolver.update(ContentUris.withAppendedId( 966 Mailbox.CONTENT_URI, mailbox.mId), cv, null, 967 null); 968 kick("calendar sync changed"); 969 } 970 971 // Save away the new value 972 mSyncEvents = newSyncEvents; 973 } 974 } 975 } finally { 976 c.close(); 977 } 978 } catch (ProviderUnavailableException e) { 979 Log.w(TAG, "Observer failed; provider unavailable"); 980 } 981 }}, "Calendar Observer").start(); 982 } 983 } 984 } 985 986 private class MailboxObserver extends ContentObserver { 987 public MailboxObserver(Handler handler) { 988 super(handler); 989 } 990 991 @Override 992 public void onChange(boolean selfChange) { 993 // See if there's anything to do... 994 if (!selfChange) { 995 kick("mailbox changed"); 996 } 997 } 998 } 999 1000 private class SyncedMessageObserver extends ContentObserver { 1001 Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class); 1002 PendingIntent syncAlarmPendingIntent = 1003 PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0); 1004 AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE); 1005 1006 public SyncedMessageObserver(Handler handler) { 1007 super(handler); 1008 } 1009 1010 @Override 1011 public void onChange(boolean selfChange) { 1012 alarmManager.set(AlarmManager.RTC_WAKEUP, 1013 System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent); 1014 } 1015 } 1016 1017 static public IEmailServiceCallback callback() { 1018 return sCallbackProxy; 1019 } 1020 1021 static public Account getAccountById(long accountId) { 1022 ExchangeService exchangeService = INSTANCE; 1023 if (exchangeService != null) { 1024 AccountList accountList = exchangeService.mAccountList; 1025 synchronized (accountList) { 1026 return accountList.getById(accountId); 1027 } 1028 } 1029 return null; 1030 } 1031 1032 static public Account getAccountByName(String accountName) { 1033 ExchangeService exchangeService = INSTANCE; 1034 if (exchangeService != null) { 1035 AccountList accountList = exchangeService.mAccountList; 1036 synchronized (accountList) { 1037 return accountList.getByName(accountName); 1038 } 1039 } 1040 return null; 1041 } 1042 1043 static public String getEasAccountSelector() { 1044 ExchangeService exchangeService = INSTANCE; 1045 if (exchangeService != null && exchangeService.mAccountObserver != null) { 1046 return exchangeService.mAccountObserver.getAccountKeyWhere(); 1047 } 1048 return null; 1049 } 1050 1051 public class SyncStatus { 1052 static public final int NOT_RUNNING = 0; 1053 static public final int DIED = 1; 1054 static public final int SYNC = 2; 1055 static public final int IDLE = 3; 1056 } 1057 1058 /*package*/ class SyncError { 1059 int reason; 1060 boolean fatal = false; 1061 long holdDelay = 15*SECONDS; 1062 long holdEndTime = System.currentTimeMillis() + holdDelay; 1063 1064 SyncError(int _reason, boolean _fatal) { 1065 reason = _reason; 1066 fatal = _fatal; 1067 } 1068 1069 /** 1070 * We double the holdDelay from 15 seconds through 4 mins 1071 */ 1072 void escalate() { 1073 if (holdDelay < HOLD_DELAY_MAXIMUM) { 1074 holdDelay *= 2; 1075 } 1076 holdEndTime = System.currentTimeMillis() + holdDelay; 1077 } 1078 } 1079 1080 private void logSyncHolds() { 1081 if (Eas.USER_LOG) { 1082 log("Sync holds:"); 1083 long time = System.currentTimeMillis(); 1084 for (long mailboxId : mSyncErrorMap.keySet()) { 1085 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId); 1086 if (m == null) { 1087 log("Mailbox " + mailboxId + " no longer exists"); 1088 } else { 1089 SyncError error = mSyncErrorMap.get(mailboxId); 1090 if (error != null) { 1091 log("Mailbox " + m.mDisplayName + ", error = " + error.reason 1092 + ", fatal = " + error.fatal); 1093 if (error.holdEndTime > 0) { 1094 log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s"); 1095 } 1096 } 1097 } 1098 } 1099 } 1100 } 1101 1102 /** 1103 * Release security holds for the specified account 1104 * @param account the account whose Mailboxes should be released from security hold 1105 */ 1106 static public void releaseSecurityHold(Account account) { 1107 ExchangeService exchangeService = INSTANCE; 1108 if (exchangeService != null) { 1109 exchangeService.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE, 1110 account); 1111 } 1112 } 1113 1114 /** 1115 * Release a specific type of hold (the reason) for the specified Account; if the account 1116 * is null, mailboxes from all accounts with the specified hold will be released 1117 * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX) 1118 * @param account an Account whose mailboxes should be released (or all if null) 1119 * @return whether or not any mailboxes were released 1120 */ 1121 /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) { 1122 boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account); 1123 kick("security release"); 1124 return holdWasReleased; 1125 } 1126 1127 private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) { 1128 boolean holdWasReleased = false; 1129 for (long mailboxId: mSyncErrorMap.keySet()) { 1130 if (account != null) { 1131 Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId); 1132 if (m == null) { 1133 mSyncErrorMap.remove(mailboxId); 1134 } else if (m.mAccountKey != account.mId) { 1135 continue; 1136 } 1137 } 1138 SyncError error = mSyncErrorMap.get(mailboxId); 1139 if (error != null && error.reason == reason) { 1140 mSyncErrorMap.remove(mailboxId); 1141 holdWasReleased = true; 1142 } 1143 } 1144 return holdWasReleased; 1145 } 1146 1147 /** 1148 * Reconcile Exchange accounts with AccountManager (asynchronous) 1149 * @param context the caller's Context 1150 */ 1151 public static void reconcileAccounts(final Context context) { 1152 Utility.runAsync(new Runnable() { 1153 @Override 1154 public void run() { 1155 ExchangeService exchangeService = INSTANCE; 1156 if (exchangeService != null) { 1157 exchangeService.runAccountReconcilerSync(context); 1158 } 1159 }}); 1160 } 1161 1162 /** 1163 * Blocking call to the account reconciler 1164 */ 1165 public static void runAccountReconcilerSync(Context context) { 1166 alwaysLog("Reconciling accounts..."); 1167 new AccountServiceProxy(context).reconcileAccounts( 1168 HostAuth.SCHEME_EAS, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 1169 } 1170 1171 public static void log(String str) { 1172 log(TAG, str); 1173 } 1174 1175 public static void log(String tag, String str) { 1176 if (Eas.USER_LOG) { 1177 Log.d(tag, str); 1178 if (Eas.FILE_LOG) { 1179 FileLogger.log(tag, str); 1180 } 1181 } 1182 } 1183 1184 public static void alwaysLog(String str) { 1185 if (!Eas.USER_LOG) { 1186 Log.d(TAG, str); 1187 } else { 1188 log(str); 1189 } 1190 } 1191 1192 /** 1193 * EAS requires a unique device id, so that sync is possible from a variety of different 1194 * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other 1195 * device that doesn't provide one, we can create it as "device". 1196 * This would work on a real device as well, but it would be better to use the "real" id if 1197 * it's available 1198 */ 1199 static public String getDeviceId(Context context) throws IOException { 1200 if (sDeviceId == null) { 1201 sDeviceId = new AccountServiceProxy(context).getDeviceId(); 1202 alwaysLog("Received deviceId from Email app: " + sDeviceId); 1203 } 1204 return sDeviceId; 1205 } 1206 1207 @Override 1208 public IBinder onBind(Intent arg0) { 1209 return mBinder; 1210 } 1211 1212 static public ConnPerRoute sConnPerRoute = new ConnPerRoute() { 1213 public int getMaxForRoute(HttpRoute route) { 1214 return 8; 1215 } 1216 }; 1217 1218 static public synchronized EmailClientConnectionManager getClientConnectionManager() { 1219 if (sClientConnectionManager == null) { 1220 // After two tries, kill the process. Most likely, this will happen in the background 1221 // The service will restart itself after about 5 seconds 1222 if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) { 1223 alwaysLog("Shutting down process to unblock threads"); 1224 Process.killProcess(Process.myPid()); 1225 } 1226 HttpParams params = new BasicHttpParams(); 1227 params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25); 1228 params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute); 1229 sClientConnectionManager = EmailClientConnectionManager.newInstance(params); 1230 } 1231 // Null is a valid return result if we get an exception 1232 return sClientConnectionManager; 1233 } 1234 1235 static private synchronized void shutdownConnectionManager() { 1236 if (sClientConnectionManager != null) { 1237 log("Shutting down ClientConnectionManager"); 1238 sClientConnectionManager.shutdown(); 1239 sClientConnectionManagerShutdownCount++; 1240 sClientConnectionManager = null; 1241 } 1242 } 1243 1244 public static void stopAccountSyncs(long acctId) { 1245 ExchangeService exchangeService = INSTANCE; 1246 if (exchangeService != null) { 1247 exchangeService.stopAccountSyncs(acctId, true); 1248 } 1249 } 1250 1251 private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) { 1252 synchronized (sSyncLock) { 1253 List<Long> deletedBoxes = new ArrayList<Long>(); 1254 for (Long mid : mServiceMap.keySet()) { 1255 Mailbox box = Mailbox.restoreMailboxWithId(this, mid); 1256 if (box != null) { 1257 if (box.mAccountKey == acctId) { 1258 if (!includeAccountMailbox && 1259 box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 1260 AbstractSyncService svc = mServiceMap.get(mid); 1261 if (svc != null) { 1262 svc.stop(); 1263 } 1264 continue; 1265 } 1266 AbstractSyncService svc = mServiceMap.get(mid); 1267 if (svc != null) { 1268 svc.stop(); 1269 Thread t = svc.mThread; 1270 if (t != null) { 1271 t.interrupt(); 1272 } 1273 } 1274 deletedBoxes.add(mid); 1275 } 1276 } 1277 } 1278 for (Long mid : deletedBoxes) { 1279 releaseMailbox(mid); 1280 } 1281 } 1282 } 1283 1284 static private void reloadFolderListFailed(long accountId) { 1285 try { 1286 callback().syncMailboxListStatus(accountId, 1287 EmailServiceStatus.ACCOUNT_UNINITIALIZED, 0); 1288 } catch (RemoteException e1) { 1289 // Don't care if this fails 1290 } 1291 } 1292 1293 static public void reloadFolderList(Context context, long accountId, boolean force) { 1294 ExchangeService exchangeService = INSTANCE; 1295 if (exchangeService == null) return; 1296 Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI, 1297 Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " + 1298 MailboxColumns.TYPE + "=?", 1299 new String[] {Long.toString(accountId), 1300 Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null); 1301 try { 1302 if (c.moveToFirst()) { 1303 synchronized(sSyncLock) { 1304 Mailbox mailbox = new Mailbox(); 1305 mailbox.restore(c); 1306 Account acct = Account.restoreAccountWithId(context, accountId); 1307 if (acct == null) { 1308 reloadFolderListFailed(accountId); 1309 return; 1310 } 1311 String syncKey = acct.mSyncKey; 1312 // No need to reload the list if we don't have one 1313 if (!force && (syncKey == null || syncKey.equals("0"))) { 1314 reloadFolderListFailed(accountId); 1315 return; 1316 } 1317 1318 // Change all ping/push boxes to push/hold 1319 ContentValues cv = new ContentValues(); 1320 cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD); 1321 context.getContentResolver().update(Mailbox.CONTENT_URI, cv, 1322 WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX, 1323 new String[] {Long.toString(accountId)}); 1324 log("Set push/ping boxes to push/hold"); 1325 1326 long id = mailbox.mId; 1327 AbstractSyncService svc = exchangeService.mServiceMap.get(id); 1328 // Tell the service we're done 1329 if (svc != null) { 1330 synchronized (svc.getSynchronizer()) { 1331 svc.stop(); 1332 // Interrupt the thread so that it can stop 1333 Thread thread = svc.mThread; 1334 if (thread != null) { 1335 thread.setName(thread.getName() + " (Stopped)"); 1336 thread.interrupt(); 1337 } 1338 } 1339 // Abandon the service 1340 exchangeService.releaseMailbox(id); 1341 // And have it start naturally 1342 kick("reload folder list"); 1343 } 1344 } 1345 } 1346 } finally { 1347 c.close(); 1348 } 1349 } 1350 1351 /** 1352 * Informs ExchangeService that an account has a new folder list; as a result, any existing 1353 * folder might have become invalid. Therefore, we act as if the account has been deleted, and 1354 * then we reinitialize it. 1355 * 1356 * @param acctId 1357 */ 1358 static public void stopNonAccountMailboxSyncsForAccount(long acctId) { 1359 ExchangeService exchangeService = INSTANCE; 1360 if (exchangeService != null) { 1361 exchangeService.stopAccountSyncs(acctId, false); 1362 kick("reload folder list"); 1363 } 1364 } 1365 1366 private void acquireWakeLock(long id) { 1367 synchronized (mWakeLocks) { 1368 Boolean lock = mWakeLocks.get(id); 1369 if (lock == null) { 1370 if (mWakeLock == null) { 1371 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); 1372 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE"); 1373 mWakeLock.acquire(); 1374 //log("+WAKE LOCK ACQUIRED"); 1375 } 1376 mWakeLocks.put(id, true); 1377 } 1378 } 1379 } 1380 1381 private void releaseWakeLock(long id) { 1382 synchronized (mWakeLocks) { 1383 Boolean lock = mWakeLocks.get(id); 1384 if (lock != null) { 1385 mWakeLocks.remove(id); 1386 if (mWakeLocks.isEmpty()) { 1387 if (mWakeLock != null) { 1388 mWakeLock.release(); 1389 } 1390 mWakeLock = null; 1391 //log("+WAKE LOCK RELEASED"); 1392 } else { 1393 } 1394 } 1395 } 1396 } 1397 1398 static public String alarmOwner(long id) { 1399 if (id == EXTRA_MAILBOX_ID) { 1400 return "ExchangeService"; 1401 } else { 1402 String name = Long.toString(id); 1403 if (Eas.USER_LOG && INSTANCE != null) { 1404 Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id); 1405 if (m != null) { 1406 name = m.mDisplayName + '(' + m.mAccountKey + ')'; 1407 } 1408 } 1409 return "Mailbox " + name; 1410 } 1411 } 1412 1413 private void clearAlarm(long id) { 1414 synchronized (mPendingIntents) { 1415 PendingIntent pi = mPendingIntents.get(id); 1416 if (pi != null) { 1417 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 1418 alarmManager.cancel(pi); 1419 //log("+Alarm cleared for " + alarmOwner(id)); 1420 mPendingIntents.remove(id); 1421 } 1422 } 1423 } 1424 1425 private void setAlarm(long id, long millis) { 1426 synchronized (mPendingIntents) { 1427 PendingIntent pi = mPendingIntents.get(id); 1428 if (pi == null) { 1429 Intent i = new Intent(this, MailboxAlarmReceiver.class); 1430 i.putExtra("mailbox", id); 1431 i.setData(Uri.parse("Box" + id)); 1432 pi = PendingIntent.getBroadcast(this, 0, i, 0); 1433 mPendingIntents.put(id, pi); 1434 1435 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 1436 alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi); 1437 //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s"); 1438 } 1439 } 1440 } 1441 1442 private void clearAlarms() { 1443 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 1444 synchronized (mPendingIntents) { 1445 for (PendingIntent pi : mPendingIntents.values()) { 1446 alarmManager.cancel(pi); 1447 } 1448 mPendingIntents.clear(); 1449 } 1450 } 1451 1452 static public void runAwake(long id) { 1453 ExchangeService exchangeService = INSTANCE; 1454 if (exchangeService != null) { 1455 exchangeService.acquireWakeLock(id); 1456 exchangeService.clearAlarm(id); 1457 } 1458 } 1459 1460 static public void runAsleep(long id, long millis) { 1461 ExchangeService exchangeService = INSTANCE; 1462 if (exchangeService != null) { 1463 exchangeService.setAlarm(id, millis); 1464 exchangeService.releaseWakeLock(id); 1465 } 1466 } 1467 1468 static public void clearWatchdogAlarm(long id) { 1469 ExchangeService exchangeService = INSTANCE; 1470 if (exchangeService != null) { 1471 exchangeService.clearAlarm(id); 1472 } 1473 } 1474 1475 static public void setWatchdogAlarm(long id, long millis) { 1476 ExchangeService exchangeService = INSTANCE; 1477 if (exchangeService != null) { 1478 exchangeService.setAlarm(id, millis); 1479 } 1480 } 1481 1482 static public void alert(Context context, final long id) { 1483 final ExchangeService exchangeService = INSTANCE; 1484 checkExchangeServiceServiceRunning(); 1485 if (id < 0) { 1486 log("ExchangeService alert"); 1487 kick("ping ExchangeService"); 1488 } else if (exchangeService == null) { 1489 context.startService(new Intent(context, ExchangeService.class)); 1490 } else { 1491 final AbstractSyncService service = exchangeService.mServiceMap.get(id); 1492 if (service != null) { 1493 // Handle alerts in a background thread, as we are typically called from a 1494 // broadcast receiver, and are therefore running in the UI thread 1495 String threadName = "ExchangeService Alert: "; 1496 if (service.mMailbox != null) { 1497 threadName += service.mMailbox.mDisplayName; 1498 } 1499 new Thread(new Runnable() { 1500 public void run() { 1501 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, id); 1502 if (m != null) { 1503 // We ignore drafts completely (doesn't sync). Changes in Outbox are 1504 // handled in the checkMailboxes loop, so we can ignore these pings. 1505 if (Eas.USER_LOG) { 1506 Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")"); 1507 } 1508 if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) { 1509 String[] args = new String[] {Long.toString(m.mId)}; 1510 ContentResolver resolver = INSTANCE.mResolver; 1511 resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY, 1512 args); 1513 resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY, 1514 args); 1515 return; 1516 } 1517 service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey); 1518 service.mMailbox = m; 1519 // Send the alarm to the sync service 1520 if (!service.alarm()) { 1521 // A false return means that we were forced to interrupt the thread 1522 // In this case, we release the mailbox so that we can start another 1523 // thread to do the work 1524 log("Alarm failed; releasing mailbox"); 1525 synchronized(sSyncLock) { 1526 exchangeService.releaseMailbox(id); 1527 } 1528 // Shutdown the connection manager; this should close all of our 1529 // sockets and generate IOExceptions all around. 1530 ExchangeService.shutdownConnectionManager(); 1531 } 1532 } 1533 }}, threadName).start(); 1534 } 1535 } 1536 } 1537 1538 public class ConnectivityReceiver extends BroadcastReceiver { 1539 @Override 1540 public void onReceive(Context context, Intent intent) { 1541 if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { 1542 Bundle b = intent.getExtras(); 1543 if (b != null) { 1544 NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO); 1545 String info = "Connectivity alert for " + a.getTypeName(); 1546 State state = a.getState(); 1547 if (state == State.CONNECTED) { 1548 info += " CONNECTED"; 1549 log(info); 1550 synchronized (sConnectivityLock) { 1551 sConnectivityLock.notifyAll(); 1552 } 1553 kick("connected"); 1554 } else if (state == State.DISCONNECTED) { 1555 info += " DISCONNECTED"; 1556 log(info); 1557 kick("disconnected"); 1558 } 1559 } 1560 } else if (intent.getAction().equals( 1561 ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) { 1562 ConnectivityManager cm = 1563 (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); 1564 mBackgroundData = cm.getBackgroundDataSetting(); 1565 // If background data is now on, we want to kick ExchangeService 1566 if (mBackgroundData) { 1567 kick("background data on"); 1568 log("Background data on; restart syncs"); 1569 // Otherwise, stop all syncs 1570 } else { 1571 log("Background data off: stop all syncs"); 1572 EmailAsyncTask.runAsyncParallel(new Runnable() { 1573 @Override 1574 public void run() { 1575 synchronized (mAccountList) { 1576 for (Account account : mAccountList) 1577 ExchangeService.stopAccountSyncs(account.mId); 1578 } 1579 }}); 1580 } 1581 } 1582 } 1583 } 1584 1585 /** 1586 * Starts a service thread and enters it into the service map 1587 * This is the point of instantiation of all sync threads 1588 * @param service the service to start 1589 * @param m the Mailbox on which the service will operate 1590 */ 1591 private void startServiceThread(AbstractSyncService service, Mailbox m) { 1592 if (m == null) return; 1593 synchronized (sSyncLock) { 1594 String mailboxName = m.mDisplayName; 1595 String accountName = service.mAccount.mDisplayName; 1596 Thread thread = new Thread(service, mailboxName + "[" + accountName + "]"); 1597 log("Starting thread for " + mailboxName + " in account " + accountName); 1598 thread.start(); 1599 mServiceMap.put(m.mId, service); 1600 runAwake(m.mId); 1601 if ((m.mServerId != null) && !m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) { 1602 stopPing(m.mAccountKey); 1603 } 1604 } 1605 } 1606 1607 /** 1608 * Stop any ping in progress for the given account 1609 * @param accountId 1610 */ 1611 private void stopPing(long accountId) { 1612 // Go through our active mailboxes looking for the right one 1613 synchronized (sSyncLock) { 1614 for (long mailboxId: mServiceMap.keySet()) { 1615 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId); 1616 if (m != null) { 1617 String serverId = m.mServerId; 1618 if (m.mAccountKey == accountId && serverId != null && 1619 serverId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) { 1620 // Here's our account mailbox; reset him (stopping pings) 1621 AbstractSyncService svc = mServiceMap.get(mailboxId); 1622 svc.reset(); 1623 } 1624 } 1625 } 1626 } 1627 } 1628 1629 private void requestSync(Mailbox m, int reason, Request req) { 1630 // Don't sync if there's no connectivity 1631 if (sConnectivityHold || (m == null) || sStop) { 1632 if (reason >= SYNC_CALLBACK_START) { 1633 try { 1634 sCallbackProxy.syncMailboxStatus(m.mId, EmailServiceStatus.CONNECTION_ERROR, 0); 1635 } catch (RemoteException e) { 1636 // We tried... 1637 } 1638 } 1639 return; 1640 } 1641 synchronized (sSyncLock) { 1642 Account acct = Account.restoreAccountWithId(this, m.mAccountKey); 1643 if (acct != null) { 1644 // Always make sure there's not a running instance of this service 1645 AbstractSyncService service = mServiceMap.get(m.mId); 1646 if (service == null) { 1647 service = new EasSyncService(this, m); 1648 if (!((EasSyncService)service).mIsValid) return; 1649 service.mSyncReason = reason; 1650 if (req != null) { 1651 service.addRequest(req); 1652 } 1653 startServiceThread(service, m); 1654 } 1655 } 1656 } 1657 } 1658 1659 private void stopServiceThreads() { 1660 synchronized (sSyncLock) { 1661 ArrayList<Long> toStop = new ArrayList<Long>(); 1662 1663 // Keep track of which services to stop 1664 for (Long mailboxId : mServiceMap.keySet()) { 1665 toStop.add(mailboxId); 1666 } 1667 1668 // Shut down all of those running services 1669 for (Long mailboxId : toStop) { 1670 AbstractSyncService svc = mServiceMap.get(mailboxId); 1671 if (svc != null) { 1672 log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName); 1673 svc.stop(); 1674 if (svc.mThread != null) { 1675 svc.mThread.interrupt(); 1676 } 1677 } 1678 releaseWakeLock(mailboxId); 1679 } 1680 } 1681 } 1682 1683 private void waitForConnectivity() { 1684 boolean waiting = false; 1685 ConnectivityManager cm = 1686 (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); 1687 while (!sStop) { 1688 NetworkInfo info = cm.getActiveNetworkInfo(); 1689 if (info != null) { 1690 mNetworkInfo = info; 1691 // We're done if there's an active network 1692 if (waiting) { 1693 // If we've been waiting, release any I/O error holds 1694 releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null); 1695 // And log what's still being held 1696 logSyncHolds(); 1697 } 1698 return; 1699 } else { 1700 // If this is our first time through the loop, shut down running service threads 1701 if (!waiting) { 1702 waiting = true; 1703 stopServiceThreads(); 1704 } 1705 // Wait until a network is connected (or 10 mins), but let the device sleep 1706 // We'll set an alarm just in case we don't get notified (bugs happen) 1707 synchronized (sConnectivityLock) { 1708 runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS); 1709 try { 1710 log("Connectivity lock..."); 1711 sConnectivityHold = true; 1712 sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME); 1713 log("Connectivity lock released..."); 1714 } catch (InterruptedException e) { 1715 // This is fine; we just go around the loop again 1716 } finally { 1717 sConnectivityHold = false; 1718 } 1719 runAwake(EXTRA_MAILBOX_ID); 1720 } 1721 } 1722 } 1723 } 1724 1725 /** 1726 * Note that there are two ways the EAS ExchangeService service can be created: 1727 * 1728 * 1) as a background service instantiated via startService (which happens on boot, when the 1729 * first EAS account is created, etc), in which case the service thread is spun up, mailboxes 1730 * sync, etc. and 1731 * 2) to execute an RPC call from the UI, in which case the background service will already be 1732 * running most of the time (unless we're creating a first EAS account) 1733 * 1734 * If the running background service detects that there are no EAS accounts (on boot, if none 1735 * were created, or afterward if the last remaining EAS account is deleted), it will call 1736 * stopSelf() to terminate operation. 1737 * 1738 * The goal is to ensure that the background service is running at all times when there is at 1739 * least one EAS account in existence 1740 * 1741 * Because there are edge cases in which our process can crash (typically, this has been seen 1742 * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the 1743 * background service having been started. We explicitly try to start the service in Welcome 1744 * (to handle the case of the app having been reloaded). We also start the service on any 1745 * startSync call (if it isn't already running) 1746 */ 1747 @Override 1748 public void onCreate() { 1749 Utility.runAsync(new Runnable() { 1750 @Override 1751 public void run() { 1752 // Quick checks first, before getting the lock 1753 if (sStartingUp) return; 1754 synchronized (sSyncLock) { 1755 alwaysLog("!!! EAS ExchangeService, onCreate"); 1756 // Try to start up properly; we might be coming back from a crash that the Email 1757 // application isn't aware of. 1758 startService(new Intent(EmailServiceProxy.EXCHANGE_INTENT)); 1759 if (sStop) { 1760 return; 1761 } 1762 } 1763 }}); 1764 } 1765 1766 @Override 1767 public int onStartCommand(Intent intent, int flags, int startId) { 1768 alwaysLog("!!! EAS ExchangeService, onStartCommand, startingUp = " + sStartingUp + 1769 ", running = " + (INSTANCE != null)); 1770 if (!sStartingUp && INSTANCE == null) { 1771 sStartingUp = true; 1772 Utility.runAsync(new Runnable() { 1773 @Override 1774 public void run() { 1775 try { 1776 synchronized (sSyncLock) { 1777 // ExchangeService cannot start unless we can connect to AccountService 1778 if (!new AccountServiceProxy(ExchangeService.this).test()) { 1779 alwaysLog("!!! Email application not found; stopping self"); 1780 stopSelf(); 1781 } 1782 if (sDeviceId == null) { 1783 try { 1784 String deviceId = getDeviceId(ExchangeService.this); 1785 if (deviceId != null) { 1786 sDeviceId = deviceId; 1787 } 1788 } catch (IOException e) { 1789 } 1790 if (sDeviceId == null) { 1791 alwaysLog("!!! deviceId unknown; stopping self and retrying"); 1792 stopSelf(); 1793 // Try to restart ourselves in a few seconds 1794 Utility.runAsync(new Runnable() { 1795 @Override 1796 public void run() { 1797 try { 1798 Thread.sleep(5000); 1799 } catch (InterruptedException e) { 1800 } 1801 startService(new Intent( 1802 EmailServiceProxy.EXCHANGE_INTENT)); 1803 }}); 1804 return; 1805 } 1806 } 1807 // Run the reconciler and clean up mismatched accounts - if we weren't 1808 // running when accounts were deleted, it won't have been called. 1809 runAccountReconcilerSync(ExchangeService.this); 1810 // Update other services depending on final account configuration 1811 maybeStartExchangeServiceThread(); 1812 if (sServiceThread == null) { 1813 log("!!! EAS ExchangeService, stopping self"); 1814 stopSelf(); 1815 } else if (sStop) { 1816 // If we were trying to stop, attempt a restart in 5 secs 1817 setAlarm(EXCHANGE_SERVICE_MAILBOX_ID, 5*SECONDS); 1818 } 1819 } 1820 } finally { 1821 sStartingUp = false; 1822 } 1823 }}); 1824 } 1825 return Service.START_STICKY; 1826 } 1827 1828 @Override 1829 public void onDestroy() { 1830 log("!!! EAS ExchangeService, onDestroy"); 1831 // Handle shutting down off the UI thread 1832 Utility.runAsync(new Runnable() { 1833 @Override 1834 public void run() { 1835 // Quick checks first, before getting the lock 1836 if (INSTANCE == null || sServiceThread == null) return; 1837 synchronized(sSyncLock) { 1838 // Stop the sync manager thread and return 1839 if (sServiceThread != null) { 1840 sStop = true; 1841 sServiceThread.interrupt(); 1842 } 1843 } 1844 }}); 1845 } 1846 1847 void maybeStartExchangeServiceThread() { 1848 // Start our thread... 1849 // See if there are any EAS accounts; otherwise, just go away 1850 if (sServiceThread == null || !sServiceThread.isAlive()) { 1851 if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) { 1852 log(sServiceThread == null ? "Starting thread..." : "Restarting thread..."); 1853 sServiceThread = new Thread(this, "ExchangeService"); 1854 INSTANCE = this; 1855 sServiceThread.start(); 1856 } 1857 } 1858 } 1859 1860 /** 1861 * Start up the ExchangeService service if it's not already running 1862 * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in 1863 * com.android.email) and hasn't been restarted. See the comment for onCreate for details 1864 */ 1865 static void checkExchangeServiceServiceRunning() { 1866 ExchangeService exchangeService = INSTANCE; 1867 if (exchangeService == null) return; 1868 if (sServiceThread == null) { 1869 log("!!! checkExchangeServiceServiceRunning; starting service..."); 1870 exchangeService.startService(new Intent(exchangeService, ExchangeService.class)); 1871 } 1872 } 1873 1874 public void run() { 1875 sStop = false; 1876 alwaysLog("ExchangeService thread running"); 1877 // If we're really debugging, turn on all logging 1878 if (Eas.DEBUG) { 1879 Eas.USER_LOG = true; 1880 Eas.PARSER_LOG = true; 1881 Eas.FILE_LOG = true; 1882 } 1883 1884 TempDirectory.setTempDirectory(this); 1885 1886 // If we need to wait for the debugger, do so 1887 if (Eas.WAIT_DEBUG) { 1888 Debug.waitForDebugger(); 1889 } 1890 1891 // Synchronize here to prevent a shutdown from happening while we initialize our observers 1892 // and receivers 1893 synchronized (sSyncLock) { 1894 if (INSTANCE != null) { 1895 mResolver = getContentResolver(); 1896 1897 // Set up our observers; we need them to know when to start/stop various syncs based 1898 // on the insert/delete/update of mailboxes and accounts 1899 // We also observe synced messages to trigger upsyncs at the appropriate time 1900 mAccountObserver = new AccountObserver(mHandler); 1901 mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); 1902 mMailboxObserver = new MailboxObserver(mHandler); 1903 mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver); 1904 mSyncedMessageObserver = new SyncedMessageObserver(mHandler); 1905 mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, 1906 mSyncedMessageObserver); 1907 1908 // Set up receivers for connectivity and background data setting 1909 mConnectivityReceiver = new ConnectivityReceiver(); 1910 registerReceiver(mConnectivityReceiver, new IntentFilter( 1911 ConnectivityManager.CONNECTIVITY_ACTION)); 1912 1913 mBackgroundDataSettingReceiver = new ConnectivityReceiver(); 1914 registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter( 1915 ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)); 1916 // Save away the current background data setting; we'll keep track of it with the 1917 // receiver we just registered 1918 ConnectivityManager cm = (ConnectivityManager)getSystemService( 1919 Context.CONNECTIVITY_SERVICE); 1920 mBackgroundData = cm.getBackgroundDataSetting(); 1921 1922 // Do any required work to clean up our Mailboxes (this serves to upgrade 1923 // mailboxes that existed prior to EmailProvider database version 17) 1924 MailboxUtilities.fixupUninitializedParentKeys(this, getEasAccountSelector()); 1925 } 1926 } 1927 1928 try { 1929 // Loop indefinitely until we're shut down 1930 while (!sStop) { 1931 runAwake(EXTRA_MAILBOX_ID); 1932 waitForConnectivity(); 1933 mNextWaitReason = null; 1934 long nextWait = checkMailboxes(); 1935 try { 1936 synchronized (this) { 1937 if (!mKicked) { 1938 if (nextWait < 0) { 1939 log("Negative wait? Setting to 1s"); 1940 nextWait = 1*SECONDS; 1941 } 1942 if (nextWait > 10*SECONDS) { 1943 if (mNextWaitReason != null) { 1944 log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason); 1945 } 1946 runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS)); 1947 } 1948 wait(nextWait); 1949 } 1950 } 1951 } catch (InterruptedException e) { 1952 // Needs to be caught, but causes no problem 1953 log("ExchangeService interrupted"); 1954 } finally { 1955 synchronized (this) { 1956 if (mKicked) { 1957 //log("Wait deferred due to kick"); 1958 mKicked = false; 1959 } 1960 } 1961 } 1962 } 1963 log("Shutdown requested"); 1964 } catch (ProviderUnavailableException pue) { 1965 // Shutdown cleanly in this case 1966 // NOTE: Sync adapters will also crash with this error, but that is already handled 1967 // in the adapters themselves, i.e. they return cleanly via done(). When the Email 1968 // process starts running again, the Exchange process will be started again in due 1969 // course, assuming there is at least one existing EAS account. 1970 Log.e(TAG, "EmailProvider unavailable; shutting down"); 1971 // Ask for our service to be restarted; this should kick-start the Email process as well 1972 startService(new Intent(this, ExchangeService.class)); 1973 } catch (RuntimeException e) { 1974 // Crash; this is a completely unexpected runtime error 1975 Log.e(TAG, "RuntimeException in ExchangeService", e); 1976 throw e; 1977 } finally { 1978 shutdown(); 1979 } 1980 } 1981 1982 private void shutdown() { 1983 synchronized (sSyncLock) { 1984 // If INSTANCE is null, we've already been shut down 1985 if (INSTANCE != null) { 1986 log("ExchangeService shutting down..."); 1987 1988 // Stop our running syncs 1989 stopServiceThreads(); 1990 1991 // Stop receivers 1992 if (mConnectivityReceiver != null) { 1993 unregisterReceiver(mConnectivityReceiver); 1994 } 1995 if (mBackgroundDataSettingReceiver != null) { 1996 unregisterReceiver(mBackgroundDataSettingReceiver); 1997 } 1998 1999 // Unregister observers 2000 ContentResolver resolver = getContentResolver(); 2001 if (mSyncedMessageObserver != null) { 2002 resolver.unregisterContentObserver(mSyncedMessageObserver); 2003 mSyncedMessageObserver = null; 2004 } 2005 if (mAccountObserver != null) { 2006 resolver.unregisterContentObserver(mAccountObserver); 2007 mAccountObserver = null; 2008 } 2009 if (mMailboxObserver != null) { 2010 resolver.unregisterContentObserver(mMailboxObserver); 2011 mMailboxObserver = null; 2012 } 2013 unregisterCalendarObservers(); 2014 2015 // Clear pending alarms and associated Intents 2016 clearAlarms(); 2017 2018 // Release our wake lock, if we have one 2019 synchronized (mWakeLocks) { 2020 if (mWakeLock != null) { 2021 mWakeLock.release(); 2022 mWakeLock = null; 2023 } 2024 } 2025 2026 INSTANCE = null; 2027 sServiceThread = null; 2028 sStop = false; 2029 log("Goodbye"); 2030 } 2031 } 2032 } 2033 2034 /** 2035 * Release a mailbox from the service map and release its wake lock. 2036 * NOTE: This method MUST be called while holding sSyncLock! 2037 * 2038 * @param mailboxId the id of the mailbox to be released 2039 */ 2040 private void releaseMailbox(long mailboxId) { 2041 mServiceMap.remove(mailboxId); 2042 releaseWakeLock(mailboxId); 2043 } 2044 2045 /** 2046 * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent 2047 * @param c the cursor to an Outbox 2048 * @return true if there is mail to be sent 2049 */ 2050 private boolean hasSendableMessages(Cursor outboxCursor) { 2051 Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION, 2052 EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED, 2053 new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))}, 2054 null); 2055 try { 2056 while (c.moveToNext()) { 2057 if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) { 2058 return true; 2059 } 2060 } 2061 } finally { 2062 if (c != null) { 2063 c.close(); 2064 } 2065 } 2066 return false; 2067 } 2068 2069 /** 2070 * Determine whether the account is allowed to sync automatically, as opposed to manually, based 2071 * on whether the "require manual sync when roaming" policy is in force and applicable 2072 * @param account the account 2073 * @return whether or not the account can sync automatically 2074 */ 2075 /*package*/ static boolean canAutoSync(Account account) { 2076 ExchangeService exchangeService = INSTANCE; 2077 if (exchangeService == null) { 2078 return false; 2079 } 2080 NetworkInfo networkInfo = exchangeService.mNetworkInfo; 2081 2082 // Enforce manual sync only while roaming here 2083 long policyKey = account.mPolicyKey; 2084 // Quick exit from this check 2085 if ((policyKey != 0) && (networkInfo != null) && 2086 (ConnectivityManager.isNetworkTypeMobile(networkInfo.getType()))) { 2087 // We'll cache the Policy data here 2088 Policy policy = account.mPolicy; 2089 if (policy == null) { 2090 policy = Policy.restorePolicyWithId(INSTANCE, policyKey); 2091 account.mPolicy = policy; 2092 } 2093 if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) { 2094 return false; 2095 } 2096 } 2097 return true; 2098 } 2099 2100 /** 2101 * Convenience method to determine whether Email sync is enabled for a given account 2102 * @param account the Account in question 2103 * @return whether Email sync is enabled 2104 */ 2105 private boolean canSyncEmail(android.accounts.Account account) { 2106 return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY); 2107 } 2108 2109 /** 2110 * Determine whether a mailbox of a given type in a given account can be synced automatically 2111 * by ExchangeService. This is an increasingly complex determination, taking into account 2112 * security policies and user settings (both within the Email application and in the Settings 2113 * application) 2114 * 2115 * @param account the Account that the mailbox is in 2116 * @param type the type of the Mailbox 2117 * @return whether or not to start a sync 2118 */ 2119 private boolean isMailboxSyncable(Account account, int type) { 2120 // This 'if' statement performs checks to see whether or not a mailbox is a 2121 // candidate for syncing based on policies, user settings, & other restrictions 2122 if (type == Mailbox.TYPE_OUTBOX || type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) { 2123 // Outbox and account mailbox are always syncable 2124 return true; 2125 } else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) { 2126 // Contacts/Calendar obey this setting from ContentResolver 2127 if (!ContentResolver.getMasterSyncAutomatically()) { 2128 return false; 2129 } 2130 // Get the right authority for the mailbox 2131 String authority; 2132 if (type == Mailbox.TYPE_CONTACTS) { 2133 authority = ContactsContract.AUTHORITY; 2134 } else { 2135 authority = CalendarContract.AUTHORITY; 2136 if (!mCalendarObservers.containsKey(account.mId)){ 2137 // Make sure we have an observer for this Calendar, as 2138 // we need to be able to detect sync state changes, sigh 2139 registerCalendarObserver(account); 2140 } 2141 } 2142 // See if "sync automatically" is set; if not, punt 2143 if (!ContentResolver.getSyncAutomatically(account.mAmAccount, authority)) { 2144 return false; 2145 // See if the calendar is enabled from the Calendar app UI; if not, punt 2146 } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) { 2147 return false; 2148 } 2149 // Never automatically sync trash 2150 } else if (type == Mailbox.TYPE_TRASH) { 2151 return false; 2152 // For non-outbox, non-account mail, we do three checks: 2153 // 1) are we restricted by policy (i.e. manual sync only), 2154 // 2) has the user checked the "Sync Email" box in Account Settings, and 2155 // 3) does the user have the master "background data" box checked in Settings 2156 } else if (!canAutoSync(account) || !canSyncEmail(account.mAmAccount) || !mBackgroundData) { 2157 return false; 2158 } 2159 return true; 2160 } 2161 2162 private long checkMailboxes () { 2163 // First, see if any running mailboxes have been deleted 2164 ArrayList<Long> deletedMailboxes = new ArrayList<Long>(); 2165 synchronized (sSyncLock) { 2166 for (long mailboxId: mServiceMap.keySet()) { 2167 Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId); 2168 if (m == null) { 2169 deletedMailboxes.add(mailboxId); 2170 } 2171 } 2172 // If so, stop them or remove them from the map 2173 for (Long mailboxId: deletedMailboxes) { 2174 AbstractSyncService svc = mServiceMap.get(mailboxId); 2175 if (svc == null || svc.mThread == null) { 2176 releaseMailbox(mailboxId); 2177 continue; 2178 } else { 2179 boolean alive = svc.mThread.isAlive(); 2180 log("Deleted mailbox: " + svc.mMailboxName); 2181 if (alive) { 2182 stopManualSync(mailboxId); 2183 } else { 2184 log("Removing from serviceMap"); 2185 releaseMailbox(mailboxId); 2186 } 2187 } 2188 } 2189 } 2190 2191 long nextWait = EXCHANGE_SERVICE_HEARTBEAT_TIME; 2192 long now = System.currentTimeMillis(); 2193 2194 // Start up threads that need it; use a query which finds eas mailboxes where the 2195 // the sync interval is not "never". This is the set of mailboxes that we control 2196 if (mAccountObserver == null) { 2197 log("mAccountObserver null; service died??"); 2198 return nextWait; 2199 } 2200 2201 Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, 2202 mAccountObserver.getSyncableEasMailboxWhere(), null, null); 2203 if (c == null) throw new ProviderUnavailableException(); 2204 try { 2205 while (c.moveToNext()) { 2206 long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN); 2207 AbstractSyncService service = null; 2208 synchronized (sSyncLock) { 2209 service = mServiceMap.get(mailboxId); 2210 } 2211 if (service == null) { 2212 // Get the cached account 2213 Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN)); 2214 if (account == null) continue; 2215 2216 // We handle a few types of mailboxes specially 2217 int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN); 2218 if (!isMailboxSyncable(account, mailboxType)) { 2219 continue; 2220 } 2221 2222 // Check whether we're in a hold (temporary or permanent) 2223 SyncError syncError = mSyncErrorMap.get(mailboxId); 2224 if (syncError != null) { 2225 // Nothing we can do about fatal errors 2226 if (syncError.fatal) continue; 2227 if (now < syncError.holdEndTime) { 2228 // If release time is earlier than next wait time, 2229 // move next wait time up to the release time 2230 if (syncError.holdEndTime < now + nextWait) { 2231 nextWait = syncError.holdEndTime - now; 2232 mNextWaitReason = "Release hold"; 2233 } 2234 continue; 2235 } else { 2236 // Keep the error around, but clear the end time 2237 syncError.holdEndTime = 0; 2238 } 2239 } 2240 2241 // Otherwise, we use the sync interval 2242 long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN); 2243 if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) { 2244 Mailbox m = EmailContent.getContent(c, Mailbox.class); 2245 requestSync(m, SYNC_PUSH, null); 2246 } else if (mailboxType == Mailbox.TYPE_OUTBOX) { 2247 if (hasSendableMessages(c)) { 2248 Mailbox m = EmailContent.getContent(c, Mailbox.class); 2249 startServiceThread(new EasOutboxService(this, m), m); 2250 } 2251 } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) { 2252 long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN); 2253 long sinceLastSync = now - lastSync; 2254 long toNextSync = syncInterval*MINUTES - sinceLastSync; 2255 String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN); 2256 if (toNextSync <= 0) { 2257 Mailbox m = EmailContent.getContent(c, Mailbox.class); 2258 requestSync(m, SYNC_SCHEDULED, null); 2259 } else if (toNextSync < nextWait) { 2260 nextWait = toNextSync; 2261 if (Eas.USER_LOG) { 2262 log("Next sync for " + name + " in " + nextWait/1000 + "s"); 2263 } 2264 mNextWaitReason = "Scheduled sync, " + name; 2265 } else if (Eas.USER_LOG) { 2266 log("Next sync for " + name + " in " + toNextSync/1000 + "s"); 2267 } 2268 } 2269 } else { 2270 Thread thread = service.mThread; 2271 // Look for threads that have died and remove them from the map 2272 if (thread != null && !thread.isAlive()) { 2273 if (Eas.USER_LOG) { 2274 log("Dead thread, mailbox released: " + 2275 c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN)); 2276 } 2277 releaseMailbox(mailboxId); 2278 // Restart this if necessary 2279 if (nextWait > 3*SECONDS) { 2280 nextWait = 3*SECONDS; 2281 mNextWaitReason = "Clean up dead thread(s)"; 2282 } 2283 } else { 2284 long requestTime = service.mRequestTime; 2285 if (requestTime > 0) { 2286 long timeToRequest = requestTime - now; 2287 if (timeToRequest <= 0) { 2288 service.mRequestTime = 0; 2289 service.alarm(); 2290 } else if (requestTime > 0 && timeToRequest < nextWait) { 2291 if (timeToRequest < 11*MINUTES) { 2292 nextWait = timeToRequest < 250 ? 250 : timeToRequest; 2293 mNextWaitReason = "Sync data change"; 2294 } else { 2295 log("Illegal timeToRequest: " + timeToRequest); 2296 } 2297 } 2298 } 2299 } 2300 } 2301 } 2302 } finally { 2303 c.close(); 2304 } 2305 return nextWait; 2306 } 2307 2308 static public void serviceRequest(long mailboxId, int reason) { 2309 serviceRequest(mailboxId, 5*SECONDS, reason); 2310 } 2311 2312 /** 2313 * Return a boolean indicating whether the mailbox can be synced 2314 * @param m the mailbox 2315 * @return whether or not the mailbox can be synced 2316 */ 2317 public static boolean isSyncable(Mailbox m) { 2318 return m.loadsFromServer(HostAuth.SCHEME_EAS); 2319 } 2320 2321 static public void serviceRequest(long mailboxId, long ms, int reason) { 2322 ExchangeService exchangeService = INSTANCE; 2323 if (exchangeService == null) return; 2324 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 2325 if (m == null || !isSyncable(m)) return; 2326 try { 2327 AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId); 2328 if (service != null) { 2329 service.mRequestTime = System.currentTimeMillis() + ms; 2330 kick("service request"); 2331 } else { 2332 startManualSync(mailboxId, reason, null); 2333 } 2334 } catch (Exception e) { 2335 e.printStackTrace(); 2336 } 2337 } 2338 2339 static public void serviceRequestImmediate(long mailboxId) { 2340 ExchangeService exchangeService = INSTANCE; 2341 if (exchangeService == null) return; 2342 AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId); 2343 if (service != null) { 2344 service.mRequestTime = System.currentTimeMillis(); 2345 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 2346 if (m != null) { 2347 service.mAccount = Account.restoreAccountWithId(exchangeService, m.mAccountKey); 2348 service.mMailbox = m; 2349 kick("service request immediate"); 2350 } 2351 } 2352 } 2353 2354 static public void sendMessageRequest(Request req) { 2355 ExchangeService exchangeService = INSTANCE; 2356 if (exchangeService == null) return; 2357 Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId); 2358 if (msg == null) { 2359 return; 2360 } 2361 long mailboxId = msg.mMailboxKey; 2362 AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId); 2363 2364 if (service == null) { 2365 startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req); 2366 kick("part request"); 2367 } else { 2368 service.addRequest(req); 2369 } 2370 } 2371 2372 /** 2373 * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in 2374 * an error state 2375 * 2376 * @param mailboxId 2377 * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target) 2378 */ 2379 static public int pingStatus(long mailboxId) { 2380 ExchangeService exchangeService = INSTANCE; 2381 if (exchangeService == null) return PING_STATUS_OK; 2382 // Already syncing... 2383 if (exchangeService.mServiceMap.get(mailboxId) != null) { 2384 return PING_STATUS_RUNNING; 2385 } 2386 // No errors or a transient error, don't ping... 2387 SyncError error = exchangeService.mSyncErrorMap.get(mailboxId); 2388 if (error != null) { 2389 if (error.fatal) { 2390 return PING_STATUS_UNABLE; 2391 } else if (error.holdEndTime > 0) { 2392 return PING_STATUS_WAITING; 2393 } 2394 } 2395 return PING_STATUS_OK; 2396 } 2397 2398 static public void startManualSync(long mailboxId, int reason, Request req) { 2399 ExchangeService exchangeService = INSTANCE; 2400 if (exchangeService == null) return; 2401 synchronized (sSyncLock) { 2402 AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId); 2403 if (svc == null) { 2404 exchangeService.mSyncErrorMap.remove(mailboxId); 2405 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 2406 if (m != null) { 2407 log("Starting sync for " + m.mDisplayName); 2408 exchangeService.requestSync(m, reason, req); 2409 } 2410 } else { 2411 // If this is a ui request, set the sync reason for the service 2412 if (reason >= SYNC_CALLBACK_START) { 2413 svc.mSyncReason = reason; 2414 } 2415 } 2416 } 2417 } 2418 2419 // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP 2420 static public void stopManualSync(long mailboxId) { 2421 ExchangeService exchangeService = INSTANCE; 2422 if (exchangeService == null) return; 2423 synchronized (sSyncLock) { 2424 AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId); 2425 if (svc != null) { 2426 log("Stopping sync for " + svc.mMailboxName); 2427 svc.stop(); 2428 svc.mThread.interrupt(); 2429 exchangeService.releaseWakeLock(mailboxId); 2430 } 2431 } 2432 } 2433 2434 /** 2435 * Wake up ExchangeService to check for mailboxes needing service 2436 */ 2437 static public void kick(String reason) { 2438 ExchangeService exchangeService = INSTANCE; 2439 if (exchangeService != null) { 2440 synchronized (exchangeService) { 2441 //INSTANCE.log("Kick: " + reason); 2442 exchangeService.mKicked = true; 2443 exchangeService.notify(); 2444 } 2445 } 2446 if (sConnectivityLock != null) { 2447 synchronized (sConnectivityLock) { 2448 sConnectivityLock.notify(); 2449 } 2450 } 2451 } 2452 2453 static public void accountUpdated(long acctId) { 2454 ExchangeService exchangeService = INSTANCE; 2455 if (exchangeService == null) return; 2456 synchronized (sSyncLock) { 2457 for (AbstractSyncService svc : exchangeService.mServiceMap.values()) { 2458 if (svc.mAccount.mId == acctId) { 2459 svc.mAccount = Account.restoreAccountWithId(exchangeService, acctId); 2460 } 2461 } 2462 } 2463 } 2464 2465 /** 2466 * Tell ExchangeService to remove the mailbox from the map of mailboxes with sync errors 2467 * @param mailboxId the id of the mailbox 2468 */ 2469 static public void removeFromSyncErrorMap(long mailboxId) { 2470 ExchangeService exchangeService = INSTANCE; 2471 if (exchangeService != null) { 2472 exchangeService.mSyncErrorMap.remove(mailboxId); 2473 } 2474 } 2475 2476 private boolean isRunningInServiceThread(long mailboxId) { 2477 AbstractSyncService syncService = mServiceMap.get(mailboxId); 2478 Thread thisThread = Thread.currentThread(); 2479 return syncService != null && syncService.mThread != null && 2480 thisThread == syncService.mThread; 2481 } 2482 2483 /** 2484 * Sent by services indicating that their thread is finished; action depends on the exitStatus 2485 * of the service. 2486 * 2487 * @param svc the service that is finished 2488 */ 2489 static public void done(AbstractSyncService svc) { 2490 ExchangeService exchangeService = INSTANCE; 2491 if (exchangeService == null) return; 2492 synchronized(sSyncLock) { 2493 long mailboxId = svc.mMailboxId; 2494 // If we're no longer the syncing thread for the mailbox, just return 2495 if (!exchangeService.isRunningInServiceThread(mailboxId)) { 2496 return; 2497 } 2498 exchangeService.releaseMailbox(mailboxId); 2499 2500 ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap; 2501 SyncError syncError = errorMap.get(mailboxId); 2502 2503 int exitStatus = svc.mExitStatus; 2504 Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId); 2505 if (m == null) return; 2506 2507 if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) { 2508 long accountId = m.mAccountKey; 2509 Account account = Account.restoreAccountWithId(exchangeService, accountId); 2510 if (account == null) return; 2511 if (exchangeService.releaseSyncHolds(exchangeService, 2512 AbstractSyncService.EXIT_LOGIN_FAILURE, account)) { 2513 new AccountServiceProxy(exchangeService).notifyLoginSucceeded(accountId); 2514 } 2515 } 2516 2517 switch (exitStatus) { 2518 case AbstractSyncService.EXIT_DONE: 2519 if (svc.hasPendingRequests()) { 2520 // TODO Handle this case 2521 } 2522 errorMap.remove(mailboxId); 2523 // If we've had a successful sync, clear the shutdown count 2524 synchronized (ExchangeService.class) { 2525 sClientConnectionManagerShutdownCount = 0; 2526 } 2527 break; 2528 // I/O errors get retried at increasing intervals 2529 case AbstractSyncService.EXIT_IO_ERROR: 2530 if (syncError != null) { 2531 syncError.escalate(); 2532 log(m.mDisplayName + " held for " + syncError.holdDelay + "ms"); 2533 } else { 2534 errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, false)); 2535 log(m.mDisplayName + " added to syncErrorMap, hold for 15s"); 2536 } 2537 break; 2538 // These errors are not retried automatically 2539 case AbstractSyncService.EXIT_LOGIN_FAILURE: 2540 new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey); 2541 // Fall through 2542 case AbstractSyncService.EXIT_SECURITY_FAILURE: 2543 case AbstractSyncService.EXIT_ACCESS_DENIED: 2544 case AbstractSyncService.EXIT_EXCEPTION: 2545 errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true)); 2546 break; 2547 } 2548 kick("sync completed"); 2549 } 2550 } 2551 2552 /** 2553 * Given the status string from a Mailbox, return the type code for the last sync 2554 * @param status the syncStatus column of a Mailbox 2555 * @return 2556 */ 2557 static public int getStatusType(String status) { 2558 if (status == null) { 2559 return -1; 2560 } else { 2561 return status.charAt(STATUS_TYPE_CHAR) - '0'; 2562 } 2563 } 2564 2565 /** 2566 * Given the status string from a Mailbox, return the change count for the last sync 2567 * The change count is the number of adds + deletes + changes in the last sync 2568 * @param status the syncStatus column of a Mailbox 2569 * @return 2570 */ 2571 static public int getStatusChangeCount(String status) { 2572 try { 2573 String s = status.substring(STATUS_CHANGE_COUNT_OFFSET); 2574 return Integer.parseInt(s); 2575 } catch (RuntimeException e) { 2576 return -1; 2577 } 2578 } 2579 2580 static public Context getContext() { 2581 return INSTANCE; 2582 } 2583} 2584