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