summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/EmailNotificationController.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/email/EmailNotificationController.java')
-rw-r--r--src/com/android/email/EmailNotificationController.java855
1 files changed, 855 insertions, 0 deletions
diff --git a/src/com/android/email/EmailNotificationController.java b/src/com/android/email/EmailNotificationController.java
new file mode 100644
index 000000000..e57b41833
--- /dev/null
+++ b/src/com/android/email/EmailNotificationController.java
@@ -0,0 +1,855 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email;
+
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.provider.Settings;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.android.email.activity.setup.AccountSecurity;
+import com.android.email.activity.setup.HeadlessAccountSettingsLoader;
+import com.android.email.provider.EmailProvider;
+import com.android.email.service.EmailServiceUtils;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.mail.preferences.FolderPreferences;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.Clock;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.NotificationUtils;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class that manages notifications.
+ */
+public class EmailNotificationController implements NotificationController {
+ private static final String LOG_TAG = LogTag.getLogTag();
+
+ private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
+ private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
+ private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
+
+ private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000;
+ private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
+ private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000;
+ private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000;
+
+ private static NotificationThread sNotificationThread;
+ private static Handler sNotificationHandler;
+ private static EmailNotificationController sInstance;
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ private final Clock mClock;
+ /** Maps account id to its observer */
+ private final Map<Long, ContentObserver> mNotificationMap =
+ new HashMap<Long, ContentObserver>();
+ private ContentObserver mAccountObserver;
+
+ /** Constructor */
+ protected EmailNotificationController(Context context, Clock clock) {
+ mContext = context.getApplicationContext();
+ EmailContent.init(context);
+ mNotificationManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ mClock = clock;
+ }
+
+ /** Singleton access */
+ public static synchronized EmailNotificationController getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new EmailNotificationController(context, Clock.INSTANCE);
+ }
+ return sInstance;
+ }
+
+ /**
+ * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
+ * @param notificationId the notification id to check
+ * @return whether or not the notification must be "ongoing"
+ */
+ private static boolean needsOngoingNotification(int notificationId) {
+ // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
+ // be prevented until a reboot. Consider also doing this for password expired.
+ return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED;
+ }
+
+ /**
+ * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the
+ * given account. The account contains specific rules on ring tone usage and these will be used
+ * to modify the notification behaviour.
+ *
+ * @param accountId The id of the account this notification is being built for.
+ * @param ticker Text displayed when the notification is first shown. May be {@code null}.
+ * @param title The first line of text. May NOT be {@code null}.
+ * @param contentText The second line of text. May NOT be {@code null}.
+ * @param intent The intent to start if the user clicks on the notification.
+ * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}.
+ * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
+ * to the settings for the given account.
+ * @return A {@link Notification} that can be sent to the notification service.
+ */
+ private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId,
+ String ticker, CharSequence title, String contentText, Intent intent,
+ Integer number, boolean enableAudio, boolean ongoing) {
+ // Pending Intent
+ PendingIntent pending = null;
+ if (intent != null) {
+ pending = PendingIntent.getActivity(
+ mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ // NOTE: the ticker is not shown for notifications in the Holo UX
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)
+ .setContentTitle(title)
+ .setContentText(contentText)
+ .setContentIntent(pending)
+ .setNumber(number == null ? 0 : number)
+ .setSmallIcon(R.drawable.ic_notification_mail_24dp)
+ .setWhen(mClock.getTime())
+ .setTicker(ticker)
+ .setOngoing(ongoing);
+
+ if (enableAudio) {
+ Account account = Account.restoreAccountWithId(mContext, accountId);
+ setupSoundAndVibration(builder, account);
+ }
+
+ return builder;
+ }
+
+ /**
+ * Generic notifier for any account. Uses notification rules from account.
+ *
+ * @param accountId The account id this notification is being built for.
+ * @param ticker Text displayed when the notification is first shown. May be {@code null}.
+ * @param title The first line of text. May NOT be {@code null}.
+ * @param contentText The second line of text. May NOT be {@code null}.
+ * @param intent The intent to start if the user clicks on the notification.
+ * @param notificationId The ID of the notification to register with the service.
+ */
+ private void showNotification(long accountId, String ticker, String title,
+ String contentText, Intent intent, int notificationId) {
+ final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId,
+ ticker, title, contentText, intent, null, true,
+ needsOngoingNotification(notificationId));
+ mNotificationManager.notify(notificationId, builder.build());
+ }
+
+ /**
+ * Tells the notification controller if it should be watching for changes to the message table.
+ * This is the main life cycle method for message notifications. When we stop observing
+ * database changes, we save the state [e.g. message ID and count] of the most recent
+ * notification shown to the user. And, when we start observing database changes, we restore
+ * the saved state.
+ */
+ @Override
+ public void watchForMessages() {
+ ensureHandlerExists();
+ // Run this on the message notification handler
+ sNotificationHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ContentResolver resolver = mContext.getContentResolver();
+
+ // otherwise, start new observers for all notified accounts
+ registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
+ // If we're already observing account changes, don't do anything else
+ if (mAccountObserver == null) {
+ LogUtils.i(LOG_TAG, "Observing account changes for notifications");
+ mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
+ resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
+ }
+ }
+ });
+ }
+
+ /**
+ * Ensures the notification handler exists and is ready to handle requests.
+ */
+
+ /**
+ * TODO: Notifications jump around too much because we get too many content updates.
+ * We should try to make the provider generate fewer updates instead.
+ */
+
+ private static final int NOTIFICATION_DELAYED_MESSAGE = 0;
+ private static final long NOTIFICATION_DELAY = 15 * DateUtils.SECOND_IN_MILLIS;
+ // True if we're coalescing notification updates
+ private static boolean sNotificationDelayedMessagePending;
+ // True if accounts have changed and we need to refresh everything
+ private static boolean sRefreshAllNeeded;
+ // Set of accounts we need to regenerate notifications for
+ private static final HashSet<Long> sRefreshAccountSet = new HashSet<Long>();
+ // These should all be accessed on-thread, but just in case...
+ private static final Object sNotificationDelayedMessageLock = new Object();
+
+ private static synchronized void ensureHandlerExists() {
+ if (sNotificationThread == null) {
+ sNotificationThread = new NotificationThread();
+ sNotificationHandler = new Handler(sNotificationThread.getLooper(),
+ new Handler.Callback() {
+ @Override
+ public boolean handleMessage(final android.os.Message message) {
+ /**
+ * To reduce spamming the notifications, we quiesce updates for a few
+ * seconds to batch them up, then handle them here.
+ */
+ LogUtils.d(LOG_TAG, "Delayed notification processing");
+ synchronized (sNotificationDelayedMessageLock) {
+ sNotificationDelayedMessagePending = false;
+ final Context context = (Context)message.obj;
+ if (sRefreshAllNeeded) {
+ sRefreshAllNeeded = false;
+ refreshAllNotificationsInternal(context);
+ }
+ for (final Long accountId : sRefreshAccountSet) {
+ refreshNotificationsForAccountInternal(context, accountId);
+ }
+ sRefreshAccountSet.clear();
+ }
+ return true;
+ }
+ });
+ }
+ }
+
+ /**
+ * Registers an observer for changes to mailboxes in the given account.
+ * NOTE: This must be called on the notification handler thread.
+ * @param accountId The ID of the account to register the observer for. May be
+ * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
+ * accounts that allow for user notification.
+ */
+ private void registerMessageNotification(final long accountId) {
+ ContentResolver resolver = mContext.getContentResolver();
+ if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
+ Cursor c = resolver.query(
+ Account.CONTENT_URI, EmailContent.ID_PROJECTION,
+ null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
+ registerMessageNotification(id);
+ }
+ } finally {
+ c.close();
+ }
+ } else {
+ ContentObserver obs = mNotificationMap.get(accountId);
+ if (obs != null) return; // we're already observing; nothing to do
+ LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId);
+ ContentObserver observer = new MessageContentObserver(
+ sNotificationHandler, mContext, accountId);
+ resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
+ mNotificationMap.put(accountId, observer);
+ // Now, ping the observer for any initial notifications
+ observer.onChange(true);
+ }
+ }
+
+ /**
+ * Unregisters the observer for the given account. If the specified account does not have
+ * a registered observer, no action is performed. This will not clear any existing notification
+ * for the specified account. Use {@link NotificationManager#cancel(int)}.
+ * NOTE: This must be called on the notification handler thread.
+ * @param accountId The ID of the account to unregister from. To unregister all accounts that
+ * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
+ */
+ private void unregisterMessageNotification(final long accountId) {
+ ContentResolver resolver = mContext.getContentResolver();
+ if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
+ LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts");
+ // cancel all existing message observers
+ for (ContentObserver observer : mNotificationMap.values()) {
+ resolver.unregisterContentObserver(observer);
+ }
+ mNotificationMap.clear();
+ } else {
+ LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId);
+ ContentObserver observer = mNotificationMap.remove(accountId);
+ if (observer != null) {
+ resolver.unregisterContentObserver(observer);
+ }
+ }
+ }
+
+ public static final String EXTRA_ACCOUNT = "account";
+ public static final String EXTRA_CONVERSATION = "conversationUri";
+ public static final String EXTRA_FOLDER = "folder";
+
+ /** Sets up the notification's sound and vibration based upon account details. */
+ private void setupSoundAndVibration(
+ NotificationCompat.Builder builder, Account account) {
+ String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString();
+ boolean vibrate = false;
+
+ // Use the Inbox notification preferences
+ final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri(
+ "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null);
+
+ com.android.mail.providers.Account uiAccount = null;
+ try {
+ if (accountCursor.moveToFirst()) {
+ uiAccount = com.android.mail.providers.Account.builder().buildFrom(accountCursor);
+ }
+ } finally {
+ accountCursor.close();
+ }
+
+ if (uiAccount != null) {
+ final Cursor folderCursor =
+ mContext.getContentResolver().query(uiAccount.settings.defaultInbox,
+ UIProvider.FOLDERS_PROJECTION, null, null, null);
+
+ if (folderCursor == null) {
+ // This can happen when the notification is for the security policy notification
+ // that happens before the account is setup
+ LogUtils.w(LOG_TAG, "Null folder cursor for mailbox %s",
+ uiAccount.settings.defaultInbox);
+ } else {
+ Folder folder = null;
+ try {
+ if (folderCursor.moveToFirst()) {
+ folder = new Folder(folderCursor);
+ }
+ } finally {
+ folderCursor.close();
+ }
+
+ if (folder != null) {
+ final FolderPreferences folderPreferences = new FolderPreferences(
+ mContext, uiAccount.getEmailAddress(), folder, true /* inbox */);
+
+ ringtoneUri = folderPreferences.getNotificationRingtoneUri();
+ vibrate = folderPreferences.isNotificationVibrateEnabled();
+ } else {
+ LogUtils.e(LOG_TAG,
+ "Null folder for mailbox %s", uiAccount.settings.defaultInbox);
+ }
+ }
+ } else {
+ LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId);
+ }
+
+ int defaults = Notification.DEFAULT_LIGHTS;
+ if (vibrate) {
+ defaults |= Notification.DEFAULT_VIBRATE;
+ }
+
+ builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri))
+ .setDefaults(defaults);
+ }
+
+ /**
+ * Show (or update) a notification that the given attachment could not be forwarded. This
+ * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
+ * it's helpful for debugging.
+ *
+ * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
+ */
+ @Override
+ public void showDownloadForwardFailedNotificationSynchronous(Attachment attachment) {
+ final Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
+ if (message == null) return;
+ final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
+ showNotification(mailbox.mAccountKey,
+ mContext.getString(R.string.forward_download_failed_ticker),
+ mContext.getString(R.string.forward_download_failed_title),
+ attachment.mFileName,
+ null,
+ NOTIFICATION_ID_ATTACHMENT_WARNING);
+ }
+
+ /**
+ * Returns a notification ID for login failed notifications for the given account account.
+ */
+ private static int getLoginFailedNotificationId(long accountId) {
+ return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
+ }
+
+ /**
+ * Show (or update) a notification that there was a login failure for the given account.
+ *
+ * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
+ */
+ @Override
+ public void showLoginFailedNotificationSynchronous(long accountId, boolean incoming) {
+ final Account account = Account.restoreAccountWithId(mContext, accountId);
+ if (account == null) return;
+ final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId,
+ Mailbox.TYPE_INBOX);
+ if (mailbox == null) return;
+
+ final Intent settingsIntent;
+ if (incoming) {
+ settingsIntent = new Intent(Intent.ACTION_VIEW,
+ EmailProvider.getIncomingSettingsUri(accountId));
+ } else {
+ settingsIntent = new Intent(Intent.ACTION_VIEW,
+ HeadlessAccountSettingsLoader.getOutgoingSettingsUri(accountId));
+ }
+ showNotification(mailbox.mAccountKey,
+ mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
+ mContext.getString(R.string.login_failed_title),
+ account.getDisplayName(),
+ settingsIntent,
+ getLoginFailedNotificationId(accountId));
+ }
+
+ /**
+ * Cancels the login failed notification for the given account.
+ */
+ @Override
+ public void cancelLoginFailedNotification(long accountId) {
+ mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
+ }
+
+ /**
+ * Show (or update) a notification that the user's password is expiring. The given account
+ * is used to update the display text, but, all accounts share the same notification ID.
+ *
+ * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
+ */
+ @Override
+ public void showPasswordExpiringNotificationSynchronous(long accountId) {
+ final Account account = Account.restoreAccountWithId(mContext, accountId);
+ if (account == null) return;
+
+ final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
+ accountId, false);
+ final String accountName = account.getDisplayName();
+ final String ticker =
+ mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
+ final String title = mContext.getString(R.string.password_expire_warning_content_title);
+ showNotification(accountId, ticker, title, accountName, intent,
+ NOTIFICATION_ID_PASSWORD_EXPIRING);
+ }
+
+ /**
+ * Show (or update) a notification that the user's password has expired. The given account
+ * is used to update the display text, but, all accounts share the same notification ID.
+ *
+ * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
+ */
+ @Override
+ public void showPasswordExpiredNotificationSynchronous(long accountId) {
+ final Account account = Account.restoreAccountWithId(mContext, accountId);
+ if (account == null) return;
+
+ final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
+ accountId, true);
+ final String accountName = account.getDisplayName();
+ final String ticker = mContext.getString(R.string.password_expired_ticker);
+ final String title = mContext.getString(R.string.password_expired_content_title);
+ showNotification(accountId, ticker, title, accountName, intent,
+ NOTIFICATION_ID_PASSWORD_EXPIRED);
+ }
+
+ /**
+ * Cancels any password expire notifications [both expired & expiring].
+ */
+ @Override
+ public void cancelPasswordExpirationNotifications() {
+ mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
+ mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
+ }
+
+ /**
+ * Show (or update) a security needed notification. If tapped, the user is taken to a
+ * dialog asking whether he wants to update his settings.
+ */
+ @Override
+ public void showSecurityNeededNotification(Account account) {
+ Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
+ String accountName = account.getDisplayName();
+ String ticker =
+ mContext.getString(R.string.security_needed_ticker_fmt, accountName);
+ String title = mContext.getString(R.string.security_notification_content_update_title);
+ showNotification(account.mId, ticker, title, accountName, intent,
+ (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
+ }
+
+ /**
+ * Show (or update) a security changed notification. If tapped, the user is taken to the
+ * account settings screen where he can view the list of enforced policies
+ */
+ @Override
+ public void showSecurityChangedNotification(Account account) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW,
+ EmailProvider.getIncomingSettingsUri(account.getId()));
+ final String accountName = account.getDisplayName();
+ final String ticker =
+ mContext.getString(R.string.security_changed_ticker_fmt, accountName);
+ final String title =
+ mContext.getString(R.string.security_notification_content_change_title);
+ showNotification(account.mId, ticker, title, accountName, intent,
+ (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
+ }
+
+ /**
+ * Show (or update) a security unsupported notification. If tapped, the user is taken to the
+ * account settings screen where he can view the list of unsupported policies
+ */
+ @Override
+ public void showSecurityUnsupportedNotification(Account account) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW,
+ EmailProvider.getIncomingSettingsUri(account.getId()));
+ final String accountName = account.getDisplayName();
+ final String ticker =
+ mContext.getString(R.string.security_unsupported_ticker_fmt, accountName);
+ final String title =
+ mContext.getString(R.string.security_notification_content_unsupported_title);
+ showNotification(account.mId, ticker, title, accountName, intent,
+ (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
+ }
+
+ /**
+ * Cancels all security needed notifications.
+ */
+ @Override
+ public void cancelSecurityNeededNotification() {
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
+ Account.ID_PROJECTION, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long id = c.getLong(Account.ID_PROJECTION_COLUMN);
+ mNotificationManager.cancel(
+ (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id));
+ }
+ }
+ finally {
+ c.close();
+ }
+ }});
+ }
+
+ /**
+ * Cancels all notifications for the specified account id. This includes new mail notifications,
+ * as well as special login/security notifications.
+ */
+ @Override
+ public void cancelNotifications(final Context context, final Account account) {
+ final EmailServiceUtils.EmailServiceInfo serviceInfo
+ = EmailServiceUtils.getServiceInfoForAccount(context, account.mId);
+ if (serviceInfo == null) {
+ LogUtils.d(LOG_TAG, "Can't cancel notification for missing account %d", account.mId);
+ return;
+ }
+ final android.accounts.Account notifAccount
+ = account.getAccountManagerAccount(serviceInfo.accountType);
+
+ NotificationUtils.clearAccountNotifications(context, notifAccount);
+
+ final NotificationManager notificationManager = getInstance(context).mNotificationManager;
+
+ notificationManager.cancel((int) (NOTIFICATION_ID_BASE_LOGIN_WARNING + account.mId));
+ notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
+ notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
+ }
+
+ private static void refreshNotificationsForAccount(final Context context,
+ final long accountId) {
+ synchronized (sNotificationDelayedMessageLock) {
+ if (sNotificationDelayedMessagePending) {
+ sRefreshAccountSet.add(accountId);
+ } else {
+ ensureHandlerExists();
+ sNotificationHandler.sendMessageDelayed(
+ android.os.Message.obtain(sNotificationHandler,
+ NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY);
+ sNotificationDelayedMessagePending = true;
+ refreshNotificationsForAccountInternal(context, accountId);
+ }
+ }
+ }
+
+ private static void refreshNotificationsForAccountInternal(final Context context,
+ final long accountId) {
+ final Uri accountUri = EmailProvider.uiUri("uiaccount", accountId);
+
+ final ContentResolver contentResolver = context.getContentResolver();
+
+ final Cursor mailboxCursor = contentResolver.query(
+ ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, accountId),
+ null, null, null, null);
+ try {
+ while (mailboxCursor.moveToNext()) {
+ final long mailboxId =
+ mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN);
+ if (mailboxId == 0) continue;
+
+ final int unseenCount = mailboxCursor.getInt(
+ EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN);
+
+ final int unreadCount;
+ // If nothing is unseen, clear the notification
+ if (unseenCount == 0) {
+ unreadCount = 0;
+ } else {
+ unreadCount = mailboxCursor.getInt(
+ EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN);
+ }
+
+ final Uri folderUri = EmailProvider.uiUri("uifolder", mailboxId);
+
+
+ LogUtils.d(LOG_TAG, "Changes to account " + accountId + ", folder: "
+ + mailboxId + ", unreadCount: " + unreadCount + ", unseenCount: "
+ + unseenCount);
+
+ final Intent intent = new Intent(UIProvider.ACTION_UPDATE_NOTIFICATION);
+ intent.setPackage(context.getPackageName());
+ intent.setType(EmailProvider.EMAIL_APP_MIME_TYPE);
+
+ intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT, accountUri);
+ intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER, folderUri);
+ intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT,
+ unreadCount);
+ intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT,
+ unseenCount);
+
+ context.sendOrderedBroadcast(intent, null);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+
+ @Override
+ public void handleUpdateNotificationIntent(Context context, Intent intent) {
+ final Uri accountUri =
+ intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT);
+ final Uri folderUri =
+ intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER);
+ final int unreadCount = intent.getIntExtra(
+ UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, 0);
+ final int unseenCount = intent.getIntExtra(
+ UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, 0);
+
+ final ContentResolver contentResolver = context.getContentResolver();
+
+ final Cursor accountCursor = contentResolver.query(accountUri,
+ UIProvider.ACCOUNTS_PROJECTION, null, null, null);
+
+ if (accountCursor == null) {
+ LogUtils.e(LOG_TAG, "Null account cursor for account " + accountUri);
+ return;
+ }
+
+ com.android.mail.providers.Account account = null;
+ try {
+ if (accountCursor.moveToFirst()) {
+ account = com.android.mail.providers.Account.builder().buildFrom(accountCursor);
+ }
+ } finally {
+ accountCursor.close();
+ }
+
+ if (account == null) {
+ LogUtils.d(LOG_TAG, "Tried to create a notification for a missing account "
+ + accountUri);
+ return;
+ }
+
+ final Cursor folderCursor = contentResolver.query(folderUri, UIProvider.FOLDERS_PROJECTION,
+ null, null, null);
+
+ if (folderCursor == null) {
+ LogUtils.e(LOG_TAG, "Null folder cursor for account " + accountUri + ", mailbox "
+ + folderUri);
+ return;
+ }
+
+ Folder folder = null;
+ try {
+ if (folderCursor.moveToFirst()) {
+ folder = new Folder(folderCursor);
+ } else {
+ LogUtils.e(LOG_TAG, "Empty folder cursor for account " + accountUri + ", mailbox "
+ + folderUri);
+ return;
+ }
+ } finally {
+ folderCursor.close();
+ }
+
+ // TODO: we don't always want getAttention to be true, but we don't necessarily have a
+ // good heuristic for when it should or shouldn't be.
+ NotificationUtils.sendSetNewEmailIndicatorIntent(context, unreadCount, unseenCount,
+ account, folder, true /* getAttention */);
+ }
+
+ private static void refreshAllNotifications(final Context context) {
+ synchronized (sNotificationDelayedMessageLock) {
+ if (sNotificationDelayedMessagePending) {
+ sRefreshAllNeeded = true;
+ } else {
+ ensureHandlerExists();
+ sNotificationHandler.sendMessageDelayed(
+ android.os.Message.obtain(sNotificationHandler,
+ NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY);
+ sNotificationDelayedMessagePending = true;
+ refreshAllNotificationsInternal(context);
+ }
+ }
+ }
+
+ private static void refreshAllNotificationsInternal(final Context context) {
+ NotificationUtils.resendNotifications(
+ context, false, null, null, null /* ContactFetcher */);
+ }
+
+ /**
+ * Observer invoked whenever a message we're notifying the user about changes.
+ */
+ private static class MessageContentObserver extends ContentObserver {
+ private final Context mContext;
+ private final long mAccountId;
+
+ public MessageContentObserver(
+ final Handler handler, final Context context, final long accountId) {
+ super(handler);
+ mContext = context;
+ mAccountId = accountId;
+ }
+
+ @Override
+ public void onChange(final boolean selfChange) {
+ refreshNotificationsForAccount(mContext, mAccountId);
+ }
+ }
+
+ /**
+ * Observer invoked whenever an account is modified. This could mean the user changed the
+ * notification settings.
+ */
+ private static class AccountContentObserver extends ContentObserver {
+ private final Context mContext;
+ public AccountContentObserver(final Handler handler, final Context context) {
+ super(handler);
+ mContext = context;
+ }
+
+ @Override
+ public void onChange(final boolean selfChange) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION,
+ null, null, null);
+ final Set<Long> newAccountList = new HashSet<Long>();
+ final Set<Long> removedAccountList = new HashSet<Long>();
+ if (c == null) {
+ // Suspender time ... theoretically, this will never happen
+ LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query");
+ return;
+ }
+ try {
+ while (c.moveToNext()) {
+ long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
+ newAccountList.add(accountId);
+ }
+ } finally {
+ c.close();
+ }
+ // NOTE: Looping over three lists is not necessarily the most efficient. However, the
+ // account lists are going to be very small, so, this will not be necessarily bad.
+ // Cycle through existing notification list and adjust as necessary
+ for (final long accountId : sInstance.mNotificationMap.keySet()) {
+ if (!newAccountList.remove(accountId)) {
+ // account id not in the current set of notifiable accounts
+ removedAccountList.add(accountId);
+ }
+ }
+ // A new account was added to the notification list
+ for (final long accountId : newAccountList) {
+ sInstance.registerMessageNotification(accountId);
+ }
+ // An account was removed from the notification list
+ for (final long accountId : removedAccountList) {
+ sInstance.unregisterMessageNotification(accountId);
+ }
+
+ refreshAllNotifications(mContext);
+ }
+ }
+
+ /**
+ * Thread to handle all notification actions through its own {@link Looper}.
+ */
+ private static class NotificationThread implements Runnable {
+ /** Lock to ensure proper initialization */
+ private final Object mLock = new Object();
+ /** The {@link Looper} that handles messages for this thread */
+ private Looper mLooper;
+
+ public NotificationThread() {
+ new Thread(null, this, "EmailNotification").start();
+ synchronized (mLock) {
+ while (mLooper == null) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException ex) {
+ // Loop around and wait again
+ }
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ Looper.prepare();
+ mLooper = Looper.myLooper();
+ mLock.notifyAll();
+ }
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ Looper.loop();
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+ }
+}