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