NotificationController.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
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 17package com.android.email; 18 19import com.android.email.activity.ContactStatusLoader; 20import com.android.email.activity.Welcome; 21import com.android.email.activity.setup.AccountSecurity; 22import com.android.email.activity.setup.AccountSettings; 23import com.android.emailcommon.Logging; 24import com.android.emailcommon.mail.Address; 25import com.android.emailcommon.provider.Account; 26import com.android.emailcommon.provider.EmailContent; 27import com.android.emailcommon.provider.EmailContent.Attachment; 28import com.android.emailcommon.provider.EmailContent.MailboxColumns; 29import com.android.emailcommon.provider.EmailContent.Message; 30import com.android.emailcommon.provider.EmailContent.MessageColumns; 31import com.android.emailcommon.provider.Mailbox; 32import com.android.emailcommon.utility.Utility; 33 34import com.google.common.annotations.VisibleForTesting; 35 36import android.app.Notification; 37import android.app.Notification.Builder; 38import android.app.NotificationManager; 39import android.app.PendingIntent; 40import android.content.ContentResolver; 41import android.content.Context; 42import android.content.Intent; 43import android.database.ContentObserver; 44import android.database.Cursor; 45import android.graphics.Bitmap; 46import android.graphics.BitmapFactory; 47import android.media.AudioManager; 48import android.net.Uri; 49import android.os.Handler; 50import android.os.Looper; 51import android.os.Process; 52import android.text.SpannableString; 53import android.text.TextUtils; 54import android.text.style.TextAppearanceSpan; 55import android.util.Log; 56 57import java.util.HashMap; 58import java.util.HashSet; 59 60/** 61 * Class that manages notifications. 62 */ 63public class NotificationController { 64 private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; 65 /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */ 66 @SuppressWarnings("unused") 67 private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; 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_NEW_MESSAGES = 0x10000000; 73 private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; 74 75 /** Selection to retrieve accounts that should we notify user for changes */ 76 private final static String NOTIFIED_ACCOUNT_SELECTION = 77 Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0"; 78 79 private static NotificationThread sNotificationThread; 80 private static Handler sNotificationHandler; 81 private static NotificationController sInstance; 82 private final Context mContext; 83 private final NotificationManager mNotificationManager; 84 private final AudioManager mAudioManager; 85 private final Bitmap mGenericSenderIcon; 86 private final Clock mClock; 87 // TODO We're maintaining all of our structures based upon the account ID. This is fine 88 // for now since the assumption is that we only ever look for changes in an account's 89 // INBOX. We should adjust our logic to use the mailbox ID instead. 90 /** Maps account id to the message data */ 91 private final HashMap<Long, MessageData> mNotificationMap; 92 private ContentObserver mAccountObserver; 93 /** 94 * Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no 95 * account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW}, 96 * notifications for all accounts are suspended. 97 */ 98 private long mSuspendAccountId = Account.NO_ACCOUNT; 99 100 /** Constructor */ 101 @VisibleForTesting 102 NotificationController(Context context, Clock clock) { 103 mContext = context.getApplicationContext(); 104 mNotificationManager = (NotificationManager) context.getSystemService( 105 Context.NOTIFICATION_SERVICE); 106 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 107 mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), 108 R.drawable.ic_contact_picture); 109 mClock = clock; 110 mNotificationMap = new HashMap<Long, MessageData>(); 111 } 112 113 /** Singleton access */ 114 public static synchronized NotificationController getInstance(Context context) { 115 if (sInstance == null) { 116 sInstance = new NotificationController(context, Clock.INSTANCE); 117 } 118 return sInstance; 119 } 120 121 /** 122 * Returns a {@link Notification} for an event with the given account. The account contains 123 * specific rules on ring tone usage and these will be used to modify the notification 124 * behaviour. 125 * 126 * @param account The account this notification is being built for. 127 * @param ticker Text displayed when the notification is first shown. May be {@code null}. 128 * @param title The first line of text. May NOT be {@code null}. 129 * @param contentText The second line of text. May NOT be {@code null}. 130 * @param intent The intent to start if the user clicks on the notification. 131 * @param largeIcon A large icon. May be {@code null} 132 * @param number A number to display using {@link Builder#setNumber(int)}. May 133 * be {@code null}. 134 * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according 135 * to the settings for the given account. 136 * @return A {@link Notification} that can be sent to the notification service. 137 */ 138 private Notification createAccountNotification(Account account, String ticker, 139 CharSequence title, String contentText, Intent intent, Bitmap largeIcon, 140 Integer number, boolean enableAudio) { 141 // Pending Intent 142 PendingIntent pending = null; 143 if (intent != null) { 144 pending = PendingIntent.getActivity( 145 mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 146 } 147 148 // NOTE: the ticker is not shown for notifications in the Holo UX 149 Notification.Builder builder = new Notification.Builder(mContext) 150 .setContentTitle(title) 151 .setContentText(contentText) 152 .setContentIntent(pending) 153 .setLargeIcon(largeIcon) 154 .setNumber(number == null ? 0 : number) 155 .setSmallIcon(R.drawable.stat_notify_email_generic) 156 .setWhen(mClock.getTime()) 157 .setTicker(ticker); 158 159 if (enableAudio) { 160 setupSoundAndVibration(builder, account); 161 } 162 163 Notification notification = builder.getNotification(); 164 return notification; 165 } 166 167 /** 168 * Generic notifier for any account. Uses notification rules from account. 169 * 170 * @param account The account this notification is being built for. 171 * @param ticker Text displayed when the notification is first shown. May be {@code null}. 172 * @param title The first line of text. May NOT be {@code null}. 173 * @param contentText The second line of text. May NOT be {@code null}. 174 * @param intent The intent to start if the user clicks on the notification. 175 * @param notificationId The ID of the notification to register with the service. 176 */ 177 private void showAccountNotification(Account account, String ticker, String title, 178 String contentText, Intent intent, int notificationId) { 179 Notification notification = createAccountNotification(account, ticker, title, contentText, 180 intent, null, null, true); 181 mNotificationManager.notify(notificationId, notification); 182 } 183 184 /** 185 * Returns a notification ID for new message notifications for the given account. 186 */ 187 private int getNewMessageNotificationId(long accountId) { 188 // We assume accountId will always be less than 0x0FFFFFFF; is there a better way? 189 return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId); 190 } 191 192 /** 193 * Tells the notification controller if it should be watching for changes to the message table. 194 * This is the main life cycle method for message notifications. When we stop observing 195 * database changes, we save the state [e.g. message ID and count] of the most recent 196 * notification shown to the user. And, when we start observing database changes, we restore 197 * the saved state. 198 * @param watch If {@code true}, we register observers for all accounts whose settings have 199 * notifications enabled. Otherwise, all observers are unregistered. 200 */ 201 public void watchForMessages(final boolean watch) { 202 // Don't create the thread if we're only going to stop watching 203 if (!watch && sNotificationThread == null) return; 204 205 ensureHandlerExists(); 206 // Run this on the message notification handler 207 sNotificationHandler.post(new Runnable() { 208 @Override 209 public void run() { 210 ContentResolver resolver = mContext.getContentResolver(); 211 HashMap<Long, long[]> table; 212 if (!watch) { 213 unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); 214 if (mAccountObserver != null) { 215 resolver.unregisterContentObserver(mAccountObserver); 216 mAccountObserver = null; 217 } 218 219 // tear down the event loop 220 sNotificationThread.quit(); 221 sNotificationThread = null; 222 return; 223 } 224 225 // otherwise, start new observers for all notified accounts 226 registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); 227 // If we're already observing account changes, don't do anything else 228 if (mAccountObserver == null) { 229 mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); 230 resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); 231 } 232 } 233 }); 234 } 235 236 /** 237 * Temporarily suspend a single account from receiving notifications. NOTE: only a single 238 * account may ever be suspended at a time. So, if this method is invoked a second time, 239 * notifications for the previously suspended account will automatically be re-activated. 240 * @param suspend If {@code true}, suspend notifications for the given account. Otherwise, 241 * re-activate notifications for the previously suspended account. 242 * @param accountId The ID of the account. If this is the special account ID 243 * {@link Account#ACCOUNT_ID_COMBINED_VIEW}, notifications for all accounts are 244 * suspended. If {@code suspend} is {@code false}, the account ID is ignored. 245 */ 246 public void suspendMessageNotification(boolean suspend, long accountId) { 247 if (mSuspendAccountId != Account.NO_ACCOUNT) { 248 // we're already suspending an account; un-suspend it 249 mSuspendAccountId = Account.NO_ACCOUNT; 250 } 251 if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) { 252 mSuspendAccountId = accountId; 253 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 254 // Only go onto the notification handler if we really, absolutely need to 255 ensureHandlerExists(); 256 sNotificationHandler.post(new Runnable() { 257 @Override 258 public void run() { 259 for (long accountId : mNotificationMap.keySet()) { 260 mNotificationManager.cancel(getNewMessageNotificationId(accountId)); 261 } 262 } 263 }); 264 } else { 265 mNotificationManager.cancel(getNewMessageNotificationId(accountId)); 266 } 267 } 268 } 269 270 /** 271 * Ensures the notification handler exists and is ready to handle requests. 272 */ 273 private static synchronized void ensureHandlerExists() { 274 if (sNotificationThread == null) { 275 sNotificationThread = new NotificationThread(); 276 sNotificationHandler = new Handler(sNotificationThread.getLooper()); 277 } 278 } 279 280 /** 281 * Registers an observer for changes to the INBOX for the given account. Since accounts 282 * may only have a single INBOX, we will never have more than one observer for an account. 283 * NOTE: This must be called on the notification handler thread. 284 * @param accountId The ID of the account to register the observer for. May be 285 * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all 286 * accounts that allow for user notification. 287 */ 288 private void registerMessageNotification(long accountId) { 289 ContentResolver resolver = mContext.getContentResolver(); 290 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 291 Cursor c = resolver.query( 292 Account.CONTENT_URI, EmailContent.ID_PROJECTION, 293 NOTIFIED_ACCOUNT_SELECTION, null, null); 294 try { 295 while (c.moveToNext()) { 296 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 297 registerMessageNotification(id); 298 } 299 } finally { 300 c.close(); 301 } 302 } else { 303 MessageData data = mNotificationMap.get(accountId); 304 if (data != null) return; // we're already observing; nothing to do 305 306 data = new MessageData(); 307 Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); 308 if (mailbox == null) { 309 Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId); 310 return; 311 } 312 ContentObserver observer = new MessageContentObserver( 313 sNotificationHandler, mContext, mailbox.mId, accountId); 314 resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); 315 data.mObserver = observer; 316 mNotificationMap.put(accountId, data); 317 // Now, ping the observer for any initial notifications 318 data.mObserver.onChange(true); 319 } 320 } 321 322 /** 323 * Unregisters the observer for the given account. If the specified account does not have 324 * a registered observer, no action is performed. This will not clear any existing notification 325 * for the specified account. Use {@link NotificationManager#cancel(int)}. 326 * NOTE: This must be called on the notification handler thread. 327 * @param accountId The ID of the account to unregister from. To unregister all accounts that 328 * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 329 */ 330 private void unregisterMessageNotification(long accountId) { 331 ContentResolver resolver = mContext.getContentResolver(); 332 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 333 // cancel all existing message observers 334 for (MessageData data : mNotificationMap.values()) { 335 ContentObserver observer = data.mObserver; 336 resolver.unregisterContentObserver(observer); 337 } 338 mNotificationMap.clear(); 339 } else { 340 MessageData data = mNotificationMap.remove(accountId); 341 if (data != null) { 342 ContentObserver observer = data.mObserver; 343 resolver.unregisterContentObserver(observer); 344 } 345 } 346 } 347 348 /** 349 * Returns a picture of the sender of the given message. If no picture is available, returns 350 * {@code null}. 351 * 352 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 353 */ 354 private Bitmap getSenderPhoto(Message message) { 355 Address sender = Address.unpackFirst(message.mFrom); 356 if (sender == null) { 357 return null; 358 } 359 String email = sender.getAddress(); 360 if (TextUtils.isEmpty(email)) { 361 return null; 362 } 363 return ContactStatusLoader.getContactInfo(mContext, email).mPhoto; 364 } 365 366 /** 367 * Returns a "new message" notification for the given account. 368 * 369 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 370 */ 371 @VisibleForTesting 372 Notification createNewMessageNotification(long accountId, long mailboxId, long messageId, 373 int unseenMessageCount, boolean enableAudio) { 374 final Account account = Account.restoreAccountWithId(mContext, accountId); 375 if (account == null) { 376 return null; 377 } 378 // Get the latest message 379 final Message message = Message.restoreMessageWithId(mContext, messageId); 380 if (message == null) { 381 return null; // no message found??? 382 } 383 384 String senderName = Address.toFriendly(Address.unpack(message.mFrom)); 385 if (senderName == null) { 386 senderName = ""; // Happens when a message has no from. 387 } 388 final String subject = message.mSubject; 389 final Bitmap senderPhoto = getSenderPhoto(message); 390 final SpannableString title = getNewMessageTitle(senderName, account.mDisplayName); 391 final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon; 392 final Integer number = unseenMessageCount > 1 ? unseenMessageCount : null; 393 final Intent intent; 394 if (unseenMessageCount >= 1) { 395 intent = Welcome.createOpenMessageIntent(mContext, accountId, mailboxId, messageId); 396 } else { 397 intent = Welcome.createOpenAccountInboxIntent(mContext, accountId); 398 } 399 400 Notification notification = createAccountNotification(account, null, title, subject, 401 intent, largeIcon, number, enableAudio); 402 return notification; 403 } 404 405 /** 406 * Creates a notification title for a new message. If there is only 1 email account, just 407 * show the sender name. Otherwise, show both the sender and the account name, but, grey 408 * out the account name. 409 */ 410 @VisibleForTesting 411 SpannableString getNewMessageTitle(String sender, String receiverDisplayName) { 412 final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI); 413 if (numAccounts == 1) { 414 return new SpannableString(sender); 415 } else { 416 // "to [account name]" 417 String toAcccount = mContext.getResources().getString(R.string.notification_to_account, 418 receiverDisplayName); 419 // "[Sender] to [account name]" 420 SpannableString senderToAccount = new SpannableString(sender + " " + toAcccount); 421 422 // "[Sender] to [account name]" 423 // ^^^^^^^^^^^^^^^^^ <- Make this part gray 424 TextAppearanceSpan secondarySpan = new TextAppearanceSpan( 425 mContext, R.style.notification_secondary_text); 426 senderToAccount.setSpan(secondarySpan, sender.length() + 1, senderToAccount.length(), 427 0); 428 return senderToAccount; 429 } 430 } 431 432 /** Returns the system's current ringer mode */ 433 @VisibleForTesting 434 int getRingerMode() { 435 return mAudioManager.getRingerMode(); 436 } 437 438 /** Sets up the notification's sound and vibration based upon account details. */ 439 @VisibleForTesting 440 void setupSoundAndVibration(Notification.Builder builder, Account account) { 441 final int flags = account.mFlags; 442 final String ringtoneUri = account.mRingtoneUri; 443 final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0; 444 final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0; 445 final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL; 446 447 int defaults = Notification.DEFAULT_LIGHTS; 448 if (vibrate || (vibrateWhenSilent && isRingerSilent)) { 449 defaults |= Notification.DEFAULT_VIBRATE; 450 } 451 452 builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri)) 453 .setDefaults(defaults); 454 } 455 456 /** 457 * Show (or update) a notification that the given attachment could not be forwarded. This 458 * is a very unusual case, and perhaps we shouldn't even send a notification. For now, 459 * it's helpful for debugging. 460 * 461 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 462 */ 463 public void showDownloadForwardFailedNotification(Attachment attachment) { 464 final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey); 465 if (account == null) return; 466 showAccountNotification(account, 467 mContext.getString(R.string.forward_download_failed_ticker), 468 mContext.getString(R.string.forward_download_failed_title), 469 attachment.mFileName, 470 null, 471 NOTIFICATION_ID_ATTACHMENT_WARNING); 472 } 473 474 /** 475 * Returns a notification ID for login failed notifications for the given account account. 476 */ 477 private int getLoginFailedNotificationId(long accountId) { 478 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; 479 } 480 481 /** 482 * Show (or update) a notification that there was a login failure for the given account. 483 * 484 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 485 */ 486 public void showLoginFailedNotification(long accountId) { 487 final Account account = Account.restoreAccountWithId(mContext, accountId); 488 if (account == null) return; 489 showAccountNotification(account, 490 mContext.getString(R.string.login_failed_ticker, account.mDisplayName), 491 mContext.getString(R.string.login_failed_title), 492 account.getDisplayName(), 493 AccountSettings.createAccountSettingsIntent(mContext, accountId, 494 account.mDisplayName), 495 getLoginFailedNotificationId(accountId)); 496 } 497 498 /** 499 * Cancels the login failed notification for the given account. 500 */ 501 public void cancelLoginFailedNotification(long accountId) { 502 mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); 503 } 504 505 /** 506 * Show (or update) a notification that the user's password is expiring. The given account 507 * is used to update the display text, but, all accounts share the same notification ID. 508 * 509 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 510 */ 511 public void showPasswordExpiringNotification(long accountId) { 512 Account account = Account.restoreAccountWithId(mContext, accountId); 513 if (account == null) return; 514 515 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, 516 accountId, false); 517 String accountName = account.getDisplayName(); 518 String ticker = 519 mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName); 520 String title = mContext.getString(R.string.password_expire_warning_content_title); 521 showAccountNotification(account, ticker, title, accountName, intent, 522 NOTIFICATION_ID_PASSWORD_EXPIRING); 523 } 524 525 /** 526 * Show (or update) a notification that the user's password has expired. The given account 527 * is used to update the display text, but, all accounts share the same notification ID. 528 * 529 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 530 */ 531 public void showPasswordExpiredNotification(long accountId) { 532 Account account = Account.restoreAccountWithId(mContext, accountId); 533 if (account == null) return; 534 535 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, 536 accountId, true); 537 String accountName = account.getDisplayName(); 538 String ticker = mContext.getString(R.string.password_expired_ticker); 539 String title = mContext.getString(R.string.password_expired_content_title); 540 showAccountNotification(account, ticker, title, accountName, intent, 541 NOTIFICATION_ID_PASSWORD_EXPIRED); 542 } 543 544 /** 545 * Cancels any password expire notifications [both expired & expiring]. 546 */ 547 public void cancelPasswordExpirationNotifications() { 548 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); 549 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); 550 } 551 552 /** 553 * Show (or update) a security needed notification. The given account is used to update 554 * the display text, but, all accounts share the same notification ID. 555 */ 556 public void showSecurityNeededNotification(Account account) { 557 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); 558 String accountName = account.getDisplayName(); 559 String ticker = 560 mContext.getString(R.string.security_notification_ticker_fmt, accountName); 561 String title = mContext.getString(R.string.security_notification_content_title); 562 showAccountNotification(account, ticker, title, accountName, intent, 563 NOTIFICATION_ID_SECURITY_NEEDED); 564 } 565 566 /** 567 * Cancels the security needed notification. 568 */ 569 public void cancelSecurityNeededNotification() { 570 mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED); 571 } 572 573 /** 574 * Observer invoked whenever a message we're notifying the user about changes. 575 */ 576 private static class MessageContentObserver extends ContentObserver { 577 /** A selection to get messages the user hasn't seen before */ 578 private final static String MESSAGE_SELECTION = 579 MessageColumns.MAILBOX_KEY + "=? AND " + MessageColumns.ID + ">? AND " 580 + MessageColumns.FLAG_READ + "=0"; 581 private final Context mContext; 582 private final long mMailboxId; 583 private final long mAccountId; 584 585 public MessageContentObserver( 586 Handler handler, Context context, long mailboxId, long accountId) { 587 super(handler); 588 mContext = context; 589 mMailboxId = mailboxId; 590 mAccountId = accountId; 591 } 592 593 @Override 594 public void onChange(boolean selfChange) { 595 if (mAccountId == sInstance.mSuspendAccountId 596 || sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 597 return; 598 } 599 600 MessageData data = sInstance.mNotificationMap.get(mAccountId); 601 if (data == null) { 602 // notification for a mailbox that we aren't observing; this should not happen 603 Log.e(Logging.LOG_TAG, "Received notifiaction when observer data was null"); 604 return; 605 } 606 long oldMessageId = data.mNotifiedMessageId; 607 int oldMessageCount = data.mNotifiedMessageCount; 608 609 ContentResolver resolver = mContext.getContentResolver(); 610 long lastSeenMessageId = Utility.getFirstRowLong( 611 mContext, Mailbox.CONTENT_URI, 612 new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY }, 613 EmailContent.ID_SELECTION, 614 new String[] { Long.toString(mMailboxId) }, null, 0, 0L); 615 Cursor c = resolver.query( 616 Message.CONTENT_URI, EmailContent.ID_PROJECTION, 617 MESSAGE_SELECTION, 618 new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) }, 619 MessageColumns.ID + " DESC"); 620 if (c == null) { 621 // Suspender time ... theoretically, this will never happen 622 Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for message id query"); 623 return; 624 } 625 try { 626 int newMessageCount = c.getCount(); 627 long newMessageId = 0L; 628 if (c.moveToNext()) { 629 newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 630 } 631 632 if (newMessageCount == 0) { 633 // No messages to notify for; clear the notification 634 int notificationId = sInstance.getNewMessageNotificationId(mAccountId); 635 sInstance.mNotificationManager.cancel(notificationId); 636 } else if (newMessageCount != oldMessageCount 637 || (newMessageId != 0 && newMessageId != oldMessageId)) { 638 // Either the count or last message has changed; update the notification 639 boolean playAudio = (oldMessageCount == 0); // play audio on first notification 640 Notification n = sInstance.createNewMessageNotification( 641 mAccountId, mMailboxId, newMessageId, newMessageCount, playAudio); 642 if (n != null) { 643 // Make the notification visible 644 sInstance.mNotificationManager.notify( 645 sInstance.getNewMessageNotificationId(mAccountId), n); 646 } 647 } 648 data.mNotifiedMessageId = newMessageId; 649 data.mNotifiedMessageCount = newMessageCount; 650 } finally { 651 c.close(); 652 } 653 } 654 } 655 656 /** 657 * Observer invoked whenever an account is modified. This could mean the user changed the 658 * notification settings. 659 */ 660 private static class AccountContentObserver extends ContentObserver { 661 private final Context mContext; 662 public AccountContentObserver(Handler handler, Context context) { 663 super(handler); 664 mContext = context; 665 } 666 667 @Override 668 public void onChange(boolean selfChange) { 669 final ContentResolver resolver = mContext.getContentResolver(); 670 final Cursor c = resolver.query( 671 Account.CONTENT_URI, EmailContent.ID_PROJECTION, 672 NOTIFIED_ACCOUNT_SELECTION, null, null); 673 final HashSet<Long> newAccountList = new HashSet<Long>(); 674 final HashSet<Long> removedAccountList = new HashSet<Long>(); 675 if (c == null) { 676 // Suspender time ... theoretically, this will never happen 677 Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query"); 678 return; 679 } 680 try { 681 while (c.moveToNext()) { 682 long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 683 newAccountList.add(accountId); 684 } 685 } finally { 686 if (c != null) { 687 c.close(); 688 } 689 } 690 // NOTE: Looping over three lists is not necessarily the most efficient. However, the 691 // account lists are going to be very small, so, this will not be necessarily bad. 692 // Cycle through existing notification list and adjust as necessary 693 for (long accountId : sInstance.mNotificationMap.keySet()) { 694 if (!newAccountList.remove(accountId)) { 695 // account id not in the current set of notifiable accounts 696 removedAccountList.add(accountId); 697 } 698 } 699 // A new account was added to the notification list 700 for (long accountId : newAccountList) { 701 sInstance.registerMessageNotification(accountId); 702 } 703 // An account was removed from the notification list 704 for (long accountId : removedAccountList) { 705 sInstance.unregisterMessageNotification(accountId); 706 int notificationId = sInstance.getNewMessageNotificationId(accountId); 707 sInstance.mNotificationManager.cancel(notificationId); 708 } 709 } 710 } 711 712 /** Information about the message(s) we're notifying the user about. */ 713 private static class MessageData { 714 /** The database observer */ 715 ContentObserver mObserver; 716 /** Message ID used in the user notification */ 717 long mNotifiedMessageId; 718 /** Message count used in the user notification */ 719 int mNotifiedMessageCount; 720 } 721 722 /** 723 * Thread to handle all notification actions through its own {@link Looper}. 724 */ 725 private static class NotificationThread implements Runnable { 726 /** Lock to ensure proper initialization */ 727 private final Object mLock = new Object(); 728 /** The {@link Looper} that handles messages for this thread */ 729 private Looper mLooper; 730 731 NotificationThread() { 732 new Thread(null, this, "EmailNotification").start(); 733 synchronized (mLock) { 734 while (mLooper == null) { 735 try { 736 mLock.wait(); 737 } catch (InterruptedException ex) { 738 } 739 } 740 } 741 } 742 743 @Override 744 public void run() { 745 synchronized (mLock) { 746 Looper.prepare(); 747 mLooper = Looper.myLooper(); 748 mLock.notifyAll(); 749 } 750 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 751 Looper.loop(); 752 } 753 void quit() { 754 mLooper.quit(); 755 } 756 Looper getLooper() { 757 return mLooper; 758 } 759 } 760} 761