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