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