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