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.AccountSettings;
     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 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