1/*
2 * Copyright (C) 2008-2009 Marc Blank
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.exchange;
19
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.database.ContentObserver;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.provider.CalendarContract;
33import android.provider.CalendarContract.Calendars;
34import android.provider.CalendarContract.Events;
35
36import com.android.emailcommon.Api;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent.Attachment;
39import com.android.emailcommon.provider.EmailContent.MailboxColumns;
40import com.android.emailcommon.provider.EmailContent.Message;
41import com.android.emailcommon.provider.EmailContent.SyncColumns;
42import com.android.emailcommon.provider.HostAuth;
43import com.android.emailcommon.provider.Mailbox;
44import com.android.emailcommon.provider.MailboxUtilities;
45import com.android.emailcommon.provider.ProviderUnavailableException;
46import com.android.emailcommon.service.AccountServiceProxy;
47import com.android.emailcommon.service.IEmailService;
48import com.android.emailcommon.service.IEmailServiceCallback;
49import com.android.emailcommon.service.IEmailServiceCallback.Stub;
50import com.android.emailcommon.service.SearchParams;
51import com.android.emailsync.AbstractSyncService;
52import com.android.emailsync.PartRequest;
53import com.android.emailsync.SyncManager;
54import com.android.exchange.adapter.Search;
55import com.android.exchange.utility.FileLogger;
56import com.android.mail.providers.UIProvider.AccountCapabilities;
57import com.android.mail.utils.LogUtils;
58
59import java.util.concurrent.ConcurrentHashMap;
60
61/**
62 * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync
63 * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it
64 * would be appropriate to use for IMAP push, when that functionality is added to the Email
65 * application.
66 *
67 * The Email application communicates with EAS sync adapters via ExchangeService's binder interface,
68 * which exposes UI-related functionality to the application (see the definitions below)
69 *
70 * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in
71 * order to maintain proper 2-way syncing of data.  (More documentation to follow)
72 *
73 */
74public class ExchangeService extends SyncManager {
75
76    private static final String TAG = Eas.LOG_TAG;
77
78    private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
79        MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
80        Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
81        " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
82    private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
83    private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
84    private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
85
86    // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
87    // The format is S<type_char>:<exit_char>:<change_count>
88    public static final int STATUS_TYPE_CHAR = 1;
89    public static final int STATUS_EXIT_CHAR = 3;
90    public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
91
92    private static final int EAS_12_CAPABILITIES =
93            AccountCapabilities.SYNCABLE_FOLDERS |
94            AccountCapabilities.SERVER_SEARCH |
95            AccountCapabilities.FOLDER_SERVER_SEARCH |
96            AccountCapabilities.SMART_REPLY |
97            AccountCapabilities.SERVER_SEARCH |
98            AccountCapabilities.UNDO;
99
100    private static final int EAS_2_CAPABILITIES =
101            AccountCapabilities.SYNCABLE_FOLDERS |
102            AccountCapabilities.SMART_REPLY |
103            AccountCapabilities.UNDO;
104
105    // We synchronize on this for all actions affecting the service and error maps
106    private static final Object sSyncLock = new Object();
107    private String mEasAccountSelector;
108
109    // Concurrent because CalendarSyncAdapter can modify the map during a wipe
110    private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
111            new ConcurrentHashMap<Long, CalendarObserver>();
112
113    private final Intent mIntent = new Intent(Eas.EXCHANGE_SERVICE_INTENT_ACTION);
114
115    /**
116     * Create our EmailService implementation here.
117     */
118    private final IEmailService.Stub mBinder = new IEmailService.Stub() {
119
120        @Override
121        public int getApiLevel() {
122            return Api.LEVEL;
123        }
124
125        @Override
126        public Bundle validate(HostAuth hostAuth) throws RemoteException {
127            return AbstractSyncService.validate(EasSyncService.class,
128                    hostAuth, ExchangeService.this);
129        }
130
131        @Override
132        public Bundle autoDiscover(String userName, String password) throws RemoteException {
133            HostAuth hostAuth = new HostAuth();
134            hostAuth.mLogin = userName;
135            hostAuth.mPassword = password;
136            hostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL;
137            hostAuth.mPort = 443;
138            return new EasSyncService().tryAutodiscover(ExchangeService.this, hostAuth);
139        }
140
141        /**
142         * This is the remote call from the Email app, currently unused.
143         * TODO: remove this when it's been deleted from IEmailService.aidl.
144         */
145        @Deprecated
146        @Override
147        public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount)
148                throws RemoteException {
149            SyncManager exchangeService = INSTANCE;
150            if (exchangeService == null) return;
151            checkExchangeServiceServiceRunning();
152            Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
153            if (m == null) return;
154            Account acct = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
155            if (acct == null) return;
156            // If this is a user request and we're being held, release the hold; this allows us to
157            // try again (the hold might have been specific to this account and released already)
158            if (userRequest) {
159                if (onSyncDisabledHold(acct)) {
160                    releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_ACCESS_DENIED, acct);
161                    log("User requested sync of account in sync disabled hold; releasing");
162                } else if (onSecurityHold(acct)) {
163                    releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_SECURITY_FAILURE,
164                            acct);
165                    log("User requested sync of account in security hold; releasing");
166                }
167                if (sConnectivityHold) {
168                    return;
169                }
170            }
171            if (m.mType == Mailbox.TYPE_OUTBOX) {
172                // We're using SERVER_ID to indicate an error condition (it has no other use for
173                // sent mail)  Upon request to sync the Outbox, we clear this so that all messages
174                // are candidates for sending.
175                ContentValues cv = new ContentValues();
176                cv.put(SyncColumns.SERVER_ID, 0);
177                exchangeService.getContentResolver().update(Message.CONTENT_URI,
178                    cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
179                // Clear the error state; the Outbox sync will be started from checkMailboxes
180                exchangeService.mSyncErrorMap.remove(mailboxId);
181                kick("start outbox");
182                // Outbox can't be synced in EAS
183                return;
184            } else if (!isSyncable(m)) {
185                return;
186            }
187            startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST :
188                ExchangeService.SYNC_SERVICE_START_SYNC, null);
189        }
190
191        @Override
192        public void stopSync(long mailboxId) throws RemoteException {
193            stopManualSync(mailboxId);
194        }
195
196        @Override
197        public void loadAttachment(final IEmailServiceCallback callback, final long attachmentId,
198                final boolean background) throws RemoteException {
199            Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
200            log("loadAttachment " + attachmentId + ": " + att.mFileName);
201            sendMessageRequest(new PartRequest(att, null, null));
202        }
203
204        @Override
205        public void updateFolderList(long accountId) throws RemoteException {
206            reloadFolderList(ExchangeService.this, accountId, false);
207        }
208
209        @Override
210        public void hostChanged(long accountId) throws RemoteException {
211            SyncManager exchangeService = INSTANCE;
212            if (exchangeService == null) return;
213            ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
214            // Go through the various error mailboxes
215            for (long mailboxId: syncErrorMap.keySet()) {
216                SyncError error = syncErrorMap.get(mailboxId);
217                // If it's a login failure, look a little harder
218                Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
219                // If it's for the account whose host has changed, clear the error
220                // If the mailbox is no longer around, remove the entry in the map
221                if (m == null) {
222                    syncErrorMap.remove(mailboxId);
223                } else if (error != null && m.mAccountKey == accountId) {
224                    error.fatal = false;
225                    error.holdEndTime = 0;
226                }
227            }
228            // Stop any running syncs
229            exchangeService.stopAccountSyncs(accountId, true);
230            // Kick ExchangeService
231            kick("host changed");
232        }
233
234        @Override
235        public void setLogging(int flags) throws RemoteException {
236            // Protocol logging
237            Eas.setUserDebug(flags);
238            // Sync logging
239            setUserDebug(flags);
240        }
241
242        @Override
243        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
244            sendMessageRequest(new MeetingResponseRequest(messageId, response));
245        }
246
247        @Override
248        public void loadMore(long messageId) throws RemoteException {
249        }
250
251        // The following three methods are not implemented in this version
252        @Override
253        public boolean createFolder(long accountId, String name) throws RemoteException {
254            return false;
255        }
256
257        @Override
258        public boolean deleteFolder(long accountId, String name) throws RemoteException {
259            return false;
260        }
261
262        @Override
263        public boolean renameFolder(long accountId, String oldName, String newName)
264                throws RemoteException {
265            return false;
266        }
267
268        /**
269         * Delete PIM (calendar, contacts) data for the specified account
270         *
271         * @param emailAddress the email address for the account whose data should be deleted
272         * @throws RemoteException
273         */
274        @Override
275        public void deleteAccountPIMData(final String emailAddress) throws RemoteException {
276            // ExchangeService is deprecated so I am deleting rather than fixing this function.
277        }
278
279        @Override
280        public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
281            SyncManager exchangeService = INSTANCE;
282            if (exchangeService == null) return 0;
283            return Search.searchMessages(exchangeService, accountId, searchParams,
284                    destMailboxId);
285        }
286
287        @Override
288        public void sendMail(long accountId) throws RemoteException {
289        }
290
291        @Override
292        public int getCapabilities(Account acct) throws RemoteException {
293            String easVersion = acct.mProtocolVersion;
294            Double easVersionDouble = 2.5D;
295            if (easVersion != null) {
296                try {
297                    easVersionDouble = Double.parseDouble(easVersion);
298                } catch (NumberFormatException e) {
299                    // Stick with 2.5
300                }
301            }
302            if (easVersionDouble >= 12.0D) {
303                return EAS_12_CAPABILITIES;
304            } else {
305                return EAS_2_CAPABILITIES;
306            }
307        }
308
309        @Override
310        public void serviceUpdated(String emailAddress) throws RemoteException {
311            // Not required for EAS
312        }
313    };
314
315    /**
316     * Return a list of all Accounts in EmailProvider.  Because the result of this call may be used
317     * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
318     * @param context the caller's context
319     * @param accounts a list that Accounts will be added into
320     * @return the list of Accounts
321     * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
322     */
323    @Override
324    public AccountList collectAccounts(Context context, AccountList accounts) {
325        ContentResolver resolver = context.getContentResolver();
326        Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
327                null);
328        // We must throw here; callers might use the information we provide for reconciliation, etc.
329        if (c == null) throw new ProviderUnavailableException();
330        try {
331            ContentValues cv = new ContentValues();
332            while (c.moveToNext()) {
333                long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
334                if (hostAuthId > 0) {
335                    HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
336                    if (ha != null && ha.mProtocol.equals(Eas.PROTOCOL)) {
337                        Account account = new Account();
338                        account.restore(c);
339                        // Cache the HostAuth
340                        account.mHostAuthRecv = ha;
341                        accounts.add(account);
342                        // Fixup flags for inbox (should accept moved mail)
343                        Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId,
344                                Mailbox.TYPE_INBOX);
345                        if (inbox != null &&
346                                ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) {
347                            cv.put(MailboxColumns.FLAGS,
348                                    inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL);
349                            resolver.update(
350                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv,
351                                    null, null);
352                        }
353                    }
354                }
355            }
356        } finally {
357            c.close();
358        }
359        return accounts;
360    }
361
362    public static boolean onSecurityHold(Account account) {
363        return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
364    }
365
366    private static boolean onSyncDisabledHold(Account account) {
367        return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0;
368    }
369
370    private static Uri eventsAsSyncAdapter(final Uri uri, final String account,
371            final String accountType) {
372        return uri.buildUpon()
373                .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
374                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
375                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
376    }
377
378    /**
379     * Unregister all CalendarObserver's
380     */
381    static public void unregisterCalendarObservers() {
382        ExchangeService exchangeService = (ExchangeService)INSTANCE;
383        if (exchangeService == null) return;
384        ContentResolver resolver = exchangeService.mResolver;
385        for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) {
386            resolver.unregisterContentObserver(observer);
387        }
388        exchangeService.mCalendarObservers.clear();
389    }
390
391    private class CalendarObserver extends ContentObserver {
392        long mAccountId;
393        long mCalendarId;
394        long mSyncEvents;
395        String mAccountName;
396
397        public CalendarObserver(Handler handler, Account account) {
398            super(handler);
399            mAccountId = account.mId;
400            mAccountName = account.mEmailAddress;
401
402            // Find the Calendar for this account
403            Cursor c = mResolver.query(Calendars.CONTENT_URI,
404                    new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
405                    CALENDAR_SELECTION,
406                    new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE},
407                    null);
408            if (c != null) {
409                // Save its id and its sync events status
410                try {
411                    if (c.moveToFirst()) {
412                        mCalendarId = c.getLong(0);
413                        mSyncEvents = c.getLong(1);
414                    }
415                } finally {
416                    c.close();
417                }
418            }
419        }
420
421        private void onChangeInBackground() {
422            try {
423                Cursor c = mResolver.query(Calendars.CONTENT_URI,
424                        new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
425                        new String[] {Long.toString(mCalendarId)}, null);
426                if (c == null) return;
427                // Get its sync events; if it's changed, we've got work to do
428                try {
429                    if (c.moveToFirst()) {
430                        long newSyncEvents = c.getLong(0);
431                        if (newSyncEvents != mSyncEvents) {
432                            log("_sync_events changed for calendar in " + mAccountName);
433                            Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
434                                    mAccountId, Mailbox.TYPE_CALENDAR);
435                            // Sanity check for mailbox deletion
436                            if (mailbox == null) return;
437                            ContentValues cv = new ContentValues();
438                            if (newSyncEvents == 0) {
439                                // When sync is disabled, we're supposed to delete
440                                // all events in the calendar
441                                log("Deleting events and setting syncKey to 0 for " +
442                                        mAccountName);
443                                // First, stop any sync that's ongoing
444                                stopManualSync(mailbox.mId);
445                                // Set the syncKey to 0 (reset)
446                                EasSyncService service =
447                                    EasSyncService.getServiceForMailbox(
448                                            INSTANCE, mailbox);
449
450                                // CalendarSyncAdapter is gone, and this class is deprecated.
451                                // Just leaving this commented out code here for reference:
452                                // Reset the sync key locally and stop syncing
453//                                CalendarSyncAdapter adapter =
454//                                    new CalendarSyncAdapter(service);
455//                                try {
456//                                    adapter.setSyncKey("0", false);
457//                                } catch (IOException e) {
458//                                    // The provider can't be reached; nothing to be done
459//                                }
460
461                                cv.put(Mailbox.SYNC_KEY, "0");
462                                cv.put(Mailbox.SYNC_INTERVAL,
463                                        Mailbox.CHECK_INTERVAL_NEVER);
464                                mResolver.update(ContentUris.withAppendedId(
465                                        Mailbox.CONTENT_URI, mailbox.mId), cv, null,
466                                        null);
467                                // Delete all events using the sync adapter
468                                // parameter so that the deletion is only local
469                                Uri eventsAsSyncAdapter = eventsAsSyncAdapter(Events.CONTENT_URI,
470                                        mAccountName, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
471                                mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
472                                        new String[] {Long.toString(mCalendarId)});
473                            } else {
474                                // Make this a push mailbox and kick; this will start
475                                // a resync of the Calendar; the account mailbox will
476                                // ping on this during the next cycle of the ping loop
477                                cv.put(Mailbox.SYNC_INTERVAL,
478                                        Mailbox.CHECK_INTERVAL_PUSH);
479                                mResolver.update(ContentUris.withAppendedId(
480                                        Mailbox.CONTENT_URI, mailbox.mId), cv, null,
481                                        null);
482                                kick("calendar sync changed");
483                            }
484
485                            // Save away the new value
486                            mSyncEvents = newSyncEvents;
487                        }
488                    }
489                } finally {
490                    c.close();
491                }
492            } catch (ProviderUnavailableException e) {
493                LogUtils.w(TAG, "Observer failed; provider unavailable");
494            }
495        }
496
497
498        @Override
499        public synchronized void onChange(boolean selfChange) {
500            // See if the user has changed syncing of our calendar
501            if (!selfChange) {
502                new Thread(new Runnable() {
503                    @Override
504                    public void run() {
505                        onChangeInBackground();
506                    }
507                }, "Calendar Observer").start();
508            }
509        }
510    }
511
512    /**
513     * Blocking call to the account reconciler
514     */
515    @Override
516    public void runAccountReconcilerSync(Context context) {
517        alwaysLog("Reconciling accounts...");
518        new AccountServiceProxy(context).reconcileAccounts(
519                Eas.PROTOCOL, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
520    }
521
522    public static void log(String str) {
523        log(TAG, str);
524    }
525
526    public static void log(String tag, String str) {
527        if (Eas.USER_LOG) {
528            LogUtils.d(tag, str);
529            if (Eas.FILE_LOG) {
530                FileLogger.log(tag, str);
531            }
532        }
533    }
534
535    public static void alwaysLog(String str) {
536        if (!Eas.USER_LOG) {
537            LogUtils.d(TAG, str);
538        } else {
539            log(str);
540        }
541    }
542
543    /**
544     * EAS requires a unique device id, so that sync is possible from a variety of different
545     * devices (e.g. the syncKey is specific to a device)  If we're on an emulator or some other
546     * device that doesn't provide one, we can create it as "device".
547     * This would work on a real device as well, but it would be better to use the "real" id if
548     * it's available
549     */
550    static public String getDeviceId(Context context) {
551        if (sDeviceId == null) {
552            sDeviceId = new AccountServiceProxy(context).getDeviceId();
553            alwaysLog("Received deviceId from Email app: " + sDeviceId);
554        }
555        return sDeviceId;
556    }
557
558    @Override
559    public IBinder onBind(Intent arg0) {
560        return mBinder;
561    }
562
563    static private void reloadFolderListFailed(long accountId) {
564
565    }
566
567    static public void reloadFolderList(Context context, long accountId, boolean force) {
568        SyncManager exchangeService = INSTANCE;
569        if (exchangeService == null) return;
570        Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
571                Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
572                MailboxColumns.TYPE + "=?",
573                new String[] {Long.toString(accountId),
574                    Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
575        try {
576            if (c.moveToFirst()) {
577                synchronized(sSyncLock) {
578                    Mailbox mailbox = new Mailbox();
579                    mailbox.restore(c);
580                    Account acct = Account.restoreAccountWithId(context, accountId);
581                    if (acct == null) {
582                        reloadFolderListFailed(accountId);
583                        return;
584                    }
585                    String syncKey = acct.mSyncKey;
586                    // No need to reload the list if we don't have one
587                    if (!force && (syncKey == null || syncKey.equals("0"))) {
588                        reloadFolderListFailed(accountId);
589                        return;
590                    }
591
592                    // Change all ping/push boxes to push/hold
593                    ContentValues cv = new ContentValues();
594                    cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
595                    context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
596                            WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
597                            new String[] {Long.toString(accountId)});
598                    log("Set push/ping boxes to push/hold");
599
600                    long id = mailbox.mId;
601                    AbstractSyncService svc = exchangeService.mServiceMap.get(id);
602                    // Tell the service we're done
603                    if (svc != null) {
604                        synchronized (svc.getSynchronizer()) {
605                            svc.stop();
606                            // Interrupt the thread so that it can stop
607                            Thread thread = svc.mThread;
608                            if (thread != null) {
609                                thread.setName(thread.getName() + " (Stopped)");
610                                thread.interrupt();
611                            }
612                        }
613                        // Abandon the service
614                        exchangeService.releaseMailbox(id);
615                        // And have it start naturally
616                        kick("reload folder list");
617                    }
618                }
619            }
620        } finally {
621            c.close();
622        }
623    }
624
625    /**
626     * Informs ExchangeService that an account has a new folder list; as a result, any existing
627     * folder might have become invalid.  Therefore, we act as if the account has been deleted, and
628     * then we reinitialize it.
629     *
630     * @param acctId
631     */
632    static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
633        SyncManager exchangeService = INSTANCE;
634        if (exchangeService != null) {
635            exchangeService.stopAccountSyncs(acctId, false);
636            kick("reload folder list");
637        }
638    }
639
640    /**
641     * Start up the ExchangeService service if it's not already running
642     * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
643     * com.android.email) and hasn't been restarted. See the comment for onCreate for details
644     */
645    static void checkExchangeServiceServiceRunning() {
646        SyncManager exchangeService = INSTANCE;
647        if (exchangeService == null) return;
648        if (sServiceThread == null) {
649            log("!!! checkExchangeServiceServiceRunning; starting service...");
650            exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
651        }
652    }
653
654    @Override
655    public AccountObserver getAccountObserver(
656            Handler handler) {
657        return new AccountObserver(handler) {
658            @Override
659            public void newAccount(long acctId) {
660                Account acct = Account.restoreAccountWithId(getContext(), acctId);
661                if (acct == null) {
662                    // This account is in a bad state; don't create the mailbox.
663                    LogUtils.e(TAG, "Cannot initialize bad acctId: " + acctId);
664                    return;
665                }
666                Mailbox main = new Mailbox();
667                main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
668                main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
669                main.mAccountKey = acct.mId;
670                main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
671                main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
672                main.mFlagVisible = false;
673                main.save(getContext());
674                log("Initializing account: " + acct.mDisplayName);
675            }
676        };
677    }
678
679    @Override
680    public void onStartup() {
681        // Do any required work to clean up our Mailboxes (this serves to upgrade
682        // mailboxes that existed prior to EmailProvider database version 17)
683        MailboxUtilities.fixupUninitializedParentKeys(this, getAccountsSelector());
684    }
685
686    @Override
687    public AbstractSyncService getServiceForMailbox(Context context,
688            Mailbox m) {
689        switch(m.mType) {
690            case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
691                return new EasAccountService(context, m);
692            case Mailbox.TYPE_OUTBOX:
693                return new EasOutboxService(context, m);
694            default:
695                return new EasSyncService(context, m);
696        }
697    }
698
699    @Override
700    public String getAccountsSelector() {
701        if (mEasAccountSelector == null) {
702            StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
703            boolean first = true;
704            synchronized (mAccountList) {
705                for (Account account : mAccountList) {
706                    if (!first) {
707                        sb.append(',');
708                    } else {
709                        first = false;
710                    }
711                    sb.append(account.mId);
712                }
713            }
714            sb.append(')');
715            mEasAccountSelector = sb.toString();
716        }
717        return mEasAccountSelector;
718    }
719
720    @Override
721    public String getAccountManagerType() {
722        return Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
723    }
724
725    @Override
726    public Intent getServiceIntent() {
727        return mIntent;
728    }
729
730    @Override
731    public Stub getCallbackProxy() {
732        return null;
733    }
734
735    /**
736     * Stop any ping in progress if required
737     *
738     * @param mailbox whose service has started
739     */
740    @Override
741    public void onStartService(Mailbox mailbox) {
742        // If this is a ping mailbox, stop the ping
743        if (mailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PING) return;
744        long accountMailboxId = Mailbox.findMailboxOfType(this, mailbox.mAccountKey,
745                Mailbox.TYPE_EAS_ACCOUNT_MAILBOX);
746        // If our ping is running, stop it
747        final AbstractSyncService svc = getRunningService(accountMailboxId);
748        if (svc != null) {
749            log("Stopping ping due to sync of mailbox: " + mailbox.mDisplayName);
750            // Don't block; reset might perform network activity
751            new Thread(new Runnable() {
752                @Override
753                public void run() {
754                    svc.reset();
755                }}).start();
756        }
757    }
758}
759