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