MailService.java revision 34e6205d4432c8b591c35fbcfc504852608f35a6
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.AccountBackupRestore;
20import com.android.email.Controller;
21import com.android.email.Email;
22import com.android.email.NotificationController;
23import com.android.email.SecurityPolicy;
24import com.android.email.Utility;
25import com.android.email.mail.MessagingException;
26import com.android.email.provider.EmailContent;
27import com.android.email.provider.EmailProvider;
28import com.android.email.provider.EmailContent.Account;
29import com.android.email.provider.EmailContent.AccountColumns;
30import com.android.email.provider.EmailContent.HostAuth;
31import com.android.email.provider.EmailContent.Mailbox;
32
33import android.accounts.AccountManager;
34import android.accounts.AccountManagerCallback;
35import android.accounts.AccountManagerFuture;
36import android.accounts.AuthenticatorException;
37import android.accounts.OnAccountsUpdateListener;
38import android.accounts.OperationCanceledException;
39import android.app.AlarmManager;
40import android.app.PendingIntent;
41import android.app.Service;
42import android.content.ContentResolver;
43import android.content.ContentUris;
44import android.content.Context;
45import android.content.Intent;
46import android.content.SyncStatusObserver;
47import android.database.Cursor;
48import android.net.ConnectivityManager;
49import android.net.Uri;
50import android.os.AsyncTask;
51import android.os.Bundle;
52import android.os.Handler;
53import android.os.IBinder;
54import android.os.SystemClock;
55import android.util.Config;
56import android.util.Log;
57
58import java.io.IOException;
59import java.util.ArrayList;
60import java.util.HashMap;
61import java.util.List;
62import java.util.concurrent.atomic.AtomicInteger;
63
64/**
65 * Background service for refreshing non-push email accounts.
66 */
67public class MailService extends Service {
68    /** DO NOT CHECK IN "TRUE" */
69    private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;     // force 1-minute refresh
70
71    private static final String LOG_TAG = "Email-MailService";
72
73    private static final String ACTION_CHECK_MAIL =
74        "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
75    private static final String ACTION_RESCHEDULE =
76        "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
77    private static final String ACTION_CANCEL =
78        "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
79    private static final String ACTION_NOTIFY_MAIL =
80        "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
81    private static final String ACTION_SEND_PENDING_MAIL =
82        "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
83
84    private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
85    private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
86    private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
87
88    private static final int WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
89
90    // Sentinel value asking to update mSyncReports if it's currently empty
91    /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
92    // Sentinel value asking that mSyncReports be rebuilt
93    /*package*/ static final int SYNC_REPORTS_RESET = -2;
94
95    private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
96        new String[] {AccountColumns.NEW_MESSAGE_COUNT};
97
98    // Keep track of the number of times we're calling the reconciler
99    // If the count is > 1, don't try to run another one (it means that one is running and one is
100    // already queued).
101    private static AtomicInteger sReconcilerCount = new AtomicInteger(0);
102    private static MailService sMailService;
103
104    /*package*/ Controller mController;
105    private final Controller.Result mControllerCallback = new ControllerResults();
106    private ContentResolver mContentResolver;
107    private Context mContext;
108    /*package*/ AccountsUpdatedListener mAccountsUpdatedListener;
109    private Handler mHandler = new Handler();
110
111    private int mStartId;
112
113    /**
114     * Access must be synchronized, because there are accesses from the Controller callback
115     */
116    /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
117        new HashMap<Long,AccountSyncReport>();
118
119    public static void actionReschedule(Context context) {
120        Intent i = new Intent();
121        i.setClass(context, MailService.class);
122        i.setAction(MailService.ACTION_RESCHEDULE);
123        context.startService(i);
124    }
125
126    public static void actionCancel(Context context)  {
127        Intent i = new Intent();
128        i.setClass(context, MailService.class);
129        i.setAction(MailService.ACTION_CANCEL);
130        context.startService(i);
131    }
132
133    /**
134     * Entry point for AttachmentDownloadService to ask that pending mail be sent
135     * @param context the caller's context
136     * @param accountId the account whose pending mail should be sent
137     */
138    public static void actionSendPendingMail(Context context, long accountId)  {
139        Intent i = new Intent();
140        i.setClass(context, MailService.class);
141        i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
142        i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
143        context.startService(i);
144    }
145
146    /**
147     * Reset new message counts for one or all accounts.  This clears both our local copy and
148     * the values (if any) stored in the account records.
149     *
150     * @param accountId account to clear, or -1 for all accounts
151     */
152    public static void resetNewMessageCount(final Context context, final long accountId) {
153        synchronized (mSyncReports) {
154            for (AccountSyncReport report : mSyncReports.values()) {
155                if (accountId == -1 || accountId == report.accountId) {
156                    report.unseenMessageCount = 0;
157                    report.lastUnseenMessageCount = 0;
158                }
159            }
160        }
161        // Clear notification
162        NotificationController.getInstance(context).cancelNewMessageNotification(accountId);
163
164        // now do the database - all accounts, or just one of them
165        Utility.runAsync(new Runnable() {
166            @Override
167            public void run() {
168                Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
169                if (accountId != -1) {
170                    uri = ContentUris.withAppendedId(uri, accountId);
171                }
172                context.getContentResolver().update(uri, null, null, null);
173            }
174        });
175    }
176
177    /**
178     * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
179     * messages.  This assumes that the push provider has already synced the messages into the
180     * appropriate database - this simply triggers the notification mechanism.
181     *
182     * @param context a context
183     * @param accountId the id of the account that is reporting new messages
184     * @param newCount the number of new messages
185     */
186    public static void actionNotifyNewMessages(Context context, long accountId) {
187        Intent i = new Intent(ACTION_NOTIFY_MAIL);
188        i.setClass(context, MailService.class);
189        i.putExtra(EXTRA_ACCOUNT, accountId);
190        context.startService(i);
191    }
192
193    /*package*/ static MailService getMailServiceForTest() {
194        return sMailService;
195    }
196
197    @Override
198    public int onStartCommand(Intent intent, int flags, int startId) {
199        super.onStartCommand(intent, flags, startId);
200
201        // Save the service away (for unit tests)
202        sMailService = this;
203
204        // Restore accounts, if it has not happened already
205        AccountBackupRestore.restoreAccountsIfNeeded(this);
206
207        // Set up our observer for AccountManager
208        mAccountsUpdatedListener = new AccountsUpdatedListener();
209        AccountManager.get(getApplication()).addOnAccountsUpdatedListener(
210                mAccountsUpdatedListener, mHandler, true);
211        // Run reconciliation to make sure we're up-to-date on account status
212        mAccountsUpdatedListener.onAccountsUpdated(null);
213
214        // TODO this needs to be passed through the controller and back to us
215        this.mStartId = startId;
216        String action = intent.getAction();
217
218        mController = Controller.getInstance(this);
219        mController.addResultCallback(mControllerCallback);
220        mContentResolver = getContentResolver();
221        mContext = this;
222
223        if (ACTION_CHECK_MAIL.equals(action)) {
224            // If we have the data, restore the last-sync-times for each account
225            // These are cached in the wakeup intent in case the process was killed.
226            restoreSyncReports(intent);
227
228            // Sync a specific account if given
229            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
230            long checkAccountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
231            if (Config.LOGD && Email.DEBUG) {
232                Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId);
233            }
234            if (checkAccountId >= 0) {
235                setWatchdog(checkAccountId, alarmManager);
236            }
237
238            // Start sync if account is given and background data is enabled and the account itself
239            // has sync enabled
240            boolean syncStarted = false;
241            if (checkAccountId != -1 && isBackgroundDataEnabled()) {
242                synchronized(mSyncReports) {
243                    for (AccountSyncReport report: mSyncReports.values()) {
244                        if (report.accountId == checkAccountId) {
245                            if (report.syncEnabled) {
246                                syncStarted = syncOneAccount(mController, checkAccountId, startId);
247                            }
248                            break;
249                        }
250                    }
251                }
252            }
253
254            // Reschedule if we didn't start sync.
255            if (!syncStarted) {
256                // Prevent runaway on the current account by pretending it updated
257                if (checkAccountId != -1) {
258                    updateAccountReport(checkAccountId, 0);
259                }
260                // Find next account to sync, and reschedule
261                reschedule(alarmManager);
262                stopSelf(startId);
263            }
264        }
265        else if (ACTION_CANCEL.equals(action)) {
266            if (Config.LOGD && Email.DEBUG) {
267                Log.d(LOG_TAG, "action: cancel");
268            }
269            cancel();
270            stopSelf(startId);
271        }
272        else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
273            if (Config.LOGD && Email.DEBUG) {
274                Log.d(LOG_TAG, "action: send pending mail");
275            }
276            final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
277            EmailContent.Account account =
278                EmailContent.Account.restoreAccountWithId(this, accountId);
279            if (account != null) {
280                Utility.runAsync(new Runnable() {
281                    public void run() {
282                        mController.sendPendingMessages(accountId);
283                    }
284                });
285            }
286            stopSelf(startId);
287        }
288        else if (ACTION_RESCHEDULE.equals(action)) {
289            if (Config.LOGD && Email.DEBUG) {
290                Log.d(LOG_TAG, "action: reschedule");
291            }
292            // Clear all notifications, in case account list has changed.
293            //
294            // TODO Clear notifications for non-existing accounts.  Now that we have separate
295            // notification for each account, NotificationController should be able to do that.
296            NotificationController.getInstance(this).cancelNewMessageNotification(-1);
297
298            // When called externally, we refresh the sync reports table to pick up
299            // any changes in the account list or account settings
300            refreshSyncReports();
301            // Finally, scan for the next needing update, and set an alarm for it
302            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
303            reschedule(alarmManager);
304            stopSelf(startId);
305        } else if (ACTION_NOTIFY_MAIL.equals(action)) {
306            long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
307            // Get the current new message count
308            Cursor c = mContentResolver.query(
309                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
310                    NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
311            int newMessageCount = 0;
312            try {
313                if (c.moveToFirst()) {
314                    newMessageCount = c.getInt(0);
315                } else {
316                    // If the account no longer exists, set to -1 (which is handled below)
317                    accountId = -1;
318                }
319            } finally {
320                c.close();
321            }
322            if (Config.LOGD && Email.DEBUG) {
323                Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
324                        + " count=" + newMessageCount);
325            }
326            if (accountId != -1) {
327                updateAccountReport(accountId, newMessageCount);
328                notifyNewMessages(accountId);
329            }
330            stopSelf(startId);
331        }
332
333        // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
334        // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
335        // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
336        // will fire and get things running again.
337        return START_NOT_STICKY;
338    }
339
340    @Override
341    public IBinder onBind(Intent intent) {
342        return null;
343    }
344
345    @Override
346    public void onDestroy() {
347        super.onDestroy();
348        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
349        // Unregister our account listener
350        if (mAccountsUpdatedListener != null) {
351            AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
352        }
353    }
354
355    private void cancel() {
356        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
357        PendingIntent pi = createAlarmIntent(-1, null, false);
358        alarmMgr.cancel(pi);
359    }
360
361    /**
362     * Refresh the sync reports, to pick up any changes in the account list or account settings.
363     */
364    /*package*/ void refreshSyncReports() {
365        synchronized (mSyncReports) {
366            // Make shallow copy of sync reports so we can recover the prev sync times
367            HashMap<Long,AccountSyncReport> oldSyncReports =
368                new HashMap<Long,AccountSyncReport>(mSyncReports);
369
370            // Delete the sync reports to force a refresh from live account db data
371            setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
372
373            // Restore prev-sync & next-sync times for any reports in the new list
374            for (AccountSyncReport newReport : mSyncReports.values()) {
375                AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
376                if (oldReport != null) {
377                    newReport.prevSyncTime = oldReport.prevSyncTime;
378                    if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
379                        newReport.nextSyncTime =
380                            newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
381                    }
382                }
383            }
384        }
385    }
386
387    /**
388     * Create and send an alarm with the entire list.  This also sends a list of known last-sync
389     * times with the alarm, so if we are killed between alarms, we don't lose this info.
390     *
391     * @param alarmMgr passed in so we can mock for testing.
392     */
393    /* package */ void reschedule(AlarmManager alarmMgr) {
394        // restore the reports if lost
395        setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
396        synchronized (mSyncReports) {
397            int numAccounts = mSyncReports.size();
398            long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
399            int accountInfoIndex = 0;
400
401            long nextCheckTime = Long.MAX_VALUE;
402            AccountSyncReport nextAccount = null;
403            long timeNow = SystemClock.elapsedRealtime();
404
405            for (AccountSyncReport report : mSyncReports.values()) {
406                if (report.syncInterval <= 0) {                         // no timed checks - skip
407                    continue;
408                }
409                long prevSyncTime = report.prevSyncTime;
410                long nextSyncTime = report.nextSyncTime;
411
412                // select next account to sync
413                if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
414                    nextCheckTime = 0;
415                    nextAccount = report;
416                } else if (nextSyncTime < nextCheckTime) {              // next to be checked
417                    nextCheckTime = nextSyncTime;
418                    nextAccount = report;
419                }
420                // collect last-sync-times for all accounts
421                // this is using pairs of {long,long} to simplify passing in a bundle
422                accountInfo[accountInfoIndex++] = report.accountId;
423                accountInfo[accountInfoIndex++] = report.prevSyncTime;
424            }
425
426            // Clear out any unused elements in the array
427            while (accountInfoIndex < accountInfo.length) {
428                accountInfo[accountInfoIndex++] = -1;
429            }
430
431            // set/clear alarm as needed
432            long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
433            PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
434
435            if (nextAccount == null) {
436                alarmMgr.cancel(pi);
437                if (Config.LOGD && Email.DEBUG) {
438                    Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
439                }
440            } else {
441                alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
442                if (Config.LOGD && Email.DEBUG) {
443                    Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
444                            + " for " + nextAccount);
445                }
446            }
447        }
448    }
449
450    /**
451     * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
452     * killed by the system due to memory pressure.)  Normally, a mail check will complete and
453     * the watchdog will be replaced by the call to reschedule().
454    * @param accountId the account we were trying to check
455     * @param alarmMgr system alarm manager
456     */
457    private void setWatchdog(long accountId, AlarmManager alarmMgr) {
458        PendingIntent pi = createAlarmIntent(accountId, null, true);
459        long timeNow = SystemClock.elapsedRealtime();
460        long nextCheckTime = timeNow + WATCHDOG_DELAY;
461        alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
462    }
463
464    /**
465     * Return a pending intent for use by this alarm.  Most of the fields must be the same
466     * (in order for the intent to be recognized by the alarm manager) but the extras can
467     * be different, and are passed in here as parameters.
468     */
469    /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
470            boolean isWatchdog) {
471        Intent i = new Intent();
472        i.setClass(this, MailService.class);
473        i.setAction(ACTION_CHECK_MAIL);
474        i.putExtra(EXTRA_ACCOUNT, checkId);
475        i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
476        if (isWatchdog) {
477            i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
478        }
479        PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
480        return pi;
481    }
482
483    /**
484     * Start a controller sync for a specific account
485     *
486     * @param controller The controller to do the sync work
487     * @param checkAccountId the account Id to try and check
488     * @param startId the id of this service launch
489     * @return true if mail checking has started, false if it could not (e.g. bad account id)
490     */
491    private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
492        long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
493        if (inboxId == Mailbox.NO_MAILBOX) {
494            return false;
495        } else {
496            controller.serviceCheckMail(checkAccountId, inboxId, startId);
497            return true;
498        }
499    }
500
501    /**
502     * Note:  Times are relative to SystemClock.elapsedRealtime()
503     *
504     * TODO:  Look more closely at syncEnabled and see if we can simply coalesce it into
505     * syncInterval (e.g. if !syncEnabled, set syncInterval to -1).
506     */
507    /*package*/ static class AccountSyncReport {
508        long accountId;
509        long prevSyncTime;      // 0 == unknown
510        long nextSyncTime;      // 0 == ASAP  -1 == don't sync
511
512        /** # of "unseen" messages to show in notification */
513        int unseenMessageCount;
514
515        /**
516         * # of unseen, the value shown on the last notification. Used to
517         * calculate "the number of messages that have just been fetched".
518         *
519         * TODO It's a sort of cheating.  Should we use the "real" number?  The only difference
520         * is the first notification after reboot / process restart.
521         */
522        int lastUnseenMessageCount;
523
524        int syncInterval;
525        boolean notify;
526
527        boolean syncEnabled;    // whether auto sync is enabled for this account
528
529        /** # of messages that have just been fetched */
530        int getJustFetchedMessageCount() {
531            return unseenMessageCount - lastUnseenMessageCount;
532        }
533
534        @Override
535        public String toString() {
536            return "id=" + accountId
537                    + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
538                    + unseenMessageCount;
539        }
540    }
541
542    /**
543     * scan accounts to create a list of { acct, prev sync, next sync, #new }
544     * use this to create a fresh copy.  assumes all accounts need sync
545     *
546     * @param accountId -1 will rebuild the list if empty.  other values will force loading
547     *   of a single account (e.g if it was created after the original list population)
548     */
549    /* package */ void setupSyncReports(long accountId) {
550        synchronized (mSyncReports) {
551            setupSyncReportsLocked(accountId, mContext);
552        }
553    }
554
555    /**
556     * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
557     */
558    /*package*/ void setupSyncReportsLocked(long accountId, Context context) {
559        ContentResolver resolver = context.getContentResolver();
560        if (accountId == SYNC_REPORTS_RESET) {
561            // For test purposes, force refresh of mSyncReports
562            mSyncReports.clear();
563            accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
564        } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
565            // -1 == reload the list if empty, otherwise exit immediately
566            if (mSyncReports.size() > 0) {
567                return;
568            }
569        } else {
570            // load a single account if it doesn't already have a sync record
571            if (mSyncReports.containsKey(accountId)) {
572                return;
573            }
574        }
575
576        // setup to add a single account or all accounts
577        Uri uri;
578        if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
579            uri = Account.CONTENT_URI;
580        } else {
581            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
582        }
583
584        // We use a full projection here because we'll restore each account object from it
585        Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null);
586        try {
587            while (c.moveToNext()) {
588                AccountSyncReport report = new AccountSyncReport();
589                Account account = Account.getContent(c, Account.class);
590                int syncInterval = account.mSyncInterval;
591
592                // If we're not using MessagingController (EAS at this point), don't schedule syncs
593                if (!mController.isMessagingController(account.mId)) {
594                    syncInterval = Account.CHECK_INTERVAL_NEVER;
595                } else if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) {
596                    syncInterval = 1;
597                }
598
599                report.accountId = account.mId;
600                report.prevSyncTime = 0;
601                report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
602                report.unseenMessageCount = 0;
603                report.lastUnseenMessageCount = 0;
604
605                report.syncInterval = syncInterval;
606                report.notify = (account.mFlags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
607
608                // See if the account is enabled for sync in AccountManager
609                android.accounts.Account accountManagerAccount =
610                    new android.accounts.Account(account.mEmailAddress,
611                            Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
612                report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount,
613                        EmailProvider.EMAIL_AUTHORITY);
614
615                // TODO lookup # new in inbox
616                mSyncReports.put(report.accountId, report);
617            }
618        } finally {
619            c.close();
620        }
621    }
622
623    /**
624     * Update list with a single account's sync times and unread count
625     *
626     * @param accountId the account being updated
627     * @param newCount the number of new messages, or -1 if not being reported (don't update)
628     * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
629     */
630    /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
631        // restore the reports if lost
632        setupSyncReports(accountId);
633        synchronized (mSyncReports) {
634            AccountSyncReport report = mSyncReports.get(accountId);
635            if (report == null) {
636                // discard result - there is no longer an account with this id
637                Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
638                return null;
639            }
640
641            // report found - update it (note - editing the report while in-place in the hashmap)
642            report.prevSyncTime = SystemClock.elapsedRealtime();
643            if (report.syncInterval > 0) {
644                report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
645            }
646            if (newCount != -1) {
647                report.unseenMessageCount = newCount;
648            }
649            if (Config.LOGD && Email.DEBUG) {
650                Log.d(LOG_TAG, "update account " + report.toString());
651            }
652            return report;
653        }
654    }
655
656    /**
657     * when we receive an alarm, update the account sync reports list if necessary
658     * this will be the case when if we have restarted the process and lost the data
659     * in the global.
660     *
661     * @param restoreIntent the intent with the list
662     */
663    /* package */ void restoreSyncReports(Intent restoreIntent) {
664        // restore the reports if lost
665        setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
666        synchronized (mSyncReports) {
667            long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
668            if (accountInfo == null) {
669                Log.d(LOG_TAG, "no data in intent to restore");
670                return;
671            }
672            int accountInfoIndex = 0;
673            int accountInfoLimit = accountInfo.length;
674            while (accountInfoIndex < accountInfoLimit) {
675                long accountId = accountInfo[accountInfoIndex++];
676                long prevSync = accountInfo[accountInfoIndex++];
677                AccountSyncReport report = mSyncReports.get(accountId);
678                if (report != null) {
679                    if (report.prevSyncTime == 0) {
680                        report.prevSyncTime = prevSync;
681                        if (report.syncInterval > 0 && report.prevSyncTime != 0) {
682                            report.nextSyncTime =
683                                report.prevSyncTime + (report.syncInterval * 1000 * 60);
684                        }
685                    }
686                }
687            }
688        }
689    }
690
691    class ControllerResults extends Controller.Result {
692        @Override
693        public void updateMailboxCallback(MessagingException result, long accountId,
694                long mailboxId, int progress, int numNewMessages) {
695            // First, look for authentication failures and notify
696           //checkAuthenticationStatus(result, accountId);
697           if (result != null || progress == 100) {
698                // We only track the inbox here in the service - ignore other mailboxes
699                long inboxId = Mailbox.findMailboxOfType(MailService.this,
700                        accountId, Mailbox.TYPE_INBOX);
701                if (mailboxId == inboxId) {
702                    if (progress == 100) {
703                        updateAccountReport(accountId, numNewMessages);
704                        if (numNewMessages > 0) {
705                            notifyNewMessages(accountId);
706                        }
707                    } else {
708                        updateAccountReport(accountId, -1);
709                    }
710                }
711            }
712        }
713
714        @Override
715        public void serviceCheckMailCallback(MessagingException result, long accountId,
716                long mailboxId, int progress, long tag) {
717            if (result != null || progress == 100) {
718                if (result != null) {
719                    // the checkmail ended in an error.  force an update of the refresh
720                    // time, so we don't just spin on this account
721                    updateAccountReport(accountId, -1);
722                }
723                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
724                reschedule(alarmManager);
725                int serviceId = MailService.this.mStartId;
726                if (tag != 0) {
727                    serviceId = (int) tag;
728                }
729                stopSelf(serviceId);
730            }
731        }
732    }
733
734    /**
735     * Show "new message" notification for an account.  (Notification is shown per account.)
736     */
737    private void notifyNewMessages(final long accountId) {
738        final int unseenMessageCount;
739        final int justFetchedCount;
740        synchronized (mSyncReports) {
741            AccountSyncReport report = mSyncReports.get(accountId);
742            if (report == null || report.unseenMessageCount == 0 || !report.notify) {
743                return;
744            }
745            unseenMessageCount = report.unseenMessageCount;
746            justFetchedCount = report.getJustFetchedMessageCount();
747            report.lastUnseenMessageCount = report.unseenMessageCount;
748        }
749
750        NotificationController.getInstance(this).showNewMessageNotification(accountId,
751                unseenMessageCount, justFetchedCount);
752    }
753
754    /**
755     * @see ConnectivityManager#getBackgroundDataSetting()
756     */
757    private boolean isBackgroundDataEnabled() {
758        ConnectivityManager cm =
759                (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
760        return cm.getBackgroundDataSetting();
761    }
762
763    public class EmailSyncStatusObserver implements SyncStatusObserver {
764        public void onStatusChanged(int which) {
765            // We ignore the argument (we can only get called in one case - when settings change)
766        }
767    }
768
769    public static ArrayList<Account> getPopImapAccountList(Context context) {
770        ArrayList<Account> providerAccounts = new ArrayList<Account>();
771        Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION,
772                null, null, null);
773        try {
774            while (c.moveToNext()) {
775                long accountId = c.getLong(Account.CONTENT_ID_COLUMN);
776                String protocol = Account.getProtocol(context, accountId);
777                if ((protocol != null) && ("pop3".equals(protocol) || "imap".equals(protocol))) {
778                    Account account = Account.restoreAccountWithId(context, accountId);
779                    if (account != null) {
780                        providerAccounts.add(account);
781                    }
782                }
783            }
784        } finally {
785            c.close();
786        }
787        return providerAccounts;
788    }
789
790    /**
791     * We synchronize this, since it can be called from multiple threads
792     */
793    public static synchronized void reconcilePopImapAccounts(Context context) {
794        android.accounts.Account[] accountManagerAccounts = AccountManager.get(context)
795            .getAccountsByType(Email.POP_IMAP_ACCOUNT_MANAGER_TYPE);
796        ArrayList<Account> providerAccounts = getPopImapAccountList(context);
797        MailService.reconcileAccountsWithAccountManager(context, providerAccounts,
798                accountManagerAccounts, false, context.getContentResolver());
799
800    }
801
802    /**
803     * Reconcile accounts when accounts are added/removed from AccountManager; note that the
804     * required argument is ignored (we request those of our specific type within the method)
805     */
806    public class AccountsUpdatedListener implements OnAccountsUpdateListener {
807
808        public void onAccountsUpdated(android.accounts.Account[] accounts) {
809            // Only allow one to be queued at a time (and one running)
810            if (sReconcilerCount.getAndIncrement() > 1) {
811                sReconcilerCount.decrementAndGet();
812                return;
813            }
814            new AccountReconcilerTask().execute();
815        }
816
817        private class AccountReconcilerTask extends AsyncTask<Void, Void, Void> {
818            @Override
819            protected Void doInBackground(Void... params) {
820                try {
821                    reconcilePopImapAccounts(MailService.this);
822                } finally {
823                    // Belt & suspenders; most likely, any RuntimeException in this code would
824                    // kill the Email process
825                    sReconcilerCount.decrementAndGet();
826                }
827                return null;
828            }
829        }
830    }
831
832    /**
833     * Compare our account list (obtained from EmailProvider) with the account list owned by
834     * AccountManager.  If there are any orphans (an account in one list without a corresponding
835     * account in the other list), delete the orphan, as these must remain in sync.
836     *
837     * Note that the duplication of account information is caused by the Email application's
838     * incomplete integration with AccountManager.
839     *
840     * This function may not be called from the main/UI thread, because it makes blocking calls
841     * into the account manager.
842     *
843     * @param context The context in which to operate
844     * @param emailProviderAccounts the exchange provider accounts to work from
845     * @param accountManagerAccounts The account manager accounts to work from
846     * @param blockExternalChanges FOR TESTING ONLY - block backups, security changes, etc.
847     * @param resolver the content resolver for making provider updates (injected for testability)
848     */
849    /* package */ public static void reconcileAccountsWithAccountManager(Context context,
850            List<Account> emailProviderAccounts, android.accounts.Account[] accountManagerAccounts,
851            boolean blockExternalChanges, ContentResolver resolver) {
852        // First, look through our EmailProvider accounts to make sure there's a corresponding
853        // AccountManager account
854        boolean accountsDeleted = false;
855        for (Account providerAccount: emailProviderAccounts) {
856            String providerAccountName = providerAccount.mEmailAddress;
857            boolean found = false;
858            for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
859                if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
860                    found = true;
861                    break;
862                }
863            }
864            if (!found) {
865                if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
866                    if (Email.DEBUG) {
867                        Log.d(LOG_TAG, "Account reconciler noticed incomplete account; ignoring");
868                    }
869                    continue;
870                }
871                // This account has been deleted in the AccountManager!
872                Log.d(LOG_TAG, "Account deleted in AccountManager; deleting from provider: " +
873                        providerAccountName);
874                // TODO This will orphan downloaded attachments; need to handle this
875                resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
876                        providerAccount.mId), null, null);
877                accountsDeleted = true;
878            }
879        }
880        // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
881        // account from EmailProvider
882        for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
883            String accountManagerAccountName = accountManagerAccount.name;
884            boolean found = false;
885            for (Account cachedEasAccount: emailProviderAccounts) {
886                if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
887                    found = true;
888                }
889            }
890            if (!found) {
891                // This account has been deleted from the EmailProvider database
892                Log.d(LOG_TAG, "Account deleted from provider; deleting from AccountManager: " +
893                        accountManagerAccountName);
894                // Delete the account
895                AccountManagerFuture<Boolean> blockingResult = AccountManager.get(context)
896                        .removeAccount(accountManagerAccount, null, null);
897                try {
898                    // Note: All of the potential errors from removeAccount() are simply logged
899                    // here, as there is nothing to actually do about them.
900                    blockingResult.getResult();
901                } catch (OperationCanceledException e) {
902                    Log.w(Email.LOG_TAG, e.toString());
903                } catch (AuthenticatorException e) {
904                    Log.w(Email.LOG_TAG, e.toString());
905                } catch (IOException e) {
906                    Log.w(Email.LOG_TAG, e.toString());
907                }
908                accountsDeleted = true;
909            }
910        }
911        // If we changed the list of accounts, refresh the backup & security settings
912        if (!blockExternalChanges && accountsDeleted) {
913            AccountBackupRestore.backupAccounts(context);
914            SecurityPolicy.getInstance(context).reducePolicies();
915            Email.setNotifyUiAccountsChanged(true);
916            MailService.actionReschedule(context);
917        }
918    }
919
920    public static void setupAccountManagerAccount(Context context, EmailContent.Account account,
921            boolean email, boolean calendar, boolean contacts,
922            AccountManagerCallback<Bundle> callback) {
923        Bundle options = new Bundle();
924        HostAuth hostAuthRecv = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
925        // Set up username/password
926        options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
927        options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
928        options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
929        options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
930        options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
931        String accountType = hostAuthRecv.mProtocol.equals("eas") ?
932                Email.EXCHANGE_ACCOUNT_MANAGER_TYPE :
933                Email.POP_IMAP_ACCOUNT_MANAGER_TYPE;
934        AccountManager.get(context).addAccount(accountType, null, null, options, null, callback,
935                null);
936    }
937}
938