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