MailService.java revision fe6e3eae2a318f89634c82222acfdaac32ee6cf1
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.Bundle;
51import android.os.Handler;
52import android.os.IBinder;
53import android.os.SystemClock;
54import android.util.Config;
55import android.util.Log;
56
57import java.io.IOException;
58import java.util.ArrayList;
59import java.util.HashMap;
60import java.util.List;
61
62/**
63 * Background service for refreshing non-push email accounts.
64 */
65public class MailService extends Service {
66    /** DO NOT CHECK IN "TRUE" */
67    private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;     // force 1-minute refresh
68
69    private static final String LOG_TAG = "Email-MailService";
70
71    private static final String ACTION_CHECK_MAIL =
72        "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
73    private static final String ACTION_RESCHEDULE =
74        "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
75    private static final String ACTION_CANCEL =
76        "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
77    private static final String ACTION_NOTIFY_MAIL =
78        "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
79    private static final String ACTION_SEND_PENDING_MAIL =
80        "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING";
81
82    private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
83    private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
84    private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
85
86    private static final int WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
87
88    // Sentinel value asking to update mSyncReports if it's currently empty
89    /*package*/ static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1;
90    // Sentinel value asking that mSyncReports be rebuilt
91    /*package*/ static final int SYNC_REPORTS_RESET = -2;
92
93    private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
94        new String[] {AccountColumns.NEW_MESSAGE_COUNT};
95
96    /*package*/ Controller mController;
97    private final Controller.Result mControllerCallback = new ControllerResults();
98    private ContentResolver mContentResolver;
99    private Context mContext;
100    private AccountsUpdatedListener mAccountsUpdatedListener;
101    private Handler mHandler = new Handler();
102
103    private int mStartId;
104
105    /**
106     * Access must be synchronized, because there are accesses from the Controller callback
107     */
108    /*package*/ static HashMap<Long,AccountSyncReport> mSyncReports =
109        new HashMap<Long,AccountSyncReport>();
110
111    public static void actionReschedule(Context context) {
112        Intent i = new Intent();
113        i.setClass(context, MailService.class);
114        i.setAction(MailService.ACTION_RESCHEDULE);
115        context.startService(i);
116    }
117
118    public static void actionCancel(Context context)  {
119        Intent i = new Intent();
120        i.setClass(context, MailService.class);
121        i.setAction(MailService.ACTION_CANCEL);
122        context.startService(i);
123    }
124
125    /**
126     * Entry point for AttachmentDownloadService to ask that pending mail be sent
127     * @param context the caller's context
128     * @param accountId the account whose pending mail should be sent
129     */
130    public static void actionSendPendingMail(Context context, long accountId)  {
131        Intent i = new Intent();
132        i.setClass(context, MailService.class);
133        i.setAction(MailService.ACTION_SEND_PENDING_MAIL);
134        i.putExtra(MailService.EXTRA_ACCOUNT, accountId);
135        context.startService(i);
136    }
137
138    /**
139     * Reset new message counts for one or all accounts.  This clears both our local copy and
140     * the values (if any) stored in the account records.
141     *
142     * @param accountId account to clear, or -1 for all accounts
143     */
144    public static void resetNewMessageCount(final Context context, final long accountId) {
145        synchronized (mSyncReports) {
146            for (AccountSyncReport report : mSyncReports.values()) {
147                if (accountId == -1 || accountId == report.accountId) {
148                    report.unseenMessageCount = 0;
149                }
150            }
151        }
152        // now do the database - all accounts, or just one of them
153        Utility.runAsync(new Runnable() {
154            @Override
155            public void run() {
156                Uri uri = Account.RESET_NEW_MESSAGE_COUNT_URI;
157                if (accountId != -1) {
158                    uri = ContentUris.withAppendedId(uri, accountId);
159                }
160                context.getContentResolver().update(uri, null, null, null);
161            }
162        });
163    }
164
165    /**
166     * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
167     * messages.  This assumes that the push provider has already synced the messages into the
168     * appropriate database - this simply triggers the notification mechanism.
169     *
170     * @param context a context
171     * @param accountId the id of the account that is reporting new messages
172     * @param newCount the number of new messages
173     */
174    public static void actionNotifyNewMessages(Context context, long accountId) {
175        Intent i = new Intent(ACTION_NOTIFY_MAIL);
176        i.setClass(context, MailService.class);
177        i.putExtra(EXTRA_ACCOUNT, accountId);
178        context.startService(i);
179    }
180
181    @Override
182    public int onStartCommand(Intent intent, int flags, int startId) {
183        super.onStartCommand(intent, flags, startId);
184
185        // Restore accounts, if it has not happened already
186        AccountBackupRestore.restoreAccountsIfNeeded(this);
187
188        // Set up our observer for AccountManager
189        mAccountsUpdatedListener = new AccountsUpdatedListener();
190        AccountManager.get(getApplication()).addOnAccountsUpdatedListener(
191                mAccountsUpdatedListener, mHandler, true);
192        // Run reconciliation to make sure we're up-to-date on account status
193        mAccountsUpdatedListener.onAccountsUpdated(null);
194
195        // TODO this needs to be passed through the controller and back to us
196        this.mStartId = startId;
197        String action = intent.getAction();
198
199        mController = Controller.getInstance(this);
200        mController.addResultCallback(mControllerCallback);
201        mContentResolver = getContentResolver();
202        mContext = this;
203
204        if (ACTION_CHECK_MAIL.equals(action)) {
205            // If we have the data, restore the last-sync-times for each account
206            // These are cached in the wakeup intent in case the process was killed.
207            restoreSyncReports(intent);
208
209            // Sync a specific account if given
210            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
211            long checkAccountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
212            if (Config.LOGD && Email.DEBUG) {
213                Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId);
214            }
215            if (checkAccountId >= 0) {
216                setWatchdog(checkAccountId, alarmManager);
217            }
218
219            // Start sync if account is given and background data is enabled and the account itself
220            // has sync enabled
221            boolean syncStarted = false;
222            if (checkAccountId != -1 && isBackgroundDataEnabled()) {
223                synchronized(mSyncReports) {
224                    for (AccountSyncReport report: mSyncReports.values()) {
225                        if (report.accountId == checkAccountId) {
226                            if (report.syncEnabled) {
227                                syncStarted = syncOneAccount(mController, checkAccountId, startId);
228                            }
229                            break;
230                        }
231                    }
232                }
233            }
234
235            // Reschedule if we didn't start sync.
236            if (!syncStarted) {
237                // Prevent runaway on the current account by pretending it updated
238                if (checkAccountId != -1) {
239                    updateAccountReport(checkAccountId, 0);
240                }
241                // Find next account to sync, and reschedule
242                reschedule(alarmManager);
243                stopSelf(startId);
244            }
245        }
246        else if (ACTION_CANCEL.equals(action)) {
247            if (Config.LOGD && Email.DEBUG) {
248                Log.d(LOG_TAG, "action: cancel");
249            }
250            cancel();
251            stopSelf(startId);
252        }
253        else if (ACTION_SEND_PENDING_MAIL.equals(action)) {
254            if (Config.LOGD && Email.DEBUG) {
255                Log.d(LOG_TAG, "action: send pending mail");
256            }
257            final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
258            EmailContent.Account account =
259                EmailContent.Account.restoreAccountWithId(this, accountId);
260            if (account != null) {
261                Utility.runAsync(new Runnable() {
262                    public void run() {
263                        mController.sendPendingMessages(accountId);
264                    }
265                });
266            }
267            stopSelf(startId);
268        }
269        else if (ACTION_RESCHEDULE.equals(action)) {
270            if (Config.LOGD && Email.DEBUG) {
271                Log.d(LOG_TAG, "action: reschedule");
272            }
273            // As a precaution, clear any outstanding Email notifications
274            // We could be smarter and only do this when the list of accounts changes,
275            // but that's an edge condition and this is much safer.
276            NotificationController.getInstance(this).cancelNewMessageNotification(-1);
277
278            // When called externally, we refresh the sync reports table to pick up
279            // any changes in the account list or account settings
280            refreshSyncReports();
281            // Finally, scan for the next needing update, and set an alarm for it
282            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
283            reschedule(alarmManager);
284            stopSelf(startId);
285        } else if (ACTION_NOTIFY_MAIL.equals(action)) {
286            long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1);
287            // Get the current new message count
288            Cursor c = mContentResolver.query(
289                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
290                    NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
291            int newMessageCount = 0;
292            try {
293                if (c.moveToFirst()) {
294                    newMessageCount = c.getInt(0);
295                } else {
296                    // If the account no longer exists, set to -1 (which is handled below)
297                    accountId = -1;
298                }
299            } finally {
300                c.close();
301            }
302            if (Config.LOGD && Email.DEBUG) {
303                Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
304                        + " count=" + newMessageCount);
305            }
306            if (accountId != -1) {
307                updateAccountReport(accountId, newMessageCount);
308                notifyNewMessages(accountId);
309            }
310            stopSelf(startId);
311        }
312
313        // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
314        // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
315        // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
316        // will fire and get things running again.
317        return START_NOT_STICKY;
318    }
319
320    @Override
321    public IBinder onBind(Intent intent) {
322        return null;
323    }
324
325    @Override
326    public void onDestroy() {
327        super.onDestroy();
328        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
329        // Unregister our account listener
330        if (mAccountsUpdatedListener != null) {
331            AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
332        }
333    }
334
335    private void cancel() {
336        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
337        PendingIntent pi = createAlarmIntent(-1, null, false);
338        alarmMgr.cancel(pi);
339    }
340
341    /**
342     * Refresh the sync reports, to pick up any changes in the account list or account settings.
343     */
344    /*package*/ void refreshSyncReports() {
345        synchronized (mSyncReports) {
346            // Make shallow copy of sync reports so we can recover the prev sync times
347            HashMap<Long,AccountSyncReport> oldSyncReports =
348                new HashMap<Long,AccountSyncReport>(mSyncReports);
349
350            // Delete the sync reports to force a refresh from live account db data
351            setupSyncReportsLocked(SYNC_REPORTS_RESET, this);
352
353            // Restore prev-sync & next-sync times for any reports in the new list
354            for (AccountSyncReport newReport : mSyncReports.values()) {
355                AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
356                if (oldReport != null) {
357                    newReport.prevSyncTime = oldReport.prevSyncTime;
358                    if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
359                        newReport.nextSyncTime =
360                            newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
361                    }
362                }
363            }
364        }
365    }
366
367    /**
368     * Create and send an alarm with the entire list.  This also sends a list of known last-sync
369     * times with the alarm, so if we are killed between alarms, we don't lose this info.
370     *
371     * @param alarmMgr passed in so we can mock for testing.
372     */
373    /* package */ void reschedule(AlarmManager alarmMgr) {
374        // restore the reports if lost
375        setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
376        synchronized (mSyncReports) {
377            int numAccounts = mSyncReports.size();
378            long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
379            int accountInfoIndex = 0;
380
381            long nextCheckTime = Long.MAX_VALUE;
382            AccountSyncReport nextAccount = null;
383            long timeNow = SystemClock.elapsedRealtime();
384
385            for (AccountSyncReport report : mSyncReports.values()) {
386                if (report.syncInterval <= 0) {  // no timed checks - skip
387                    continue;
388                }
389
390                long prevSyncTime = report.prevSyncTime;
391                long nextSyncTime = report.nextSyncTime;
392
393                // select next account to sync
394                if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
395                    nextCheckTime = 0;
396                    nextAccount = report;
397                } else if (nextSyncTime < nextCheckTime) {              // next to be checked
398                    nextCheckTime = nextSyncTime;
399                    nextAccount = report;
400                }
401                // collect last-sync-times for all accounts
402                // this is using pairs of {long,long} to simplify passing in a bundle
403                accountInfo[accountInfoIndex++] = report.accountId;
404                accountInfo[accountInfoIndex++] = report.prevSyncTime;
405            }
406
407            // Clear out any unused elements in the array
408            while (accountInfoIndex < accountInfo.length) {
409                accountInfo[accountInfoIndex++] = -1;
410            }
411
412            // set/clear alarm as needed
413            long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
414            PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
415
416            if (nextAccount == null) {
417                alarmMgr.cancel(pi);
418                if (Config.LOGD && Email.DEBUG) {
419                    Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
420                }
421            } else {
422                alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
423                if (Config.LOGD && Email.DEBUG) {
424                    Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
425                            + " for " + nextAccount);
426                }
427            }
428        }
429    }
430
431    /**
432     * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
433     * killed by the system due to memory pressure.)  Normally, a mail check will complete and
434     * the watchdog will be replaced by the call to reschedule().
435     * @param accountId the account we were trying to check
436     * @param alarmMgr system alarm manager
437     */
438    private void setWatchdog(long accountId, AlarmManager alarmMgr) {
439        PendingIntent pi = createAlarmIntent(accountId, null, true);
440        long timeNow = SystemClock.elapsedRealtime();
441        long nextCheckTime = timeNow + WATCHDOG_DELAY;
442        alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
443    }
444
445    /**
446     * Return a pending intent for use by this alarm.  Most of the fields must be the same
447     * (in order for the intent to be recognized by the alarm manager) but the extras can
448     * be different, and are passed in here as parameters.
449     */
450    /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
451            boolean isWatchdog) {
452        Intent i = new Intent();
453        i.setClass(this, MailService.class);
454        i.setAction(ACTION_CHECK_MAIL);
455        i.putExtra(EXTRA_ACCOUNT, checkId);
456        i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
457        if (isWatchdog) {
458            i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
459        }
460        PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
461        return pi;
462    }
463
464    /**
465     * Start a controller sync for a specific account
466     *
467     * @param controller The controller to do the sync work
468     * @param checkAccountId the account Id to try and check
469     * @param startId the id of this service launch
470     * @return true if mail checking has started, false if it could not (e.g. bad account id)
471     */
472    private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
473        long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
474        if (inboxId == Mailbox.NO_MAILBOX) {
475            return false;
476        } else {
477            controller.serviceCheckMail(checkAccountId, inboxId, startId);
478            return true;
479        }
480    }
481
482    /**
483     * Note:  Times are relative to SystemClock.elapsedRealtime()
484     */
485    /*package*/ static class AccountSyncReport {
486        long accountId;
487        long prevSyncTime;      // 0 == unknown
488        long nextSyncTime;      // 0 == ASAP  -1 == don't sync
489
490        /** # of "unseen" messages to show in notification */
491        int unseenMessageCount;
492
493        /**
494         * # of unseen, the value shown on the last notification. Used to
495         * calculate "the number of messages that have just been fetched".
496         *
497         * TODO It's a sort of cheating.  Should we use the "real" number?  The only difference
498         * is the first notification after reboot / process restart.
499         */
500        int lastUnseenMessageCount;
501
502        int syncInterval;
503        boolean notify;
504
505        boolean syncEnabled;    // whether auto sync is enabled for this account
506
507        /** # of messages that have just been fetched */
508        int getJustFetchedMessageCount() {
509            return unseenMessageCount - lastUnseenMessageCount;
510        }
511
512        @Override
513        public String toString() {
514            return "id=" + accountId
515                    + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime + " numUnseen="
516                    + unseenMessageCount;
517        }
518    }
519
520    /**
521     * scan accounts to create a list of { acct, prev sync, next sync, #new }
522     * use this to create a fresh copy.  assumes all accounts need sync
523     *
524     * @param accountId -1 will rebuild the list if empty.  other values will force loading
525     *   of a single account (e.g if it was created after the original list population)
526     */
527    /* package */ void setupSyncReports(long accountId) {
528        synchronized (mSyncReports) {
529            setupSyncReportsLocked(accountId, mContext);
530        }
531    }
532
533    /**
534     * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
535     */
536    /*package*/ void setupSyncReportsLocked(long accountId, Context context) {
537        ContentResolver resolver = context.getContentResolver();
538        if (accountId == SYNC_REPORTS_RESET) {
539            // For test purposes, force refresh of mSyncReports
540            mSyncReports.clear();
541            accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY;
542        } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
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 == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) {
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(context, 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(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY);
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