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