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