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