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