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