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