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