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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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. */ 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 */ 391 public void showDownloadForwardFailedNotificationSynchronous(Attachment attachment) { 392 final Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 393 if (message == null) return; 394 final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 395 showNotification(mailbox.mAccountKey, 396 mContext.getString(R.string.forward_download_failed_ticker), 397 mContext.getString(R.string.forward_download_failed_title), 398 attachment.mFileName, 399 null, 400 NOTIFICATION_ID_ATTACHMENT_WARNING); 401 } 402 403 /** 404 * Returns a notification ID for login failed notifications for the given account account. 405 */ 406 private static int getLoginFailedNotificationId(long accountId) { 407 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; 408 } 409 410 /** 411 * Show (or update) a notification that there was a login failure for the given account. 412 * 413 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 414 */ 415 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 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 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 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; 760 public AccountContentObserver(final Handler handler, final Context context) { 761 super(handler); 762 mContext = context; 763 } 764 765 @Override 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 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 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 840 public Looper getLooper() { 841 return mLooper; 842 } 843 } 844 } 845