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