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