MailService.java revision 002a1802cad235ffa8f7152e0d0a5a2ebad14f63
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.service;
18
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.app.AlarmManager;
22import android.app.PendingIntent;
23import android.app.Service;
24import android.content.ContentResolver;
25import android.content.ContentUris;
26import android.content.Context;
27import android.content.Intent;
28import android.content.SyncStatusObserver;
29import android.database.Cursor;
30import android.net.ConnectivityManager;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.IBinder;
34import android.os.SystemClock;
35import android.text.TextUtils;
36import android.util.Log;
37
38import com.android.email.Controller;
39import com.android.email.Email;
40import com.android.email.Preferences;
41import com.android.email.SingleRunningTask;
42import com.android.email.provider.AccountReconciler;
43import com.android.emailcommon.AccountManagerTypes;
44import com.android.emailcommon.mail.MessagingException;
45import com.android.emailcommon.provider.Account;
46import com.android.emailcommon.provider.EmailContent;
47import com.android.emailcommon.provider.HostAuth;
48import com.android.emailcommon.provider.Mailbox;
49import com.android.emailcommon.utility.EmailAsyncTask;
50import com.google.common.annotations.VisibleForTesting;
51
52import java.util.ArrayList;
53import java.util.HashMap;
54import java.util.List;
55
56/**
57 * Background service for refreshing non-push email accounts.
58 *
59 * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid
60 * possible problems with out-of-order startId processing.
61 */
62public class MailService extends Service {
63    private static final String LOG_TAG = "Email-MailService";
64
65    private static final String ACTION_CHECK_MAIL =
66        "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
67    private static final String ACTION_RESCHEDULE =
68        "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
69    private static final String ACTION_CANCEL =
70        "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
71    private static final String ACTION_SEND_PENDING_MAIL =
72        "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
73    private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS =
74        "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS";
75
76    private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
77    private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
78    private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
79
80    /** Time between watchdog checks; in milliseconds */
81    private static final long WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
82
83    /** Sentinel value asking to update mSyncReports if it's currently empty */
84    @VisibleForTesting
85    static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
86    /** Sentinel value asking that mSyncReports be rebuilt */
87    @VisibleForTesting
88    static final int SYNC_REPORTS_RESET = -2;
89
90    @VisibleForTesting
91    Controller mController;
92    private final Controller.Result mControllerCallback = new ControllerResults();
93    private ContentResolver mContentResolver;
94    private Context mContext;
95
96    private int mStartId;
97
98    /**
99     * Access must be synchronized, because there are accesses from the Controller callback
100     */
101    /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
102        new HashMap<Long,AccountSyncReport>();
103
104    public static void actionReschedule(Context context) {
105        Intent i = new Intent();
106        i.setClass(context, MailService.class);
107        i.setAction(MailService.ACTION_RESCHEDULE);
108        context.startService(i);
109    }
110
111    public static void actionCancel(Context context)  {
112        Intent i = new Intent();
113        i.setClass(context, MailService.class);
114        i.setAction(MailService.ACTION_CANCEL);
115        context.startService(i);
116    }
117
118    public static void actionDeleteExchangeAccounts(Context context)  {
119        Intent i = new Intent();
120        i.setClass(context, MailService.class);
121        i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS);
122        context.startService(i);
123    }
124
125    /**
126     * Entry point for AttachmentDownloadService to ask that pending mail be sent
127     * @param context the caller's context
128     * @param accountId the account whose pending mail should be sent
129     */
130    public static void actionSendPendingMail(Context context, long accountId)  {
131        Intent i = new Intent();
132        i.setClass(context, MailService.class);
133        i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
134        i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
135        context.startService(i);
136    }
137
138    @Override
139    public int onStartCommand(final Intent intent, int flags, final int startId) {
140        super.onStartCommand(intent, flags, startId);
141
142        EmailAsyncTask.runAsyncParallel(new Runnable() {
143            @Override
144            public void run() {
145                reconcilePopImapAccountsSync(MailService.this);
146            }
147        });
148
149        // TODO this needs to be passed through the controller and back to us
150        mStartId = startId;
151        String action = intent.getAction();
152        final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
153
154        mController = Controller.getInstance(this);
155        mController.addResultCallback(mControllerCallback);
156        mContentResolver = getContentResolver();
157        mContext = this;
158
159        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
160
161        if (ACTION_CHECK_MAIL.equals(action)) {
162            // DB access required to satisfy this intent, so offload from UI thread
163            EmailAsyncTask.runAsyncParallel(new Runnable() {
164                @Override
165                public void run() {
166                    // If we have the data, restore the last-sync-times for each account
167                    // These are cached in the wakeup intent in case the process was killed.
168                    restoreSyncReports(intent);
169
170                    // Sync a specific account if given
171                    if (Email.DEBUG) {
172                        Log.d(LOG_TAG, "action: check mail for id=" + accountId);
173                    }
174                    if (accountId >= 0) {
175                        setWatchdog(accountId, alarmManager);
176                    }
177
178                    // Start sync if account is given && bg data enabled && account has sync enabled
179                    boolean syncStarted = false;
180                    if (accountId != -1 && isBackgroundDataEnabled()) {
181                        synchronized(mSyncReports) {
182                            for (AccountSyncReport report: mSyncReports.values()) {
183                                if (report.accountId == accountId) {
184                                    if (report.syncEnabled) {
185                                        syncStarted = syncOneAccount(mController, accountId,
186                                                startId);
187                                    }
188                                    break;
189                                }
190                            }
191                        }
192                    }
193
194                    // Reschedule if we didn't start sync.
195                    if (!syncStarted) {
196                        // Prevent runaway on the current account by pretending it updated
197                        if (accountId != -1) {
198                            updateAccountReport(accountId, 0);
199                        }
200                        // Find next account to sync, and reschedule
201                        reschedule(alarmManager);
202                        // Stop the service, unless actually syncing (which will stop the service)
203                        stopSelf(startId);
204                    }
205                }
206            });
207        }
208        else if (ACTION_CANCEL.equals(action)) {
209            if (Email.DEBUG) {
210                Log.d(LOG_TAG, "action: cancel");
211            }
212            cancel();
213            stopSelf(startId);
214        }
215        else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) {
216            if (Email.DEBUG) {
217                Log.d(LOG_TAG, "action: delete exchange accounts");
218            }
219            EmailAsyncTask.runAsyncParallel(new Runnable() {
220                @Override
221                public void run() {
222                    Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION,
223                            null, null, null);
224                    try {
225                        while (c.moveToNext()) {
226                            long accountId = c.getLong(Account.ID_PROJECTION_COLUMN);
227                            if ("eas".equals(Account.getProtocol(mContext, accountId))) {
228                                // Always log this
229                                Log.d(LOG_TAG, "Deleting EAS account: " + accountId);
230                                mController.deleteAccountSync(accountId, mContext);
231                            }
232                       }
233                    } finally {
234                        c.close();
235                    }
236                }
237            });
238            stopSelf(startId);
239        }
240        else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
241            if (Email.DEBUG) {
242                Log.d(LOG_TAG, "action: send pending mail");
243            }
244            EmailAsyncTask.runAsyncParallel(new Runnable() {
245                @Override
246                public void run() {
247                    mController.sendPendingMessages(accountId);
248                }
249            });
250            stopSelf(startId);
251        }
252        else if (ACTION_RESCHEDULE.equals(action)) {
253            if (Email.DEBUG) {
254                Log.d(LOG_TAG, "action: reschedule");
255            }
256            // DB access required to satisfy this intent, so offload from UI thread
257            EmailAsyncTask.runAsyncParallel(new Runnable() {
258                @Override
259                public void run() {
260                    // When called externally, we refresh the sync reports table to pick up
261                    // any changes in the account list or account settings
262                    refreshSyncReports();
263                    // Finally, scan for the next needing update, and set an alarm for it
264                    reschedule(alarmManager);
265                    stopSelf(startId);
266                }
267            });
268        }
269
270        // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
271        // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
272        // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
273        // will fire and get things running again.
274        return START_NOT_STICKY;
275    }
276
277    @Override
278    public IBinder onBind(Intent intent) {
279        return null;
280    }
281
282    @Override
283    public void onDestroy() {
284        super.onDestroy();
285        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
286    }
287
288    private void cancel() {
289        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
290        PendingIntent pi = createAlarmIntent(-1, null, false);
291        alarmMgr.cancel(pi);
292    }
293
294    /**
295     * Refresh the sync reports, to pick up any changes in the account list or account settings.
296     */
297    private void refreshSyncReports() {
298        synchronized (mSyncReports) {
299            // Make shallow copy of sync reports so we can recover the prev sync times
300            HashMap<Long,AccountSyncReport> oldSyncReports =
301                new HashMap<Long,AccountSyncReport>(mSyncReports);
302
303            // Delete the sync reports to force a refresh from live account db data
304            setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
305
306            // Restore prev-sync & next-sync times for any reports in the new list
307            for (AccountSyncReport newReport : mSyncReports.values()) {
308                AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
309                if (oldReport != null) {
310                    newReport.prevSyncTime = oldReport.prevSyncTime;
311                    newReport.setNextSyncTime();
312                }
313            }
314        }
315    }
316
317    /**
318     * Create and send an alarm with the entire list.  This also sends a list of known last-sync
319     * times with the alarm, so if we are killed between alarms, we don't lose this info.
320     *
321     * @param alarmMgr passed in so we can mock for testing.
322     */
323    private void reschedule(AlarmManager alarmMgr) {
324        // restore the reports if lost
325        setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
326        synchronized (mSyncReports) {
327            int numAccounts = mSyncReports.size();
328            long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
329            int accountInfoIndex = 0;
330
331            long nextCheckTime = Long.MAX_VALUE;
332            AccountSyncReport nextAccount = null;
333            long timeNow = SystemClock.elapsedRealtime();
334
335            for (AccountSyncReport report : mSyncReports.values()) {
336                if (report.syncInterval <= 0) {                         // no timed checks - skip
337                    continue;
338                }
339                long prevSyncTime = report.prevSyncTime;
340                long nextSyncTime = report.nextSyncTime;
341
342                // select next account to sync
343                if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
344                    nextCheckTime = 0;
345                    nextAccount = report;
346                } else if (nextSyncTime < nextCheckTime) {              // next to be checked
347                    nextCheckTime = nextSyncTime;
348                    nextAccount = report;
349                }
350                // collect last-sync-times for all accounts
351                // this is using pairs of {long,long} to simplify passing in a bundle
352                accountInfo[accountInfoIndex++] = report.accountId;
353                accountInfo[accountInfoIndex++] = report.prevSyncTime;
354            }
355
356            // Clear out any unused elements in the array
357            while (accountInfoIndex < accountInfo.length) {
358                accountInfo[accountInfoIndex++] = -1;
359            }
360
361            // set/clear alarm as needed
362            long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
363            PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
364
365            if (nextAccount == null) {
366                alarmMgr.cancel(pi);
367                if (Email.DEBUG) {
368                    Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
369                }
370            } else {
371                alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
372                if (Email.DEBUG) {
373                    Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
374                            + " for " + nextAccount);
375                }
376            }
377        }
378    }
379
380    /**
381     * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
382     * killed by the system due to memory pressure.)  Normally, a mail check will complete and
383     * the watchdog will be replaced by the call to reschedule().
384    * @param accountId the account we were trying to check
385     * @param alarmMgr system alarm manager
386     */
387    private void setWatchdog(long accountId, AlarmManager alarmMgr) {
388        PendingIntent pi = createAlarmIntent(accountId, null, true);
389        long timeNow = SystemClock.elapsedRealtime();
390        long nextCheckTime = timeNow + WATCHDOG_DELAY;
391        alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
392    }
393
394    /**
395     * Return a pending intent for use by this alarm.  Most of the fields must be the same
396     * (in order for the intent to be recognized by the alarm manager) but the extras can
397     * be different, and are passed in here as parameters.
398     */
399    private PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) {
400        Intent i = new Intent();
401        i.setClass(this, MailService.class);
402        i.setAction(ACTION_CHECK_MAIL);
403        i.putExtra(EXTRA_ACCOUNT, checkId);
404        i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
405        if (isWatchdog) {
406            i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
407        }
408        PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
409        return pi;
410    }
411
412    /**
413     * Start a controller sync for a specific account
414     *
415     * @param controller The controller to do the sync work
416     * @param checkAccountId the account Id to try and check
417     * @param startId the id of this service launch
418     * @return true if mail checking has started, false if it could not (e.g. bad account id)
419     */
420    private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
421        long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
422        if (inboxId == Mailbox.NO_MAILBOX) {
423            return false;
424        } else {
425            controller.serviceCheckMail(checkAccountId, inboxId, startId);
426            return true;
427        }
428    }
429
430    /**
431     * Note:  Times are relative to SystemClock.elapsedRealtime()
432     *
433     * TODO:  Look more closely at syncEnabled and see if we can simply coalesce it into
434     * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
435     */
436    @VisibleForTesting
437    static class AccountSyncReport {
438        long accountId;
439        /** The time of the last sync, or, {@code 0}, the last sync time is unknown. */
440        long prevSyncTime;
441        /** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */
442        long nextSyncTime;
443        /** Minimum time between syncs; in minutes. */
444        int syncInterval;
445        /** If {@code true}, auto sync is enabled. */
446        boolean syncEnabled;
447
448        /**
449         * Sets the next sync time using the previous sync time and sync interval.
450         */
451        private void setNextSyncTime() {
452            if (syncInterval > 0 && prevSyncTime != 0) {
453                nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60);
454            }
455        }
456
457        @Override
458        public String toString() {
459            return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime;
460        }
461    }
462
463    /**
464     * scan accounts to create a list of { acct, prev sync, next sync, #new }
465     * use this to create a fresh copy.  assumes all accounts need sync
466     *
467     * @param accountId -1 will rebuild the list if empty.  other values will force loading
468     *   of a single account (e.g if it was created after the original list population)
469     */
470    private void setupSyncReports(long accountId) {
471        synchronized (mSyncReports) {
472            setupSyncReportsLocked(accountId, mContext);
473        }
474    }
475
476    /**
477     * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
478     */
479    @VisibleForTesting
480    void setupSyncReportsLocked(long accountId, Context context) {
481        ContentResolver resolver = context.getContentResolver();
482        if (accountId == SYNC_REPORTS_RESET) {
483            // For test purposes, force refresh of mSyncReports
484            mSyncReports.clear();
485            accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
486        } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
487            // -1 == reload the list if empty, otherwise exit immediately
488            if (mSyncReports.size() > 0) {
489                return;
490            }
491        } else {
492            // load a single account if it doesn't already have a sync record
493            if (mSyncReports.containsKey(accountId)) {
494                return;
495            }
496        }
497
498        // setup to add a single account or all accounts
499        Uri uri;
500        if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
501            uri = Account.CONTENT_URI;
502        } else {
503            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
504        }
505
506        final boolean oneMinuteRefresh
507                = Preferences.getPreferences(this).getForceOneMinuteRefresh();
508        if (oneMinuteRefresh) {
509            Log.w(LOG_TAG, "One-minute refresh enabled.");
510        }
511
512        // We use a full projection here because we'll restore each account object from it
513        Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
514        try {
515            while (c.moveToNext()) {
516                Account account = Account.getContent(c, Account.class);
517                // The following sanity checks are primarily for the sake of ignoring non-user
518                // accounts that may have been left behind e.g. by failed unit tests.
519                // Properly-formed accounts will always pass these simple checks.
520                if (TextUtils.isEmpty(account.mEmailAddress)
521                        || account.mHostAuthKeyRecv <= 0
522                        || account.mHostAuthKeySend <= 0) {
523                    continue;
524                }
525
526                // The account is OK, so proceed
527                AccountSyncReport report = new AccountSyncReport();
528                int syncInterval = account.mSyncInterval;
529
530                // If we're not using MessagingController (EAS at this point), don't schedule syncs
531                if (!mController.isMessagingController(account.mId)) {
532                    syncInterval = Account.CHECK_INTERVAL_NEVER;
533                } else if (oneMinuteRefresh && syncInterval >= 0) {
534                    syncInterval = 1;
535                }
536
537                report.accountId = account.mId;
538                report.prevSyncTime = 0;
539                report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
540
541                report.syncInterval = syncInterval;
542
543                // See if the account is enabled for sync in AccountManager
544                android.accounts.Account accountManagerAccount =
545                    new android.accounts.Account(account.mEmailAddress,
546                            AccountManagerTypes.TYPE_POP_IMAP);
547                report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
548                        EmailContent.AUTHORITY);
549
550                // TODO lookup # new in inbox
551                mSyncReports.put(report.accountId, report);
552            }
553        } finally {
554            c.close();
555        }
556    }
557
558    /**
559     * Update list with a single account's sync times and unread count
560     *
561     * @param accountId the account being updated
562     * @param newCount the number of new messages, or -1 if not being reported (don't update)
563     * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
564     */
565    private AccountSyncReport updateAccountReport(long accountId, int newCount) {
566        // restore the reports if lost
567        setupSyncReports(accountId);
568        synchronized (mSyncReports) {
569            AccountSyncReport report = mSyncReports.get(accountId);
570            if (report == null) {
571                // discard result - there is no longer an account with this id
572                Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
573                return null;
574            }
575
576            // report found - update it (note - editing the report while in-place in the hashmap)
577            report.prevSyncTime = SystemClock.elapsedRealtime();
578            report.setNextSyncTime();
579            if (Email.DEBUG) {
580                Log.d(LOG_TAG, "update account " + report.toString());
581            }
582            return report;
583        }
584    }
585
586    /**
587     * when we receive an alarm, update the account sync reports list if necessary
588     * this will be the case when if we have restarted the process and lost the data
589     * in the global.
590     *
591     * @param restoreIntent the intent with the list
592     */
593    private void restoreSyncReports(Intent restoreIntent) {
594        // restore the reports if lost
595        setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
596        synchronized (mSyncReports) {
597            long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
598            if (accountInfo == null) {
599                Log.d(LOG_TAG, "no data in intent to restore");
600                return;
601            }
602            int accountInfoIndex = 0;
603            int accountInfoLimit = accountInfo.length;
604            while (accountInfoIndex < accountInfoLimit) {
605                long accountId = accountInfo[accountInfoIndex++];
606                long prevSync = accountInfo[accountInfoIndex++];
607                AccountSyncReport report = mSyncReports.get(accountId);
608                if (report != null) {
609                    if (report.prevSyncTime == 0) {
610                        report.prevSyncTime = prevSync;
611                        report.setNextSyncTime();
612                    }
613                }
614            }
615        }
616    }
617
618    class ControllerResults extends Controller.Result {
619        @Override
620        public void updateMailboxCallback(MessagingException result, long accountId,
621                long mailboxId, int progress, int numNewMessages,
622                ArrayList<Long> addedMessages) {
623            // First, look for authentication failures and notify
624           //checkAuthenticationStatus(result, accountId);
625           if (result != null || progress == 100) {
626                // We only track the inbox here in the service - ignore other mailboxes
627                long inboxId = Mailbox.findMailboxOfType(MailService.this,
628                        accountId, Mailbox.TYPE_INBOX);
629                if (mailboxId == inboxId) {
630                    if (progress == 100) {
631                        updateAccountReport(accountId, numNewMessages);
632                    } else {
633                        updateAccountReport(accountId, -1);
634                    }
635                }
636            }
637        }
638
639        @Override
640        public void serviceCheckMailCallback(MessagingException result, long accountId,
641                long mailboxId, int progress, long tag) {
642            if (result != null || progress == 100) {
643                if (result != null) {
644                    // the checkmail ended in an error.  force an update of the refresh
645                    // time, so we don't just spin on this account
646                    updateAccountReport(accountId, -1);
647                }
648                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
649                reschedule(alarmManager);
650                int serviceId = mStartId;
651                if (tag != 0) {
652                    serviceId = (int) tag;
653                }
654                stopSelf(serviceId);
655            }
656        }
657    }
658
659    /**
660     * @see ConnectivityManager#getBackgroundDataSetting()
661     */
662    private boolean isBackgroundDataEnabled() {
663        ConnectivityManager cm =
664                (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
665        return cm.getBackgroundDataSetting();
666    }
667
668    public class EmailSyncStatusObserver implements SyncStatusObserver {
669        @Override
670        public void onStatusChanged(int which) {
671            // We ignore the argument (we can only get called in one case - when settings change)
672        }
673    }
674
675    public static ArrayList<Account> getPopImapAccountList(Context context) {
676        ArrayList<Account> providerAccounts = new ArrayList<Account>();
677        Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
678                null, null, null);
679        try {
680            while (c.moveToNext()) {
681                long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
682                String protocol = Account.getProtocol(context, accountId);
683                if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
684                    Account account = Account.restoreAccountWithId(context, accountId);
685                    if (account != null) {
686                        providerAccounts.add(account);
687                    }
688                }
689            }
690        } finally {
691            c.close();
692        }
693        return providerAccounts;
694    }
695
696    private static final SingleRunningTask<Context> sReconcilePopImapAccountsSyncExecutor =
697            new SingleRunningTask<Context>("ReconcilePopImapAccountsSync") {
698                @Override
699                protected void runInternal(Context context) {
700                    android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
701                            .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP);
702                    ArrayList<Account> providerAccounts = getPopImapAccountList(context);
703                    MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
704                            accountManagerAccounts, context);
705
706                }
707    };
708
709    /**
710     * Reconcile POP/IMAP accounts.
711     */
712    public static void reconcilePopImapAccountsSync(Context context) {
713        sReconcilePopImapAccountsSyncExecutor.run(context);
714    }
715
716    /**
717     * Determines whether or not POP/IMAP accounts need reconciling or not. This is a safe operation
718     * to perform on the UI thread.
719     */
720    public static boolean hasMismatchInPopImapAccounts(Context context) {
721        android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
722                .getAccountsByType(AccountManagerTypes.TYPE_POP_IMAP);
723        ArrayList<Account> providerAccounts = getPopImapAccountList(context);
724        return AccountReconciler.accountsNeedReconciling(
725                context, providerAccounts, accountManagerAccounts);
726    }
727
728    /**
729     * See Utility.reconcileAccounts for details
730     * @param context The context in which to operate
731     * @param emailProviderAccounts the exchange provider accounts to work from
732     * @param accountManagerAccounts The account manager accounts to work from
733     * @param providerContext the provider's context (in unit tests, this may differ from context)
734     */
735    @VisibleForTesting
736    public static void reconcileAccountsWithAccountManager(Context context,
737            List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
738            Context providerContext) {
739        AccountReconciler.reconcileAccounts(context, emailProviderAccounts, accountManagerAccounts,
740                providerContext);
741    }
742
743    public static void setupAccountManagerAccount(Context context, Account account,
744            boolean email, boolean calendar, boolean contacts,
745            AccountManagerCallback<Bundle> callback) {
746        Bundle options = new Bundle();
747        HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
748        if (hostAuthRecv == null) return;
749        // Set up username/password
750        options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
751        options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
752        options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
753        options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
754        options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
755        String accountType = hostAuthRecv.mProtocol.equals("eas") ?
756                AccountManagerTypes.TYPE_EXCHANGE :
757                AccountManagerTypes.TYPE_POP_IMAP;
758        AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
759                null);
760    }
761}
762