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