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