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