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