Home | History | Annotate | Download | only in exchange
      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