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 17 package com.android.email; 18 19 import android.app.Notification; 20 import android.app.Notification.Builder; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.content.ContentResolver; 24 import android.content.ContentUris; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.ContentObserver; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.Process; 34 import android.provider.Settings; 35 import android.support.v4.app.NotificationCompat; 36 import android.text.TextUtils; 37 import android.text.format.DateUtils; 38 39 import com.android.email.activity.setup.AccountSecurity; 40 import com.android.email.activity.setup.HeadlessAccountSettingsLoader; 41 import com.android.email.provider.EmailProvider; 42 import com.android.email.service.EmailServiceUtils; 43 import com.android.emailcommon.provider.Account; 44 import com.android.emailcommon.provider.EmailContent; 45 import com.android.emailcommon.provider.EmailContent.Attachment; 46 import com.android.emailcommon.provider.EmailContent.Message; 47 import com.android.emailcommon.provider.Mailbox; 48 import com.android.emailcommon.utility.EmailAsyncTask; 49 import com.android.mail.preferences.FolderPreferences; 50 import com.android.mail.providers.Folder; 51 import com.android.mail.providers.UIProvider; 52 import com.android.mail.utils.Clock; 53 import com.android.mail.utils.LogTag; 54 import com.android.mail.utils.LogUtils; 55 import com.android.mail.utils.NotificationUtils; 56 57 import java.util.HashMap; 58 import java.util.HashSet; 59 import java.util.Map; 60 import java.util.Set; 61 62 /** 63 * Class that manages notifications. 64 */ 65 public class EmailNotificationController implements NotificationController { 66 private static final String LOG_TAG = LogTag.getLogTag(); 67 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_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 private static NotificationThread sNotificationThread; 78 private static Handler sNotificationHandler; 79 private static EmailNotificationController sInstance; 80 private final Context mContext; 81 private final NotificationManager mNotificationManager; 82 private final Clock mClock; 83 /** Maps account id to its observer */ 84 private final Map<Long, ContentObserver> mNotificationMap = 85 new HashMap<Long, ContentObserver>(); 86 private ContentObserver mAccountObserver; 87 88 /** Constructor */ EmailNotificationController(Context context, Clock clock)89 protected EmailNotificationController(Context context, Clock clock) { 90 mContext = context.getApplicationContext(); 91 EmailContent.init(context); 92 mNotificationManager = (NotificationManager) context.getSystemService( 93 Context.NOTIFICATION_SERVICE); 94 mClock = clock; 95 } 96 97 /** Singleton access */ getInstance(Context context)98 public static synchronized EmailNotificationController getInstance(Context context) { 99 if (sInstance == null) { 100 sInstance = new EmailNotificationController(context, Clock.INSTANCE); 101 } 102 return sInstance; 103 } 104 105 /** 106 * Return whether or not a notification, based on the passed-in id, needs to be "ongoing" 107 * @param notificationId the notification id to check 108 * @return whether or not the notification must be "ongoing" 109 */ needsOngoingNotification(int notificationId)110 private static boolean needsOngoingNotification(int notificationId) { 111 // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will 112 // be prevented until a reboot. Consider also doing this for password expired. 113 return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED; 114 } 115 116 /** 117 * Returns a {@link android.support.v4.app.NotificationCompat.Builder} for an event with the 118 * given account. The account contains specific rules on ring tone usage and these will be used 119 * to modify the notification behaviour. 120 * 121 * @param accountId The id of the account this notification is being built for. 122 * @param ticker Text displayed when the notification is first shown. May be {@code null}. 123 * @param title The first line of text. May NOT be {@code null}. 124 * @param contentText The second line of text. May NOT be {@code null}. 125 * @param intent The intent to start if the user clicks on the notification. 126 * @param number A number to display using {@link Builder#setNumber(int)}. May be {@code null}. 127 * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according 128 * to the settings for the given account. 129 * @return A {@link Notification} that can be sent to the notification service. 130 */ createBaseAccountNotificationBuilder(long accountId, String ticker, CharSequence title, String contentText, Intent intent, Integer number, boolean enableAudio, boolean ongoing)131 private NotificationCompat.Builder createBaseAccountNotificationBuilder(long accountId, 132 String ticker, CharSequence title, String contentText, Intent intent, 133 Integer number, boolean enableAudio, boolean ongoing) { 134 // Pending Intent 135 PendingIntent pending = null; 136 if (intent != null) { 137 pending = PendingIntent.getActivity( 138 mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 139 } 140 141 // NOTE: the ticker is not shown for notifications in the Holo UX 142 final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) 143 .setContentTitle(title) 144 .setContentText(contentText) 145 .setContentIntent(pending) 146 .setNumber(number == null ? 0 : number) 147 .setSmallIcon(R.drawable.ic_notification_mail_24dp) 148 .setWhen(mClock.getTime()) 149 .setTicker(ticker) 150 .setOngoing(ongoing); 151 152 if (enableAudio) { 153 Account account = Account.restoreAccountWithId(mContext, accountId); 154 setupSoundAndVibration(builder, account); 155 } 156 157 return builder; 158 } 159 160 /** 161 * Generic notifier for any account. Uses notification rules from account. 162 * 163 * @param accountId The account id this notification is being built for. 164 * @param ticker Text displayed when the notification is first shown. May be {@code null}. 165 * @param title The first line of text. May NOT be {@code null}. 166 * @param contentText The second line of text. May NOT be {@code null}. 167 * @param intent The intent to start if the user clicks on the notification. 168 * @param notificationId The ID of the notification to register with the service. 169 */ showNotification(long accountId, String ticker, String title, String contentText, Intent intent, int notificationId)170 private void showNotification(long accountId, String ticker, String title, 171 String contentText, Intent intent, int notificationId) { 172 final NotificationCompat.Builder builder = createBaseAccountNotificationBuilder(accountId, 173 ticker, title, contentText, intent, null, true, 174 needsOngoingNotification(notificationId)); 175 mNotificationManager.notify(notificationId, builder.build()); 176 } 177 178 /** 179 * Tells the notification controller if it should be watching for changes to the message table. 180 * This is the main life cycle method for message notifications. When we stop observing 181 * database changes, we save the state [e.g. message ID and count] of the most recent 182 * notification shown to the user. And, when we start observing database changes, we restore 183 * the saved state. 184 */ 185 @Override watchForMessages()186 public void watchForMessages() { 187 ensureHandlerExists(); 188 // Run this on the message notification handler 189 sNotificationHandler.post(new Runnable() { 190 @Override 191 public void run() { 192 ContentResolver resolver = mContext.getContentResolver(); 193 194 // otherwise, start new observers for all notified accounts 195 registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); 196 // If we're already observing account changes, don't do anything else 197 if (mAccountObserver == null) { 198 LogUtils.i(LOG_TAG, "Observing account changes for notifications"); 199 mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); 200 resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); 201 } 202 } 203 }); 204 } 205 206 /** 207 * Ensures the notification handler exists and is ready to handle requests. 208 */ 209 210 /** 211 * TODO: Notifications jump around too much because we get too many content updates. 212 * We should try to make the provider generate fewer updates instead. 213 */ 214 215 private static final int NOTIFICATION_DELAYED_MESSAGE = 0; 216 private static final long NOTIFICATION_DELAY = 15 * DateUtils.SECOND_IN_MILLIS; 217 // True if we're coalescing notification updates 218 private static boolean sNotificationDelayedMessagePending; 219 // True if accounts have changed and we need to refresh everything 220 private static boolean sRefreshAllNeeded; 221 // Set of accounts we need to regenerate notifications for 222 private static final HashSet<Long> sRefreshAccountSet = new HashSet<Long>(); 223 // These should all be accessed on-thread, but just in case... 224 private static final Object sNotificationDelayedMessageLock = new Object(); 225 ensureHandlerExists()226 private static synchronized void ensureHandlerExists() { 227 if (sNotificationThread == null) { 228 sNotificationThread = new NotificationThread(); 229 sNotificationHandler = new Handler(sNotificationThread.getLooper(), 230 new Handler.Callback() { 231 @Override 232 public boolean handleMessage(final android.os.Message message) { 233 /** 234 * To reduce spamming the notifications, we quiesce updates for a few 235 * seconds to batch them up, then handle them here. 236 */ 237 LogUtils.d(LOG_TAG, "Delayed notification processing"); 238 synchronized (sNotificationDelayedMessageLock) { 239 sNotificationDelayedMessagePending = false; 240 final Context context = (Context)message.obj; 241 if (sRefreshAllNeeded) { 242 sRefreshAllNeeded = false; 243 refreshAllNotificationsInternal(context); 244 } 245 for (final Long accountId : sRefreshAccountSet) { 246 refreshNotificationsForAccountInternal(context, accountId); 247 } 248 sRefreshAccountSet.clear(); 249 } 250 return true; 251 } 252 }); 253 } 254 } 255 256 /** 257 * Registers an observer for changes to mailboxes in the given account. 258 * NOTE: This must be called on the notification handler thread. 259 * @param accountId The ID of the account to register the observer for. May be 260 * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all 261 * accounts that allow for user notification. 262 */ registerMessageNotification(final long accountId)263 private void registerMessageNotification(final long accountId) { 264 ContentResolver resolver = mContext.getContentResolver(); 265 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 266 Cursor c = resolver.query( 267 Account.CONTENT_URI, EmailContent.ID_PROJECTION, 268 null, null, null); 269 try { 270 while (c.moveToNext()) { 271 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 272 registerMessageNotification(id); 273 } 274 } finally { 275 c.close(); 276 } 277 } else { 278 ContentObserver obs = mNotificationMap.get(accountId); 279 if (obs != null) return; // we're already observing; nothing to do 280 LogUtils.i(LOG_TAG, "Registering for notifications for account " + accountId); 281 ContentObserver observer = new MessageContentObserver( 282 sNotificationHandler, mContext, accountId); 283 resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); 284 mNotificationMap.put(accountId, observer); 285 // Now, ping the observer for any initial notifications 286 observer.onChange(true); 287 } 288 } 289 290 /** 291 * Unregisters the observer for the given account. If the specified account does not have 292 * a registered observer, no action is performed. This will not clear any existing notification 293 * for the specified account. Use {@link NotificationManager#cancel(int)}. 294 * NOTE: This must be called on the notification handler thread. 295 * @param accountId The ID of the account to unregister from. To unregister all accounts that 296 * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 297 */ unregisterMessageNotification(final long accountId)298 private void unregisterMessageNotification(final long accountId) { 299 ContentResolver resolver = mContext.getContentResolver(); 300 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 301 LogUtils.i(LOG_TAG, "Unregistering notifications for all accounts"); 302 // cancel all existing message observers 303 for (ContentObserver observer : mNotificationMap.values()) { 304 resolver.unregisterContentObserver(observer); 305 } 306 mNotificationMap.clear(); 307 } else { 308 LogUtils.i(LOG_TAG, "Unregistering notifications for account " + accountId); 309 ContentObserver observer = mNotificationMap.remove(accountId); 310 if (observer != null) { 311 resolver.unregisterContentObserver(observer); 312 } 313 } 314 } 315 316 public static final String EXTRA_ACCOUNT = "account"; 317 public static final String EXTRA_CONVERSATION = "conversationUri"; 318 public static final String EXTRA_FOLDER = "folder"; 319 320 /** Sets up the notification's sound and vibration based upon account details. */ setupSoundAndVibration( NotificationCompat.Builder builder, Account account)321 private void setupSoundAndVibration( 322 NotificationCompat.Builder builder, Account account) { 323 String ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI.toString(); 324 boolean vibrate = false; 325 326 // Use the Inbox notification preferences 327 final Cursor accountCursor = mContext.getContentResolver().query(EmailProvider.uiUri( 328 "uiaccount", account.mId), UIProvider.ACCOUNTS_PROJECTION, null, null, null); 329 330 com.android.mail.providers.Account uiAccount = null; 331 try { 332 if (accountCursor.moveToFirst()) { 333 uiAccount = com.android.mail.providers.Account.builder().buildFrom(accountCursor); 334 } 335 } finally { 336 accountCursor.close(); 337 } 338 339 if (uiAccount != null) { 340 final Cursor folderCursor = 341 mContext.getContentResolver().query(uiAccount.settings.defaultInbox, 342 UIProvider.FOLDERS_PROJECTION, null, null, null); 343 344 if (folderCursor == null) { 345 // This can happen when the notification is for the security policy notification 346 // that happens before the account is setup 347 LogUtils.w(LOG_TAG, "Null folder cursor for mailbox %s", 348 uiAccount.settings.defaultInbox); 349 } else { 350 Folder folder = null; 351 try { 352 if (folderCursor.moveToFirst()) { 353 folder = new Folder(folderCursor); 354 } 355 } finally { 356 folderCursor.close(); 357 } 358 359 if (folder != null) { 360 final FolderPreferences folderPreferences = new FolderPreferences( 361 mContext, uiAccount.getEmailAddress(), folder, true /* inbox */); 362 363 ringtoneUri = folderPreferences.getNotificationRingtoneUri(); 364 vibrate = folderPreferences.isNotificationVibrateEnabled(); 365 } else { 366 LogUtils.e(LOG_TAG, 367 "Null folder for mailbox %s", uiAccount.settings.defaultInbox); 368 } 369 } 370 } else { 371 LogUtils.e(LOG_TAG, "Null uiAccount for account id %d", account.mId); 372 } 373 374 int defaults = Notification.DEFAULT_LIGHTS; 375 if (vibrate) { 376 defaults |= Notification.DEFAULT_VIBRATE; 377 } 378 379 builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri)) 380 .setDefaults(defaults); 381 } 382 383 /** 384 * Show (or update) a notification that the given attachment could not be forwarded. This 385 * is a very unusual case, and perhaps we shouldn't even send a notification. For now, 386 * it's helpful for debugging. 387 * 388 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 389 */ 390 @Override showDownloadForwardFailedNotificationSynchronous(Attachment attachment)391 public void showDownloadForwardFailedNotificationSynchronous(Attachment attachment) { 392 final Message message = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 393 if (message == null) return; 394 final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 395 showNotification(mailbox.mAccountKey, 396 mContext.getString(R.string.forward_download_failed_ticker), 397 mContext.getString(R.string.forward_download_failed_title), 398 attachment.mFileName, 399 null, 400 NOTIFICATION_ID_ATTACHMENT_WARNING); 401 } 402 403 /** 404 * Returns a notification ID for login failed notifications for the given account account. 405 */ getLoginFailedNotificationId(long accountId)406 private static int getLoginFailedNotificationId(long accountId) { 407 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; 408 } 409 410 /** 411 * Show (or update) a notification that there was a login failure for the given account. 412 * 413 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 414 */ 415 @Override showLoginFailedNotificationSynchronous(long accountId, boolean incoming)416 public void showLoginFailedNotificationSynchronous(long accountId, boolean incoming) { 417 final Account account = Account.restoreAccountWithId(mContext, accountId); 418 if (account == null) return; 419 final Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, 420 Mailbox.TYPE_INBOX); 421 if (mailbox == null) return; 422 423 final Intent settingsIntent; 424 if (incoming) { 425 settingsIntent = new Intent(Intent.ACTION_VIEW, 426 EmailProvider.getIncomingSettingsUri(accountId)); 427 } else { 428 settingsIntent = new Intent(Intent.ACTION_VIEW, 429 HeadlessAccountSettingsLoader.getOutgoingSettingsUri(accountId)); 430 } 431 showNotification(mailbox.mAccountKey, 432 mContext.getString(R.string.login_failed_ticker, account.mDisplayName), 433 mContext.getString(R.string.login_failed_title), 434 account.getDisplayName(), 435 settingsIntent, 436 getLoginFailedNotificationId(accountId)); 437 } 438 439 /** 440 * Cancels the login failed notification for the given account. 441 */ 442 @Override cancelLoginFailedNotification(long accountId)443 public void cancelLoginFailedNotification(long accountId) { 444 mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); 445 } 446 447 /** 448 * Show (or update) a notification that the user's password is expiring. The given account 449 * is used to update the display text, but, all accounts share the same notification ID. 450 * 451 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 452 */ 453 @Override showPasswordExpiringNotificationSynchronous(long accountId)454 public void showPasswordExpiringNotificationSynchronous(long accountId) { 455 final Account account = Account.restoreAccountWithId(mContext, accountId); 456 if (account == null) return; 457 458 final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, 459 accountId, false); 460 final String accountName = account.getDisplayName(); 461 final String ticker = 462 mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName); 463 final String title = mContext.getString(R.string.password_expire_warning_content_title); 464 showNotification(accountId, ticker, title, accountName, intent, 465 NOTIFICATION_ID_PASSWORD_EXPIRING); 466 } 467 468 /** 469 * Show (or update) a notification that the user's password has expired. The given account 470 * is used to update the display text, but, all accounts share the same notification ID. 471 * 472 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 473 */ 474 @Override showPasswordExpiredNotificationSynchronous(long accountId)475 public void showPasswordExpiredNotificationSynchronous(long accountId) { 476 final Account account = Account.restoreAccountWithId(mContext, accountId); 477 if (account == null) return; 478 479 final Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, 480 accountId, true); 481 final String accountName = account.getDisplayName(); 482 final String ticker = mContext.getString(R.string.password_expired_ticker); 483 final String title = mContext.getString(R.string.password_expired_content_title); 484 showNotification(accountId, ticker, title, accountName, intent, 485 NOTIFICATION_ID_PASSWORD_EXPIRED); 486 } 487 488 /** 489 * Cancels any password expire notifications [both expired & expiring]. 490 */ 491 @Override cancelPasswordExpirationNotifications()492 public void cancelPasswordExpirationNotifications() { 493 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); 494 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); 495 } 496 497 /** 498 * Show (or update) a security needed notification. If tapped, the user is taken to a 499 * dialog asking whether he wants to update his settings. 500 */ 501 @Override showSecurityNeededNotification(Account account)502 public void showSecurityNeededNotification(Account account) { 503 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); 504 String accountName = account.getDisplayName(); 505 String ticker = 506 mContext.getString(R.string.security_needed_ticker_fmt, accountName); 507 String title = mContext.getString(R.string.security_notification_content_update_title); 508 showNotification(account.mId, ticker, title, accountName, intent, 509 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); 510 } 511 512 /** 513 * Show (or update) a security changed notification. If tapped, the user is taken to the 514 * account settings screen where he can view the list of enforced policies 515 */ 516 @Override showSecurityChangedNotification(Account account)517 public void showSecurityChangedNotification(Account account) { 518 final Intent intent = new Intent(Intent.ACTION_VIEW, 519 EmailProvider.getIncomingSettingsUri(account.getId())); 520 final String accountName = account.getDisplayName(); 521 final String ticker = 522 mContext.getString(R.string.security_changed_ticker_fmt, accountName); 523 final String title = 524 mContext.getString(R.string.security_notification_content_change_title); 525 showNotification(account.mId, ticker, title, accountName, intent, 526 (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); 527 } 528 529 /** 530 * Show (or update) a security unsupported notification. If tapped, the user is taken to the 531 * account settings screen where he can view the list of unsupported policies 532 */ 533 @Override showSecurityUnsupportedNotification(Account account)534 public void showSecurityUnsupportedNotification(Account account) { 535 final Intent intent = new Intent(Intent.ACTION_VIEW, 536 EmailProvider.getIncomingSettingsUri(account.getId())); 537 final String accountName = account.getDisplayName(); 538 final String ticker = 539 mContext.getString(R.string.security_unsupported_ticker_fmt, accountName); 540 final String title = 541 mContext.getString(R.string.security_notification_content_unsupported_title); 542 showNotification(account.mId, ticker, title, accountName, intent, 543 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); 544 } 545 546 /** 547 * Cancels all security needed notifications. 548 */ 549 @Override cancelSecurityNeededNotification()550 public void cancelSecurityNeededNotification() { 551 EmailAsyncTask.runAsyncParallel(new Runnable() { 552 @Override 553 public void run() { 554 Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, 555 Account.ID_PROJECTION, null, null, null); 556 try { 557 while (c.moveToNext()) { 558 long id = c.getLong(Account.ID_PROJECTION_COLUMN); 559 mNotificationManager.cancel( 560 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id)); 561 } 562 } 563 finally { 564 c.close(); 565 } 566 }}); 567 } 568 569 /** 570 * Cancels all notifications for the specified account id. This includes new mail notifications, 571 * as well as special login/security notifications. 572 */ 573 @Override cancelNotifications(final Context context, final Account account)574 public void cancelNotifications(final Context context, final Account account) { 575 final EmailServiceUtils.EmailServiceInfo serviceInfo 576 = EmailServiceUtils.getServiceInfoForAccount(context, account.mId); 577 if (serviceInfo == null) { 578 LogUtils.d(LOG_TAG, "Can't cancel notification for missing account %d", account.mId); 579 return; 580 } 581 final android.accounts.Account notifAccount 582 = account.getAccountManagerAccount(serviceInfo.accountType); 583 584 NotificationUtils.clearAccountNotifications(context, notifAccount); 585 586 final NotificationManager notificationManager = getInstance(context).mNotificationManager; 587 588 notificationManager.cancel((int) (NOTIFICATION_ID_BASE_LOGIN_WARNING + account.mId)); 589 notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); 590 notificationManager.cancel((int) (NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); 591 } 592 refreshNotificationsForAccount(final Context context, final long accountId)593 private static void refreshNotificationsForAccount(final Context context, 594 final long accountId) { 595 synchronized (sNotificationDelayedMessageLock) { 596 if (sNotificationDelayedMessagePending) { 597 sRefreshAccountSet.add(accountId); 598 } else { 599 ensureHandlerExists(); 600 sNotificationHandler.sendMessageDelayed( 601 android.os.Message.obtain(sNotificationHandler, 602 NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); 603 sNotificationDelayedMessagePending = true; 604 refreshNotificationsForAccountInternal(context, accountId); 605 } 606 } 607 } 608 refreshNotificationsForAccountInternal(final Context context, final long accountId)609 private static void refreshNotificationsForAccountInternal(final Context context, 610 final long accountId) { 611 final Uri accountUri = EmailProvider.uiUri("uiaccount", accountId); 612 613 final ContentResolver contentResolver = context.getContentResolver(); 614 615 final Cursor mailboxCursor = contentResolver.query( 616 ContentUris.withAppendedId(EmailContent.MAILBOX_NOTIFICATION_URI, accountId), 617 null, null, null, null); 618 try { 619 while (mailboxCursor.moveToNext()) { 620 final long mailboxId = 621 mailboxCursor.getLong(EmailContent.NOTIFICATION_MAILBOX_ID_COLUMN); 622 if (mailboxId == 0) continue; 623 624 final int unseenCount = mailboxCursor.getInt( 625 EmailContent.NOTIFICATION_MAILBOX_UNSEEN_COUNT_COLUMN); 626 627 final int unreadCount; 628 // If nothing is unseen, clear the notification 629 if (unseenCount == 0) { 630 unreadCount = 0; 631 } else { 632 unreadCount = mailboxCursor.getInt( 633 EmailContent.NOTIFICATION_MAILBOX_UNREAD_COUNT_COLUMN); 634 } 635 636 final Uri folderUri = EmailProvider.uiUri("uifolder", mailboxId); 637 638 639 LogUtils.d(LOG_TAG, "Changes to account " + accountId + ", folder: " 640 + mailboxId + ", unreadCount: " + unreadCount + ", unseenCount: " 641 + unseenCount); 642 643 final Intent intent = new Intent(UIProvider.ACTION_UPDATE_NOTIFICATION); 644 intent.setPackage(context.getPackageName()); 645 intent.setType(EmailProvider.EMAIL_APP_MIME_TYPE); 646 647 intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT, accountUri); 648 intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER, folderUri); 649 intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, 650 unreadCount); 651 intent.putExtra(UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, 652 unseenCount); 653 654 context.sendOrderedBroadcast(intent, null); 655 } 656 } finally { 657 mailboxCursor.close(); 658 } 659 } 660 661 @Override handleUpdateNotificationIntent(Context context, Intent intent)662 public void handleUpdateNotificationIntent(Context context, Intent intent) { 663 final Uri accountUri = 664 intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_ACCOUNT); 665 final Uri folderUri = 666 intent.getParcelableExtra(UIProvider.UpdateNotificationExtras.EXTRA_FOLDER); 667 final int unreadCount = intent.getIntExtra( 668 UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNREAD_COUNT, 0); 669 final int unseenCount = intent.getIntExtra( 670 UIProvider.UpdateNotificationExtras.EXTRA_UPDATED_UNSEEN_COUNT, 0); 671 672 final ContentResolver contentResolver = context.getContentResolver(); 673 674 final Cursor accountCursor = contentResolver.query(accountUri, 675 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 676 677 if (accountCursor == null) { 678 LogUtils.e(LOG_TAG, "Null account cursor for account " + accountUri); 679 return; 680 } 681 682 com.android.mail.providers.Account account = null; 683 try { 684 if (accountCursor.moveToFirst()) { 685 account = com.android.mail.providers.Account.builder().buildFrom(accountCursor); 686 } 687 } finally { 688 accountCursor.close(); 689 } 690 691 if (account == null) { 692 LogUtils.d(LOG_TAG, "Tried to create a notification for a missing account " 693 + accountUri); 694 return; 695 } 696 697 final Cursor folderCursor = contentResolver.query(folderUri, UIProvider.FOLDERS_PROJECTION, 698 null, null, null); 699 700 if (folderCursor == null) { 701 LogUtils.e(LOG_TAG, "Null folder cursor for account " + accountUri + ", mailbox " 702 + folderUri); 703 return; 704 } 705 706 Folder folder = null; 707 try { 708 if (folderCursor.moveToFirst()) { 709 folder = new Folder(folderCursor); 710 } else { 711 LogUtils.e(LOG_TAG, "Empty folder cursor for account " + accountUri + ", mailbox " 712 + folderUri); 713 return; 714 } 715 } finally { 716 folderCursor.close(); 717 } 718 719 // TODO: we don't always want getAttention to be true, but we don't necessarily have a 720 // good heuristic for when it should or shouldn't be. 721 NotificationUtils.sendSetNewEmailIndicatorIntent(context, unreadCount, unseenCount, 722 account, folder, true /* getAttention */); 723 } 724 refreshAllNotifications(final Context context)725 private static void refreshAllNotifications(final Context context) { 726 synchronized (sNotificationDelayedMessageLock) { 727 if (sNotificationDelayedMessagePending) { 728 sRefreshAllNeeded = true; 729 } else { 730 ensureHandlerExists(); 731 sNotificationHandler.sendMessageDelayed( 732 android.os.Message.obtain(sNotificationHandler, 733 NOTIFICATION_DELAYED_MESSAGE, context), NOTIFICATION_DELAY); 734 sNotificationDelayedMessagePending = true; 735 refreshAllNotificationsInternal(context); 736 } 737 } 738 } 739 refreshAllNotificationsInternal(final Context context)740 private static void refreshAllNotificationsInternal(final Context context) { 741 NotificationUtils.resendNotifications( 742 context, false, null, null, null /* ContactFetcher */); 743 } 744 745 /** 746 * Observer invoked whenever a message we're notifying the user about changes. 747 */ 748 private static class MessageContentObserver extends ContentObserver { 749 private final Context mContext; 750 private final long mAccountId; 751 MessageContentObserver( final Handler handler, final Context context, final long accountId)752 public MessageContentObserver( 753 final Handler handler, final Context context, final long accountId) { 754 super(handler); 755 mContext = context; 756 mAccountId = accountId; 757 } 758 759 @Override onChange(final boolean selfChange)760 public void onChange(final boolean selfChange) { 761 refreshNotificationsForAccount(mContext, mAccountId); 762 } 763 } 764 765 /** 766 * Observer invoked whenever an account is modified. This could mean the user changed the 767 * notification settings. 768 */ 769 private static class AccountContentObserver extends ContentObserver { 770 private final Context mContext; AccountContentObserver(final Handler handler, final Context context)771 public AccountContentObserver(final Handler handler, final Context context) { 772 super(handler); 773 mContext = context; 774 } 775 776 @Override onChange(final boolean selfChange)777 public void onChange(final boolean selfChange) { 778 final ContentResolver resolver = mContext.getContentResolver(); 779 final Cursor c = resolver.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 780 null, null, null); 781 final Set<Long> newAccountList = new HashSet<Long>(); 782 final Set<Long> removedAccountList = new HashSet<Long>(); 783 if (c == null) { 784 // Suspender time ... theoretically, this will never happen 785 LogUtils.wtf(LOG_TAG, "#onChange(); NULL response for account id query"); 786 return; 787 } 788 try { 789 while (c.moveToNext()) { 790 long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 791 newAccountList.add(accountId); 792 } 793 } finally { 794 c.close(); 795 } 796 // NOTE: Looping over three lists is not necessarily the most efficient. However, the 797 // account lists are going to be very small, so, this will not be necessarily bad. 798 // Cycle through existing notification list and adjust as necessary 799 for (final long accountId : sInstance.mNotificationMap.keySet()) { 800 if (!newAccountList.remove(accountId)) { 801 // account id not in the current set of notifiable accounts 802 removedAccountList.add(accountId); 803 } 804 } 805 // A new account was added to the notification list 806 for (final long accountId : newAccountList) { 807 sInstance.registerMessageNotification(accountId); 808 } 809 // An account was removed from the notification list 810 for (final long accountId : removedAccountList) { 811 sInstance.unregisterMessageNotification(accountId); 812 } 813 814 refreshAllNotifications(mContext); 815 } 816 } 817 818 /** 819 * Thread to handle all notification actions through its own {@link Looper}. 820 */ 821 private static class NotificationThread implements Runnable { 822 /** Lock to ensure proper initialization */ 823 private final Object mLock = new Object(); 824 /** The {@link Looper} that handles messages for this thread */ 825 private Looper mLooper; 826 NotificationThread()827 public NotificationThread() { 828 new Thread(null, this, "EmailNotification").start(); 829 synchronized (mLock) { 830 while (mLooper == null) { 831 try { 832 mLock.wait(); 833 } catch (InterruptedException ex) { 834 // Loop around and wait again 835 } 836 } 837 } 838 } 839 840 @Override run()841 public void run() { 842 synchronized (mLock) { 843 Looper.prepare(); 844 mLooper = Looper.myLooper(); 845 mLock.notifyAll(); 846 } 847 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 848 Looper.loop(); 849 } 850 getLooper()851 public Looper getLooper() { 852 return mLooper; 853 } 854 } 855 } 856