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