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