NotificationUtils.java revision a242bf72f4c5d085a00b723b8cd09e8856eaf265
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 // Conversations are sorted in descending order, but notification sort 1010 // key is in ascending order. Invert the order key to get the right 1011 // order. Left pad 19 zeros because it's a long. 1012 String groupSortKey = String.format("%019d", 1013 (Long.MAX_VALUE - conversation.orderKey)); 1014 conversationNotif.setGroup(notificationGroupKey); 1015 conversationNotif.setSortKey(groupSortKey); 1016 1017 int conversationNotificationId = getNotificationId( 1018 summaryNotificationId, conversation.hashCode()); 1019 1020 final NotificationCompat.WearableExtender conversationWearExtender = 1021 new NotificationCompat.WearableExtender(); 1022 final ConfigResult result = 1023 configureNotifForOneConversation(context, account, 1024 folderPreferences, conversationNotif, conversationWearExtender, 1025 conversationCursor, notificationIntent, folder, when, res, 1026 notificationAccountDisplayName, notificationAccountEmail, 1027 isInbox, notificationLabelName, conversationNotificationId, 1028 photoFetcher); 1029 msgNotifications.put(conversationNotificationId, 1030 NotificationBuilders.of(conversationNotif, 1031 conversationWearExtender)); 1032 1033 if (firstResult == null) { 1034 firstResult = result; 1035 } 1036 } finally { 1037 if (messageCursor != null) { 1038 messageCursor.close(); 1039 } 1040 if (cursor != null) { 1041 cursor.close(); 1042 } 1043 } 1044 } 1045 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext()); 1046 1047 if (firstResult != null && firstResult.contactIconInfo != null) { 1048 wearableExtender.setBackground(firstResult.contactIconInfo.wearableBg); 1049 } else { 1050 LogUtils.w(LOG_TAG, "First contact icon is null!"); 1051 wearableExtender.setBackground(getDefaultWearableBg(context)); 1052 } 1053 } else { 1054 // The body of the notification is the account name, or the label name. 1055 notification.setContentText( 1056 isInbox ? notificationAccountDisplayName : notificationLabelName); 1057 } 1058 } else { 1059 // For notifications for a single new conversation, we want to get the information 1060 // from the conversation 1061 1062 // Move the cursor to the most recent unread conversation 1063 seekToLatestUnreadConversation(conversationCursor); 1064 1065 final ConfigResult result = configureNotifForOneConversation(context, account, 1066 folderPreferences, notification, wearableExtender, conversationCursor, 1067 notificationIntent, folder, when, res, notificationAccountDisplayName, 1068 notificationAccountEmail, isInbox, notificationLabelName, 1069 summaryNotificationId, photoFetcher); 1070 notificationTicker = result.notificationTicker; 1071 1072 wearableExtender.setBackground(result.contactIconInfo.wearableBg); 1073 } 1074 1075 // Build the notification ticker 1076 if (notificationLabelName != null && notificationTicker != null) { 1077 // This is a per label notification, format the ticker with that information 1078 notificationTicker = res.getString(R.string.label_notification_ticker, 1079 notificationLabelName, notificationTicker); 1080 } 1081 1082 if (notificationTicker != null) { 1083 // If we didn't generate a notification ticker, it will default to account name 1084 notification.setTicker(notificationTicker); 1085 } 1086 1087 // Set the number in the notification 1088 if (unreadCount > 1) { 1089 notification.setNumber(unreadCount); 1090 } 1091 1092 notification.setContentIntent(clickIntent); 1093 } 1094 1095 /** 1096 * Configure the notification for one conversation. When there are multiple conversations, 1097 * this method is used to configure bundled notification for Android Wear. 1098 */ 1099 private static ConfigResult configureNotifForOneConversation(Context context, 1100 Account account, FolderPreferences folderPreferences, 1101 NotificationCompat.Builder notification, 1102 NotificationCompat.WearableExtender wearExtender, Cursor conversationCursor, 1103 Intent notificationIntent, Folder folder, long when, Resources res, 1104 String notificationAccountDisplayName, String notificationAccountEmail, 1105 boolean isInbox, String notificationLabelName, int notificationId, 1106 final ContactPhotoFetcher photoFetcher) { 1107 1108 final ConfigResult result = new ConfigResult(); 1109 1110 final Conversation conversation = new Conversation(conversationCursor); 1111 1112 Cursor cursor = null; 1113 MessageCursor messageCursor = null; 1114 boolean multipleUnseenThread = false; 1115 String from = null; 1116 try { 1117 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter( 1118 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build(); 1119 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION, 1120 null, null, null); 1121 messageCursor = new MessageCursor(cursor); 1122 // Use the information from the last sender in the conversation that triggered 1123 // this notification. 1124 1125 String fromAddress = ""; 1126 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) { 1127 final Message message = messageCursor.getMessage(); 1128 fromAddress = message.getFrom(); 1129 if (fromAddress == null) { 1130 // No sender. Go back to default value. 1131 LogUtils.e(LOG_TAG, "No sender found for message: %d", message.getId()); 1132 fromAddress = ""; 1133 } 1134 from = getDisplayableSender(fromAddress); 1135 result.contactIconInfo = getContactIcon( 1136 context, account.getAccountManagerAccount().name, from, 1137 getSenderAddress(fromAddress), folder, photoFetcher); 1138 notification.setLargeIcon(result.contactIconInfo.icon); 1139 } 1140 1141 // Assume that the last message in this conversation is unread 1142 int firstUnseenMessagePos = messageCursor.getPosition(); 1143 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) { 1144 final Message message = messageCursor.getMessage(); 1145 final boolean unseen = !message.seen; 1146 if (unseen) { 1147 firstUnseenMessagePos = messageCursor.getPosition(); 1148 if (!multipleUnseenThread 1149 && !fromAddress.contentEquals(message.getFrom())) { 1150 multipleUnseenThread = true; 1151 } 1152 } 1153 } 1154 1155 final String subject = ConversationItemView.filterTag(context, conversation.subject); 1156 1157 // TODO(skennedy) Can we remove this check? 1158 if (Utils.isRunningJellybeanOrLater()) { 1159 // For a new-style notification 1160 1161 if (multipleUnseenThread) { 1162 // The title of a single conversation is the list of senders. 1163 int sendersLength = res.getInteger(R.integer.swipe_senders_length); 1164 1165 final SpannableStringBuilder sendersBuilder = getStyledSenders( 1166 context, conversationCursor, sendersLength, 1167 notificationAccountEmail); 1168 1169 notification.setContentTitle(sendersBuilder); 1170 // For a single new conversation, the ticker is based on the sender's name. 1171 result.notificationTicker = sendersBuilder.toString(); 1172 } else { 1173 from = getWrappedFromString(from); 1174 // The title of a single message the sender. 1175 notification.setContentTitle(from); 1176 // For a single new conversation, the ticker is based on the sender's name. 1177 result.notificationTicker = from; 1178 } 1179 1180 // The notification content will be the subject of the conversation. 1181 notification.setContentText(getSingleMessageLittleText(context, subject)); 1182 1183 // The notification subtext will be the subject of the conversation for inbox 1184 // notifications, or will based on the the label name for user label 1185 // notifications. 1186 notification.setSubText(isInbox ? 1187 notificationAccountDisplayName : notificationLabelName); 1188 1189 if (multipleUnseenThread) { 1190 notification.setLargeIcon( 1191 getDefaultNotificationIcon(context, folder, true)); 1192 } 1193 final NotificationCompat.BigTextStyle bigText = 1194 new NotificationCompat.BigTextStyle(notification); 1195 1196 // Seek the message cursor to the first unread message 1197 final Message message; 1198 if (messageCursor.moveToPosition(firstUnseenMessagePos)) { 1199 message = messageCursor.getMessage(); 1200 bigText.bigText(getSingleMessageBigText(context, subject, message)); 1201 } else { 1202 LogUtils.e(LOG_TAG, "Failed to load message"); 1203 message = null; 1204 } 1205 1206 if (message != null) { 1207 final Set<String> notificationActions = 1208 folderPreferences.getNotificationActions(account); 1209 1210 NotificationActionUtils.addNotificationActions(context, notificationIntent, 1211 notification, wearExtender, account, conversation, message, 1212 folder, notificationId, when, notificationActions); 1213 } 1214 } else { 1215 // For an old-style notification 1216 1217 // The title of a single conversation notification is built from both the sender 1218 // and subject of the new message. 1219 notification.setContentTitle( 1220 getSingleMessageNotificationTitle(context, from, subject)); 1221 1222 // The notification content will be the subject of the conversation for inbox 1223 // notifications, or will based on the the label name for user label 1224 // notifications. 1225 notification.setContentText( 1226 isInbox ? notificationAccountDisplayName : notificationLabelName); 1227 1228 // For a single new conversation, the ticker is based on the sender's name. 1229 result.notificationTicker = from; 1230 } 1231 } finally { 1232 if (messageCursor != null) { 1233 messageCursor.close(); 1234 } 1235 if (cursor != null) { 1236 cursor.close(); 1237 } 1238 } 1239 return result; 1240 } 1241 1242 private static String getWrappedFromString(String from) { 1243 if (from == null) { 1244 LogUtils.e(LOG_TAG, "null from string in getWrappedFromString"); 1245 from = ""; 1246 } 1247 from = sBidiFormatter.unicodeWrap(from); 1248 return from; 1249 } 1250 1251 private static SpannableStringBuilder getStyledSenders(final Context context, 1252 final Cursor conversationCursor, final int maxLength, final String account) { 1253 final Conversation conversation = new Conversation(conversationCursor); 1254 final com.android.mail.providers.ConversationInfo conversationInfo = 1255 conversation.conversationInfo; 1256 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>(); 1257 if (sNotificationUnreadStyleSpan == null) { 1258 sNotificationUnreadStyleSpan = new TextAppearanceSpan( 1259 context, R.style.NotificationSendersUnreadTextAppearance); 1260 sNotificationReadStyleSpan = 1261 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance); 1262 } 1263 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account, 1264 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, 1265 false /* showToHeader */, false /* resourceCachingRequired */); 1266 1267 return ellipsizeStyledSenders(context, senders); 1268 } 1269 1270 private static String sSendersSplitToken = null; 1271 private static String sElidedPaddingToken = null; 1272 1273 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context, 1274 ArrayList<SpannableString> styledSenders) { 1275 if (sSendersSplitToken == null) { 1276 sSendersSplitToken = context.getString(R.string.senders_split_token); 1277 sElidedPaddingToken = context.getString(R.string.elided_padding_token); 1278 } 1279 1280 SpannableStringBuilder builder = new SpannableStringBuilder(); 1281 SpannableString prevSender = null; 1282 for (SpannableString sender : styledSenders) { 1283 if (sender == null) { 1284 LogUtils.e(LOG_TAG, "null sender iterating over styledSenders"); 1285 continue; 1286 } 1287 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 1288 if (SendersView.sElidedString.equals(sender.toString())) { 1289 prevSender = sender; 1290 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 1291 } else if (builder.length() > 0 1292 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 1293 .toString()))) { 1294 prevSender = sender; 1295 sender = copyStyles(spans, sSendersSplitToken + sender); 1296 } else { 1297 prevSender = sender; 1298 } 1299 builder.append(sender); 1300 } 1301 return builder; 1302 } 1303 1304 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 1305 SpannableString s = new SpannableString(newText); 1306 if (spans != null && spans.length > 0) { 1307 s.setSpan(spans[0], 0, s.length(), 0); 1308 } 1309 return s; 1310 } 1311 1312 /** 1313 * Seeks the cursor to the position of the most recent unread conversation. If no unread 1314 * conversation is found, the position of the cursor will be restored, and false will be 1315 * returned. 1316 */ 1317 private static boolean seekToLatestUnreadConversation(final Cursor cursor) { 1318 final int initialPosition = cursor.getPosition(); 1319 do { 1320 final Conversation conversation = new Conversation(cursor); 1321 if (!conversation.read) { 1322 return true; 1323 } 1324 } while (cursor.moveToNext()); 1325 1326 // Didn't find an unread conversation, reset the position. 1327 cursor.moveToPosition(initialPosition); 1328 return false; 1329 } 1330 1331 /** 1332 * Sets the bigtext for a notification for a single new conversation 1333 * 1334 * @param context 1335 * @param senders Sender of the new message that triggered the notification. 1336 * @param subject Subject of the new message that triggered the notification 1337 * @param snippet Snippet of the new message that triggered the notification 1338 * @return a {@link CharSequence} suitable for use in 1339 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1340 */ 1341 private static CharSequence getSingleMessageInboxLine(Context context, 1342 String senders, String subject, String snippet) { 1343 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText 1344 1345 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet; 1346 1347 final TextAppearanceSpan notificationPrimarySpan = 1348 new TextAppearanceSpan(context, R.style.NotificationPrimaryText); 1349 1350 if (TextUtils.isEmpty(senders)) { 1351 // If the senders are empty, just use the subject/snippet. 1352 return subjectSnippet; 1353 } else if (TextUtils.isEmpty(subjectSnippet)) { 1354 // If the subject/snippet is empty, just use the senders. 1355 final SpannableString spannableString = new SpannableString(senders); 1356 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0); 1357 1358 return spannableString; 1359 } else { 1360 final String formatString = context.getResources().getString( 1361 R.string.multiple_new_message_notification_item); 1362 final TextAppearanceSpan notificationSecondarySpan = 1363 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1364 1365 // senders is already individually unicode wrapped so it does not need to be done here 1366 final String instantiatedString = String.format(formatString, 1367 senders, 1368 sBidiFormatter.unicodeWrap(subjectSnippet)); 1369 1370 final SpannableString spannableString = new SpannableString(instantiatedString); 1371 1372 final boolean isOrderReversed = formatString.indexOf("%2$s") < 1373 formatString.indexOf("%1$s"); 1374 final int primaryOffset = 1375 (isOrderReversed ? instantiatedString.lastIndexOf(senders) : 1376 instantiatedString.indexOf(senders)); 1377 final int secondaryOffset = 1378 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) : 1379 instantiatedString.indexOf(subjectSnippet)); 1380 spannableString.setSpan(notificationPrimarySpan, 1381 primaryOffset, primaryOffset + senders.length(), 0); 1382 spannableString.setSpan(notificationSecondarySpan, 1383 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0); 1384 return spannableString; 1385 } 1386 } 1387 1388 /** 1389 * Sets the bigtext for a notification for a single new conversation 1390 * @param context 1391 * @param subject Subject of the new message that triggered the notification 1392 * @return a {@link CharSequence} suitable for use in 1393 * {@link NotificationCompat.Builder#setContentText} 1394 */ 1395 private static CharSequence getSingleMessageLittleText(Context context, String subject) { 1396 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1397 context, R.style.NotificationPrimaryText); 1398 1399 final SpannableString spannableString = new SpannableString(subject); 1400 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1401 1402 return spannableString; 1403 } 1404 1405 /** 1406 * Sets the bigtext for a notification for a single new conversation 1407 * 1408 * @param context 1409 * @param subject Subject of the new message that triggered the notification 1410 * @param message the {@link Message} to be displayed. 1411 * @return a {@link CharSequence} suitable for use in 1412 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1413 */ 1414 private static CharSequence getSingleMessageBigText(Context context, String subject, 1415 final Message message) { 1416 1417 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1418 context, R.style.NotificationPrimaryText); 1419 1420 final String snippet = getMessageBodyWithoutElidedText(message); 1421 1422 // Change multiple newlines (with potential white space between), into a single new line 1423 final String collapsedSnippet = 1424 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : ""; 1425 1426 if (TextUtils.isEmpty(subject)) { 1427 // If the subject is empty, just use the snippet. 1428 return snippet; 1429 } else if (TextUtils.isEmpty(collapsedSnippet)) { 1430 // If the snippet is empty, just use the subject. 1431 final SpannableString spannableString = new SpannableString(subject); 1432 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1433 1434 return spannableString; 1435 } else { 1436 final String notificationBigTextFormat = context.getResources().getString( 1437 R.string.single_new_message_notification_big_text); 1438 1439 // Localizers may change the order of the parameters, look at how the format 1440 // string is structured. 1441 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") > 1442 notificationBigTextFormat.indexOf("%1$s"); 1443 final String bigText = 1444 String.format(notificationBigTextFormat, subject, collapsedSnippet); 1445 final SpannableString spannableString = new SpannableString(bigText); 1446 1447 final int subjectOffset = 1448 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject)); 1449 spannableString.setSpan(notificationSubjectSpan, 1450 subjectOffset, subjectOffset + subject.length(), 0); 1451 1452 return spannableString; 1453 } 1454 } 1455 1456 /** 1457 * Gets the title for a notification for a single new conversation 1458 * @param context 1459 * @param sender Sender of the new message that triggered the notification. 1460 * @param subject Subject of the new message that triggered the notification 1461 * @return a {@link CharSequence} suitable for use as a {@link Notification} title. 1462 */ 1463 private static CharSequence getSingleMessageNotificationTitle(Context context, 1464 String sender, String subject) { 1465 1466 if (TextUtils.isEmpty(subject)) { 1467 // If the subject is empty, just set the title to the sender's information. 1468 return sender; 1469 } else { 1470 final String notificationTitleFormat = context.getResources().getString( 1471 R.string.single_new_message_notification_title); 1472 1473 // Localizers may change the order of the parameters, look at how the format 1474 // string is structured. 1475 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") > 1476 notificationTitleFormat.indexOf("%1$s"); 1477 final String titleString = String.format(notificationTitleFormat, sender, subject); 1478 1479 // Format the string so the subject is using the secondaryText style 1480 final SpannableString titleSpannable = new SpannableString(titleString); 1481 1482 // Find the offset of the subject. 1483 final int subjectOffset = 1484 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject); 1485 final TextAppearanceSpan notificationSubjectSpan = 1486 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1487 titleSpannable.setSpan(notificationSubjectSpan, 1488 subjectOffset, subjectOffset + subject.length(), 0); 1489 return titleSpannable; 1490 } 1491 } 1492 1493 /** 1494 * Clears the notifications for the specified account/folder. 1495 */ 1496 public static void clearFolderNotification(Context context, Account account, Folder folder, 1497 final boolean markSeen) { 1498 LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.getEmailAddress(), 1499 folder.name); 1500 final NotificationMap notificationMap = getNotificationMap(context); 1501 final NotificationKey key = new NotificationKey(account, folder); 1502 notificationMap.remove(key); 1503 notificationMap.saveNotificationMap(context); 1504 1505 final NotificationManagerCompat notificationManager = 1506 NotificationManagerCompat.from(context); 1507 notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder)); 1508 1509 cancelConversationNotifications(key, notificationManager); 1510 1511 if (markSeen) { 1512 markSeen(context, folder); 1513 } 1514 } 1515 1516 /** 1517 * Use content resolver to update a conversation. Should not be called from a main thread. 1518 */ 1519 public static void markConversationAsReadAndSeen(Context context, Uri conversationUri) { 1520 LogUtils.v(LOG_TAG, "markConversationAsReadAndSeen=%s", conversationUri); 1521 1522 final ContentValues values = new ContentValues(2); 1523 values.put(UIProvider.ConversationColumns.SEEN, Boolean.TRUE); 1524 values.put(UIProvider.ConversationColumns.READ, Boolean.TRUE); 1525 context.getContentResolver().update(conversationUri, values, null, null); 1526 } 1527 1528 /** 1529 * Clears all notifications for the specified account. 1530 */ 1531 public static void clearAccountNotifications(final Context context, 1532 final android.accounts.Account account) { 1533 LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account); 1534 final NotificationMap notificationMap = getNotificationMap(context); 1535 1536 // Find all NotificationKeys for this account 1537 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder(); 1538 1539 for (final NotificationKey key : notificationMap.keySet()) { 1540 if (account.equals(key.account.getAccountManagerAccount())) { 1541 keyBuilder.add(key); 1542 } 1543 } 1544 1545 final List<NotificationKey> notificationKeys = keyBuilder.build(); 1546 1547 final NotificationManagerCompat notificationManager = 1548 NotificationManagerCompat.from(context); 1549 1550 for (final NotificationKey notificationKey : notificationKeys) { 1551 final Folder folder = notificationKey.folder; 1552 notificationManager.cancel(getNotificationId(account, folder)); 1553 notificationMap.remove(notificationKey); 1554 1555 cancelConversationNotifications(notificationKey, notificationManager); 1556 } 1557 1558 notificationMap.saveNotificationMap(context); 1559 } 1560 1561 private static void cancelConversationNotifications(NotificationKey key, 1562 NotificationManagerCompat nm) { 1563 final Set<Integer> conversationNotifications = sConversationNotificationMap.get(key); 1564 if (conversationNotifications != null) { 1565 for (Integer conversationNotification : conversationNotifications) { 1566 nm.cancel(conversationNotification); 1567 } 1568 sConversationNotificationMap.remove(key); 1569 } 1570 } 1571 1572 private static ContactIconInfo getContactIcon(final Context context, String accountName, 1573 final String displayName, final String senderAddress, final Folder folder, 1574 final ContactPhotoFetcher photoFetcher) { 1575 if (Looper.myLooper() == Looper.getMainLooper()) { 1576 throw new IllegalStateException( 1577 "getContactIcon should not be called on the main thread."); 1578 } 1579 1580 final ContactIconInfo contactIconInfo; 1581 if (senderAddress == null) { 1582 contactIconInfo = new ContactIconInfo(); 1583 } else { 1584 // Get the ideal size for this icon. 1585 final Resources res = context.getResources(); 1586 final int idealIconHeight = 1587 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 1588 final int idealIconWidth = 1589 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 1590 final int idealWearableBgWidth = 1591 res.getDimensionPixelSize(R.dimen.wearable_background_width); 1592 final int idealWearableBgHeight = 1593 res.getDimensionPixelSize(R.dimen.wearable_background_height); 1594 1595 if (photoFetcher != null) { 1596 contactIconInfo = photoFetcher.getContactPhoto(context, accountName, 1597 senderAddress, idealIconWidth, idealIconHeight, idealWearableBgWidth, 1598 idealWearableBgHeight); 1599 } else { 1600 contactIconInfo = getContactInfo(context, senderAddress, idealIconWidth, 1601 idealIconHeight, idealWearableBgWidth, idealWearableBgHeight); 1602 } 1603 1604 if (contactIconInfo.icon == null) { 1605 // Make a colorful tile! 1606 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight, 1607 Dimensions.SCALE_ONE); 1608 1609 contactIconInfo.icon = new LetterTileProvider(context).getLetterTile(dimensions, 1610 displayName, senderAddress); 1611 } 1612 } 1613 1614 if (contactIconInfo.icon == null) { 1615 // Icon should be the default mail icon. 1616 contactIconInfo.icon = getDefaultNotificationIcon(context, folder, 1617 false /* single new message */); 1618 } 1619 1620 if (contactIconInfo.wearableBg == null) { 1621 contactIconInfo.wearableBg = getDefaultWearableBg(context); 1622 } 1623 1624 return contactIconInfo; 1625 } 1626 1627 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) { 1628 ArrayList<String> whereArgs = new ArrayList<String>(); 1629 StringBuilder whereBuilder = new StringBuilder(); 1630 String[] questionMarks = new String[addresses.size()]; 1631 1632 whereArgs.addAll(addresses); 1633 Arrays.fill(questionMarks, "?"); 1634 whereBuilder.append(Email.DATA1 + " IN ("). 1635 append(TextUtils.join(",", questionMarks)). 1636 append(")"); 1637 1638 ContentResolver resolver = context.getContentResolver(); 1639 Cursor c = resolver.query(Email.CONTENT_URI, 1640 new String[] {Email.CONTACT_ID}, whereBuilder.toString(), 1641 whereArgs.toArray(new String[0]), null); 1642 1643 ArrayList<Long> contactIds = new ArrayList<Long>(); 1644 if (c == null) { 1645 return contactIds; 1646 } 1647 try { 1648 while (c.moveToNext()) { 1649 contactIds.add(c.getLong(0)); 1650 } 1651 } finally { 1652 c.close(); 1653 } 1654 return contactIds; 1655 } 1656 1657 public static ContactIconInfo getContactInfo( 1658 final Context context, final String senderAddress, 1659 final int idealIconWidth, final int idealIconHeight, 1660 final int idealWearableBgWidth, final int idealWearableBgHeight) { 1661 final ContactIconInfo contactIconInfo = new ContactIconInfo(); 1662 final List<Long> contactIds = findContacts( context, Arrays.asList( 1663 new String[] { senderAddress })); 1664 1665 if (contactIds != null) { 1666 for (final long id : contactIds) { 1667 final Uri contactUri = ContentUris.withAppendedId( 1668 ContactsContract.Contacts.CONTENT_URI, id); 1669 final InputStream inputStream = 1670 ContactsContract.Contacts.openContactPhotoInputStream( 1671 context.getContentResolver(), contactUri, true /*preferHighres*/); 1672 1673 if (inputStream != null) { 1674 try { 1675 final Bitmap source = BitmapFactory.decodeStream(inputStream); 1676 if (source != null) { 1677 // We should scale this image to fit the intended size 1678 contactIconInfo.icon = Bitmap.createScaledBitmap(source, idealIconWidth, 1679 idealIconHeight, true); 1680 1681 contactIconInfo.wearableBg = Bitmap.createScaledBitmap(source, 1682 idealWearableBgWidth, idealWearableBgHeight, true); 1683 } 1684 1685 if (contactIconInfo.icon != null) { 1686 break; 1687 } 1688 } finally { 1689 Closeables.closeQuietly(inputStream); 1690 } 1691 } 1692 } 1693 } 1694 1695 return contactIconInfo; 1696 } 1697 1698 private static String getMessageBodyWithoutElidedText(final Message message) { 1699 return getMessageBodyWithoutElidedText(message.getBodyAsHtml()); 1700 } 1701 1702 public static String getMessageBodyWithoutElidedText(String html) { 1703 if (TextUtils.isEmpty(html)) { 1704 return ""; 1705 } 1706 // Get the html "tree" for this message body 1707 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html); 1708 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY); 1709 1710 return htmlTree.getPlainText(); 1711 } 1712 1713 public static void markSeen(final Context context, final Folder folder) { 1714 final Uri uri = folder.folderUri.fullUri; 1715 1716 final ContentValues values = new ContentValues(1); 1717 values.put(UIProvider.ConversationColumns.SEEN, 1); 1718 1719 context.getContentResolver().update(uri, values, null, null); 1720 } 1721 1722 /** 1723 * Returns a displayable string representing 1724 * the message sender. It has a preference toward showing the name, 1725 * but will fall back to the address if that is all that is available. 1726 */ 1727 private static String getDisplayableSender(String sender) { 1728 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1729 1730 String displayableSender = address.getName(); 1731 1732 if (!TextUtils.isEmpty(displayableSender)) { 1733 return Address.decodeAddressPersonal(displayableSender); 1734 } 1735 1736 // If that fails, default to the sender address. 1737 displayableSender = address.getAddress(); 1738 1739 // If we were unable to tokenize a name or address, 1740 // just use whatever was in the sender. 1741 if (TextUtils.isEmpty(displayableSender)) { 1742 displayableSender = sender; 1743 } 1744 return displayableSender; 1745 } 1746 1747 /** 1748 * Returns only the address portion of a message sender. 1749 */ 1750 private static String getSenderAddress(String sender) { 1751 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1752 1753 String tokenizedAddress = address.getAddress(); 1754 1755 // If we were unable to tokenize a name or address, 1756 // just use whatever was in the sender. 1757 if (TextUtils.isEmpty(tokenizedAddress)) { 1758 tokenizedAddress = sender; 1759 } 1760 return tokenizedAddress; 1761 } 1762 1763 public static int getNotificationId(final android.accounts.Account account, 1764 final Folder folder) { 1765 return 1 ^ account.hashCode() ^ folder.hashCode(); 1766 } 1767 1768 private static int getNotificationId(int summaryNotificationId, int conversationHashCode) { 1769 return summaryNotificationId ^ conversationHashCode; 1770 } 1771 1772 private static class NotificationKey { 1773 public final Account account; 1774 public final Folder folder; 1775 1776 public NotificationKey(Account account, Folder folder) { 1777 this.account = account; 1778 this.folder = folder; 1779 } 1780 1781 @Override 1782 public boolean equals(Object other) { 1783 if (!(other instanceof NotificationKey)) { 1784 return false; 1785 } 1786 NotificationKey key = (NotificationKey) other; 1787 return account.getAccountManagerAccount().equals(key.account.getAccountManagerAccount()) 1788 && folder.equals(key.folder); 1789 } 1790 1791 @Override 1792 public String toString() { 1793 return account.getDisplayName() + " " + folder.name; 1794 } 1795 1796 @Override 1797 public int hashCode() { 1798 final int accountHashCode = account.getAccountManagerAccount().hashCode(); 1799 final int folderHashCode = folder.hashCode(); 1800 return accountHashCode ^ folderHashCode; 1801 } 1802 } 1803 1804 /** 1805 * Contains the logic for converting the contents of one HtmlTree into 1806 * plaintext. 1807 */ 1808 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter { 1809 // Strings for parsing html message bodies 1810 private static final String ELIDED_TEXT_ELEMENT_NAME = "div"; 1811 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class"; 1812 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text"; 1813 1814 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE = 1815 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE); 1816 1817 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE = 1818 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null); 1819 1820 private int mEndNodeElidedTextBlock = -1; 1821 1822 @Override 1823 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) { 1824 // If we are in the middle of an elided text block, don't add this node 1825 if (nodeNum < mEndNodeElidedTextBlock) { 1826 return; 1827 } else if (nodeNum == mEndNodeElidedTextBlock) { 1828 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum); 1829 return; 1830 } 1831 1832 // If this tag starts another elided text block, we want to remember the end 1833 if (n instanceof HtmlDocument.Tag) { 1834 boolean foundElidedTextTag = false; 1835 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n; 1836 final HTML.Element htmlElement = htmlTag.getElement(); 1837 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) { 1838 // Make sure that the class is what is expected 1839 final List<HtmlDocument.TagAttribute> attributes = 1840 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE); 1841 for (HtmlDocument.TagAttribute attribute : attributes) { 1842 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals( 1843 attribute.getValue())) { 1844 // Found an "elided-text" div. Remember information about this tag 1845 mEndNodeElidedTextBlock = endNum; 1846 foundElidedTextTag = true; 1847 break; 1848 } 1849 } 1850 } 1851 1852 if (foundElidedTextTag) { 1853 return; 1854 } 1855 } 1856 1857 super.addNode(n, nodeNum, endNum); 1858 } 1859 } 1860 1861 /** 1862 * During account setup in Email, we may not have an inbox yet, so the notification setting had 1863 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the 1864 * {@link FolderPreferences} now. 1865 */ 1866 public static void moveNotificationSetting(final AccountPreferences accountPreferences, 1867 final FolderPreferences folderPreferences) { 1868 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) { 1869 // If this setting has been changed some other way, don't overwrite it 1870 if (!folderPreferences.isNotificationsEnabledSet()) { 1871 final boolean notificationsEnabled = 1872 accountPreferences.getDefaultInboxNotificationsEnabled(); 1873 1874 folderPreferences.setNotificationsEnabled(notificationsEnabled); 1875 } 1876 1877 accountPreferences.clearDefaultInboxNotificationsEnabled(); 1878 } 1879 } 1880 1881 private static class NotificationBuilders { 1882 public final NotificationCompat.Builder notifBuilder; 1883 public final NotificationCompat.WearableExtender wearableNotifBuilder; 1884 1885 private NotificationBuilders(NotificationCompat.Builder notifBuilder, 1886 NotificationCompat.WearableExtender wearableNotifBuilder) { 1887 this.notifBuilder = notifBuilder; 1888 this.wearableNotifBuilder = wearableNotifBuilder; 1889 } 1890 1891 public static NotificationBuilders of(NotificationCompat.Builder notifBuilder, 1892 NotificationCompat.WearableExtender wearableNotifBuilder) { 1893 return new NotificationBuilders(notifBuilder, wearableNotifBuilder); 1894 } 1895 } 1896 1897 private static class ConfigResult { 1898 public String notificationTicker; 1899 public ContactIconInfo contactIconInfo; 1900 } 1901 1902 public static class ContactIconInfo { 1903 public Bitmap icon; 1904 public Bitmap wearableBg; 1905 } 1906} 1907