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