NotificationController.java revision bf5caf97c547c8fa4bbdc2b500e8d43ee6322fcf
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 android.app.Notification;
20import android.app.Notification.Builder;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.Intent;
28import android.database.ContentObserver;
29import android.database.Cursor;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.media.AudioManager;
33import android.net.Uri;
34import android.os.Handler;
35import android.os.Looper;
36import android.os.Process;
37import android.text.SpannableString;
38import android.text.TextUtils;
39import android.util.Log;
40
41import com.android.email.activity.ContactStatusLoader;
42import com.android.email.activity.Welcome;
43import com.android.email.activity.setup.AccountSecurity;
44import com.android.email.activity.setup.AccountSettings;
45import com.android.email.provider.EmailProvider;
46import com.android.emailcommon.Logging;
47import com.android.emailcommon.mail.Address;
48import com.android.emailcommon.provider.Account;
49import com.android.emailcommon.provider.EmailContent;
50import com.android.emailcommon.provider.EmailContent.Attachment;
51import com.android.emailcommon.provider.EmailContent.MailboxColumns;
52import com.android.emailcommon.provider.EmailContent.Message;
53import com.android.emailcommon.provider.Mailbox;
54import com.android.emailcommon.utility.EmailAsyncTask;
55import com.android.emailcommon.utility.Utility;
56import com.google.common.annotations.VisibleForTesting;
57
58import java.util.HashMap;
59import java.util.HashSet;
60
61/**
62 * Class that manages notifications.
63 */
64public class NotificationController {
65    /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
66    @SuppressWarnings("unused")
67    private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
68    private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
69    private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
70    private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
71
72    private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000;
73    private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
74    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
75    private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000;
76    private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000;
77
78    /** Selection to retrieve accounts that should we notify user for changes */
79    private final static String NOTIFIED_ACCOUNT_SELECTION =
80        Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
81
82    private static NotificationThread sNotificationThread;
83    private static Handler sNotificationHandler;
84    private static NotificationController sInstance;
85    private final Context mContext;
86    private final NotificationManager mNotificationManager;
87    private final AudioManager mAudioManager;
88    private final Bitmap mGenericSenderIcon;
89    private final Bitmap mGenericMultipleSenderIcon;
90    private final Clock mClock;
91    /** Maps account id to its observer */
92    private final HashMap<Long, ContentObserver> mNotificationMap;
93    private ContentObserver mAccountObserver;
94    /**
95     * Suspend notifications for this mailbox. If {@link Mailbox.NO_MAILBOX}, no
96     * account notifications are suspended. If {@link Mailbox.COMBINED_INBOX},
97     * notifications for all inboxes are suspended.
98     */
99    private long mSuspendMailboxId = Mailbox.NO_MAILBOX;
100
101    /**
102     * Timestamp indicating when the last message notification sound was played.
103     * Used for throttling.
104     */
105    private long mLastMessageNotifyTime;
106
107    /**
108     * Minimum interval between notification sounds.
109     * Since a long sync (either on account setup or after a long period of being offline) can cause
110     * several notifications consecutively, it can be pretty overwhelming to get a barrage of
111     * notification sounds. Throttle them using this value.
112     */
113    private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds
114
115    /** Constructor */
116    @VisibleForTesting
117    NotificationController(Context context, Clock clock) {
118        mContext = context.getApplicationContext();
119        mNotificationManager = (NotificationManager) context.getSystemService(
120                Context.NOTIFICATION_SERVICE);
121        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
122        mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
123                R.drawable.ic_contact_picture);
124        mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
125                R.drawable.ic_notification_multiple_mail_holo_dark);
126        mClock = clock;
127        mNotificationMap = new HashMap<Long, ContentObserver>();
128    }
129
130    /** Singleton access */
131    public static synchronized NotificationController getInstance(Context context) {
132        if (sInstance == null) {
133            sInstance = new NotificationController(context, Clock.INSTANCE);
134        }
135        return sInstance;
136    }
137
138    /**
139     * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
140     * @param notificationId the notification id to check
141     * @return whether or not the notification must be "ongoing"
142     */
143    private boolean needsOngoingNotification(int notificationId) {
144        // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
145        // be prevented until a reboot.  Consider also doing this for password expired.
146        return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED;
147    }
148
149    /**
150     * Returns a {@link Notification} for an event with the given account. The account contains
151     * specific rules on ring tone usage and these will be used to modify the notification
152     * behaviour.
153     *
154     * @param account The account this notification is being built for.
155     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
156     * @param title The first line of text. May NOT be {@code null}.
157     * @param contentText The second line of text. May NOT be {@code null}.
158     * @param intent The intent to start if the user clicks on the notification.
159     * @param largeIcon A large icon. May be {@code null}
160     * @param number A number to display using {@link Builder#setNumber(int)}. May
161     *        be {@code null}.
162     * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
163     *        to the settings for the given account.
164     * @return A {@link Notification} that can be sent to the notification service.
165     */
166    private Notification createMailboxNotification(Mailbox mailbox, String ticker,
167            CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
168            Integer number, boolean enableAudio, boolean ongoing) {
169        // Pending Intent
170        PendingIntent pending = null;
171        if (intent != null) {
172            pending = PendingIntent.getActivity(
173                    mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
174        }
175
176        // NOTE: the ticker is not shown for notifications in the Holo UX
177        Notification.Builder builder = new Notification.Builder(mContext)
178                .setContentTitle(title)
179                .setContentText(contentText)
180                .setContentIntent(pending)
181                .setLargeIcon(largeIcon)
182                .setNumber(number == null ? 0 : number)
183                .setSmallIcon(R.drawable.stat_notify_email_generic)
184                .setWhen(mClock.getTime())
185                .setTicker(ticker)
186                .setOngoing(ongoing);
187
188        if (enableAudio) {
189            Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
190            setupSoundAndVibration(builder, account);
191        }
192
193        Notification notification = builder.getNotification();
194        return notification;
195    }
196
197    /**
198     * Generic notifier for any account.  Uses notification rules from account.
199     *
200     * @param account The account this notification is being built for.
201     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
202     * @param title The first line of text. May NOT be {@code null}.
203     * @param contentText The second line of text. May NOT be {@code null}.
204     * @param intent The intent to start if the user clicks on the notification.
205     * @param notificationId The ID of the notification to register with the service.
206     */
207    private void showMailboxNotification(Mailbox mailbox, String ticker, String title,
208            String contentText, Intent intent, int notificationId) {
209        Notification notification = createMailboxNotification(mailbox, ticker, title, contentText,
210                intent, null, null, true, needsOngoingNotification(notificationId));
211        mNotificationManager.notify(notificationId, notification);
212    }
213
214    /**
215     * Returns a notification ID for new message notifications for the given account.
216     */
217    private int getNewMessageNotificationId(long mailboxId) {
218        // We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
219        return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + mailboxId);
220    }
221
222    /**
223     * Tells the notification controller if it should be watching for changes to the message table.
224     * This is the main life cycle method for message notifications. When we stop observing
225     * database changes, we save the state [e.g. message ID and count] of the most recent
226     * notification shown to the user. And, when we start observing database changes, we restore
227     * the saved state.
228     * @param watch If {@code true}, we register observers for all accounts whose settings have
229     *              notifications enabled. Otherwise, all observers are unregistered.
230     */
231    public void watchForMessages(final boolean watch) {
232        if (Email.DEBUG) {
233            Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
234        }
235        // Don't create the thread if we're only going to stop watching
236        if (!watch && sNotificationThread == null) return;
237
238        ensureHandlerExists();
239        // Run this on the message notification handler
240        sNotificationHandler.post(new Runnable() {
241            @Override
242            public void run() {
243                ContentResolver resolver = mContext.getContentResolver();
244                if (!watch) {
245                    unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
246                    if (mAccountObserver != null) {
247                        resolver.unregisterContentObserver(mAccountObserver);
248                        mAccountObserver = null;
249                    }
250
251                    // tear down the event loop
252                    sNotificationThread.quit();
253                    sNotificationThread = null;
254                    return;
255                }
256
257                // otherwise, start new observers for all notified accounts
258                registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
259                // If we're already observing account changes, don't do anything else
260                if (mAccountObserver == null) {
261                    if (Email.DEBUG) {
262                        Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
263                    }
264                    mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
265                    resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
266                }
267            }
268        });
269    }
270
271    /**
272     * Temporarily suspend a single mailbox from receiving notifications. NOTE: only a single
273     * mailbox may ever be suspended at a time. So, if this method is invoked a second time,
274     * notifications for the previously mailbox will automatically be re-activated.
275     * @param suspend If {@code true}, suspend notifications for the given mailbox. Otherwise,
276     *              re-activate notifications for the previously suspended mailbox.
277     * @param mailboxId The ID of the mailbox. If this is the special mailbox ID
278     *              {@link Account#ACCOUNT_ID_COMBINED_VIEW},  notifications for all inboxes are
279     *              suspended. If {@code suspend} is {@code false}, the mailbox ID is ignored.
280     */
281    public void suspendMessageNotification(boolean suspend, long mailboxId) {
282        if (mSuspendMailboxId != Mailbox.NO_MAILBOX) {
283            // we're already suspending a mailbox; un-suspend it
284            mSuspendMailboxId = Mailbox.NO_MAILBOX;
285        }
286        if (suspend && mailboxId != Mailbox.NO_MAILBOX) {
287            mSuspendMailboxId = mailboxId;
288        }
289    }
290
291    /**
292     * Ensures the notification handler exists and is ready to handle requests.
293     */
294    private static synchronized void ensureHandlerExists() {
295        if (sNotificationThread == null) {
296            sNotificationThread = new NotificationThread();
297            sNotificationHandler = new Handler(sNotificationThread.getLooper());
298        }
299    }
300
301    /**
302     * Registers an observer for changes to the INBOX for the given account. Since accounts
303     * may only have a single INBOX, we will never have more than one observer for an account.
304     * NOTE: This must be called on the notification handler thread.
305     * @param accountId The ID of the account to register the observer for. May be
306     *                  {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
307     *                  accounts that allow for user notification.
308     */
309    private void registerMessageNotification(long accountId) {
310        ContentResolver resolver = mContext.getContentResolver();
311        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
312            Cursor c = resolver.query(
313                    Account.CONTENT_URI, EmailContent.ID_PROJECTION,
314                    NOTIFIED_ACCOUNT_SELECTION, null, null);
315            try {
316                while (c.moveToNext()) {
317                    long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
318                    registerMessageNotification(id);
319                }
320            } finally {
321                c.close();
322            }
323        } else {
324            ContentObserver obs = mNotificationMap.get(accountId);
325            if (obs != null) return;  // we're already observing; nothing to do
326
327            Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
328            if (mailbox == null) {
329                Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId);
330                return;
331            }
332            if (Email.DEBUG) {
333                Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
334            }
335            ContentObserver observer = new MessageContentObserver(
336                    sNotificationHandler, mContext, accountId);
337            resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
338            mNotificationMap.put(accountId, observer);
339            // Now, ping the observer for any initial notifications
340            observer.onChange(true);
341        }
342    }
343
344    /**
345     * Unregisters the observer for the given account. If the specified account does not have
346     * a registered observer, no action is performed. This will not clear any existing notification
347     * for the specified account. Use {@link NotificationManager#cancel(int)}.
348     * NOTE: This must be called on the notification handler thread.
349     * @param accountId The ID of the account to unregister from. To unregister all accounts that
350     *                  have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
351     */
352    private void unregisterMessageNotification(long accountId) {
353        ContentResolver resolver = mContext.getContentResolver();
354        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
355            if (Email.DEBUG) {
356                Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
357            }
358            // cancel all existing message observers
359            for (ContentObserver observer : mNotificationMap.values()) {
360                resolver.unregisterContentObserver(observer);
361            }
362            mNotificationMap.clear();
363        } else {
364            if (Email.DEBUG) {
365                Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
366            }
367            ContentObserver observer = mNotificationMap.remove(accountId);
368            if (observer != null) {
369                resolver.unregisterContentObserver(observer);
370            }
371        }
372    }
373
374    /**
375     * Returns a picture of the sender of the given message. If no picture is available, returns
376     * {@code null}.
377     *
378     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
379     */
380    private Bitmap getSenderPhoto(Message message) {
381        Address sender = Address.unpackFirst(message.mFrom);
382        if (sender == null) {
383            return null;
384        }
385        String email = sender.getAddress();
386        if (TextUtils.isEmpty(email)) {
387            return null;
388        }
389        return ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
390    }
391
392    /**
393     * Returns a "new message" notification for the given account.
394     *
395     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
396     */
397    @VisibleForTesting
398    Notification createNewMessageNotification(long mailboxId, long messageId,
399            int unseenMessageCount, int unreadCount) {
400        final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
401        if (mailbox == null) {
402            return null;
403        }
404        // No notification if we're suspended...
405        if (mSuspendMailboxId == mailboxId || (mSuspendMailboxId == Mailbox.QUERY_ALL_INBOXES &&
406                mailbox.mType == Mailbox.TYPE_INBOX)) {
407            return null;
408        }
409        final Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey);
410        if (account == null) {
411            return null;
412        }
413        // Get the latest message
414        final Message message = Message.restoreMessageWithId(mContext, messageId);
415        if (message == null) {
416            return null; // no message found???
417        }
418
419        String senderName = Address.toFriendly(Address.unpack(message.mFrom));
420        if (senderName == null) {
421            senderName = ""; // Happens when a message has no from.
422        }
423        final boolean multipleUnseen = unseenMessageCount > 1;
424        final Bitmap senderPhoto = multipleUnseen
425                ? mGenericMultipleSenderIcon
426                : getSenderPhoto(message);
427        final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount);
428        // TODO: add in display name on the second line for the text, once framework supports
429        // multiline texts.
430        // Show account name if an inbox; otherwise mailbox name
431        final String text = multipleUnseen
432                ? ((mailbox.mType == Mailbox.TYPE_INBOX) ? account.mDisplayName :
433                    mailbox.mDisplayName)
434                : message.mSubject;
435        final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
436        final Integer number = unreadCount > 1 ? unreadCount : null;
437        final Intent intent;
438        if (unseenMessageCount > 1) {
439            intent = Welcome.createOpenAccountInboxIntent(mContext, account.mId);
440        } else {
441            intent = Welcome.createOpenMessageIntent(mContext, account.mId, mailboxId, messageId);
442        }
443        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
444        long now = mClock.getTime();
445        boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
446        Notification notification = createMailboxNotification(
447                mailbox, title.toString(), title, text,
448                intent, largeIcon, number, enableAudio, false);
449        mLastMessageNotifyTime = now;
450        return notification;
451    }
452
453    /**
454     * Creates a notification title for a new message. If there is only a single message,
455     * show the sender name. Otherwise, show "X new messages".
456     */
457    @VisibleForTesting
458    SpannableString getNewMessageTitle(String sender, int unseenCount) {
459        String title;
460        if (unseenCount > 1) {
461            title = String.format(
462                    mContext.getString(R.string.notification_multiple_new_messages_fmt),
463                    unseenCount);
464        } else {
465            title = sender;
466        }
467        return new SpannableString(title);
468    }
469
470    /** Returns the system's current ringer mode */
471    @VisibleForTesting
472    int getRingerMode() {
473        return mAudioManager.getRingerMode();
474    }
475
476    /** Sets up the notification's sound and vibration based upon account details. */
477    @VisibleForTesting
478    void setupSoundAndVibration(Notification.Builder builder, Account account) {
479        final int flags = account.mFlags;
480        final String ringtoneUri = account.mRingtoneUri;
481        final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
482        final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
483        final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
484
485        int defaults = Notification.DEFAULT_LIGHTS;
486        if (vibrate || (vibrateWhenSilent && isRingerSilent)) {
487            defaults |= Notification.DEFAULT_VIBRATE;
488        }
489
490        builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri))
491            .setDefaults(defaults);
492    }
493
494    /**
495     * Show (or update) a notification that the given attachment could not be forwarded. This
496     * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
497     * it's helpful for debugging.
498     *
499     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
500     */
501    public void showDownloadForwardFailedNotification(Attachment attachment) {
502        Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
503        if (message == null) return;
504        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
505        showMailboxNotification(mailbox,
506                mContext.getString(R.string.forward_download_failed_ticker),
507                mContext.getString(R.string.forward_download_failed_title),
508                attachment.mFileName,
509                null,
510                NOTIFICATION_ID_ATTACHMENT_WARNING);
511    }
512
513    /**
514     * Returns a notification ID for login failed notifications for the given account account.
515     */
516    private int getLoginFailedNotificationId(long accountId) {
517        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
518    }
519
520    /**
521     * Show (or update) a notification that there was a login failure for the given account.
522     *
523     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
524     */
525    public void showLoginFailedNotification(long accountId) {
526        final Account account = Account.restoreAccountWithId(mContext, accountId);
527        if (account == null) return;
528        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
529                Mailbox.TYPE_INBOX);
530        if (mailbox == null) return;
531        showMailboxNotification(mailbox,
532                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
533                mContext.getString(R.string.login_failed_title),
534                account.getDisplayName(),
535                AccountSettings.createAccountSettingsIntent(mContext, accountId,
536                        account.mDisplayName),
537                getLoginFailedNotificationId(accountId));
538    }
539
540    /**
541     * Cancels the login failed notification for the given account.
542     */
543    public void cancelLoginFailedNotification(long accountId) {
544        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
545    }
546
547    /**
548     * Show (or update) a notification that the user's password is expiring. The given account
549     * is used to update the display text, but, all accounts share the same notification ID.
550     *
551     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
552     */
553    public void showPasswordExpiringNotification(long accountId) {
554        Account account = Account.restoreAccountWithId(mContext, accountId);
555        if (account == null) return;
556        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
557                Mailbox.TYPE_INBOX);
558        if (mailbox == null) return;
559
560        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
561                accountId, false);
562        String accountName = account.getDisplayName();
563        String ticker =
564            mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
565        String title = mContext.getString(R.string.password_expire_warning_content_title);
566        showMailboxNotification(mailbox, ticker, title, accountName, intent,
567                NOTIFICATION_ID_PASSWORD_EXPIRING);
568    }
569
570    /**
571     * Show (or update) a notification that the user's password has expired. The given account
572     * is used to update the display text, but, all accounts share the same notification ID.
573     *
574     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
575     */
576    public void showPasswordExpiredNotification(long accountId) {
577        Account account = Account.restoreAccountWithId(mContext, accountId);
578        if (account == null) return;
579        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
580                Mailbox.TYPE_INBOX);
581        if (mailbox == null) return;
582
583        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
584                accountId, true);
585        String accountName = account.getDisplayName();
586        String ticker = mContext.getString(R.string.password_expired_ticker);
587        String title = mContext.getString(R.string.password_expired_content_title);
588        showMailboxNotification(mailbox, ticker, title, accountName, intent,
589                NOTIFICATION_ID_PASSWORD_EXPIRED);
590    }
591
592    /**
593     * Cancels any password expire notifications [both expired & expiring].
594     */
595    public void cancelPasswordExpirationNotifications() {
596        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
597        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
598    }
599
600    /**
601     * Show (or update) a security needed notification. If tapped, the user is taken to a
602     * dialog asking whether he wants to update his settings.
603     */
604    public void showSecurityNeededNotification(Account account) {
605        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
606                Mailbox.TYPE_INBOX);
607        if (mailbox == null) return;
608        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
609        String accountName = account.getDisplayName();
610        String ticker =
611            mContext.getString(R.string.security_needed_ticker_fmt, accountName);
612        String title = mContext.getString(R.string.security_notification_content_update_title);
613        showMailboxNotification(mailbox, ticker, title, accountName, intent,
614                (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
615    }
616
617    /**
618     * Show (or update) a security changed notification. If tapped, the user is taken to the
619     * account settings screen where he can view the list of enforced policies
620     */
621    public void showSecurityChangedNotification(Account account) {
622        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
623                Mailbox.TYPE_INBOX);
624        if (mailbox == null) return;
625        Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null);
626        String accountName = account.getDisplayName();
627        String ticker =
628            mContext.getString(R.string.security_changed_ticker_fmt, accountName);
629        String title = mContext.getString(R.string.security_notification_content_change_title);
630        showMailboxNotification(mailbox, ticker, title, accountName, intent,
631                (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
632    }
633
634    /**
635     * Show (or update) a security unsupported notification. If tapped, the user is taken to the
636     * account settings screen where he can view the list of unsupported policies
637     */
638    public void showSecurityUnsupportedNotification(Account account) {
639        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
640                Mailbox.TYPE_INBOX);
641        if (mailbox == null) return;
642        Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null);
643        String accountName = account.getDisplayName();
644        String ticker =
645            mContext.getString(R.string.security_unsupported_ticker_fmt, accountName);
646        String title = mContext.getString(R.string.security_notification_content_unsupported_title);
647        showMailboxNotification(mailbox, ticker, title, accountName, intent,
648                (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
649   }
650
651    /**
652     * Cancels all security needed notifications.
653     */
654    public void cancelSecurityNeededNotification() {
655        EmailAsyncTask.runAsyncParallel(new Runnable() {
656            @Override
657            public void run() {
658                Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
659                        Account.ID_PROJECTION, null, null, null);
660                try {
661                    while (c.moveToNext()) {
662                        long id = c.getLong(Account.ID_PROJECTION_COLUMN);
663                        mNotificationManager.cancel(
664                               (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id));
665                    }
666                }
667                finally {
668                    c.close();
669                }
670            }});
671    }
672
673    /**
674     * Observer invoked whenever a message we're notifying the user about changes.
675     */
676    private static class MessageContentObserver extends ContentObserver {
677        private final Context mContext;
678        private final long mAccountId;
679
680        public MessageContentObserver(
681                Handler handler, Context context, long accountId) {
682            super(handler);
683            mContext = context;
684            mAccountId = accountId;
685        }
686
687        @Override
688        public void onChange(boolean selfChange) {
689            ContentObserver observer = sInstance.mNotificationMap.get(mAccountId);
690            Account account = Account.restoreAccountWithId(mContext, mAccountId);
691            if (observer == null || account == null) {
692                Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification");
693                return;
694            }
695
696            ContentResolver resolver = mContext.getContentResolver();
697            Cursor c = resolver.query(ContentUris.withAppendedId(
698                    EmailProvider.MAILBOX_NOTIFICATION_URI, mAccountId),
699                    EmailProvider.NOTIFICATION_PROJECTION, null, null, null);
700            try {
701                while (c.moveToNext()) {
702                    long mailboxId = c.getLong(EmailProvider.NOTIFICATION_MAILBOX_ID_COLUMN);
703                    int messageCount =
704                            c.getInt(EmailProvider.NOTIFICATION_MAILBOX_MESSAGE_COUNT_COLUMN);
705                    int unreadCount =
706                            c.getInt(EmailProvider.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN);
707                    System.err.println("Changes in " + c.getLong(0) + ", unread: " + unreadCount);
708                    long newMessageId = Utility.getFirstRowLong(mContext,
709                            ContentUris.withAppendedId(
710                                    EmailProvider.MAILBOX_MOST_RECENT_MESSAGE_URI, mailboxId),
711                            Message.ID_COLUMN_PROJECTION, null, null, null,
712                            Message.ID_MAILBOX_COLUMN_ID, -1L);
713                    Notification n = sInstance.createNewMessageNotification(mailboxId, newMessageId,
714                            messageCount, unreadCount);
715                    if (n != null) {
716                        // Make the notification visible
717                        sInstance.mNotificationManager.notify(
718                                sInstance.getNewMessageNotificationId(mailboxId), n);
719                    }
720                    // Save away the new values
721                    ContentValues cv = new ContentValues();
722                    cv.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_KEY, newMessageId);
723                    cv.put(MailboxColumns.LAST_NOTIFIED_MESSAGE_COUNT, messageCount);
724                    resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), cv,
725                            null, null);
726                }
727            } finally {
728                c.close();
729            }
730        }
731    }
732
733    /**
734     * Observer invoked whenever an account is modified. This could mean the user changed the
735     * notification settings.
736     */
737    private static class AccountContentObserver extends ContentObserver {
738        private final Context mContext;
739        public AccountContentObserver(Handler handler, Context context) {
740            super(handler);
741            mContext = context;
742        }
743
744        @Override
745        public void onChange(boolean selfChange) {
746            final ContentResolver resolver = mContext.getContentResolver();
747            final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
748                NOTIFIED_ACCOUNT_SELECTION, null, null);
749            final HashSet<Long> newAccountList = new HashSet<Long>();
750            final HashSet<Long> removedAccountList = new HashSet<Long>();
751            if (c == null) {
752                // Suspender time ... theoretically, this will never happen
753                Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query");
754                return;
755            }
756            try {
757                while (c.moveToNext()) {
758                    long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
759                    newAccountList.add(accountId);
760                }
761            } finally {
762                if (c != null) {
763                    c.close();
764                }
765            }
766            // NOTE: Looping over three lists is not necessarily the most efficient. However, the
767            // account lists are going to be very small, so, this will not be necessarily bad.
768            // Cycle through existing notification list and adjust as necessary
769            for (long accountId : sInstance.mNotificationMap.keySet()) {
770                if (!newAccountList.remove(accountId)) {
771                    // account id not in the current set of notifiable accounts
772                    removedAccountList.add(accountId);
773                }
774            }
775            // A new account was added to the notification list
776            for (long accountId : newAccountList) {
777                sInstance.registerMessageNotification(accountId);
778            }
779            // An account was removed from the notification list
780            for (long accountId : removedAccountList) {
781                sInstance.unregisterMessageNotification(accountId);
782                int notificationId = sInstance.getNewMessageNotificationId(accountId);
783                sInstance.mNotificationManager.cancel(notificationId);
784            }
785        }
786    }
787
788    /**
789     * Thread to handle all notification actions through its own {@link Looper}.
790     */
791    private static class NotificationThread implements Runnable {
792        /** Lock to ensure proper initialization */
793        private final Object mLock = new Object();
794        /** The {@link Looper} that handles messages for this thread */
795        private Looper mLooper;
796
797        NotificationThread() {
798            new Thread(null, this, "EmailNotification").start();
799            synchronized (mLock) {
800                while (mLooper == null) {
801                    try {
802                        mLock.wait();
803                    } catch (InterruptedException ex) {
804                    }
805                }
806            }
807        }
808
809        @Override
810        public void run() {
811            synchronized (mLock) {
812                Looper.prepare();
813                mLooper = Looper.myLooper();
814                mLock.notifyAll();
815            }
816            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
817            Looper.loop();
818        }
819        void quit() {
820            mLooper.quit();
821        }
822        Looper getLooper() {
823            return mLooper;
824        }
825    }
826}
827