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