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