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