NotificationUtils.java revision c94c80c8b8c92c019f19f2e315dea1ff03ad573f
1/* 2 * Copyright (C) 2013 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 */ 16package com.android.mail.utils; 17 18import android.app.Notification; 19import android.app.NotificationManager; 20import android.app.PendingIntent; 21import android.content.ContentResolver; 22import android.content.ContentUris; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.Intent; 26import android.content.res.Resources; 27import android.database.Cursor; 28import android.graphics.Bitmap; 29import android.graphics.BitmapFactory; 30import android.net.Uri; 31import android.provider.ContactsContract; 32import android.provider.ContactsContract.CommonDataKinds.Email; 33import android.provider.ContactsContract.Contacts.Photo; 34import android.support.v4.app.NotificationCompat; 35import android.support.v4.text.BidiFormatter; 36import android.text.SpannableString; 37import android.text.SpannableStringBuilder; 38import android.text.TextUtils; 39import android.text.style.CharacterStyle; 40import android.text.style.TextAppearanceSpan; 41import android.util.Pair; 42import android.util.SparseArray; 43 44import com.android.mail.EmailAddress; 45import com.android.mail.MailIntentService; 46import com.android.mail.R; 47import com.android.mail.browse.MessageCursor; 48import com.android.mail.browse.SendersView; 49import com.android.mail.photomanager.ContactPhotoManager; 50import com.android.mail.preferences.AccountPreferences; 51import com.android.mail.preferences.FolderPreferences; 52import com.android.mail.preferences.MailPrefs; 53import com.android.mail.providers.Account; 54import com.android.mail.providers.Address; 55import com.android.mail.providers.Conversation; 56import com.android.mail.providers.Folder; 57import com.android.mail.providers.Message; 58import com.android.mail.providers.UIProvider; 59import com.android.mail.ui.ImageCanvas.Dimensions; 60import com.android.mail.utils.NotificationActionUtils.NotificationAction; 61import com.google.android.common.html.parser.HTML; 62import com.google.android.common.html.parser.HTML4; 63import com.google.android.common.html.parser.HtmlDocument; 64import com.google.android.common.html.parser.HtmlTree; 65import com.google.common.base.Objects; 66import com.google.common.collect.ImmutableList; 67import com.google.common.collect.Lists; 68import com.google.common.collect.Sets; 69 70import java.io.ByteArrayInputStream; 71import java.util.ArrayList; 72import java.util.Arrays; 73import java.util.Collection; 74import java.util.List; 75import java.util.Set; 76import java.util.concurrent.ConcurrentHashMap; 77 78public class NotificationUtils { 79 public static final String LOG_TAG = LogTag.getLogTag(); 80 81 /** Contains a list of <(account, label), unread conversations> */ 82 private static NotificationMap sActiveNotificationMap = null; 83 84 private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>(); 85 86 private static TextAppearanceSpan sNotificationUnreadStyleSpan; 87 private static CharacterStyle sNotificationReadStyleSpan; 88 89 /** A factory that produces a plain text converter that removes elided text. */ 90 private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY = 91 new HtmlTree.PlainTextConverterFactory() { 92 @Override 93 public HtmlTree.PlainTextConverter createInstance() { 94 return new MailMessagePlainTextConverter(); 95 } 96 }; 97 98 private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance(); 99 100 /** 101 * Clears all notifications in response to the user tapping "Clear" in the status bar. 102 */ 103 public static void clearAllNotfications(Context context) { 104 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications."); 105 final NotificationMap notificationMap = getNotificationMap(context); 106 notificationMap.clear(); 107 notificationMap.saveNotificationMap(context); 108 } 109 110 /** 111 * Returns the notification map, creating it if necessary. 112 */ 113 private static synchronized NotificationMap getNotificationMap(Context context) { 114 if (sActiveNotificationMap == null) { 115 sActiveNotificationMap = new NotificationMap(); 116 117 // populate the map from the cached data 118 sActiveNotificationMap.loadNotificationMap(context); 119 } 120 return sActiveNotificationMap; 121 } 122 123 /** 124 * Class representing the existing notifications, and the number of unread and 125 * unseen conversations that triggered each. 126 */ 127 private static class NotificationMap 128 extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> { 129 130 private static final String NOTIFICATION_PART_SEPARATOR = " "; 131 private static final int NUM_NOTIFICATION_PARTS= 4; 132 133 /** 134 * Retuns the unread count for the given NotificationKey. 135 */ 136 public Integer getUnread(NotificationKey key) { 137 final Pair<Integer, Integer> value = get(key); 138 return value != null ? value.first : null; 139 } 140 141 /** 142 * Retuns the unread unseen count for the given NotificationKey. 143 */ 144 public Integer getUnseen(NotificationKey key) { 145 final Pair<Integer, Integer> value = get(key); 146 return value != null ? value.second : null; 147 } 148 149 /** 150 * Store the unread and unseen value for the given NotificationKey 151 */ 152 public void put(NotificationKey key, int unread, int unseen) { 153 final Pair<Integer, Integer> value = 154 new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen)); 155 put(key, value); 156 } 157 158 /** 159 * Populates the notification map with previously cached data. 160 */ 161 public synchronized void loadNotificationMap(final Context context) { 162 final MailPrefs mailPrefs = MailPrefs.get(context); 163 final Set<String> notificationSet = mailPrefs.getActiveNotificationSet(); 164 if (notificationSet != null) { 165 for (String notificationEntry : notificationSet) { 166 // Get the parts of the string that make the notification entry 167 final String[] notificationParts = 168 TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR); 169 if (notificationParts.length == NUM_NOTIFICATION_PARTS) { 170 final Uri accountUri = Uri.parse(notificationParts[0]); 171 final Cursor accountCursor = context.getContentResolver().query( 172 accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null); 173 final Account account; 174 try { 175 if (accountCursor.moveToFirst()) { 176 account = new Account(accountCursor); 177 } else { 178 continue; 179 } 180 } finally { 181 accountCursor.close(); 182 } 183 184 final Uri folderUri = Uri.parse(notificationParts[1]); 185 final Cursor folderCursor = context.getContentResolver().query( 186 folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null); 187 final Folder folder; 188 try { 189 if (folderCursor.moveToFirst()) { 190 folder = new Folder(folderCursor); 191 } else { 192 continue; 193 } 194 } finally { 195 folderCursor.close(); 196 } 197 198 final NotificationKey key = new NotificationKey(account, folder); 199 final Integer unreadValue = Integer.valueOf(notificationParts[2]); 200 final Integer unseenValue = Integer.valueOf(notificationParts[3]); 201 final Pair<Integer, Integer> unreadUnseenValue = 202 new Pair<Integer, Integer>(unreadValue, unseenValue); 203 put(key, unreadUnseenValue); 204 } 205 } 206 } 207 } 208 209 /** 210 * Cache the notification map. 211 */ 212 public synchronized void saveNotificationMap(Context context) { 213 final Set<String> notificationSet = Sets.newHashSet(); 214 final Set<NotificationKey> keys = keySet(); 215 for (NotificationKey key : keys) { 216 final Pair<Integer, Integer> value = get(key); 217 final Integer unreadCount = value.first; 218 final Integer unseenCount = value.second; 219 if (unreadCount != null && unseenCount != null) { 220 final String[] partValues = new String[] { 221 key.account.uri.toString(), key.folder.uri.toString(), 222 unreadCount.toString(), unseenCount.toString()}; 223 notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues)); 224 } 225 } 226 final MailPrefs mailPrefs = MailPrefs.get(context); 227 mailPrefs.cacheActiveNotificationSet(notificationSet); 228 } 229 } 230 231 /** 232 * @return the title of this notification with each account and the number of unread and unseen 233 * conversations for it. Also remove any account in the map that has 0 unread. 234 */ 235 private static String createNotificationString(NotificationMap notifications) { 236 StringBuilder result = new StringBuilder(); 237 int i = 0; 238 Set<NotificationKey> keysToRemove = Sets.newHashSet(); 239 for (NotificationKey key : notifications.keySet()) { 240 Integer unread = notifications.getUnread(key); 241 Integer unseen = notifications.getUnseen(key); 242 if (unread == null || unread.intValue() == 0) { 243 keysToRemove.add(key); 244 } else { 245 if (i > 0) result.append(", "); 246 result.append(key.toString() + " (" + unread + ", " + unseen + ")"); 247 i++; 248 } 249 } 250 251 for (NotificationKey key : keysToRemove) { 252 notifications.remove(key); 253 } 254 255 return result.toString(); 256 } 257 258 /** 259 * Get all notifications for all accounts and cancel them. 260 **/ 261 public static void cancelAllNotifications(Context context) { 262 LogUtils.d(LOG_TAG, "NotificationUtils: cancelAllNotifications - cancelling all"); 263 NotificationManager nm = (NotificationManager) context.getSystemService( 264 Context.NOTIFICATION_SERVICE); 265 nm.cancelAll(); 266 clearAllNotfications(context); 267 } 268 269 /** 270 * Get all notifications for all accounts, cancel them, and repost. 271 * This happens when locale changes. 272 **/ 273 public static void cancelAndResendNotifications(Context context) { 274 LogUtils.d(LOG_TAG, "NotificationUtils: cancelAndResendNotifications"); 275 resendNotifications(context, true, null, null); 276 } 277 278 /** 279 * Get all notifications for all accounts, optionally cancel them, and repost. 280 * This happens when locale changes. If you only want to resend messages from one 281 * account-folder pair, pass in the account and folder that should be resent. 282 * All other account-folder pairs will not have their notifications resent. 283 * All notifications will be resent if account or folder is null. 284 * 285 * @param context Current context. 286 * @param cancelExisting True, if all notifications should be canceled before resending. 287 * False, otherwise. 288 * @param accountUri The {@link Uri} of the {@link Account} of the notification 289 * upon which an action occurred. 290 * @param folderUri The {@link Uri} of the {@link Folder} of the notification 291 * upon which an action occurred. 292 */ 293 public static void resendNotifications(Context context, final boolean cancelExisting, 294 final Uri accountUri, final Uri folderUri) { 295 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications "); 296 297 if (cancelExisting) { 298 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - cancelling all"); 299 NotificationManager nm = 300 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 301 nm.cancelAll(); 302 } 303 // Re-validate the notifications. 304 final NotificationMap notificationMap = getNotificationMap(context); 305 final Set<NotificationKey> keys = notificationMap.keySet(); 306 for (NotificationKey notification : keys) { 307 final Folder folder = notification.folder; 308 final int notificationId = getNotificationId(notification.account.name, 309 folder); 310 311 // Only resend notifications if the notifications are from the same folder 312 // and same account as the undo notification that was previously displayed. 313 if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) && 314 folderUri != null && !Objects.equal(folderUri, folder.uri)) { 315 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - not resending %s / %s" 316 + " because it doesn't match %s / %s", 317 notification.account.uri, folder.uri, accountUri, folderUri); 318 continue; 319 } 320 321 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - resending %s / %s", 322 notification.account.uri, folder.uri); 323 324 final NotificationAction undoableAction = 325 NotificationActionUtils.sUndoNotifications.get(notificationId); 326 if (undoableAction == null) { 327 validateNotifications(context, folder, notification.account, true, 328 false, notification); 329 } else { 330 // Create an undo notification 331 NotificationActionUtils.createUndoNotification(context, undoableAction); 332 } 333 } 334 } 335 336 /** 337 * Validate the notifications for the specified account. 338 */ 339 public static void validateAccountNotifications(Context context, String account) { 340 LogUtils.d(LOG_TAG, "NotificationUtils: validateAccountNotifications - %s", account); 341 342 List<NotificationKey> notificationsToCancel = Lists.newArrayList(); 343 // Iterate through the notification map to see if there are any entries that correspond to 344 // labels that are not in the sync set. 345 final NotificationMap notificationMap = getNotificationMap(context); 346 Set<NotificationKey> keys = notificationMap.keySet(); 347 final AccountPreferences accountPreferences = new AccountPreferences(context, account); 348 final boolean enabled = accountPreferences.areNotificationsEnabled(); 349 if (!enabled) { 350 // Cancel all notifications for this account 351 for (NotificationKey notification : keys) { 352 if (notification.account.name.equals(account)) { 353 notificationsToCancel.add(notification); 354 } 355 } 356 } else { 357 // Iterate through the notification map to see if there are any entries that 358 // correspond to labels that are not in the notification set. 359 for (NotificationKey notification : keys) { 360 if (notification.account.name.equals(account)) { 361 // If notification is not enabled for this label, remember this NotificationKey 362 // to later cancel the notification, and remove the entry from the map 363 final Folder folder = notification.folder; 364 final boolean isInbox = 365 notification.account.settings.defaultInbox.equals(folder.uri); 366 final FolderPreferences folderPreferences = new FolderPreferences( 367 context, notification.account.name, folder, isInbox); 368 369 if (!folderPreferences.areNotificationsEnabled()) { 370 notificationsToCancel.add(notification); 371 } 372 } 373 } 374 } 375 376 // Cancel & remove the invalid notifications. 377 if (notificationsToCancel.size() > 0) { 378 NotificationManager nm = (NotificationManager) context.getSystemService( 379 Context.NOTIFICATION_SERVICE); 380 for (NotificationKey notification : notificationsToCancel) { 381 final Folder folder = notification.folder; 382 final int notificationId = getNotificationId(notification.account.name, folder); 383 LogUtils.d(LOG_TAG, 384 "NotificationUtils: validateAccountNotifications - cancelling %s / %s", 385 notification.account.name, folder.persistentId); 386 nm.cancel(notificationId); 387 notificationMap.remove(notification); 388 NotificationActionUtils.sUndoNotifications.remove(notificationId); 389 NotificationActionUtils.sNotificationTimestamps.delete(notificationId); 390 } 391 notificationMap.saveNotificationMap(context); 392 } 393 } 394 395 /** 396 * Display only one notification. 397 */ 398 public static void setNewEmailIndicator(Context context, final int unreadCount, 399 final int unseenCount, final Account account, final Folder folder, 400 final boolean getAttention) { 401 LogUtils.d(LOG_TAG, "NotificationUtils: setNewEmailIndicator unreadCount = %d, " 402 + "unseenCount = %d, account = %s, folder = %s, getAttention = %b", unreadCount, 403 unseenCount, account.name, folder.uri, getAttention); 404 405 boolean ignoreUnobtrusiveSetting = false; 406 407 final int notificationId = getNotificationId(account.name, folder); 408 409 // Update the notification map 410 final NotificationMap notificationMap = getNotificationMap(context); 411 final NotificationKey key = new NotificationKey(account, folder); 412 if (unreadCount == 0) { 413 LogUtils.d(LOG_TAG, 414 "NotificationUtils: setNewEmailIndicator - cancelling %s / %s", 415 account.name, folder.persistentId); 416 notificationMap.remove(key); 417 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)) 418 .cancel(notificationId); 419 } else { 420 if (!notificationMap.containsKey(key)) { 421 // This account previously didn't have any unread mail; ignore the "unobtrusive 422 // notifications" setting and play sound and/or vibrate the device even if a 423 // notification already exists (bug 2412348). 424 ignoreUnobtrusiveSetting = true; 425 } 426 notificationMap.put(key, unreadCount, unseenCount); 427 } 428 notificationMap.saveNotificationMap(context); 429 430 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) { 431 LogUtils.v(LOG_TAG, "NotificationUtils: New email: %s mapSize: %d getAttention: %b", 432 createNotificationString(notificationMap), notificationMap.size(), 433 getAttention); 434 } 435 436 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) { 437 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting, 438 key); 439 } 440 } 441 442 /** 443 * Validate the notifications notification. 444 */ 445 private static void validateNotifications(Context context, final Folder folder, 446 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting, 447 NotificationKey key) { 448 449 NotificationManager nm = (NotificationManager) 450 context.getSystemService(Context.NOTIFICATION_SERVICE); 451 452 final NotificationMap notificationMap = getNotificationMap(context); 453 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) { 454 LogUtils.v(LOG_TAG, "NotificationUtils: Validating Notification: %s mapSize: %d " 455 + "folder: %s getAttention: %b", createNotificationString(notificationMap), 456 notificationMap.size(), folder.name, getAttention); 457 } 458 // The number of unread messages for this account and label. 459 final Integer unread = notificationMap.getUnread(key); 460 final int unreadCount = unread != null ? unread.intValue() : 0; 461 final Integer unseen = notificationMap.getUnseen(key); 462 int unseenCount = unseen != null ? unseen.intValue() : 0; 463 464 Cursor cursor = null; 465 466 try { 467 final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon(); 468 uriBuilder.appendQueryParameter( 469 UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString()); 470 // Do not allow this quick check to disrupt any active network-enabled conversation 471 // cursor. 472 uriBuilder.appendQueryParameter( 473 UIProvider.ConversationListQueryParameters.USE_NETWORK, 474 Boolean.FALSE.toString()); 475 cursor = context.getContentResolver().query(uriBuilder.build(), 476 UIProvider.CONVERSATION_PROJECTION, null, null, null); 477 if (cursor == null) { 478 // This folder doesn't exist. 479 clearFolderNotification(context, account, folder, false); 480 return; 481 } 482 final int cursorUnseenCount = cursor.getCount(); 483 484 // Make sure the unseen count matches the number of items in the cursor. But, we don't 485 // want to overwrite a 0 unseen count that was specified in the intent 486 if (unseenCount != 0 && unseenCount != cursorUnseenCount) { 487 LogUtils.d(LOG_TAG, "NotificationUtils: " 488 + "Unseen count doesn't match cursor count. unseen: %d cursor count: %d", 489 unseenCount, cursorUnseenCount); 490 unseenCount = cursorUnseenCount; 491 } 492 493 // For the purpose of the notifications, the unseen count should be capped at the num of 494 // unread conversations. 495 if (unseenCount > unreadCount) { 496 unseenCount = unreadCount; 497 } 498 499 final int notificationId = getNotificationId(account.name, folder); 500 501 if (unseenCount == 0) { 502 LogUtils.d(LOG_TAG, 503 "NotificationUtils: validateNotifications - cancelling %s / %s", 504 account.name, folder.persistentId); 505 nm.cancel(notificationId); 506 return; 507 } 508 509 // We now have all we need to create the notification and the pending intent 510 PendingIntent clickIntent; 511 512 NotificationCompat.Builder notification = new NotificationCompat.Builder(context); 513 notification.setSmallIcon(R.drawable.stat_notify_email); 514 notification.setTicker(account.name); 515 516 final long when; 517 518 final long oldWhen = 519 NotificationActionUtils.sNotificationTimestamps.get(notificationId); 520 if (oldWhen != 0) { 521 when = oldWhen; 522 } else { 523 when = System.currentTimeMillis(); 524 } 525 526 notification.setWhen(when); 527 528 // The timestamp is now stored in the notification, so we can remove it from here 529 NotificationActionUtils.sNotificationTimestamps.delete(notificationId); 530 531 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a 532 // notification. Also this intent gets fired when the user taps on a notification as 533 // the AutoCancel flag has been set 534 final Intent cancelNotificationIntent = 535 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS); 536 cancelNotificationIntent.setPackage(context.getPackageName()); 537 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context, 538 folder.uri)); 539 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account); 540 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder); 541 542 notification.setDeleteIntent(PendingIntent.getService( 543 context, notificationId, cancelNotificationIntent, 0)); 544 545 // Ensure that the notification is cleared when the user selects it 546 notification.setAutoCancel(true); 547 548 boolean eventInfoConfigured = false; 549 550 final boolean isInbox = account.settings.defaultInbox.equals(folder.uri); 551 final FolderPreferences folderPreferences = 552 new FolderPreferences(context, account.name, folder, isInbox); 553 554 if (isInbox) { 555 final AccountPreferences accountPreferences = 556 new AccountPreferences(context, account.name); 557 moveNotificationSetting(accountPreferences, folderPreferences); 558 } 559 560 if (!folderPreferences.areNotificationsEnabled()) { 561 // Don't notify 562 return; 563 } 564 565 if (unreadCount > 0) { 566 // How can I order this properly? 567 if (cursor.moveToNext()) { 568 final Intent notificationIntent; 569 570 // Launch directly to the conversation, if there is only 1 unseen conversation 571 if (unseenCount == 1) { 572 notificationIntent = createViewConversationIntent(context, account, folder, 573 cursor); 574 } else { 575 notificationIntent = createViewConversationIntent(context, account, folder, 576 null); 577 } 578 579 if (notificationIntent == null) { 580 LogUtils.e(LOG_TAG, "NotificationUtils: " 581 + "Null intent when building notification"); 582 return; 583 } 584 585 clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0); 586 configureLatestEventInfoFromConversation(context, account, folderPreferences, 587 notification, cursor, clickIntent, notificationIntent, 588 account.name, unreadCount, unseenCount, folder, when); 589 eventInfoConfigured = true; 590 } 591 } 592 593 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled(); 594 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri(); 595 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled(); 596 597 if (!ignoreUnobtrusiveSetting && notifyOnce) { 598 // If the user has "unobtrusive notifications" enabled, only alert the first time 599 // new mail is received in this account. This is the default behavior. See 600 // bugs 2412348 and 2413490. 601 notification.setOnlyAlertOnce(true); 602 } 603 604 LogUtils.d(LOG_TAG, "NotificationUtils: Account: %s vibrate: %s", account.name, 605 Boolean.toString(folderPreferences.isNotificationVibrateEnabled())); 606 607 int defaults = 0; 608 609 /* 610 * We do not want to notify if this is coming back from an Undo notification, hence the 611 * oldWhen check. 612 */ 613 if (getAttention && oldWhen == 0) { 614 final AccountPreferences accountPreferences = 615 new AccountPreferences(context, account.name); 616 if (accountPreferences.areNotificationsEnabled()) { 617 if (vibrate) { 618 defaults |= Notification.DEFAULT_VIBRATE; 619 } 620 621 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null 622 : Uri.parse(ringtoneUri)); 623 LogUtils.d(LOG_TAG, "NotificationUtils: " 624 + "New email in %s vibrateWhen: %s, playing notification: %s", 625 account.name, vibrate, ringtoneUri); 626 } 627 } 628 629 if (eventInfoConfigured) { 630 defaults |= Notification.DEFAULT_LIGHTS; 631 notification.setDefaults(defaults); 632 633 if (oldWhen != 0) { 634 // We do not want to display the ticker again if we are re-displaying this 635 // notification (like from an Undo notification) 636 notification.setTicker(null); 637 } 638 639 nm.notify(notificationId, notification.build()); 640 } 641 } finally { 642 if (cursor != null) { 643 cursor.close(); 644 } 645 } 646 } 647 648 /** 649 * @return an {@link Intent} which, if launched, will display the corresponding conversation 650 */ 651 private static Intent createViewConversationIntent(final Context context, final Account account, 652 final Folder folder, final Cursor cursor) { 653 if (folder == null || account == null) { 654 LogUtils.e(LOG_TAG, "NotificationUtils#createViewConversationIntent(): " 655 + "Null account or folder. account: %s folder: %s", account, folder); 656 return null; 657 } 658 659 final Intent intent; 660 661 if (cursor == null) { 662 intent = Utils.createViewFolderIntent(context, folder.uri, account); 663 } else { 664 // A conversation cursor has been specified, so this intent is intended to be go 665 // directly to the one new conversation 666 667 // Get the Conversation object 668 final Conversation conversation = new Conversation(cursor); 669 intent = Utils.createViewConversationIntent(context, conversation, folder.uri, 670 account); 671 } 672 673 return intent; 674 } 675 676 private static Bitmap getDefaultNotificationIcon( 677 final Context context, final Folder folder, final boolean multipleNew) { 678 final int resId; 679 if (folder.notificationIconResId != 0) { 680 resId = folder.notificationIconResId; 681 } else if (multipleNew) { 682 resId = R.drawable.ic_notification_multiple_mail_holo_dark; 683 } else { 684 resId = R.drawable.ic_contact_picture; 685 } 686 687 final Bitmap icon = getIcon(context, resId); 688 689 if (icon == null) { 690 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId); 691 } 692 693 return icon; 694 } 695 696 private static Bitmap getIcon(final Context context, final int resId) { 697 final Bitmap cachedIcon = sNotificationIcons.get(resId); 698 if (cachedIcon != null) { 699 return cachedIcon; 700 } 701 702 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId); 703 sNotificationIcons.put(resId, icon); 704 705 return icon; 706 } 707 708 private static void configureLatestEventInfoFromConversation(final Context context, 709 final Account account, final FolderPreferences folderPreferences, 710 final NotificationCompat.Builder notification, final Cursor conversationCursor, 711 final PendingIntent clickIntent, final Intent notificationIntent, 712 final String notificationAccount, final int unreadCount, final int unseenCount, 713 final Folder folder, final long when) { 714 final Resources res = context.getResources(); 715 716 LogUtils.w(LOG_TAG, "NotificationUtils: Showing notification with unreadCount of %d and " 717 + "unseenCount of %d", unreadCount, unseenCount); 718 719 String notificationTicker = null; 720 721 // Boolean indicating that this notification is for a non-inbox label. 722 final boolean isInbox = account.settings.defaultInbox.equals(folder.uri); 723 724 // Notification label name for user label notifications. 725 final String notificationLabelName = isInbox ? null : folder.name; 726 727 if (unseenCount > 1) { 728 // Build the string that describes the number of new messages 729 final String newMessagesString = res.getString(R.string.new_messages, unseenCount); 730 731 // Use the default notification icon 732 notification.setLargeIcon( 733 getDefaultNotificationIcon(context, folder, true /* multiple new messages */)); 734 735 // The ticker initially start as the new messages string. 736 notificationTicker = newMessagesString; 737 738 // The title of the notification is the new messages string 739 notification.setContentTitle(newMessagesString); 740 741 // TODO(skennedy) Can we remove this check? 742 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) { 743 // For a new-style notification 744 final int maxNumDigestItems = context.getResources().getInteger( 745 R.integer.max_num_notification_digest_items); 746 747 // The body of the notification is the account name, or the label name. 748 notification.setSubText(isInbox ? notificationAccount : notificationLabelName); 749 750 final NotificationCompat.InboxStyle digest = 751 new NotificationCompat.InboxStyle(notification); 752 753 int numDigestItems = 0; 754 do { 755 final Conversation conversation = new Conversation(conversationCursor); 756 757 if (!conversation.read) { 758 boolean multipleUnreadThread = false; 759 // TODO(cwren) extract this pattern into a helper 760 761 Cursor cursor = null; 762 MessageCursor messageCursor = null; 763 try { 764 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon(); 765 uriBuilder.appendQueryParameter( 766 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName); 767 cursor = context.getContentResolver().query(uriBuilder.build(), 768 UIProvider.MESSAGE_PROJECTION, null, null, null); 769 messageCursor = new MessageCursor(cursor); 770 771 String from = ""; 772 String fromAddress = ""; 773 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) { 774 final Message message = messageCursor.getMessage(); 775 fromAddress = message.getFrom(); 776 if (fromAddress == null) { 777 fromAddress = ""; 778 } 779 from = getDisplayableSender(fromAddress); 780 } 781 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) { 782 final Message message = messageCursor.getMessage(); 783 if (!message.read && 784 !fromAddress.contentEquals(message.getFrom())) { 785 multipleUnreadThread = true; 786 break; 787 } 788 } 789 final SpannableStringBuilder sendersBuilder; 790 if (multipleUnreadThread) { 791 final int sendersLength = 792 res.getInteger(R.integer.swipe_senders_length); 793 794 sendersBuilder = getStyledSenders(context, conversationCursor, 795 sendersLength, notificationAccount); 796 } else { 797 if (from == null) { 798 LogUtils.e(LOG_TAG, "NotificationUtils: null from string in " + 799 "configureLatestEventInfoFromConversation"); 800 from = ""; 801 } 802 sendersBuilder = new SpannableStringBuilder(from); 803 } 804 final CharSequence digestLine = getSingleMessageInboxLine(context, 805 sendersBuilder.toString(), 806 conversation.subject, 807 conversation.snippet); 808 digest.addLine(digestLine); 809 numDigestItems++; 810 } finally { 811 if (messageCursor != null) { 812 messageCursor.close(); 813 } 814 if (cursor != null) { 815 cursor.close(); 816 } 817 } 818 } 819 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext()); 820 } else { 821 // The body of the notification is the account name, or the label name. 822 notification.setContentText( 823 isInbox ? notificationAccount : notificationLabelName); 824 } 825 } else { 826 // For notifications for a single new conversation, we want to get the information from 827 // the conversation 828 829 // Move the cursor to the most recent unread conversation 830 seekToLatestUnreadConversation(conversationCursor); 831 832 final Conversation conversation = new Conversation(conversationCursor); 833 834 Cursor cursor = null; 835 MessageCursor messageCursor = null; 836 boolean multipleUnseenThread = false; 837 String from = null; 838 try { 839 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter( 840 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build(); 841 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION, 842 null, null, null); 843 messageCursor = new MessageCursor(cursor); 844 // Use the information from the last sender in the conversation that triggered 845 // this notification. 846 847 String fromAddress = ""; 848 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) { 849 final Message message = messageCursor.getMessage(); 850 fromAddress = message.getFrom(); 851 from = getDisplayableSender(fromAddress); 852 notification.setLargeIcon( 853 getContactIcon(context, from, getSenderAddress(fromAddress), folder)); 854 } 855 856 // Assume that the last message in this conversation is unread 857 int firstUnseenMessagePos = messageCursor.getPosition(); 858 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) { 859 final Message message = messageCursor.getMessage(); 860 final boolean unseen = !message.seen; 861 if (unseen) { 862 firstUnseenMessagePos = messageCursor.getPosition(); 863 if (!multipleUnseenThread 864 && !fromAddress.contentEquals(message.getFrom())) { 865 multipleUnseenThread = true; 866 } 867 } 868 } 869 870 // TODO(skennedy) Can we remove this check? 871 if (Utils.isRunningJellybeanOrLater()) { 872 // For a new-style notification 873 874 if (multipleUnseenThread) { 875 // The title of a single conversation is the list of senders. 876 int sendersLength = res.getInteger(R.integer.swipe_senders_length); 877 878 final SpannableStringBuilder sendersBuilder = getStyledSenders( 879 context, conversationCursor, sendersLength, notificationAccount); 880 881 notification.setContentTitle(sendersBuilder); 882 // For a single new conversation, the ticker is based on the sender's name. 883 notificationTicker = sendersBuilder.toString(); 884 } else { 885 // The title of a single message the sender. 886 notification.setContentTitle(from); 887 // For a single new conversation, the ticker is based on the sender's name. 888 notificationTicker = from; 889 } 890 891 // The notification content will be the subject of the conversation. 892 notification.setContentText( 893 getSingleMessageLittleText(context, conversation.subject)); 894 895 // The notification subtext will be the subject of the conversation for inbox 896 // notifications, or will based on the the label name for user label 897 // notifications. 898 notification.setSubText(isInbox ? notificationAccount : notificationLabelName); 899 900 if (multipleUnseenThread) { 901 notification.setLargeIcon( 902 getDefaultNotificationIcon(context, folder, true)); 903 } 904 final NotificationCompat.BigTextStyle bigText = 905 new NotificationCompat.BigTextStyle(notification); 906 907 // Seek the message cursor to the first unread message 908 final Message message; 909 if (messageCursor.moveToPosition(firstUnseenMessagePos)) { 910 message = messageCursor.getMessage(); 911 bigText.bigText(getSingleMessageBigText(context, 912 conversation.subject, message)); 913 } else { 914 LogUtils.e(LOG_TAG, "NotificationUtils: Failed to load message"); 915 message = null; 916 } 917 918 if (message != null) { 919 final Set<String> notificationActions = 920 folderPreferences.getNotificationActions(account); 921 922 final int notificationId = getNotificationId(notificationAccount, folder); 923 924 NotificationActionUtils.addNotificationActions(context, notificationIntent, 925 notification, account, conversation, message, folder, 926 notificationId, when, notificationActions); 927 } 928 } else { 929 // For an old-style notification 930 931 // The title of a single conversation notification is built from both the sender 932 // and subject of the new message. 933 notification.setContentTitle(getSingleMessageNotificationTitle(context, 934 from, conversation.subject)); 935 936 // The notification content will be the subject of the conversation for inbox 937 // notifications, or will based on the the label name for user label 938 // notifications. 939 notification.setContentText( 940 isInbox ? notificationAccount : notificationLabelName); 941 942 // For a single new conversation, the ticker is based on the sender's name. 943 notificationTicker = from; 944 } 945 } finally { 946 if (messageCursor != null) { 947 messageCursor.close(); 948 } 949 if (cursor != null) { 950 cursor.close(); 951 } 952 } 953 } 954 955 // Build the notification ticker 956 if (notificationLabelName != null && notificationTicker != null) { 957 // This is a per label notification, format the ticker with that information 958 notificationTicker = res.getString(R.string.label_notification_ticker, 959 notificationLabelName, notificationTicker); 960 } 961 962 if (notificationTicker != null) { 963 // If we didn't generate a notification ticker, it will default to account name 964 notification.setTicker(notificationTicker); 965 } 966 967 // Set the number in the notification 968 if (unreadCount > 1) { 969 notification.setNumber(unreadCount); 970 } 971 972 notification.setContentIntent(clickIntent); 973 } 974 975 private static SpannableStringBuilder getStyledSenders(final Context context, 976 final Cursor conversationCursor, final int maxLength, final String account) { 977 final Conversation conversation = new Conversation(conversationCursor); 978 final com.android.mail.providers.ConversationInfo conversationInfo = 979 conversation.conversationInfo; 980 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>(); 981 if (sNotificationUnreadStyleSpan == null) { 982 sNotificationUnreadStyleSpan = new TextAppearanceSpan( 983 context, R.style.NotificationSendersUnreadTextAppearance); 984 sNotificationReadStyleSpan = 985 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance); 986 } 987 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account, 988 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false); 989 990 return ellipsizeStyledSenders(context, senders); 991 } 992 993 private static String sSendersSplitToken = null; 994 private static String sElidedPaddingToken = null; 995 996 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context, 997 ArrayList<SpannableString> styledSenders) { 998 if (sSendersSplitToken == null) { 999 sSendersSplitToken = context.getString(R.string.senders_split_token); 1000 sElidedPaddingToken = context.getString(R.string.elided_padding_token); 1001 } 1002 1003 SpannableStringBuilder builder = new SpannableStringBuilder(); 1004 SpannableString prevSender = null; 1005 for (SpannableString sender : styledSenders) { 1006 if (sender == null) { 1007 LogUtils.e(LOG_TAG, "NotificationUtils: null sender iterating over styledSenders"); 1008 continue; 1009 } 1010 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 1011 if (SendersView.sElidedString.equals(sender.toString())) { 1012 prevSender = sender; 1013 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 1014 } else if (builder.length() > 0 1015 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 1016 .toString()))) { 1017 prevSender = sender; 1018 sender = copyStyles(spans, sSendersSplitToken + sender); 1019 } else { 1020 prevSender = sender; 1021 } 1022 builder.append(sender); 1023 } 1024 return builder; 1025 } 1026 1027 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 1028 SpannableString s = new SpannableString(newText); 1029 if (spans != null && spans.length > 0) { 1030 s.setSpan(spans[0], 0, s.length(), 0); 1031 } 1032 return s; 1033 } 1034 1035 /** 1036 * Seeks the cursor to the position of the most recent unread conversation. If no unread 1037 * conversation is found, the position of the cursor will be restored, and false will be 1038 * returned. 1039 */ 1040 private static boolean seekToLatestUnreadConversation(final Cursor cursor) { 1041 final int initialPosition = cursor.getPosition(); 1042 do { 1043 final Conversation conversation = new Conversation(cursor); 1044 if (!conversation.read) { 1045 return true; 1046 } 1047 } while (cursor.moveToNext()); 1048 1049 // Didn't find an unread conversation, reset the position. 1050 cursor.moveToPosition(initialPosition); 1051 return false; 1052 } 1053 1054 /** 1055 * Sets the bigtext for a notification for a single new conversation 1056 * 1057 * @param context 1058 * @param senders Sender of the new message that triggered the notification. 1059 * @param subject Subject of the new message that triggered the notification 1060 * @param snippet Snippet of the new message that triggered the notification 1061 * @return a {@link CharSequence} suitable for use in 1062 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1063 */ 1064 private static CharSequence getSingleMessageInboxLine(Context context, 1065 String senders, String subject, String snippet) { 1066 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText 1067 1068 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet; 1069 1070 final TextAppearanceSpan notificationPrimarySpan = 1071 new TextAppearanceSpan(context, R.style.NotificationPrimaryText); 1072 1073 if (TextUtils.isEmpty(senders)) { 1074 // If the senders are empty, just use the subject/snippet. 1075 return subjectSnippet; 1076 } else if (TextUtils.isEmpty(subjectSnippet)) { 1077 // If the subject/snippet is empty, just use the senders. 1078 final SpannableString spannableString = new SpannableString(senders); 1079 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0); 1080 1081 return spannableString; 1082 } else { 1083 final String formatString = context.getResources().getString( 1084 R.string.multiple_new_message_notification_item); 1085 final TextAppearanceSpan notificationSecondarySpan = 1086 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1087 1088 final String instantiatedString = String.format(formatString, 1089 sBidiFormatter.unicodeWrap(senders), 1090 sBidiFormatter.unicodeWrap(subjectSnippet)); 1091 1092 final SpannableString spannableString = new SpannableString(instantiatedString); 1093 1094 final boolean isOrderReversed = formatString.indexOf("%2$s") < 1095 formatString.indexOf("%1$s"); 1096 final int primaryOffset = 1097 (isOrderReversed ? instantiatedString.lastIndexOf(senders) : 1098 instantiatedString.indexOf(senders)); 1099 final int secondaryOffset = 1100 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) : 1101 instantiatedString.indexOf(subjectSnippet)); 1102 spannableString.setSpan(notificationPrimarySpan, 1103 primaryOffset, primaryOffset + senders.length(), 0); 1104 spannableString.setSpan(notificationSecondarySpan, 1105 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0); 1106 return spannableString; 1107 } 1108 } 1109 1110 /** 1111 * Sets the bigtext for a notification for a single new conversation 1112 * @param context 1113 * @param subject Subject of the new message that triggered the notification 1114 * @return a {@link CharSequence} suitable for use in 1115 * {@link NotificationCompat.Builder#setContentText} 1116 */ 1117 private static CharSequence getSingleMessageLittleText(Context context, String subject) { 1118 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1119 context, R.style.NotificationPrimaryText); 1120 1121 final SpannableString spannableString = new SpannableString(subject); 1122 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1123 1124 return spannableString; 1125 } 1126 1127 /** 1128 * Sets the bigtext for a notification for a single new conversation 1129 * 1130 * @param context 1131 * @param subject Subject of the new message that triggered the notification 1132 * @param message the {@link Message} to be displayed. 1133 * @return a {@link CharSequence} suitable for use in 1134 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1135 */ 1136 private static CharSequence getSingleMessageBigText(Context context, String subject, 1137 final Message message) { 1138 1139 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1140 context, R.style.NotificationPrimaryText); 1141 1142 final String snippet = getMessageBodyWithoutElidedText(message); 1143 1144 // Change multiple newlines (with potential white space between), into a single new line 1145 final String collapsedSnippet = 1146 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : ""; 1147 1148 if (TextUtils.isEmpty(subject)) { 1149 // If the subject is empty, just use the snippet. 1150 return snippet; 1151 } else if (TextUtils.isEmpty(collapsedSnippet)) { 1152 // If the snippet is empty, just use the subject. 1153 final SpannableString spannableString = new SpannableString(subject); 1154 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1155 1156 return spannableString; 1157 } else { 1158 final String notificationBigTextFormat = context.getResources().getString( 1159 R.string.single_new_message_notification_big_text); 1160 1161 // Localizers may change the order of the parameters, look at how the format 1162 // string is structured. 1163 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") > 1164 notificationBigTextFormat.indexOf("%1$s"); 1165 final String bigText = 1166 String.format(notificationBigTextFormat, subject, collapsedSnippet); 1167 final SpannableString spannableString = new SpannableString(bigText); 1168 1169 final int subjectOffset = 1170 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject)); 1171 spannableString.setSpan(notificationSubjectSpan, 1172 subjectOffset, subjectOffset + subject.length(), 0); 1173 1174 return spannableString; 1175 } 1176 } 1177 1178 /** 1179 * Gets the title for a notification for a single new conversation 1180 * @param context 1181 * @param sender Sender of the new message that triggered the notification. 1182 * @param subject Subject of the new message that triggered the notification 1183 * @return a {@link CharSequence} suitable for use as a {@link Notification} title. 1184 */ 1185 private static CharSequence getSingleMessageNotificationTitle(Context context, 1186 String sender, String subject) { 1187 1188 if (TextUtils.isEmpty(subject)) { 1189 // If the subject is empty, just set the title to the sender's information. 1190 return sender; 1191 } else { 1192 final String notificationTitleFormat = context.getResources().getString( 1193 R.string.single_new_message_notification_title); 1194 1195 // Localizers may change the order of the parameters, look at how the format 1196 // string is structured. 1197 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") > 1198 notificationTitleFormat.indexOf("%1$s"); 1199 final String titleString = String.format(notificationTitleFormat, sender, subject); 1200 1201 // Format the string so the subject is using the secondaryText style 1202 final SpannableString titleSpannable = new SpannableString(titleString); 1203 1204 // Find the offset of the subject. 1205 final int subjectOffset = 1206 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject); 1207 final TextAppearanceSpan notificationSubjectSpan = 1208 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1209 titleSpannable.setSpan(notificationSubjectSpan, 1210 subjectOffset, subjectOffset + subject.length(), 0); 1211 return titleSpannable; 1212 } 1213 } 1214 1215 /** 1216 * Clears the notifications for the specified account/folder. 1217 */ 1218 public static void clearFolderNotification(Context context, Account account, Folder folder, 1219 final boolean markSeen) { 1220 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name, 1221 folder.name); 1222 final NotificationMap notificationMap = getNotificationMap(context); 1223 final NotificationKey key = new NotificationKey(account, folder); 1224 notificationMap.remove(key); 1225 notificationMap.saveNotificationMap(context); 1226 1227 final NotificationManager notificationManager = 1228 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 1229 notificationManager.cancel(getNotificationId(account.name, folder)); 1230 1231 if (markSeen) { 1232 markSeen(context, folder); 1233 } 1234 } 1235 1236 /** 1237 * Clears all notifications for the specified account. 1238 */ 1239 public static void clearAccountNotifications(final Context context, final String account) { 1240 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account); 1241 final NotificationMap notificationMap = getNotificationMap(context); 1242 1243 // Find all NotificationKeys for this account 1244 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder(); 1245 1246 for (final NotificationKey key : notificationMap.keySet()) { 1247 if (account.equals(key.account.name)) { 1248 keyBuilder.add(key); 1249 } 1250 } 1251 1252 final List<NotificationKey> notificationKeys = keyBuilder.build(); 1253 1254 final NotificationManager notificationManager = 1255 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 1256 1257 for (final NotificationKey notificationKey : notificationKeys) { 1258 final Folder folder = notificationKey.folder; 1259 notificationManager.cancel(getNotificationId(account, folder)); 1260 notificationMap.remove(notificationKey); 1261 } 1262 1263 notificationMap.saveNotificationMap(context); 1264 } 1265 1266 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) { 1267 ArrayList<String> whereArgs = new ArrayList<String>(); 1268 StringBuilder whereBuilder = new StringBuilder(); 1269 String[] questionMarks = new String[addresses.size()]; 1270 1271 whereArgs.addAll(addresses); 1272 Arrays.fill(questionMarks, "?"); 1273 whereBuilder.append(Email.DATA1 + " IN ("). 1274 append(TextUtils.join(",", questionMarks)). 1275 append(")"); 1276 1277 ContentResolver resolver = context.getContentResolver(); 1278 Cursor c = resolver.query(Email.CONTENT_URI, 1279 new String[]{Email.CONTACT_ID}, whereBuilder.toString(), 1280 whereArgs.toArray(new String[0]), null); 1281 1282 ArrayList<Long> contactIds = new ArrayList<Long>(); 1283 if (c == null) { 1284 return contactIds; 1285 } 1286 try { 1287 while (c.moveToNext()) { 1288 contactIds.add(c.getLong(0)); 1289 } 1290 } finally { 1291 c.close(); 1292 } 1293 return contactIds; 1294 } 1295 1296 private static Bitmap getContactIcon(final Context context, final String displayName, 1297 final String senderAddress, final Folder folder) { 1298 if (senderAddress == null) { 1299 return null; 1300 } 1301 1302 Bitmap icon = null; 1303 1304 final List<Long> contactIds = findContacts( context, Arrays.asList( 1305 new String[] { senderAddress })); 1306 1307 // Get the ideal size for this icon. 1308 final Resources res = context.getResources(); 1309 final int idealIconHeight = 1310 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 1311 final int idealIconWidth = 1312 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 1313 1314 if (contactIds != null) { 1315 for (final long id : contactIds) { 1316 final Uri contactUri = 1317 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); 1318 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY); 1319 final Cursor cursor = context.getContentResolver().query( 1320 photoUri, new String[] { Photo.PHOTO }, null, null, null); 1321 1322 if (cursor != null) { 1323 try { 1324 if (cursor.moveToFirst()) { 1325 final byte[] data = cursor.getBlob(0); 1326 if (data != null) { 1327 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data)); 1328 if (icon != null && icon.getHeight() < idealIconHeight) { 1329 // We should scale this image to fit the intended size 1330 icon = Bitmap.createScaledBitmap( 1331 icon, idealIconWidth, idealIconHeight, true); 1332 } 1333 if (icon != null) { 1334 break; 1335 } 1336 } 1337 } 1338 } finally { 1339 cursor.close(); 1340 } 1341 } 1342 } 1343 } 1344 1345 if (icon == null) { 1346 // Make a colorful tile! 1347 final ContactPhotoManager contactPhotoManager = 1348 ContactPhotoManager.getInstance(context); 1349 1350 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight, 1351 Dimensions.SCALE_ONE); 1352 1353 icon = contactPhotoManager.getDefaultImageProvider().getLetterTile(dimensions, 1354 displayName, senderAddress); 1355 } 1356 1357 if (icon == null) { 1358 // Icon should be the default mail icon. 1359 icon = getDefaultNotificationIcon(context, folder, false /* single new message */); 1360 } 1361 return icon; 1362 } 1363 1364 private static String getMessageBodyWithoutElidedText(final Message message) { 1365 return getMessageBodyWithoutElidedText(message.getBodyAsHtml()); 1366 } 1367 1368 public static String getMessageBodyWithoutElidedText(String html) { 1369 if (TextUtils.isEmpty(html)) { 1370 return ""; 1371 } 1372 // Get the html "tree" for this message body 1373 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html); 1374 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY); 1375 1376 return htmlTree.getPlainText(); 1377 } 1378 1379 public static void markSeen(final Context context, final Folder folder) { 1380 final Uri uri = folder.uri; 1381 1382 final ContentValues values = new ContentValues(1); 1383 values.put(UIProvider.ConversationColumns.SEEN, 1); 1384 1385 context.getContentResolver().update(uri, values, null, null); 1386 } 1387 1388 /** 1389 * Returns a displayable string representing 1390 * the message sender. It has a preference toward showing the name, 1391 * but will fall back to the address if that is all that is available. 1392 */ 1393 private static String getDisplayableSender(String sender) { 1394 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1395 1396 String displayableSender = address.getName(); 1397 1398 if (!TextUtils.isEmpty(displayableSender)) { 1399 return Address.decodeAddressName(displayableSender); 1400 } 1401 1402 // If that fails, default to the sender address. 1403 displayableSender = address.getAddress(); 1404 1405 // If we were unable to tokenize a name or address, 1406 // just use whatever was in the sender. 1407 if (TextUtils.isEmpty(displayableSender)) { 1408 displayableSender = sender; 1409 } 1410 return displayableSender; 1411 } 1412 1413 /** 1414 * Returns only the address portion of a message sender. 1415 */ 1416 private static String getSenderAddress(String sender) { 1417 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1418 1419 String tokenizedAddress = address.getAddress(); 1420 1421 // If we were unable to tokenize a name or address, 1422 // just use whatever was in the sender. 1423 if (TextUtils.isEmpty(tokenizedAddress)) { 1424 tokenizedAddress = sender; 1425 } 1426 return tokenizedAddress; 1427 } 1428 1429 public static int getNotificationId(final String account, final Folder folder) { 1430 return 1 ^ account.hashCode() ^ folder.hashCode(); 1431 } 1432 1433 private static class NotificationKey { 1434 public final Account account; 1435 public final Folder folder; 1436 1437 public NotificationKey(Account account, Folder folder) { 1438 this.account = account; 1439 this.folder = folder; 1440 } 1441 1442 @Override 1443 public boolean equals(Object other) { 1444 if (!(other instanceof NotificationKey)) { 1445 return false; 1446 } 1447 NotificationKey key = (NotificationKey) other; 1448 return account.equals(key.account) && folder.equals(key.folder); 1449 } 1450 1451 @Override 1452 public String toString() { 1453 return account.name + " " + folder.name; 1454 } 1455 1456 @Override 1457 public int hashCode() { 1458 final int accountHashCode = account.hashCode(); 1459 final int folderHashCode = folder.hashCode(); 1460 return accountHashCode ^ folderHashCode; 1461 } 1462 } 1463 1464 /** 1465 * Contains the logic for converting the contents of one HtmlTree into 1466 * plaintext. 1467 */ 1468 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter { 1469 // Strings for parsing html message bodies 1470 private static final String ELIDED_TEXT_ELEMENT_NAME = "div"; 1471 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class"; 1472 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text"; 1473 1474 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE = 1475 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE); 1476 1477 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE = 1478 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null); 1479 1480 private int mEndNodeElidedTextBlock = -1; 1481 1482 @Override 1483 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) { 1484 // If we are in the middle of an elided text block, don't add this node 1485 if (nodeNum < mEndNodeElidedTextBlock) { 1486 return; 1487 } else if (nodeNum == mEndNodeElidedTextBlock) { 1488 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum); 1489 return; 1490 } 1491 1492 // If this tag starts another elided text block, we want to remember the end 1493 if (n instanceof HtmlDocument.Tag) { 1494 boolean foundElidedTextTag = false; 1495 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n; 1496 final HTML.Element htmlElement = htmlTag.getElement(); 1497 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) { 1498 // Make sure that the class is what is expected 1499 final List<HtmlDocument.TagAttribute> attributes = 1500 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE); 1501 for (HtmlDocument.TagAttribute attribute : attributes) { 1502 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals( 1503 attribute.getValue())) { 1504 // Found an "elided-text" div. Remember information about this tag 1505 mEndNodeElidedTextBlock = endNum; 1506 foundElidedTextTag = true; 1507 break; 1508 } 1509 } 1510 } 1511 1512 if (foundElidedTextTag) { 1513 return; 1514 } 1515 } 1516 1517 super.addNode(n, nodeNum, endNum); 1518 } 1519 } 1520 1521 /** 1522 * During account setup in Email, we may not have an inbox yet, so the notification setting had 1523 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the 1524 * {@link FolderPreferences} now. 1525 */ 1526 public static void moveNotificationSetting(final AccountPreferences accountPreferences, 1527 final FolderPreferences folderPreferences) { 1528 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) { 1529 // If this setting has been changed some other way, don't overwrite it 1530 if (!folderPreferences.isNotificationsEnabledSet()) { 1531 final boolean notificationsEnabled = 1532 accountPreferences.getDefaultInboxNotificationsEnabled(); 1533 1534 folderPreferences.setNotificationsEnabled(notificationsEnabled); 1535 } 1536 1537 accountPreferences.clearDefaultInboxNotificationsEnabled(); 1538 } 1539 } 1540} 1541