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