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