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