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