MailService.java revision 01f61ef912879c12665d2073917626cb3ee7df0a
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.Controller;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.activity.MessageList;
23import com.android.email.mail.MessagingException;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.AccountColumns;
26import com.android.email.provider.EmailContent.Mailbox;
27
28import android.app.AlarmManager;
29import android.app.Notification;
30import android.app.NotificationManager;
31import android.app.PendingIntent;
32import android.app.Service;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.database.Cursor;
38import android.net.Uri;
39import android.os.IBinder;
40import android.os.SystemClock;
41import android.util.Config;
42import android.util.Log;
43
44import java.util.HashMap;
45
46/**
47 * Background service for refreshing non-push email accounts.
48 */
49public class MailService extends Service {
50    /** DO NOT CHECK IN "TRUE" */
51    private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;     // force 1-minute refresh
52    /** DO NOT CHECK IN "TRUE" */
53    private static final boolean DEBUG_TAG_ALL_INTENTS = true;         // add tagging to intents
54
55    private static final String LOG_TAG = "Email-MailService";
56
57    public static int NEW_MESSAGE_NOTIFICATION_ID = 1;
58
59    private static final String ACTION_CHECK_MAIL =
60        "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
61    private static final String ACTION_RESCHEDULE =
62        "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
63    private static final String ACTION_CANCEL =
64        "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
65    private static final String ACTION_NOTIFY_MAIL =
66        "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
67
68    private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
69    private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
70    private static final String EXTRA_DEBUG_TAG = "com.android.email.intent.extra.TAG";
71    private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
72
73    private static final int WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
74
75    private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
76        new String[] {AccountColumns.NEW_MESSAGE_COUNT};
77
78    private Controller.Result mControllerCallback = new ControllerResults();
79
80    private static int sDebugIntentTag = 1;
81    private int mStartId;
82
83    /**
84     * Access must be synchronized, because there are accesses from the Controller callback
85     */
86    private static HashMap<Long,AccountSyncReport> mSyncReports =
87        new HashMap<Long,AccountSyncReport>();
88
89    /**
90     * Simple template used for clearing new message count in accounts
91     */
92    static ContentValues mClearNewMessages;
93    static {
94        mClearNewMessages = new ContentValues();
95        mClearNewMessages.put(Account.NEW_MESSAGE_COUNT, 0);
96    }
97
98    public static void actionReschedule(Context context) {
99        Intent i = new Intent();
100        i.setClass(context, MailService.class);
101        i.setAction(MailService.ACTION_RESCHEDULE);
102        context.startService(i);
103    }
104
105    public static void actionCancel(Context context)  {
106        Intent i = new Intent();
107        i.setClass(context, MailService.class);
108        i.setAction(MailService.ACTION_CANCEL);
109        context.startService(i);
110    }
111
112    /**
113     * Reset new message counts for one or all accounts.  This clears both our local copy and
114     * the values (if any) stored in the account records.
115     *
116     * @param accountId account to clear, or -1 for all accounts
117     */
118    public static void resetNewMessageCount(Context context, long accountId) {
119        synchronized (mSyncReports) {
120            for (AccountSyncReport report : mSyncReports.values()) {
121                if (accountId == -1 || accountId == report.accountId) {
122                    report.numNewMessages = 0;
123                }
124            }
125        }
126        // now do the database - all accounts, or just one of them
127        Uri uri;
128        if (accountId == -1) {
129            uri = Account.CONTENT_URI;
130        } else {
131            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
132        }
133        context.getContentResolver().update(uri, mClearNewMessages, null, null);
134    }
135
136    /**
137     * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
138     * messages.  This assumes that the push provider has already synced the messages into the
139     * appropriate database - this simply triggers the notification mechanism.
140     *
141     * @param context a context
142     * @param accountId the id of the account that is reporting new messages
143     * @param newCount the number of new messages
144     */
145    public static void actionNotifyNewMessages(Context context, long accountId) {
146        Intent i = new Intent(ACTION_NOTIFY_MAIL);
147        i.setClass(context, MailService.class);
148        i.putExtra(EXTRA_CHECK_ACCOUNT, accountId);
149        context.startService(i);
150    }
151
152    @Override
153    public int onStartCommand(Intent intent, int flags, int startId) {
154        // Force Email debugging until we catch this in the act, fix the bug, and REMOVE THIS
155        Email.DEBUG = true;
156
157        super.onStartCommand(intent, flags, startId);
158
159        // TODO this needs to be passed through the controller and back to us
160        this.mStartId = startId;
161        String action = intent.getAction();
162
163        Controller controller = Controller.getInstance(getApplication());
164        controller.addResultCallback(mControllerCallback);
165
166        if (DEBUG_TAG_ALL_INTENTS) {
167            int tag = intent.getIntExtra(EXTRA_DEBUG_TAG, -1);
168            if (tag == -1) {
169                Log.d(LOG_TAG, "Intent Has No Tag");
170            } else {
171                Log.d(LOG_TAG, "Received intent tag=" + Integer.toString(tag));
172            }
173            if (intent.getBooleanExtra(EXTRA_DEBUG_WATCHDOG, false)) {
174                Log.d(LOG_TAG, "Received watchdog wakeup");
175            }
176        }
177
178        if (ACTION_CHECK_MAIL.equals(action)) {
179            // If we have the data, restore the last-sync-times for each account
180            // These are cached in the wakeup intent in case the process was killed.
181            restoreSyncReports(intent);
182
183            // Sync a specific account if given
184            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
185            long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
186            if (Config.LOGD && Email.DEBUG) {
187                Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId);
188            }
189            if (checkAccountId >= 0) {
190                setWatchdog(checkAccountId, alarmManager);
191            }
192            // if no account given, or the given account cannot be synced - reschedule
193            if (checkAccountId == -1 || !syncOneAccount(controller, checkAccountId, startId)) {
194                // Prevent runaway on the current account by pretending it updated
195                if (checkAccountId != -1) {
196                    updateAccountReport(checkAccountId, 0);
197                }
198                // Find next account to sync, and reschedule
199                reschedule(alarmManager);
200                stopSelf(startId);
201            }
202        }
203        else if (ACTION_CANCEL.equals(action)) {
204            if (Config.LOGD && Email.DEBUG) {
205                Log.d(LOG_TAG, "action: cancel");
206            }
207            cancel();
208            stopSelf(startId);
209        }
210        else if (ACTION_RESCHEDULE.equals(action)) {
211            if (Config.LOGD && Email.DEBUG) {
212                Log.d(LOG_TAG, "action: reschedule");
213            }
214            // As a precaution, clear any outstanding Email notifications
215            // We could be smarter and only do this when the list of accounts changes,
216            // but that's an edge condition and this is much safer.
217            NotificationManager notificationManager =
218                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
219            notificationManager.cancel(NEW_MESSAGE_NOTIFICATION_ID);
220
221            // When called externally, we refresh the sync reports table to pick up
222            // any changes in the account list or account settings
223            refreshSyncReports();
224            // Finally, scan for the next needing update, and set an alarm for it
225            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
226            reschedule(alarmManager);
227            stopSelf(startId);
228        } else if (ACTION_NOTIFY_MAIL.equals(action)) {
229            long accountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
230            // Get the current new message count
231            Cursor c = getContentResolver().query(
232                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
233                    NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
234            int newMessageCount = 0;
235            try {
236                if (c.moveToFirst()) {
237                    newMessageCount = c.getInt(0);
238                } else {
239                    // If the account no longer exists, set to -1 (which is handled below)
240                    accountId = -1;
241                }
242            } finally {
243                c.close();
244            }
245            if (Config.LOGD && Email.DEBUG) {
246                Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
247                        + " count=" + newMessageCount);
248            }
249            if (accountId != -1) {
250                updateAccountReport(accountId, newMessageCount);
251                notifyNewMessages(accountId);
252            }
253            stopSelf(startId);
254        }
255
256        // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
257        // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
258        // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
259        // will fire and get things running again.
260        return START_NOT_STICKY;
261    }
262
263    @Override
264    public IBinder onBind(Intent intent) {
265        return null;
266    }
267
268    @Override
269    public void onDestroy() {
270        super.onDestroy();
271        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
272    }
273
274    private void cancel() {
275        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
276        PendingIntent pi = createAlarmIntent(-1, null, false);
277        alarmMgr.cancel(pi);
278    }
279
280    /**
281     * Refresh the sync reports, to pick up any changes in the account list or account settings.
282     */
283    private void refreshSyncReports() {
284        synchronized (mSyncReports) {
285            // Make shallow copy of sync reports so we can recover the prev sync times
286            HashMap<Long,AccountSyncReport> oldSyncReports =
287                new HashMap<Long,AccountSyncReport>(mSyncReports);
288
289            // Delete the sync reports to force a refresh from live account db data
290            mSyncReports.clear();
291            setupSyncReportsLocked(-1);
292
293            // Restore prev-sync & next-sync times for any reports in the new list
294            for (AccountSyncReport newReport : mSyncReports.values()) {
295                AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
296                if (oldReport != null) {
297                    newReport.prevSyncTime = oldReport.prevSyncTime;
298                    if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
299                        newReport.nextSyncTime =
300                            newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
301                    }
302                }
303            }
304        }
305    }
306
307    /**
308     * Create and send an alarm with the entire list.  This also sends a list of known last-sync
309     * times with the alarm, so if we are killed between alarms, we don't lose this info.
310     *
311     * @param alarmMgr passed in so we can mock for testing.
312     */
313    /* package */ void reschedule(AlarmManager alarmMgr) {
314        // restore the reports if lost
315        setupSyncReports(-1);
316        synchronized (mSyncReports) {
317            int numAccounts = mSyncReports.size();
318            long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
319            int accountInfoIndex = 0;
320
321            long nextCheckTime = Long.MAX_VALUE;
322            AccountSyncReport nextAccount = null;
323            long timeNow = SystemClock.elapsedRealtime();
324
325            for (AccountSyncReport report : mSyncReports.values()) {
326                if (report.syncInterval <= 0) {                         // no timed checks - skip
327                    continue;
328                }
329                long prevSyncTime = report.prevSyncTime;
330                long nextSyncTime = report.nextSyncTime;
331
332                // select next account to sync
333                if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
334                    nextCheckTime = 0;
335                    nextAccount = report;
336                } else if (nextSyncTime < nextCheckTime) {              // next to be checked
337                    nextCheckTime = nextSyncTime;
338                    nextAccount = report;
339                }
340                // collect last-sync-times for all accounts
341                // this is using pairs of {long,long} to simplify passing in a bundle
342                accountInfo[accountInfoIndex++] = report.accountId;
343                accountInfo[accountInfoIndex++] = report.prevSyncTime;
344            }
345
346            // Clear out any unused elements in the array
347            while (accountInfoIndex < accountInfo.length) {
348                accountInfo[accountInfoIndex++] = -1;
349            }
350
351            // set/clear alarm as needed
352            long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
353            PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
354
355            if (nextAccount == null) {
356                alarmMgr.cancel(pi);
357                if (Config.LOGD && Email.DEBUG) {
358                    Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
359                }
360            } else {
361                alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
362                if (Config.LOGD && Email.DEBUG) {
363                    Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
364                            + " for " + nextAccount);
365                }
366            }
367        }
368    }
369
370    /**
371     * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
372     * killed by the system due to memory pressure.)  Normally, a mail check will complete and
373     * the watchdog will be replaced by the call to reschedule().
374     * @param accountId the account we were trying to check
375     * @param alarmMgr system alarm manager
376     */
377    private void setWatchdog(long accountId, AlarmManager alarmMgr) {
378        PendingIntent pi = createAlarmIntent(accountId, null, true);
379        long timeNow = SystemClock.elapsedRealtime();
380        long nextCheckTime = timeNow + WATCHDOG_DELAY;
381        alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
382    }
383
384    /**
385     * Return a pending intent for use by this alarm.  Most of the fields must be the same
386     * (in order for the intent to be recognized by the alarm manager) but the extras can
387     * be different, and are passed in here as parameters.
388     */
389    /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
390            boolean isWatchdog) {
391        Intent i = new Intent();
392        i.setClassName("com.android.email", "com.android.email.service.MailService");
393        i.setAction(ACTION_CHECK_MAIL);
394        i.putExtra(EXTRA_CHECK_ACCOUNT, checkId);
395        i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
396        if (DEBUG_TAG_ALL_INTENTS) {
397            synchronized (this) {
398                Log.d(LOG_TAG, "Make pendingintent with tag=" + Integer.toString(sDebugIntentTag));
399                i.putExtra(EXTRA_DEBUG_TAG, sDebugIntentTag++);
400            }
401        }
402        if (isWatchdog) {
403            i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
404        }
405        PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
406        return pi;
407    }
408
409    /**
410     * Start a controller sync for a specific account
411     *
412     * @param controller The controller to do the sync work
413     * @param checkAccountId the account Id to try and check
414     * @param startId the id of this service launch
415     * @return true if mail checking has started, false if it could not (e.g. bad account id)
416     */
417    private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
418        long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
419        if (inboxId == Mailbox.NO_MAILBOX) {
420            return false;
421        } else {
422            controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback);
423            return true;
424        }
425    }
426
427    /**
428     * Note:  Times are relative to SystemClock.elapsedRealtime()
429     */
430    private static class AccountSyncReport {
431        long accountId;
432        long prevSyncTime;      // 0 == unknown
433        long nextSyncTime;      // 0 == ASAP  -1 == don't sync
434        int numNewMessages;
435
436        int syncInterval;
437        boolean notify;
438        boolean vibrate;
439        Uri ringtoneUri;
440
441        String displayName;     // temporary, for debug logging
442
443
444        @Override
445        public String toString() {
446            return displayName + ": id=" + accountId + " prevSync=" + prevSyncTime
447                    + " nextSync=" + nextSyncTime + " numNew=" + numNewMessages;
448        }
449    }
450
451    /**
452     * scan accounts to create a list of { acct, prev sync, next sync, #new }
453     * use this to create a fresh copy.  assumes all accounts need sync
454     *
455     * @param accountId -1 will rebuild the list if empty.  other values will force loading
456     *   of a single account (e.g if it was created after the original list population)
457     */
458    /* package */ void setupSyncReports(long accountId) {
459        if (Config.LOGD && Email.DEBUG) {
460            Log.d(LOG_TAG, "setupSyncReports: id=" + accountId);
461        }
462        synchronized (mSyncReports) {
463            setupSyncReportsLocked(accountId);
464        }
465    }
466
467    /**
468     * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
469     */
470    private void setupSyncReportsLocked(long accountId) {
471        if (accountId == -1) {
472            // -1 == reload the list if empty, otherwise exit immediately
473            if (mSyncReports.size() > 0) {
474                return;
475            }
476        } else {
477            // load a single account if it doesn't already have a sync record
478            if (mSyncReports.containsKey(accountId)) {
479                return;
480            }
481        }
482
483        // setup to add a single account or all accounts
484        Uri uri;
485        if (accountId == -1) {
486            uri = Account.CONTENT_URI;
487        } else {
488            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
489        }
490
491        // TODO use a narrower projection here
492        Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION,
493                null, null, null);
494        try {
495            while (c.moveToNext()) {
496                AccountSyncReport report = new AccountSyncReport();
497                int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN);
498                int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
499                String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN);
500
501                // For debugging only
502                if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) {
503                    syncInterval = 1;
504                }
505
506                report.accountId = c.getLong(Account.CONTENT_ID_COLUMN);
507                report.prevSyncTime = 0;
508                report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
509                report.numNewMessages = 0;
510
511                report.syncInterval = syncInterval;
512                report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
513                report.vibrate = (flags & Account.FLAGS_VIBRATE) != 0;
514                report.ringtoneUri = (ringtoneString == null) ? null
515                        : Uri.parse(ringtoneString);
516
517                report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
518
519                // TODO lookup # new in inbox
520                mSyncReports.put(report.accountId, report);
521
522                if (Config.LOGD && Email.DEBUG) {
523                    Log.d(LOG_TAG, "setupSyncReports: new:" + report);
524                }
525            }
526        } finally {
527            c.close();
528        }
529    }
530
531    /**
532     * Update list with a single account's sync times and unread count
533     *
534     * @param accountId the account being updated
535     * @param newCount the number of new messages, or -1 if not being reported (don't update)
536     * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
537     */
538    /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
539        // restore the reports if lost
540        setupSyncReports(accountId);
541        synchronized (mSyncReports) {
542            AccountSyncReport report = mSyncReports.get(accountId);
543            if (report == null) {
544                // discard result - there is no longer an account with this id
545                Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
546                return null;
547            }
548
549            // report found - update it (note - editing the report while in-place in the hashmap)
550            report.prevSyncTime = SystemClock.elapsedRealtime();
551            if (report.syncInterval > 0) {
552                report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
553            }
554            if (newCount != -1) {
555                report.numNewMessages = newCount;
556            }
557            if (Config.LOGD && Email.DEBUG) {
558                Log.d(LOG_TAG, "update account " + report.toString());
559            }
560            return report;
561        }
562    }
563
564    /**
565     * when we receive an alarm, update the account sync reports list if necessary
566     * this will be the case when if we have restarted the process and lost the data
567     * in the global.
568     *
569     * @param restoreIntent the intent with the list
570     */
571    /* package */ void restoreSyncReports(Intent restoreIntent) {
572        // restore the reports if lost
573        setupSyncReports(-1);
574        synchronized (mSyncReports) {
575            long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
576            if (accountInfo == null) {
577                Log.d(LOG_TAG, "no data in intent to restore");
578                return;
579            }
580            int accountInfoIndex = 0;
581            int accountInfoLimit = accountInfo.length;
582            while (accountInfoIndex < accountInfoLimit) {
583                long accountId = accountInfo[accountInfoIndex++];
584                long prevSync = accountInfo[accountInfoIndex++];
585                AccountSyncReport report = mSyncReports.get(accountId);
586                if (report != null) {
587                    if (report.prevSyncTime == 0) {
588                        report.prevSyncTime = prevSync;
589                        if (report.syncInterval > 0 && report.prevSyncTime != 0) {
590                            report.nextSyncTime =
591                                report.prevSyncTime + (report.syncInterval * 1000 * 60);
592                        }
593
594                        if (Config.LOGD && Email.DEBUG) {
595                            Log.d(LOG_TAG, "restore prev sync for account" + report);
596                        }
597                    }
598                }
599            }
600        }
601    }
602
603    class ControllerResults implements Controller.Result {
604
605        public void loadMessageForViewCallback(MessagingException result, long messageId,
606                int progress) {
607        }
608
609        public void loadAttachmentCallback(MessagingException result, long messageId,
610                long attachmentId, int progress) {
611        }
612
613        public void updateMailboxCallback(MessagingException result, long accountId,
614                long mailboxId, int progress, int numNewMessages) {
615            if (Config.LOGD && Email.DEBUG) {
616                Log.d(LOG_TAG, "updateMailboxCallback result=" + result
617                        + " accountId=" + accountId);
618            }
619            if (result != null || progress == 100) {
620                // We only track the inbox here in the service - ignore other mailboxes
621                long inboxId = Mailbox.findMailboxOfType(MailService.this,
622                        accountId, Mailbox.TYPE_INBOX);
623                if (mailboxId == inboxId) {
624                    if (progress == 100) {
625                        updateAccountReport(accountId, numNewMessages);
626                        if (numNewMessages > 0) {
627                            notifyNewMessages(accountId);
628                        }
629                    } else {
630                        updateAccountReport(accountId, -1);
631                    }
632                }
633                // Call the global refresh tracker for all mailboxes
634                Email.updateMailboxRefreshTime(mailboxId);
635            }
636        }
637
638        public void updateMailboxListCallback(MessagingException result, long accountId,
639                int progress) {
640        }
641
642        public void serviceCheckMailCallback(MessagingException result, long accountId,
643                long mailboxId, int progress, long tag) {
644            if (Config.LOGD && Email.DEBUG) {
645                Log.d(LOG_TAG, "serviceCheckMailCallback result=" + result
646                        + " accountId=" + accountId + " progress=" + progress);
647            }
648            if (result != null || progress == 100) {
649                if (result != null) {
650                    // the checkmail ended in an error.  force an update of the refresh
651                    // time, so we don't just spin on this account
652                    updateAccountReport(accountId, -1);
653                }
654                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
655                reschedule(alarmManager);
656                int serviceId = MailService.this.mStartId;
657                if (tag != 0) {
658                    serviceId = (int) tag;
659                }
660                stopSelf(serviceId);
661            }
662        }
663
664        public void sendMailCallback(MessagingException result, long accountId, long messageId,
665                int progress) {
666        }
667    }
668
669    /**
670     * Prepare notifications for a given new account having received mail
671     * The notification is organized around the account that has the new mail (e.g. selecting
672     * the alert preferences) but the notification will include a summary if other
673     * accounts also have new mail.
674     */
675    private void notifyNewMessages(long accountId) {
676        boolean notify = false;
677        boolean vibrate = false;
678        Uri ringtone = null;
679        int accountsWithNewMessages = 0;
680        int numNewMessages = 0;
681        String reportName = null;
682        synchronized (mSyncReports) {
683            for (AccountSyncReport report : mSyncReports.values()) {
684                if (report.numNewMessages == 0) {
685                    continue;
686                }
687                numNewMessages += report.numNewMessages;
688                accountsWithNewMessages += 1;
689                if (report.accountId == accountId) {
690                    notify = report.notify;
691                    vibrate = report.vibrate;
692                    ringtone = report.ringtoneUri;
693                    reportName = report.displayName;
694                }
695            }
696        }
697        if (!notify) {
698            return;
699        }
700
701        // set up to post a notification
702        Intent intent;
703        String reportString;
704
705        if (accountsWithNewMessages == 1) {
706            // Prepare a report for a single account
707            // "12 unread (gmail)"
708            reportString = getResources().getQuantityString(
709                    R.plurals.notification_new_one_account_fmt, numNewMessages,
710                    numNewMessages, reportName);
711            intent = MessageList.actionHandleAccountIntent(this,
712                    accountId, -1, Mailbox.TYPE_INBOX);
713        } else {
714            // Prepare a report for multiple accounts
715            // "4 accounts"
716            reportString = getResources().getQuantityString(
717                    R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages,
718                    accountsWithNewMessages);
719            intent = MessageList.actionHandleAccountIntent(this,
720                    -1, Mailbox.QUERY_ALL_INBOXES, -1);
721        }
722
723        // prepare appropriate pending intent, set up notification, and send
724        PendingIntent pending = PendingIntent.getActivity(this, 0, intent, 0);
725
726        Notification notification = new Notification(
727                R.drawable.stat_notify_email_generic,
728                getString(R.string.notification_new_title),
729                System.currentTimeMillis());
730        notification.setLatestEventInfo(this,
731                getString(R.string.notification_new_title),
732                reportString,
733                pending);
734
735        notification.sound = ringtone;
736        // Use same code here as in Gmail and GTalk for vibration
737        if (vibrate) {
738            notification.defaults |= Notification.DEFAULT_VIBRATE;
739        }
740
741        // This code is identical to that used by Gmail and GTalk for notifications
742        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
743        notification.ledARGB = 0xff00ff00;
744        notification.ledOnMS = 500;
745        notification.ledOffMS = 2000;
746
747        NotificationManager notificationManager =
748            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
749        notificationManager.notify(NEW_MESSAGE_NOTIFICATION_ID, notification);
750    }
751}
752