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.Context;
26import android.content.Intent;
27import android.database.ContentObserver;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.net.Uri;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Process;
34import android.provider.Settings;
35import android.support.v4.app.NotificationCompat;
36import android.text.TextUtils;
37import android.text.format.DateUtils;
38
39import com.android.email.activity.setup.AccountSecurity;
40import com.android.email.activity.setup.AccountSettings;
41import com.android.email.provider.EmailProvider;
42import com.android.email.service.EmailServiceUtils;
43import com.android.emailcommon.provider.Account;
44import com.android.emailcommon.provider.EmailContent;
45import com.android.emailcommon.provider.EmailContent.Attachment;
46import com.android.emailcommon.provider.EmailContent.Message;
47import com.android.emailcommon.provider.Mailbox;
48import com.android.emailcommon.utility.EmailAsyncTask;
49import com.android.mail.preferences.FolderPreferences;
50import com.android.mail.providers.Folder;
51import com.android.mail.providers.UIProvider;
52import com.android.mail.utils.Clock;
53import com.android.mail.utils.LogTag;
54import com.android.mail.utils.LogUtils;
55import com.android.mail.utils.NotificationUtils;
56
57import java.util.HashMap;
58import java.util.HashSet;
59import java.util.Map;
60import java.util.Set;
61
62/**
63 * Class that manages notifications.
64 */
65public class NotificationController {
66    private static final String LOG_TAG = LogTag.getLogTag();
67
68    /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
69    @SuppressWarnings("unused")
70    private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
71    private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
72    private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
73    private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
74
75    private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000;
76    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
77    private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000;
78    private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000;
79
80    private static NotificationThread sNotificationThread;
81    private static Handler sNotificationHandler;
82    private static NotificationController sInstance;
83    private final Context mContext;
84    private final NotificationManager mNotificationManager;
85    private final Clock mClock;
86    /** Maps account id to its observer */
87    private final Map<Long, ContentObserver> mNotificationMap =
88            new HashMap<Long, ContentObserver>();
89    private ContentObserver mAccountObserver;
90
91    /** Constructor */
92    private NotificationController(Context context, Clock clock) {
93        mContext = context.getApplicationContext();
94        EmailContent.init(context);
95        mNotificationManager = (NotificationManager) context.getSystemService(
96                Context.NOTIFICATION_SERVICE);
97        mClock = clock;
98    }
99
100    /** Singleton access */
101    public static synchronized NotificationController getInstance(Context context) {
102        if (sInstance == null) {
103            sInstance = new NotificationController(context, Clock.INSTANCE);
104        }
105        return sInstance;
106    }
107
108    /**
109     * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
110     * @param notificationId the notification id to check
111     * @return whether or not the notification must be "ongoing"
112     */
113    private static boolean needsOngoingNotification(int notificationId) {
114        // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
115        // be prevented until a reboot.  Consider also doing this for password expired.
116        return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED;
117    }
118
119    /**
120     * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the
121     * given account. The account contains specific rules on ring tone usage and these will be used
122     * to modify the notification behaviour.
123     *
124     * @param accountId The id of the account this notification is being built for.
125     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
126     * @param title The first line of text. May NOT be {@code null}.
127     * @param contentText The second line of text. May NOT be {@code null}.
128     * @param intent The intent to start if the user clicks on the notification.
129     * @param largeIcon A large icon. May be {@code null}
130     * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}.
131     * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
132     *        to the settings for the given account.
133     * @return A {@link Notification} that can be sent to the notification service.
134     */
135    private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId,
136            String ticker, CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
137            Integer number, boolean enableAudio, boolean ongoing) {
138        // Pending Intent
139        PendingIntent pending = null;
140        if (intent != null) {
141            pending = PendingIntent.getActivity(
142                    mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
143        }
144
145        // NOTE: the ticker is not shown for notifications in the Holo UX
146        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
147                .setContentTitle(title)
148                .setContentText(contentText)
149                .setContentIntent(pending)
150                .setLargeIcon(largeIcon)
151                .setNumber(number == null ? 0 : number)
152                .setSmallIcon(R.drawable.stat_notify_email)
153                .setWhen(mClock.getTime())
154                .setTicker(ticker)
155                .setOngoing(ongoing);
156
157        if (enableAudio) {
158            Account account = Account.restoreAccountWithId(mContext, accountId);
159            setupSoundAndVibration(builder, account);
160        }
161
162        return builder;
163    }
164
165    /**
166     * Generic notifier for any account.  Uses notification rules from account.
167     *
168     * @param accountId The account id this notification is being built for.
169     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
170     * @param title The first line of text. May NOT be {@code null}.
171     * @param contentText The second line of text. May NOT be {@code null}.
172     * @param intent The intent to start if the user clicks on the notification.
173     * @param notificationId The ID of the notification to register with the service.
174     */
175    private void showNotification(long accountId, String ticker, String title,
176            String contentText, Intent intent, int notificationId) {
177        final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId,
178                ticker, title, contentText, intent, null, null, true,
179                needsOngoingNotification(notificationId));
180        mNotificationManager.notify(notificationId, builder.build());
181    }
182
183    /**
184     * Tells the notification controller if it should be watching for changes to the message table.
185     * This is the main life cycle method for message notifications. When we stop observing
186     * database changes, we save the state [e.g. message ID and count] of the most recent
187     * notification shown to the user. And, when we start observing database changes, we restore
188     * the saved state.
189     */
190    public void watchForMessages() {
191        ensureHandlerExists();
192        // Run this on the message notification handler
193        sNotificationHandler.post(new Runnable() {
194            @Override
195            public void run() {
196                ContentResolver resolver = mContext.getContentResolver();
197
198                // otherwise, start new observers for all notified accounts
199                registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
200                // If we're already observing account changes, don't do anything else
201                if (mAccountObserver == null) {
202                    LogUtils.i(LOG_TAG, "Observing account changes for notifications");
203                    mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
204                    resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
205                }
206            }
207        });
208    }
209
210    /**
211     * Ensures the notification handler exists and is ready to handle requests.
212     */
213
214    /**
215     * TODO: Notifications jump around too much because we get too many content updates.
216     * We should try to make the provider generate fewer updates instead.
217     */
218
219    private static final int NOTIFICATION_DELAYED_MESSAGE = 0;
220    private static final long NOTIFICATION_DELAY = 15 * DateUtils.SECOND_IN_MILLIS;
221    // True if we're coalescing notification updates
222    private static boolean sNotificationDelayedMessagePending;
223    // True if accounts have changed and we need to refresh everything
224    private static boolean sRefreshAllNeeded;
225    // Set of accounts we need to regenerate notifications for
226    private static final HashSet<Long> sRefreshAccountSet = new HashSet<Long>();
227    // These should all be accessed on-thread, but just in case...
228    private static final Object sNotificationDelayedMessageLock = new Object();
229
230    private static synchronized void ensureHandlerExists() {
231        if (sNotificationThread == null) {
232            sNotificationThread = new NotificationThread();
233            sNotificationHandler = new Handler(sNotificationThread.getLooper(),
234                    new Handler.Callback() {
235                        @Override
236                        public boolean handleMessage(final android.os.Message message) {
237                            /**
238                             * To reduce spamming the notifications, we quiesce updates for a few
239                             * seconds to batch them up, then handle them here.
240                             */
241                            LogUtils.d(LOG_TAG, "Delayed notification processing");
242                            synchronized (sNotificationDelayedMessageLock) {
243                                sNotificationDelayedMessagePending = false;
244                                final Context context = (Context)message.obj;
245                                if (sRefreshAllNeeded) {
246                                    sRefreshAllNeeded = false;
247                                    refreshAllNotificationsInternal(context);
248                                }
249                                for (final Long accountId : sRefreshAccountSet) {
250                                    refreshNotificationsForAccountInternal(context, accountId);
251                                }
252                                sRefreshAccountSet.clear();
253                            }
254                            return true;
255                        }
256                    });
257        }
258    }
259
260    /**
261     * Registers an observer for changes to mailboxes in the given account.
262     * NOTE: This must be called on the notification handler thread.
263     * @param accountId The ID of the account to register the observer for. May be
264     *                  {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
265     *                  accounts that allow for user notification.
266     */
267    private void registerMessageNotification(final long accountId) {
268        ContentResolver resolver = mContext.getContentResolver();
269        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
270            Cursor c = resolver.query(
271                    Account.CONTENT_URI, EmailContent.ID_PROJECTION,
272                    null, null, null);
273            try {
274                while (c.moveToNext()) {
275                    long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
276                    registerMessageNotification(id);
277                }
278            } finally {
279                c.close();
280            }
281        } else {
282            ContentObserver obs = mNotificationMap.get(accountId);
283            if (obs != null) return;  // we're already observing; nothing to do
284            LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId);
285            ContentObserver observer = new MessageContentObserver(
286                    sNotificationHandler, mContext, accountId);
287            resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
288            mNotificationMap.put(accountId, observer);
289            // Now, ping the observer for any initial notifications
290            observer.onChange(true);
291        }
292    }
293
294    /**
295     * Unregisters the observer for the given account. If the specified account does not have
296     * a registered observer, no action is performed. This will not clear any existing notification
297     * for the specified account. Use {@link NotificationManager#cancel(int)}.
298     * NOTE: This must be called on the notification handler thread.
299     * @param accountId The ID of the account to unregister from. To unregister all accounts that
300     *                  have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
301     */
302    private void unregisterMessageNotification(final long accountId) {
303        ContentResolver resolver = mContext.getContentResolver();
304        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
305            LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts");
306            // cancel all existing message observers
307            for (ContentObserver observer : mNotificationMap.values()) {
308                resolver.unregisterContentObserver(observer);
309            }
310            mNotificationMap.clear();
311        } else {
312            LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId);
313            ContentObserver observer = mNotificationMap.remove(accountId);
314            if (observer != null) {
315                resolver.unregisterContentObserver(observer);
316            }
317        }
318    }
319
320    public static final String EXTRA_ACCOUNT = "account";
321    public static final String EXTRA_CONVERSATION = "conversationUri";
322    public static final String EXTRA_FOLDER = "folder";
323
324    /** Sets up the notification's sound and vibration based upon account details. */
325    private void setupSoundAndVibration(
326            NotificationCompat.Builder builder, Account account) {
327        String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString();
328        boolean vibrate = false;
329
330        // Use the Inbox notification preferences
331        final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri(
332                "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null);
333
334        com.android.mail.providers.Account uiAccount = null;
335        try {
336            if (accountCursor.moveToFirst()) {
337                uiAccount = new com.android.mail.providers.Account(accountCursor);
338            }
339        } finally {
340            accountCursor.close();
341        }
342
343        if (uiAccount != null) {
344            final Cursor folderCursor =
345                    mContext.getContentResolver().query(uiAccount.settings.defaultInbox,
346                            UIProvider.FOLDERS_PROJECTION, null, null, null);
347
348            if (folderCursor == null) {
349                // This can happen when the notification is for the security policy notification
350                // that happens before the account is setup
351                LogUtils.w(LOG_TAG, "Null folder cursor for mailbox %s",
352                        uiAccount.settings.defaultInbox);
353            } else {
354                Folder folder = null;
355                try {
356                    if (folderCursor.moveToFirst()) {
357                        folder = new Folder(folderCursor);
358                    }
359                } finally {
360                    folderCursor.close();
361                }
362
363                if (folder != null) {
364                    final FolderPreferences folderPreferences = new FolderPreferences(
365                            mContext, uiAccount.getEmailAddress(), folder, true /* inbox */);
366
367                    ringtoneUri = folderPreferences.getNotificationRingtoneUri();
368                    vibrate = folderPreferences.isNotificationVibrateEnabled();
369                } else {
370                    LogUtils.e(LOG_TAG,
371                            "Null folder for mailbox %s", uiAccount.settings.defaultInbox);
372                }
373            }
374        } else {
375            LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId);
376        }
377
378        int defaults = Notification.DEFAULT_LIGHTS;
379        if (vibrate) {
380            defaults |= Notification.DEFAULT_VIBRATE;
381        }
382
383        builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri))
384            .setDefaults(defaults);
385    }
386
387    /**
388     * Show (or update) a notification that the given attachment could not be forwarded. This
389     * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
390     * it's helpful for debugging.
391     *
392     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
393     */
394    public void showDownloadForwardFailedNotification(Attachment attachment) {
395        Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
396        if (message == null) return;
397        Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
398        showNotification(mailbox.mAccountKey,
399                mContext.getString(R.string.forward_download_failed_ticker),
400                mContext.getString(R.string.forward_download_failed_title),
401                attachment.mFileName,
402                null,
403                NOTIFICATION_ID_ATTACHMENT_WARNING);
404    }
405
406    /**
407     * Returns a notification ID for login failed notifications for the given account account.
408     */
409    private static int getLoginFailedNotificationId(long accountId) {
410        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
411    }
412
413    /**
414     * Show (or update) a notification that there was a login failure for the given account.
415     *
416     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
417     */
418    public void showLoginFailedNotification(long accountId) {
419        showLoginFailedNotification(accountId, null);
420    }
421
422    public void showLoginFailedNotification(long accountId, String reason) {
423        final Account account = Account.restoreAccountWithId(mContext, accountId);
424        if (account == null) return;
425        final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, account.mId,
426                Mailbox.TYPE_INBOX);
427        if (mailbox == null) return;
428        showNotification(mailbox.mAccountKey,
429                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
430                mContext.getString(R.string.login_failed_title),
431                account.getDisplayName(),
432                AccountSettings.createAccountSettingsIntent(accountId,
433                        account.mDisplayName, reason), getLoginFailedNotificationId(accountId));
434    }
435
436    /**
437     * Cancels the login failed notification for the given account.
438     */
439    public void cancelLoginFailedNotification(long accountId) {
440        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
441    }
442
443    /**
444     * Show (or update) a notification that the user's password is expiring. The given account
445     * is used to update the display text, but, all accounts share the same notification ID.
446     *
447     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
448     */
449    public void showPasswordExpiringNotification(long accountId) {
450        Account account = Account.restoreAccountWithId(mContext, accountId);
451        if (account == null) return;
452
453        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
454                accountId, false);
455        String accountName = account.getDisplayName();
456        String ticker =
457            mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
458        String title = mContext.getString(R.string.password_expire_warning_content_title);
459        showNotification(accountId, ticker, title, accountName, intent,
460                NOTIFICATION_ID_PASSWORD_EXPIRING);
461    }
462
463    /**
464     * Show (or update) a notification that the user's password has expired. The given account
465     * is used to update the display text, but, all accounts share the same notification ID.
466     *
467     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
468     */
469    public void showPasswordExpiredNotification(long accountId) {
470        Account account = Account.restoreAccountWithId(mContext, accountId);
471        if (account == null) return;
472
473        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
474                accountId, true);
475        String accountName = account.getDisplayName();
476        String ticker = mContext.getString(R.string.password_expired_ticker);
477        String title = mContext.getString(R.string.password_expired_content_title);
478        showNotification(accountId, ticker, title, accountName, intent,
479                NOTIFICATION_ID_PASSWORD_EXPIRED);
480    }
481
482    /**
483     * Cancels any password expire notifications [both expired & expiring].
484     */
485    public void cancelPasswordExpirationNotifications() {
486        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
487        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
488    }
489
490    /**
491     * Show (or update) a security needed notification. If tapped, the user is taken to a
492     * dialog asking whether he wants to update his settings.
493     */
494    public void showSecurityNeededNotification(Account account) {
495        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
496        String accountName = account.getDisplayName();
497        String ticker =
498            mContext.getString(R.string.security_needed_ticker_fmt, accountName);
499        String title = mContext.getString(R.string.security_notification_content_update_title);
500        showNotification(account.mId, ticker, title, accountName, intent,
501                (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
502    }
503
504    /**
505     * Show (or update) a security changed notification. If tapped, the user is taken to the
506     * account settings screen where he can view the list of enforced policies
507     */
508    public void showSecurityChangedNotification(Account account) {
509        Intent intent = AccountSettings.createAccountSettingsIntent(account.mId, null, null);
510        String accountName = account.getDisplayName();
511        String ticker =
512            mContext.getString(R.string.security_changed_ticker_fmt, accountName);
513        String title = mContext.getString(R.string.security_notification_content_change_title);
514        showNotification(account.mId, ticker, title, accountName, intent,
515                (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
516    }
517
518    /**
519     * Show (or update) a security unsupported notification. If tapped, the user is taken to the
520     * account settings screen where he can view the list of unsupported policies
521     */
522    public void showSecurityUnsupportedNotification(Account account) {
523        Intent intent = AccountSettings.createAccountSettingsIntent(account.mId, null, null);
524        String accountName = account.getDisplayName();
525        String ticker =
526            mContext.getString(R.string.security_unsupported_ticker_fmt, accountName);
527        String title = mContext.getString(R.string.security_notification_content_unsupported_title);
528        showNotification(account.mId, ticker, title, accountName, intent,
529                (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
530   }
531
532    /**
533     * Cancels all security needed notifications.
534     */
535    public void cancelSecurityNeededNotification() {
536        EmailAsyncTask.runAsyncParallel(new Runnable() {
537            @Override
538            public void run() {
539                Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
540                        Account.ID_PROJECTION, null, null, null);
541                try {
542                    while (c.moveToNext()) {
543                        long id = c.getLong(Account.ID_PROJECTION_COLUMN);
544                        mNotificationManager.cancel(
545                               (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id));
546                    }
547                }
548                finally {
549                    c.close();
550                }
551            }});
552    }
553
554    /**
555     * Cancels all notifications for the specified account id. This includes new mail notifications,
556     * as well as special login/security notifications.
557     */
558    public static void cancelNotifications(final Context context, final Account account) {
559        final EmailServiceUtils.EmailServiceInfo serviceInfo
560                = EmailServiceUtils.getServiceInfoForAccount(context, account.mId);
561        if (serviceInfo == null) {
562            LogUtils.d(LOG_TAG, "Can't cancel notification for missing account %d", account.mId);
563            return;
564        }
565        final android.accounts.Account notifAccount
566                = account.getAccountManagerAccount(serviceInfo.accountType);
567
568        NotificationUtils.clearAccountNotifications(context, notifAccount);
569
570        final NotificationManager notificationManager = getInstance(context).mNotificationManager;
571
572        notificationManager.cancel((int) (NOTIFICATION_ID_BASE_LOGIN_WARNING + account.mId));
573        notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
574        notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
575    }
576
577    private static void refreshNotificationsForAccount(final Context context,
578            final long accountId) {
579        synchronized (sNotificationDelayedMessageLock) {
580            if (sNotificationDelayedMessagePending) {
581                sRefreshAccountSet.add(accountId);
582            } else {
583                ensureHandlerExists();
584                sNotificationHandler.sendMessageDelayed(
585                        android.os.Message.obtain(sNotificationHandler,
586                                NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY);
587                sNotificationDelayedMessagePending = true;
588                refreshNotificationsForAccountInternal(context, accountId);
589            }
590        }
591    }
592
593    private static void refreshNotificationsForAccountInternal(final Context context,
594            final long accountId) {
595        final ContentResolver contentResolver = context.getContentResolver();
596
597        final Cursor accountCursor = contentResolver.query(
598                EmailProvider.uiUri("uiaccount", accountId), UIProvider.ACCOUNTS_PROJECTION,
599                null, null, null);
600
601        if (accountCursor == null) {
602            LogUtils.e(LOG_TAG, "Null account cursor for account id %d", accountId);
603            return;
604        }
605
606        com.android.mail.providers.Account account = null;
607        try {
608            if (accountCursor.moveToFirst()) {
609                account = new com.android.mail.providers.Account(accountCursor);
610            }
611        } finally {
612            accountCursor.close();
613        }
614
615        if (account == null) {
616            LogUtils.d(LOG_TAG, "Tried to create a notification for a missing account %d",
617                    accountId);
618            return;
619        }
620
621        final Cursor mailboxCursor = contentResolver.query(
622                ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, accountId),
623                null, null, null, null);
624        try {
625            while (mailboxCursor.moveToNext()) {
626                final long mailboxId =
627                        mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN);
628                if (mailboxId == 0) continue;
629
630                final int unreadCount = mailboxCursor.getInt(
631                        EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN);
632                final int unseenCount = mailboxCursor.getInt(
633                        EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN);
634
635                final Cursor folderCursor = contentResolver.query(
636                        EmailProvider.uiUri("uifolder", mailboxId),
637                        UIProvider.FOLDERS_PROJECTION, null, null, null);
638
639                if (folderCursor == null) {
640                    LogUtils.e(LOG_TAG, "Null folder cursor for account %d, mailbox %d",
641                            accountId, mailboxId);
642                    continue;
643                }
644
645                Folder folder = null;
646                try {
647                    if (folderCursor.moveToFirst()) {
648                        folder = new Folder(folderCursor);
649                    } else {
650                        LogUtils.e(LOG_TAG, "Empty folder cursor for account %d, mailbox %d",
651                                accountId, mailboxId);
652                        continue;
653                    }
654                } finally {
655                    folderCursor.close();
656                }
657
658                LogUtils.d(LOG_TAG, "Changes to account " + account.name + ", folder: "
659                        + folder.name + ", unreadCount: " + unreadCount + ", unseenCount: "
660                        + unseenCount);
661
662                NotificationUtils.setNewEmailIndicator(context, unreadCount, unseenCount,
663                        account, folder, true);
664            }
665        } finally {
666            mailboxCursor.close();
667        }
668    }
669
670    private static void refreshAllNotifications(final Context context) {
671        synchronized (sNotificationDelayedMessageLock) {
672            if (sNotificationDelayedMessagePending) {
673                sRefreshAllNeeded = true;
674            } else {
675                ensureHandlerExists();
676                sNotificationHandler.sendMessageDelayed(
677                        android.os.Message.obtain(sNotificationHandler,
678                                NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY);
679                sNotificationDelayedMessagePending = true;
680                refreshAllNotificationsInternal(context);
681            }
682        }
683    }
684
685    private static void refreshAllNotificationsInternal(final Context context) {
686        NotificationUtils.resendNotifications(context, false, null, null);
687    }
688
689    /**
690     * Observer invoked whenever a message we're notifying the user about changes.
691     */
692    private static class MessageContentObserver extends ContentObserver {
693        private final Context mContext;
694        private final long mAccountId;
695
696        public MessageContentObserver(
697                final Handler handler, final Context context, final long accountId) {
698            super(handler);
699            mContext = context;
700            mAccountId = accountId;
701        }
702
703        @Override
704        public void onChange(final boolean selfChange) {
705            refreshNotificationsForAccount(mContext, mAccountId);
706        }
707    }
708
709    /**
710     * Observer invoked whenever an account is modified. This could mean the user changed the
711     * notification settings.
712     */
713    private static class AccountContentObserver extends ContentObserver {
714        private final Context mContext;
715        public AccountContentObserver(final Handler handler, final Context context) {
716            super(handler);
717            mContext = context;
718        }
719
720        @Override
721        public void onChange(final boolean selfChange) {
722            final ContentResolver resolver = mContext.getContentResolver();
723            final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
724                null, null, null);
725            final Set<Long> newAccountList = new HashSet<Long>();
726            final Set<Long> removedAccountList = new HashSet<Long>();
727            if (c == null) {
728                // Suspender time ... theoretically, this will never happen
729                LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query");
730                return;
731            }
732            try {
733                while (c.moveToNext()) {
734                    long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
735                    newAccountList.add(accountId);
736                }
737            } finally {
738                c.close();
739            }
740            // NOTE: Looping over three lists is not necessarily the most efficient. However, the
741            // account lists are going to be very small, so, this will not be necessarily bad.
742            // Cycle through existing notification list and adjust as necessary
743            for (final long accountId : sInstance.mNotificationMap.keySet()) {
744                if (!newAccountList.remove(accountId)) {
745                    // account id not in the current set of notifiable accounts
746                    removedAccountList.add(accountId);
747                }
748            }
749            // A new account was added to the notification list
750            for (final long accountId : newAccountList) {
751                sInstance.registerMessageNotification(accountId);
752            }
753            // An account was removed from the notification list
754            for (final long accountId : removedAccountList) {
755                sInstance.unregisterMessageNotification(accountId);
756            }
757
758            refreshAllNotifications(mContext);
759        }
760    }
761
762    /**
763     * Thread to handle all notification actions through its own {@link Looper}.
764     */
765    private static class NotificationThread implements Runnable {
766        /** Lock to ensure proper initialization */
767        private final Object mLock = new Object();
768        /** The {@link Looper} that handles messages for this thread */
769        private Looper mLooper;
770
771        public NotificationThread() {
772            new Thread(null, this, "EmailNotification").start();
773            synchronized (mLock) {
774                while (mLooper == null) {
775                    try {
776                        mLock.wait();
777                    } catch (InterruptedException ex) {
778                        // Loop around and wait again
779                    }
780                }
781            }
782        }
783
784        @Override
785        public void run() {
786            synchronized (mLock) {
787                Looper.prepare();
788                mLooper = Looper.myLooper();
789                mLock.notifyAll();
790            }
791            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
792            Looper.loop();
793        }
794
795        public Looper getLooper() {
796            return mLooper;
797        }
798    }
799}
800