NotificationController.java revision 71bd208ddd6f8ba5a4ac928a4e121e7ad7c21495
1/*
2 * Copyright (C) 2010 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;
18
19import com.android.email.activity.ContactStatusLoader;
20import com.android.email.activity.Welcome;
21import com.android.email.activity.setup.AccountSecurity;
22import com.android.email.activity.setup.AccountSettingsXL;
23import com.android.emailcommon.Logging;
24import com.android.emailcommon.mail.Address;
25import com.android.emailcommon.provider.EmailContent;
26import com.android.emailcommon.provider.EmailContent.Account;
27import com.android.emailcommon.provider.EmailContent.Attachment;
28import com.android.emailcommon.provider.EmailContent.Mailbox;
29import com.android.emailcommon.provider.EmailContent.MailboxColumns;
30import com.android.emailcommon.provider.EmailContent.Message;
31import com.android.emailcommon.provider.EmailContent.MessageColumns;
32import com.android.emailcommon.provider.ProviderUnavailableException;
33import com.android.emailcommon.utility.Utility;
34import com.google.common.annotations.VisibleForTesting;
35
36import android.app.Notification;
37import android.app.NotificationManager;
38import android.app.PendingIntent;
39import android.content.ContentResolver;
40import android.content.Context;
41import android.content.Intent;
42import android.database.ContentObserver;
43import android.database.Cursor;
44import android.graphics.Bitmap;
45import android.graphics.BitmapFactory;
46import android.media.AudioManager;
47import android.net.Uri;
48import android.os.Handler;
49import android.os.Looper;
50import android.os.Process;
51import android.text.SpannableString;
52import android.text.TextUtils;
53import android.text.style.TextAppearanceSpan;
54import android.util.Log;
55
56import java.util.HashMap;
57
58/**
59 * Class that manages notifications.
60 */
61public class NotificationController {
62    private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
63    /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
64    @SuppressWarnings("unused")
65    private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
66    private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
67    private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
68    private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
69
70    private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
71    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
72
73    /** Selection to retrieve accounts that should we notify user for changes */
74    private final static String NOTIFIED_ACCOUNT_SELECTION =
75        Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
76    /** special account ID for the new message notification APIs to specify "all accounts" */
77    private static final long ALL_ACCOUNTS = -1L;
78
79    private static NotificationThread sNewMessageThread;
80    private static Handler sNewMessageHandler;
81    private static NotificationController sInstance;
82    private final Context mContext;
83    private final NotificationManager mNotificationManager;
84    private final AudioManager mAudioManager;
85    private final Bitmap mGenericSenderIcon;
86    private final Clock mClock;
87    // TODO We're maintaining all of our structures based upon the account ID. This is fine
88    // for now since the assumption is that we only ever look for changes in an account's
89    // INBOX. We should adjust our logic to use the mailbox ID instead.
90    /** Maps account id to the message data */
91    private final HashMap<Long, MessageData> mNotificationMap;
92
93    /** Constructor */
94    @VisibleForTesting
95    NotificationController(Context context, Clock clock) {
96        mContext = context.getApplicationContext();
97        mNotificationManager = (NotificationManager) context.getSystemService(
98                Context.NOTIFICATION_SERVICE);
99        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
100        mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
101                R.drawable.ic_contact_picture);
102        mClock = clock;
103        mNotificationMap = new HashMap<Long, MessageData>();
104    }
105
106    /** Singleton access */
107    public static synchronized NotificationController getInstance(Context context) {
108        if (sInstance == null) {
109            sInstance = new NotificationController(context, Clock.INSTANCE);
110        }
111        return sInstance;
112    }
113
114    /**
115     * Returns a {@link Notification} for an event with the given account. The account contains
116     * specific rules on ring tone usage and these will be used to modify the notification
117     * behaviour.
118     *
119     * @param account The account this notification is being built for.
120     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
121     * @param title The first line of text. May NOT be {@code null}.
122     * @param contentText The second line of text. May NOT be {@code null}.
123     * @param intent The intent to start if the user clicks on the notification.
124     * @param largeIcon A large icon. May be {@code null}
125     * @param number A number to display using {@link Notification.Builder#setNumber(int)}. May
126     *        be {@code null}.
127     * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
128     *        to the settings for the given account.
129     * @return A {@link Notification} that can be sent to the notification service.
130     */
131    private Notification createAccountNotification(Account account, String ticker,
132            CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
133            Integer number, boolean enableAudio) {
134        // Pending Intent
135        PendingIntent pending = null;
136        if (intent != null) {
137            pending = PendingIntent.getActivity(
138                    mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
139        }
140
141        // NOTE: the ticker is not shown for notifications in the Holo UX
142        Notification.Builder builder = new Notification.Builder(mContext)
143                .setContentTitle(title)
144                .setContentText(contentText)
145                .setContentIntent(pending)
146                .setLargeIcon(largeIcon)
147                .setNumber(number == null ? 0 : number)
148                .setSmallIcon(R.drawable.stat_notify_email_generic)
149                .setWhen(mClock.getTime())
150                .setTicker(ticker);
151
152        if (enableAudio) {
153            setupSoundAndVibration(builder, account);
154        }
155
156        Notification notification = builder.getNotification();
157        return notification;
158    }
159
160    /**
161     * Generic notifier for any account.  Uses notification rules from account.
162     *
163     * @param account The account this notification is being built for.
164     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
165     * @param title The first line of text. May NOT be {@code null}.
166     * @param contentText The second line of text. May NOT be {@code null}.
167     * @param intent The intent to start if the user clicks on the notification.
168     * @param notificationId The ID of the notification to register with the service.
169     */
170    private void showAccountNotification(Account account, String ticker, String title,
171            String contentText, Intent intent, int notificationId) {
172        Notification notification = createAccountNotification(account, ticker, title, contentText,
173                intent, null, null, true);
174        mNotificationManager.notify(notificationId, notification);
175    }
176
177    /**
178     * Returns a notification ID for new message notifications for the given account.
179     */
180    private int getNewMessageNotificationId(long accountId) {
181        // We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
182        return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
183    }
184
185    /**
186     * Tells the notification controller if it should be watching for changes to the message table.
187     * This is the main life cycle method for message notifications. When we stop observing
188     * database changes, we save the state [e.g. message ID and count] of the most recent
189     * notification shown to the user. And, when we start observing database changes, we restore
190     * the saved state.
191     * @param watch If {@code true}, we register observers for all accounts whose settings have
192     * notifications enabled. Otherwise, all observers are unregistered with the database.
193     */
194    public void watchForMessages(final boolean watch) {
195        // Don't create the thread if we're only going to stop watching
196        if (!watch && sNewMessageHandler == null) return;
197
198        synchronized(sInstance) {
199            if (sNewMessageHandler == null) {
200                sNewMessageThread = new NotificationThread();
201                sNewMessageHandler = new Handler(sNewMessageThread.getLooper());
202            }
203        }
204        // Run this on the message notification handler
205        sNewMessageHandler.post(new Runnable() {
206            @Override
207            public void run() {
208                ContentResolver resolver = mContext.getContentResolver();
209                HashMap<Long, long[]> table;
210                if (!watch) {
211                    unregisterMessageNotification(ALL_ACCOUNTS);
212                    // TODO cancel existing account observers
213
214                    // tear down the event loop
215                    sNewMessageThread.quit();
216                    sNewMessageHandler = null;
217                    sNewMessageThread = null;
218                    return;
219                }
220
221                // otherwise, start new observers for all notified accounts
222                registerMessageNotification(ALL_ACCOUNTS);
223                // Loop through the observers and fire them once
224                for (MessageData data : mNotificationMap.values()) {
225                    if (data.mObserver != null) {
226                        data.mObserver.onChange(true);
227                    }
228                }
229                // TODO Add an account observer to track when an account is removed
230                // TODO Add an account observer to track when an account is added
231            }
232        });
233    }
234
235    /**
236     * Registers an observer for changes to the INBOX for the given account. Since accounts
237     * may only have a single INBOX, we will never have more than one observer for an account.
238     * NOTE: This must be called on the notification handler thread.
239     * @param accountId The ID of the account to register the observer for. May be
240     *                  {@link #ALL_ACCOUNTS} to register observers for all accounts that allow
241     *                  for user notification.
242     */
243    private void registerMessageNotification(long accountId) {
244        ContentResolver resolver = mContext.getContentResolver();
245        if (accountId == ALL_ACCOUNTS) {
246            Cursor c = resolver.query(
247                    Account.CONTENT_URI, EmailContent.ID_PROJECTION,
248                    NOTIFIED_ACCOUNT_SELECTION, null, null);
249            try {
250                while (c.moveToNext()) {
251                    long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
252                    registerMessageNotification(id);
253                }
254            } finally {
255                c.close();
256            }
257        } else {
258            MessageData data = mNotificationMap.get(accountId);
259            if (data != null) return;  // we're already observing; nothing to do
260
261            data = new MessageData();
262            Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
263            ContentObserver observer = new MessageContentObserver(
264                    sNewMessageHandler, mContext, mailbox.mId, accountId);
265            resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
266            data.mObserver = observer;
267            mNotificationMap.put(accountId, data);
268        }
269    }
270
271    /**
272     * Unregisters the observer for the given account. If the specified account does not have
273     * a registered observer, no action is performed.
274     * NOTE: This must be called on the notification handler thread.
275     * @param accountId The ID of the account to unregister from. May be {@link #ALL_ACCOUNTS} to
276     *                  unregister all accounts that have observers.
277     */
278    private void unregisterMessageNotification(long accountId) {
279        ContentResolver resolver = mContext.getContentResolver();
280        if (accountId == ALL_ACCOUNTS) {
281            // cancel all existing message observers
282            for (MessageData data : mNotificationMap.values()) {
283                ContentObserver observer = data.mObserver;
284                resolver.unregisterContentObserver(observer);
285            }
286            mNotificationMap.clear();
287        } else {
288            MessageData data = mNotificationMap.remove(accountId);
289            if (data != null) {
290                ContentObserver observer = data.mObserver;
291                resolver.unregisterContentObserver(observer);
292            }
293        }
294    }
295
296    /**
297     * Cancels a "new message" notification for the specified account.
298     *
299     * @param accountId The ID of the account to cancel for. If {@link #ALL_ACCOUNTS}, "new message"
300     *                  notifications for all accounts will be canceled.
301     * @deprecated Components should not be invoking the notification controller directly.
302     */
303    @Deprecated
304    public void cancelNewMessageNotification(final long accountId) {
305        synchronized(sInstance) {
306            // If we don't have a handler, we'll figure out the correct accounts to
307            // notify the next time we start the controller.
308            if (sNewMessageHandler == null) return;
309        }
310
311        // Run this on the message notification handler
312        sNewMessageHandler.post(new Runnable() {
313            private void clearNotification(long accountId) {
314                if (accountId == ALL_ACCOUNTS) {
315                    for (long id : mNotificationMap.keySet()) {
316                        clearNotification(id);
317                    }
318                } else {
319                    unregisterMessageNotification(accountId);
320                    mNotificationManager.cancel(getNewMessageNotificationId(accountId));
321                }
322            }
323            @Override
324            public void run() {
325                clearNotification(accountId);
326            }
327        });
328    }
329
330    /**
331     * Reset the message notification for the given account to the most recent message ID.
332     * This is not complete and will still exhibit the existing (wrong) behaviour of notifying
333     * the user even if the Email UX is visible.
334     * NOTE: Only for short-term legacy support. To be replaced with a way to temporarily stop
335     * notifications for a mailbox.
336     * @deprecated
337     */
338    @Deprecated
339    public void resetMessageNotification(final long accountId) {
340        synchronized(sInstance) {
341            if (sNewMessageHandler == null) {
342                sNewMessageThread = new NotificationThread();
343                sNewMessageHandler = new Handler(sNewMessageThread.getLooper());
344            }
345        }
346
347        // Run this on the message notification handler
348        sNewMessageHandler.post(new Runnable() {
349            private void resetNotification(long accountId) {
350                if (accountId == ALL_ACCOUNTS) {
351                    for (long id : mNotificationMap.keySet()) {
352                        resetNotification(id);
353                    }
354                } else {
355                    Utility.updateLastSeenMessageKey(mContext, accountId);
356                    mNotificationManager.cancel(getNewMessageNotificationId(accountId));
357                }
358            }
359            @Override
360            public void run() {
361                resetNotification(accountId);
362            }
363        });
364    }
365
366    /**
367     * Returns a picture of the sender of the given message. If no picture is available, returns
368     * {@code null}.
369     *
370     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
371     */
372    private Bitmap getSenderPhoto(Message message) {
373        Address sender = Address.unpackFirst(message.mFrom);
374        if (sender == null) {
375            return null;
376        }
377        String email = sender.getAddress();
378        if (TextUtils.isEmpty(email)) {
379            return null;
380        }
381        return ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
382    }
383
384    /**
385     * Returns a "new message" notification for the given account.
386     *
387     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
388     */
389    @VisibleForTesting
390    Notification createNewMessageNotification(long accountId, long messageId,
391            int unseenMessageCount, boolean enableAudio) {
392        final Account account = Account.restoreAccountWithId(mContext, accountId);
393        if (account == null) {
394            return null;
395        }
396        // Get the latest message
397        final Message message = Message.restoreMessageWithId(mContext, messageId);
398        if (message == null) {
399            return null; // no message found???
400        }
401
402        String senderName = Address.toFriendly(Address.unpack(message.mFrom));
403        if (senderName == null) {
404            senderName = ""; // Happens when a message has no from.
405        }
406        final String subject = message.mSubject;
407        final Bitmap senderPhoto = getSenderPhoto(message);
408        final SpannableString title = getNewMessageTitle(senderName, account.mDisplayName);
409        final Intent intent = Welcome.createOpenAccountInboxIntent(mContext, accountId);
410        final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
411        final Integer number = unseenMessageCount > 1 ? unseenMessageCount : null;
412
413        Notification notification = createAccountNotification(account, null, title, subject,
414                intent, largeIcon, number, enableAudio);
415        return notification;
416    }
417
418    /**
419     * Creates a notification title for a new message. If there is only 1 email account, just
420     * show the sender name. Otherwise, show both the sender and the account name, but, grey
421     * out the account name.
422     */
423    @VisibleForTesting
424    SpannableString getNewMessageTitle(String sender, String receiverDisplayName) {
425        final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
426        if (numAccounts == 1) {
427            return new SpannableString(sender);
428        } else {
429            // "to [account name]"
430            String toAcccount = mContext.getResources().getString(R.string.notification_to_account,
431                    receiverDisplayName);
432            // "[Sender] to [account name]"
433            SpannableString senderToAccount = new SpannableString(sender + " " + toAcccount);
434
435            // "[Sender] to [account name]"
436            //           ^^^^^^^^^^^^^^^^^ <- Make this part gray
437            TextAppearanceSpan secondarySpan = new TextAppearanceSpan(
438                    mContext, R.style.notification_secondary_text);
439            senderToAccount.setSpan(secondarySpan, sender.length() + 1, senderToAccount.length(),
440                    0);
441            return senderToAccount;
442        }
443    }
444
445    /** Returns the system's current ringer mode */
446    @VisibleForTesting
447    int getRingerMode() {
448        return mAudioManager.getRingerMode();
449    }
450
451    /** Sets up the notification's sound and vibration based upon account details. */
452    @VisibleForTesting
453    void setupSoundAndVibration(Notification.Builder builder, Account account) {
454        final int flags = account.mFlags;
455        final String ringtoneUri = account.mRingtoneUri;
456        final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
457        final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
458        final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
459
460        int defaults = Notification.DEFAULT_LIGHTS;
461        if (vibrate || (vibrateWhenSilent && isRingerSilent)) {
462            defaults |= Notification.DEFAULT_VIBRATE;
463        }
464
465        builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri))
466            .setDefaults(defaults);
467    }
468
469    /**
470     * Show (or update) a notification that the given attachment could not be forwarded. This
471     * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
472     * it's helpful for debugging.
473     *
474     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
475     */
476    public void showDownloadForwardFailedNotification(Attachment attachment) {
477        final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey);
478        if (account == null) return;
479        showAccountNotification(account,
480                mContext.getString(R.string.forward_download_failed_ticker),
481                mContext.getString(R.string.forward_download_failed_title),
482                attachment.mFileName,
483                null,
484                NOTIFICATION_ID_ATTACHMENT_WARNING);
485    }
486
487    /**
488     * Returns a notification ID for login failed notifications for the given account account.
489     */
490    private int getLoginFailedNotificationId(long accountId) {
491        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
492    }
493
494    /**
495     * Show (or update) a notification that there was a login failure for the given account.
496     *
497     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
498     */
499    public void showLoginFailedNotification(long accountId) {
500        final Account account = Account.restoreAccountWithId(mContext, accountId);
501        if (account == null) return;
502        showAccountNotification(account,
503                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
504                mContext.getString(R.string.login_failed_title),
505                account.getDisplayName(),
506                AccountSettingsXL.createAccountSettingsIntent(mContext, accountId,
507                        account.mDisplayName),
508                getLoginFailedNotificationId(accountId));
509    }
510
511    /**
512     * Cancels the login failed notification for the given account.
513     */
514    public void cancelLoginFailedNotification(long accountId) {
515        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
516    }
517
518    /**
519     * Show (or update) a notification that the user's password is expiring. The given account
520     * is used to update the display text, but, all accounts share the same notification ID.
521     *
522     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
523     */
524    public void showPasswordExpiringNotification(long accountId) {
525        Account account = Account.restoreAccountWithId(mContext, accountId);
526        if (account == null) return;
527
528        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
529                accountId, false);
530        String accountName = account.getDisplayName();
531        String ticker =
532            mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
533        String title = mContext.getString(R.string.password_expire_warning_content_title);
534        showAccountNotification(account, ticker, title, accountName, intent,
535                NOTIFICATION_ID_PASSWORD_EXPIRING);
536    }
537
538    /**
539     * Show (or update) a notification that the user's password has expired. The given account
540     * is used to update the display text, but, all accounts share the same notification ID.
541     *
542     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
543     */
544    public void showPasswordExpiredNotification(long accountId) {
545        Account account = Account.restoreAccountWithId(mContext, accountId);
546        if (account == null) return;
547
548        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
549                accountId, true);
550        String accountName = account.getDisplayName();
551        String ticker = mContext.getString(R.string.password_expired_ticker);
552        String title = mContext.getString(R.string.password_expired_content_title);
553        showAccountNotification(account, ticker, title, accountName, intent,
554                NOTIFICATION_ID_PASSWORD_EXPIRED);
555    }
556
557    /**
558     * Cancels any password expire notifications [both expired & expiring].
559     */
560    public void cancelPasswordExpirationNotifications() {
561        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
562        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
563    }
564
565    /**
566     * Show (or update) a security needed notification. The given account is used to update
567     * the display text, but, all accounts share the same notification ID.
568     */
569    public void showSecurityNeededNotification(Account account) {
570        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
571        String accountName = account.getDisplayName();
572        String ticker =
573            mContext.getString(R.string.security_notification_ticker_fmt, accountName);
574        String title = mContext.getString(R.string.security_notification_content_title);
575        showAccountNotification(account, ticker, title, accountName, intent,
576                NOTIFICATION_ID_SECURITY_NEEDED);
577    }
578
579    /**
580     * Cancels the security needed notification.
581     */
582    public void cancelSecurityNeededNotification() {
583        mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED);
584    }
585
586    /**
587     * Observer invoked whenever a message we're notifying the user about changes.
588     */
589    private static class MessageContentObserver extends ContentObserver {
590        /** A selection to get messages the user hasn't seen before */
591        private final static String MESSAGE_SELECTION =
592            MessageColumns.MAILBOX_KEY + "=? AND " + MessageColumns.ID + ">? AND "
593            + MessageColumns.FLAG_READ + "=0";
594        private final Context mContext;
595        private final long mMailboxId;
596        private final long mAccountId;
597
598        public MessageContentObserver(
599                Handler handler, Context context, long mailboxId, long accountId) {
600            super(handler);
601            mContext = context;
602            mMailboxId = mailboxId;
603            mAccountId = accountId;
604        }
605
606        @Override
607        public void onChange(boolean selfChange) {
608            super.onChange(selfChange);
609
610            MessageData data = sInstance.mNotificationMap.get(mAccountId);
611            if (data == null) {
612                // notification for a mailbox that we aren't observing; this should not happen
613                Log.e(Logging.LOG_TAG, "Received notifiaction when observer data was null");
614                return;
615            }
616            long oldMessageId = data.mNotifiedMessageId;
617            int oldMessageCount = data.mNotifiedMessageCount;
618
619            ContentResolver resolver = mContext.getContentResolver();
620            long lastSeenMessageId = Utility.getFirstRowLong(
621                    mContext, Mailbox.CONTENT_URI,
622                    new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
623                    EmailContent.ID_SELECTION,
624                    new String[] { Long.toString(mMailboxId) }, null, 0, 0L);
625            Cursor c = resolver.query(
626                    Message.CONTENT_URI, EmailContent.ID_PROJECTION,
627                    MESSAGE_SELECTION,
628                    new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) },
629                    MessageColumns.ID + " DESC");
630            if (c == null) throw new ProviderUnavailableException();
631            try {
632                int newMessageCount = c.getCount();
633                long newMessageId = 0L;
634                if (c.moveToNext()) {
635                    newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
636                }
637
638                if (newMessageCount == 0) {
639                    // No messages to notify for; clear the notification
640                    sInstance.mNotificationManager.cancel(
641                            sInstance.getNewMessageNotificationId(mAccountId));
642                } else if (newMessageCount != oldMessageCount
643                        || (newMessageId != 0 && newMessageId != oldMessageId)) {
644                    // Either the count or last message has changed; update the notification
645                    boolean playAudio = (oldMessageCount == 0); // play audio on first notification
646                    Notification n = sInstance.createNewMessageNotification(
647                            mAccountId, newMessageId, newMessageCount, playAudio);
648                    if (n != null) {
649                        // Make the notification visible
650                        sInstance.mNotificationManager.notify(
651                                sInstance.getNewMessageNotificationId(mAccountId), n);
652                    }
653                }
654                data.mNotifiedMessageId = newMessageId;
655                data.mNotifiedMessageCount = newMessageCount;
656            } finally {
657                c.close();
658            }
659        }
660    }
661
662    /** Information about the message(s) we're notifying the user about. */
663    private static class MessageData {
664        /** The database observer */
665        ContentObserver mObserver;
666        /** Message ID used in the user notification */
667        long mNotifiedMessageId;
668        /** Message count used in the user notification */
669        int mNotifiedMessageCount;
670    }
671
672    /**
673     * Thread to handle all notification actions through its own {@link Looper}.
674     */
675    private static class NotificationThread implements Runnable {
676        /** Lock to ensure proper initialization */
677        private final Object mLock = new Object();
678        /** The {@link Looper} that handles messages for this thread */
679        private Looper mLooper;
680
681        NotificationThread() {
682            new Thread(null, this, "EmailNotification").start();
683            synchronized (mLock) {
684                while (mLooper == null) {
685                    try {
686                        mLock.wait();
687                    } catch (InterruptedException ex) {
688                    }
689                }
690            }
691        }
692
693        @Override
694        public void run() {
695            synchronized (mLock) {
696                Looper.prepare();
697                mLooper = Looper.myLooper();
698                mLock.notifyAll();
699            }
700            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
701            Looper.loop();
702        }
703        void quit() {
704            mLooper.quit();
705        }
706        Looper getLooper() {
707            return mLooper;
708        }
709    }
710}
711