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