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