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