NotificationUtils.java revision 9461561d3f0ea554138c3b24207a69306e07d4bc
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 child notification ids. 115 private static Map<NotificationKey, Set<Integer>> sChildNotificationsMap = 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 cancelChildNotifications(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 cancelChildNotifications(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 cancelChildNotifications(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 child notifications exist previously. Only notify if one of 718 // them is new. 719 boolean hasNewChildNotification; 720 Set<Integer> prevChildNotifications = sChildNotificationsMap.get(notificationKey); 721 if (prevChildNotifications != null) { 722 hasNewChildNotification = false; 723 for (Integer currentNotificationId : msgNotifications.keySet()) { 724 if (!prevChildNotifications.contains(currentNotificationId)) { 725 hasNewChildNotification = true; 726 break; 727 } 728 } 729 } else { 730 hasNewChildNotification = true; 731 } 732 733 LogUtils.d(LOG_TAG, "getAttention=%s,oldWhen=%s,hasNewChildNotification=%s", 734 getAttention, oldWhen, hasNewChildNotification); 735 736 /* 737 * We do not want to notify if this is coming back from an Undo notification, hence the 738 * oldWhen check. 739 */ 740 if (getAttention && oldWhen == 0 && hasNewChildNotification) { 741 final AccountPreferences accountPreferences = 742 new AccountPreferences(context, account.getEmailAddress()); 743 if (accountPreferences.areNotificationsEnabled()) { 744 if (vibrate) { 745 defaults |= Notification.DEFAULT_VIBRATE; 746 } 747 748 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null 749 : Uri.parse(ringtoneUri)); 750 LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s", 751 LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()), vibrate, 752 ringtoneUri); 753 } 754 } 755 756 // TODO(skennedy) Why do we do any of the above if we're just going to bail here? 757 if (eventInfoConfigured) { 758 defaults |= Notification.DEFAULT_LIGHTS; 759 notification.setDefaults(defaults); 760 761 if (oldWhen != 0) { 762 // We do not want to display the ticker again if we are re-displaying this 763 // notification (like from an Undo notification) 764 notification.setTicker(null); 765 } 766 767 notification.extend(wearableExtender); 768 nm.notify(notificationId, notification.build()); 769 770 if (prevChildNotifications != null) { 771 Set<Integer> currentNotificationIds = msgNotifications.keySet(); 772 for (Integer prevChildNotificationId : prevChildNotifications) { 773 if (!currentNotificationIds.contains(prevChildNotificationId)) { 774 nm.cancel(prevChildNotificationId); 775 LogUtils.d(LOG_TAG, "canceling child notification %s", 776 prevChildNotificationId); 777 } 778 } 779 } 780 781 for (Map.Entry<Integer, NotificationBuilders> entry : msgNotifications.entrySet()) { 782 NotificationBuilders builders = entry.getValue(); 783 builders.notifBuilder.extend(builders.wearableNotifBuilder); 784 nm.notify(entry.getKey(), builders.notifBuilder.build()); 785 LogUtils.d(LOG_TAG, "notifying child notification %s", entry.getKey()); 786 } 787 788 Set<Integer> childNotificationIds = new HashSet<Integer>(); 789 childNotificationIds.addAll(msgNotifications.keySet()); 790 sChildNotificationsMap.put(notificationKey, childNotificationIds); 791 } else { 792 LogUtils.i(LOG_TAG, "event info not configured - not notifying"); 793 } 794 } finally { 795 if (cursor != null) { 796 cursor.close(); 797 } 798 } 799 } 800 801 private static PendingIntent createClickPendingIntent(Context context, 802 Intent notificationIntent) { 803 // Amend the click intent with a hint that its source was a notification, 804 // but remove the hint before it's used to generate notification action 805 // intents. This prevents the following sequence: 806 // 1. generate single notification 807 // 2. user clicks reply, then completes Compose activity 808 // 3. main activity launches, gets FROM_NOTIFICATION hint in intent 809 notificationIntent.putExtra(Utils.EXTRA_FROM_NOTIFICATION, true); 810 PendingIntent clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 811 PendingIntent.FLAG_UPDATE_CURRENT); 812 notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION); 813 return clickIntent; 814 } 815 816 /** 817 * @return an {@link Intent} which, if launched, will display the corresponding conversation 818 */ 819 private static Intent createViewConversationIntent(final Context context, final Account account, 820 final Folder folder, final Cursor cursor) { 821 if (folder == null || account == null) { 822 LogUtils.e(LOG_TAG, "createViewConversationIntent(): " 823 + "Null account or folder. account: %s folder: %s", account, folder); 824 return null; 825 } 826 827 final Intent intent; 828 829 if (cursor == null) { 830 intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account); 831 } else { 832 // A conversation cursor has been specified, so this intent is intended to be go 833 // directly to the one new conversation 834 835 // Get the Conversation object 836 final Conversation conversation = new Conversation(cursor); 837 intent = Utils.createViewConversationIntent(context, conversation, 838 folder.folderUri.fullUri, account); 839 } 840 841 return intent; 842 } 843 844 private static Bitmap getDefaultNotificationIcon( 845 final Context context, final Folder folder, final boolean multipleNew) { 846 final int resId; 847 if (folder.notificationIconResId != 0) { 848 resId = folder.notificationIconResId; 849 } else if (multipleNew) { 850 resId = R.drawable.ic_notification_multiple_mail_holo_dark; 851 } else { 852 resId = R.drawable.ic_contact_picture; 853 } 854 855 final Bitmap icon = getIcon(context, resId); 856 857 if (icon == null) { 858 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId); 859 } 860 861 return icon; 862 } 863 864 private static Bitmap getIcon(final Context context, final int resId) { 865 final Bitmap cachedIcon = sNotificationIcons.get(resId); 866 if (cachedIcon != null) { 867 return cachedIcon; 868 } 869 870 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId); 871 sNotificationIcons.put(resId, icon); 872 873 return icon; 874 } 875 876 private static Bitmap getDefaultWearableBg(Context context) { 877 Bitmap bg = sDefaultWearableBg.get(); 878 if (bg == null) { 879 bg = BitmapFactory.decodeResource(context.getResources(), R.drawable.bg_email); 880 sDefaultWearableBg = new WeakReference<Bitmap>(bg); 881 } 882 return bg; 883 } 884 885 private static void configureLatestEventInfoFromConversation(final Context context, 886 final Account account, final FolderPreferences folderPreferences, 887 final NotificationCompat.Builder notification, 888 final NotificationCompat.WearableExtender wearableExtender, 889 final Map<Integer, NotificationBuilders> msgNotifications, 890 final int summaryNotificationId, final Cursor conversationCursor, 891 final PendingIntent clickIntent, final Intent notificationIntent, 892 final int unreadCount, final int unseenCount, 893 final Folder folder, final long when, final ContactPhotoFetcher photoFetcher) { 894 final Resources res = context.getResources(); 895 final String notificationAccountDisplayName = account.getDisplayName(); 896 final String notificationAccountEmail = account.getEmailAddress(); 897 898 LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d", 899 unreadCount, unseenCount); 900 901 String notificationTicker = null; 902 903 // Boolean indicating that this notification is for a non-inbox label. 904 final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox); 905 906 // Notification label name for user label notifications. 907 final String notificationLabelName = isInbox ? null : folder.name; 908 909 if (unseenCount > 1) { 910 // Build the string that describes the number of new messages 911 final String newMessagesString = res.getString(R.string.new_messages, unseenCount); 912 913 // Use the default notification icon 914 notification.setLargeIcon( 915 getDefaultNotificationIcon(context, folder, true /* multiple new messages */)); 916 917 // The ticker initially start as the new messages string. 918 notificationTicker = newMessagesString; 919 920 // The title of the notification is the new messages string 921 notification.setContentTitle(newMessagesString); 922 923 // TODO(skennedy) Can we remove this check? 924 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) { 925 // For a new-style notification 926 final int maxNumDigestItems = context.getResources().getInteger( 927 R.integer.max_num_notification_digest_items); 928 929 // The body of the notification is the account name, or the label name. 930 notification.setSubText( 931 isInbox ? notificationAccountDisplayName : notificationLabelName); 932 933 final NotificationCompat.InboxStyle digest = 934 new NotificationCompat.InboxStyle(notification); 935 936 // Group by account. 937 String notificationGroupKey = 938 account.uri.toString() + "/" + folder.folderUri.fullUri; 939 notification.setGroup(notificationGroupKey).setGroupSummary(true); 940 941 ConfigResult firstResult = null; 942 int numDigestItems = 0; 943 do { 944 final Conversation conversation = new Conversation(conversationCursor); 945 946 if (!conversation.read) { 947 boolean multipleUnreadThread = false; 948 // TODO(cwren) extract this pattern into a helper 949 950 Cursor cursor = null; 951 MessageCursor messageCursor = null; 952 try { 953 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon(); 954 uriBuilder.appendQueryParameter( 955 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName); 956 cursor = context.getContentResolver().query(uriBuilder.build(), 957 UIProvider.MESSAGE_PROJECTION, null, null, null); 958 messageCursor = new MessageCursor(cursor); 959 960 String from = ""; 961 String fromAddress = ""; 962 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) { 963 final Message message = messageCursor.getMessage(); 964 fromAddress = message.getFrom(); 965 if (fromAddress == null) { 966 fromAddress = ""; 967 } 968 from = getDisplayableSender(fromAddress); 969 } 970 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) { 971 final Message message = messageCursor.getMessage(); 972 if (!message.read && 973 !fromAddress.contentEquals(message.getFrom())) { 974 multipleUnreadThread = true; 975 break; 976 } 977 } 978 final SpannableStringBuilder sendersBuilder; 979 if (multipleUnreadThread) { 980 final int sendersLength = 981 res.getInteger(R.integer.swipe_senders_length); 982 983 sendersBuilder = getStyledSenders(context, conversationCursor, 984 sendersLength, notificationAccountEmail); 985 } else { 986 sendersBuilder = 987 new SpannableStringBuilder(getWrappedFromString(from)); 988 } 989 final CharSequence digestLine = getSingleMessageInboxLine(context, 990 sendersBuilder.toString(), 991 ConversationItemView.filterTag(context, conversation.subject), 992 conversation.getSnippet()); 993 digest.addLine(digestLine); 994 numDigestItems++; 995 996 // Adding child notification for Wear. 997 NotificationCompat.Builder childNotif = 998 new NotificationCompat.Builder(context); 999 childNotif.setSmallIcon(R.drawable.stat_notify_email); 1000 childNotif.setContentText(digestLine); 1001 Intent childNotificationIntent = createViewConversationIntent(context, 1002 account, folder, conversationCursor); 1003 PendingIntent childClickIntent = createClickPendingIntent(context, 1004 childNotificationIntent); 1005 childNotif.setContentIntent(childClickIntent); 1006 childNotif.setAutoCancel(true); 1007 1008 // TODO: Use a stable sort key if possible, e.g. message post time 1009 // + msgid hash 1010 String groupSortKey = String.format("%010d", numDigestItems); 1011 childNotif.setGroup(notificationGroupKey); 1012 childNotif.setSortKey(groupSortKey); 1013 1014 int childNotificationId = getNotificationId(summaryNotificationId, 1015 conversation.hashCode()); 1016 1017 final NotificationCompat.WearableExtender childWearExtender = 1018 new NotificationCompat.WearableExtender(); 1019 final ConfigResult result = 1020 configureNotifForOneConversation(context, account, 1021 folderPreferences, childNotif, childWearExtender, 1022 conversationCursor, notificationIntent, folder, when, res, 1023 notificationAccountDisplayName, notificationAccountEmail, 1024 isInbox, notificationLabelName, childNotificationId, 1025 photoFetcher); 1026 msgNotifications.put(childNotificationId, 1027 NotificationBuilders.of(childNotif, childWearExtender)); 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 if (fromAddress == null) { 1126 // No sender. Go back to default value. 1127 LogUtils.e(LOG_TAG, "No sender found for message: %d" + message.getId()); 1128 fromAddress = ""; 1129 } 1130 from = getDisplayableSender(fromAddress); 1131 result.contactIconInfo = getContactIcon( 1132 context, account.getAccountManagerAccount().name, from, 1133 getSenderAddress(fromAddress), folder, photoFetcher); 1134 notification.setLargeIcon(result.contactIconInfo.icon); 1135 } 1136 1137 // Assume that the last message in this conversation is unread 1138 int firstUnseenMessagePos = messageCursor.getPosition(); 1139 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) { 1140 final Message message = messageCursor.getMessage(); 1141 final boolean unseen = !message.seen; 1142 if (unseen) { 1143 firstUnseenMessagePos = messageCursor.getPosition(); 1144 if (!multipleUnseenThread 1145 && !fromAddress.contentEquals(message.getFrom())) { 1146 multipleUnseenThread = true; 1147 } 1148 } 1149 } 1150 1151 final String subject = ConversationItemView.filterTag(context, conversation.subject); 1152 1153 // TODO(skennedy) Can we remove this check? 1154 if (Utils.isRunningJellybeanOrLater()) { 1155 // For a new-style notification 1156 1157 if (multipleUnseenThread) { 1158 // The title of a single conversation is the list of senders. 1159 int sendersLength = res.getInteger(R.integer.swipe_senders_length); 1160 1161 final SpannableStringBuilder sendersBuilder = getStyledSenders( 1162 context, conversationCursor, sendersLength, 1163 notificationAccountEmail); 1164 1165 notification.setContentTitle(sendersBuilder); 1166 // For a single new conversation, the ticker is based on the sender's name. 1167 result.notificationTicker = sendersBuilder.toString(); 1168 } else { 1169 from = getWrappedFromString(from); 1170 // The title of a single message the sender. 1171 notification.setContentTitle(from); 1172 // For a single new conversation, the ticker is based on the sender's name. 1173 result.notificationTicker = from; 1174 } 1175 1176 // The notification content will be the subject of the conversation. 1177 notification.setContentText(getSingleMessageLittleText(context, subject)); 1178 1179 // The notification subtext will be the subject of the conversation for inbox 1180 // notifications, or will based on the the label name for user label 1181 // notifications. 1182 notification.setSubText(isInbox ? 1183 notificationAccountDisplayName : notificationLabelName); 1184 1185 if (multipleUnseenThread) { 1186 notification.setLargeIcon( 1187 getDefaultNotificationIcon(context, folder, true)); 1188 } 1189 final NotificationCompat.BigTextStyle bigText = 1190 new NotificationCompat.BigTextStyle(notification); 1191 1192 // Seek the message cursor to the first unread message 1193 final Message message; 1194 if (messageCursor.moveToPosition(firstUnseenMessagePos)) { 1195 message = messageCursor.getMessage(); 1196 bigText.bigText(getSingleMessageBigText(context, subject, message)); 1197 } else { 1198 LogUtils.e(LOG_TAG, "Failed to load message"); 1199 message = null; 1200 } 1201 1202 if (message != null) { 1203 final Set<String> notificationActions = 1204 folderPreferences.getNotificationActions(account); 1205 1206 NotificationActionUtils.addNotificationActions(context, notificationIntent, 1207 notification, wearExtender, account, conversation, message, 1208 folder, notificationId, when, notificationActions); 1209 } 1210 } else { 1211 // For an old-style notification 1212 1213 // The title of a single conversation notification is built from both the sender 1214 // and subject of the new message. 1215 notification.setContentTitle( 1216 getSingleMessageNotificationTitle(context, from, subject)); 1217 1218 // The notification content will be the subject of the conversation for inbox 1219 // notifications, or will based on the the label name for user label 1220 // notifications. 1221 notification.setContentText( 1222 isInbox ? notificationAccountDisplayName : notificationLabelName); 1223 1224 // For a single new conversation, the ticker is based on the sender's name. 1225 result.notificationTicker = from; 1226 } 1227 } finally { 1228 if (messageCursor != null) { 1229 messageCursor.close(); 1230 } 1231 if (cursor != null) { 1232 cursor.close(); 1233 } 1234 } 1235 return result; 1236 } 1237 1238 private static String getWrappedFromString(String from) { 1239 if (from == null) { 1240 LogUtils.e(LOG_TAG, "null from string in getWrappedFromString"); 1241 from = ""; 1242 } 1243 from = sBidiFormatter.unicodeWrap(from); 1244 return from; 1245 } 1246 1247 private static SpannableStringBuilder getStyledSenders(final Context context, 1248 final Cursor conversationCursor, final int maxLength, final String account) { 1249 final Conversation conversation = new Conversation(conversationCursor); 1250 final com.android.mail.providers.ConversationInfo conversationInfo = 1251 conversation.conversationInfo; 1252 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>(); 1253 if (sNotificationUnreadStyleSpan == null) { 1254 sNotificationUnreadStyleSpan = new TextAppearanceSpan( 1255 context, R.style.NotificationSendersUnreadTextAppearance); 1256 sNotificationReadStyleSpan = 1257 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance); 1258 } 1259 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account, 1260 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, 1261 false /* showToHeader */, false /* resourceCachingRequired */); 1262 1263 return ellipsizeStyledSenders(context, senders); 1264 } 1265 1266 private static String sSendersSplitToken = null; 1267 private static String sElidedPaddingToken = null; 1268 1269 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context, 1270 ArrayList<SpannableString> styledSenders) { 1271 if (sSendersSplitToken == null) { 1272 sSendersSplitToken = context.getString(R.string.senders_split_token); 1273 sElidedPaddingToken = context.getString(R.string.elided_padding_token); 1274 } 1275 1276 SpannableStringBuilder builder = new SpannableStringBuilder(); 1277 SpannableString prevSender = null; 1278 for (SpannableString sender : styledSenders) { 1279 if (sender == null) { 1280 LogUtils.e(LOG_TAG, "null sender iterating over styledSenders"); 1281 continue; 1282 } 1283 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class); 1284 if (SendersView.sElidedString.equals(sender.toString())) { 1285 prevSender = sender; 1286 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken); 1287 } else if (builder.length() > 0 1288 && (prevSender == null || !SendersView.sElidedString.equals(prevSender 1289 .toString()))) { 1290 prevSender = sender; 1291 sender = copyStyles(spans, sSendersSplitToken + sender); 1292 } else { 1293 prevSender = sender; 1294 } 1295 builder.append(sender); 1296 } 1297 return builder; 1298 } 1299 1300 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) { 1301 SpannableString s = new SpannableString(newText); 1302 if (spans != null && spans.length > 0) { 1303 s.setSpan(spans[0], 0, s.length(), 0); 1304 } 1305 return s; 1306 } 1307 1308 /** 1309 * Seeks the cursor to the position of the most recent unread conversation. If no unread 1310 * conversation is found, the position of the cursor will be restored, and false will be 1311 * returned. 1312 */ 1313 private static boolean seekToLatestUnreadConversation(final Cursor cursor) { 1314 final int initialPosition = cursor.getPosition(); 1315 do { 1316 final Conversation conversation = new Conversation(cursor); 1317 if (!conversation.read) { 1318 return true; 1319 } 1320 } while (cursor.moveToNext()); 1321 1322 // Didn't find an unread conversation, reset the position. 1323 cursor.moveToPosition(initialPosition); 1324 return false; 1325 } 1326 1327 /** 1328 * Sets the bigtext for a notification for a single new conversation 1329 * 1330 * @param context 1331 * @param senders Sender of the new message that triggered the notification. 1332 * @param subject Subject of the new message that triggered the notification 1333 * @param snippet Snippet of the new message that triggered the notification 1334 * @return a {@link CharSequence} suitable for use in 1335 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1336 */ 1337 private static CharSequence getSingleMessageInboxLine(Context context, 1338 String senders, String subject, String snippet) { 1339 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText 1340 1341 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet; 1342 1343 final TextAppearanceSpan notificationPrimarySpan = 1344 new TextAppearanceSpan(context, R.style.NotificationPrimaryText); 1345 1346 if (TextUtils.isEmpty(senders)) { 1347 // If the senders are empty, just use the subject/snippet. 1348 return subjectSnippet; 1349 } else if (TextUtils.isEmpty(subjectSnippet)) { 1350 // If the subject/snippet is empty, just use the senders. 1351 final SpannableString spannableString = new SpannableString(senders); 1352 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0); 1353 1354 return spannableString; 1355 } else { 1356 final String formatString = context.getResources().getString( 1357 R.string.multiple_new_message_notification_item); 1358 final TextAppearanceSpan notificationSecondarySpan = 1359 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1360 1361 // senders is already individually unicode wrapped so it does not need to be done here 1362 final String instantiatedString = String.format(formatString, 1363 senders, 1364 sBidiFormatter.unicodeWrap(subjectSnippet)); 1365 1366 final SpannableString spannableString = new SpannableString(instantiatedString); 1367 1368 final boolean isOrderReversed = formatString.indexOf("%2$s") < 1369 formatString.indexOf("%1$s"); 1370 final int primaryOffset = 1371 (isOrderReversed ? instantiatedString.lastIndexOf(senders) : 1372 instantiatedString.indexOf(senders)); 1373 final int secondaryOffset = 1374 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) : 1375 instantiatedString.indexOf(subjectSnippet)); 1376 spannableString.setSpan(notificationPrimarySpan, 1377 primaryOffset, primaryOffset + senders.length(), 0); 1378 spannableString.setSpan(notificationSecondarySpan, 1379 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0); 1380 return spannableString; 1381 } 1382 } 1383 1384 /** 1385 * Sets the bigtext for a notification for a single new conversation 1386 * @param context 1387 * @param subject Subject of the new message that triggered the notification 1388 * @return a {@link CharSequence} suitable for use in 1389 * {@link NotificationCompat.Builder#setContentText} 1390 */ 1391 private static CharSequence getSingleMessageLittleText(Context context, String subject) { 1392 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1393 context, R.style.NotificationPrimaryText); 1394 1395 final SpannableString spannableString = new SpannableString(subject); 1396 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1397 1398 return spannableString; 1399 } 1400 1401 /** 1402 * Sets the bigtext for a notification for a single new conversation 1403 * 1404 * @param context 1405 * @param subject Subject of the new message that triggered the notification 1406 * @param message the {@link Message} to be displayed. 1407 * @return a {@link CharSequence} suitable for use in 1408 * {@link android.support.v4.app.NotificationCompat.BigTextStyle} 1409 */ 1410 private static CharSequence getSingleMessageBigText(Context context, String subject, 1411 final Message message) { 1412 1413 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan( 1414 context, R.style.NotificationPrimaryText); 1415 1416 final String snippet = getMessageBodyWithoutElidedText(message); 1417 1418 // Change multiple newlines (with potential white space between), into a single new line 1419 final String collapsedSnippet = 1420 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : ""; 1421 1422 if (TextUtils.isEmpty(subject)) { 1423 // If the subject is empty, just use the snippet. 1424 return snippet; 1425 } else if (TextUtils.isEmpty(collapsedSnippet)) { 1426 // If the snippet is empty, just use the subject. 1427 final SpannableString spannableString = new SpannableString(subject); 1428 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0); 1429 1430 return spannableString; 1431 } else { 1432 final String notificationBigTextFormat = context.getResources().getString( 1433 R.string.single_new_message_notification_big_text); 1434 1435 // Localizers may change the order of the parameters, look at how the format 1436 // string is structured. 1437 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") > 1438 notificationBigTextFormat.indexOf("%1$s"); 1439 final String bigText = 1440 String.format(notificationBigTextFormat, subject, collapsedSnippet); 1441 final SpannableString spannableString = new SpannableString(bigText); 1442 1443 final int subjectOffset = 1444 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject)); 1445 spannableString.setSpan(notificationSubjectSpan, 1446 subjectOffset, subjectOffset + subject.length(), 0); 1447 1448 return spannableString; 1449 } 1450 } 1451 1452 /** 1453 * Gets the title for a notification for a single new conversation 1454 * @param context 1455 * @param sender Sender of the new message that triggered the notification. 1456 * @param subject Subject of the new message that triggered the notification 1457 * @return a {@link CharSequence} suitable for use as a {@link Notification} title. 1458 */ 1459 private static CharSequence getSingleMessageNotificationTitle(Context context, 1460 String sender, String subject) { 1461 1462 if (TextUtils.isEmpty(subject)) { 1463 // If the subject is empty, just set the title to the sender's information. 1464 return sender; 1465 } else { 1466 final String notificationTitleFormat = context.getResources().getString( 1467 R.string.single_new_message_notification_title); 1468 1469 // Localizers may change the order of the parameters, look at how the format 1470 // string is structured. 1471 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") > 1472 notificationTitleFormat.indexOf("%1$s"); 1473 final String titleString = String.format(notificationTitleFormat, sender, subject); 1474 1475 // Format the string so the subject is using the secondaryText style 1476 final SpannableString titleSpannable = new SpannableString(titleString); 1477 1478 // Find the offset of the subject. 1479 final int subjectOffset = 1480 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject); 1481 final TextAppearanceSpan notificationSubjectSpan = 1482 new TextAppearanceSpan(context, R.style.NotificationSecondaryText); 1483 titleSpannable.setSpan(notificationSubjectSpan, 1484 subjectOffset, subjectOffset + subject.length(), 0); 1485 return titleSpannable; 1486 } 1487 } 1488 1489 /** 1490 * Clears the notifications for the specified account/folder. 1491 */ 1492 public static void clearFolderNotification(Context context, Account account, Folder folder, 1493 final boolean markSeen) { 1494 LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.getEmailAddress(), 1495 folder.name); 1496 final NotificationMap notificationMap = getNotificationMap(context); 1497 final NotificationKey key = new NotificationKey(account, folder); 1498 notificationMap.remove(key); 1499 notificationMap.saveNotificationMap(context); 1500 1501 final NotificationManagerCompat notificationManager = 1502 NotificationManagerCompat.from(context); 1503 notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder)); 1504 1505 cancelChildNotifications(key, notificationManager); 1506 1507 if (markSeen) { 1508 markSeen(context, folder); 1509 } 1510 } 1511 1512 /** 1513 * Clears all notifications for the specified account. 1514 */ 1515 public static void clearAccountNotifications(final Context context, 1516 final android.accounts.Account account) { 1517 LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account); 1518 final NotificationMap notificationMap = getNotificationMap(context); 1519 1520 // Find all NotificationKeys for this account 1521 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder(); 1522 1523 for (final NotificationKey key : notificationMap.keySet()) { 1524 if (account.equals(key.account.getAccountManagerAccount())) { 1525 keyBuilder.add(key); 1526 } 1527 } 1528 1529 final List<NotificationKey> notificationKeys = keyBuilder.build(); 1530 1531 final NotificationManagerCompat notificationManager = 1532 NotificationManagerCompat.from(context); 1533 1534 for (final NotificationKey notificationKey : notificationKeys) { 1535 final Folder folder = notificationKey.folder; 1536 notificationManager.cancel(getNotificationId(account, folder)); 1537 notificationMap.remove(notificationKey); 1538 1539 cancelChildNotifications(notificationKey, notificationManager); 1540 } 1541 1542 notificationMap.saveNotificationMap(context); 1543 } 1544 1545 private static void cancelChildNotifications(NotificationKey key, 1546 NotificationManagerCompat nm) { 1547 Set<Integer> childNotifications = sChildNotificationsMap.get(key); 1548 if (childNotifications != null) { 1549 for (Integer childNotification : childNotifications) { 1550 nm.cancel(childNotification); 1551 } 1552 sChildNotificationsMap.remove(key); 1553 } 1554 } 1555 1556 private static ContactIconInfo getContactIcon(final Context context, String accountName, 1557 final String displayName, final String senderAddress, final Folder folder, 1558 final ContactPhotoFetcher photoFetcher) { 1559 1560 if (senderAddress == null) { 1561 return null; 1562 } 1563 1564 if (Looper.myLooper() == Looper.getMainLooper()) { 1565 throw new IllegalStateException( 1566 "getContactIcon should not be called on the main thread."); 1567 } 1568 1569 // Get the ideal size for this icon. 1570 final Resources res = context.getResources(); 1571 final int idealIconHeight = 1572 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 1573 final int idealIconWidth = 1574 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width); 1575 final int idealWearableBgWidth = 1576 res.getDimensionPixelSize(R.dimen.wearable_background_width); 1577 final int idealWearableBgHeight = 1578 res.getDimensionPixelSize(R.dimen.wearable_background_height); 1579 1580 final ContactIconInfo contactIconInfo = 1581 photoFetcher != null ? photoFetcher.getContactPhoto( 1582 context, accountName, senderAddress,idealIconWidth, idealIconHeight, 1583 idealWearableBgWidth, idealWearableBgHeight) : 1584 getContactInfo(context, senderAddress, idealIconWidth, idealIconHeight, 1585 idealWearableBgWidth, idealWearableBgHeight); 1586 1587 if (contactIconInfo.icon == null) { 1588 // Make a colorful tile! 1589 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight, 1590 Dimensions.SCALE_ONE); 1591 1592 contactIconInfo.icon = new LetterTileProvider(context).getLetterTile(dimensions, 1593 displayName, senderAddress); 1594 } 1595 1596 if (contactIconInfo.icon == null) { 1597 // Icon should be the default mail icon. 1598 contactIconInfo.icon = getDefaultNotificationIcon(context, folder, 1599 false /* single new message */); 1600 } 1601 1602 if (contactIconInfo.wearableBg == null) { 1603 contactIconInfo.wearableBg = getDefaultWearableBg(context); 1604 } 1605 1606 return contactIconInfo; 1607 } 1608 1609 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) { 1610 ArrayList<String> whereArgs = new ArrayList<String>(); 1611 StringBuilder whereBuilder = new StringBuilder(); 1612 String[] questionMarks = new String[addresses.size()]; 1613 1614 whereArgs.addAll(addresses); 1615 Arrays.fill(questionMarks, "?"); 1616 whereBuilder.append(Email.DATA1 + " IN ("). 1617 append(TextUtils.join(",", questionMarks)). 1618 append(")"); 1619 1620 ContentResolver resolver = context.getContentResolver(); 1621 Cursor c = resolver.query(Email.CONTENT_URI, 1622 new String[]{Email.CONTACT_ID}, whereBuilder.toString(), 1623 whereArgs.toArray(new String[0]), null); 1624 1625 ArrayList<Long> contactIds = new ArrayList<Long>(); 1626 if (c == null) { 1627 return contactIds; 1628 } 1629 try { 1630 while (c.moveToNext()) { 1631 contactIds.add(c.getLong(0)); 1632 } 1633 } finally { 1634 c.close(); 1635 } 1636 return contactIds; 1637 } 1638 1639 public static ContactIconInfo getContactInfo( 1640 final Context context, final String senderAddress, 1641 final int idealIconWidth, final int idealIconHeight, 1642 final int idealWearableBgWidth, final int idealWearableBgHeight) { 1643 final ContactIconInfo contactIconInfo = new ContactIconInfo(); 1644 final List<Long> contactIds = findContacts( context, Arrays.asList( 1645 new String[] { senderAddress })); 1646 1647 if (contactIds != null) { 1648 for (final long id : contactIds) { 1649 final Uri contactUri = ContentUris.withAppendedId( 1650 ContactsContract.Contacts.CONTENT_URI, id); 1651 final InputStream inputStream = 1652 ContactsContract.Contacts.openContactPhotoInputStream( 1653 context.getContentResolver(), contactUri, true /*preferHighres*/); 1654 1655 if (inputStream != null) { 1656 try { 1657 final Bitmap source = BitmapFactory.decodeStream(inputStream); 1658 if (source != null) { 1659 // We should scale this image to fit the intended size 1660 contactIconInfo.icon = Bitmap.createScaledBitmap(source, idealIconWidth, 1661 idealIconHeight, true); 1662 1663 contactIconInfo.wearableBg = Bitmap.createScaledBitmap(source, 1664 idealWearableBgWidth, idealWearableBgHeight, true); 1665 } 1666 1667 if (contactIconInfo.icon != null) { 1668 break; 1669 } 1670 } finally { 1671 Closeables.closeQuietly(inputStream); 1672 } 1673 } 1674 } 1675 } 1676 1677 return contactIconInfo; 1678 } 1679 1680 private static String getMessageBodyWithoutElidedText(final Message message) { 1681 return getMessageBodyWithoutElidedText(message.getBodyAsHtml()); 1682 } 1683 1684 public static String getMessageBodyWithoutElidedText(String html) { 1685 if (TextUtils.isEmpty(html)) { 1686 return ""; 1687 } 1688 // Get the html "tree" for this message body 1689 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html); 1690 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY); 1691 1692 return htmlTree.getPlainText(); 1693 } 1694 1695 public static void markSeen(final Context context, final Folder folder) { 1696 final Uri uri = folder.folderUri.fullUri; 1697 1698 final ContentValues values = new ContentValues(1); 1699 values.put(UIProvider.ConversationColumns.SEEN, 1); 1700 1701 context.getContentResolver().update(uri, values, null, null); 1702 } 1703 1704 /** 1705 * Returns a displayable string representing 1706 * the message sender. It has a preference toward showing the name, 1707 * but will fall back to the address if that is all that is available. 1708 */ 1709 private static String getDisplayableSender(String sender) { 1710 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1711 1712 String displayableSender = address.getName(); 1713 1714 if (!TextUtils.isEmpty(displayableSender)) { 1715 return Address.decodeAddressPersonal(displayableSender); 1716 } 1717 1718 // If that fails, default to the sender address. 1719 displayableSender = address.getAddress(); 1720 1721 // If we were unable to tokenize a name or address, 1722 // just use whatever was in the sender. 1723 if (TextUtils.isEmpty(displayableSender)) { 1724 displayableSender = sender; 1725 } 1726 return displayableSender; 1727 } 1728 1729 /** 1730 * Returns only the address portion of a message sender. 1731 */ 1732 private static String getSenderAddress(String sender) { 1733 final EmailAddress address = EmailAddress.getEmailAddress(sender); 1734 1735 String tokenizedAddress = address.getAddress(); 1736 1737 // If we were unable to tokenize a name or address, 1738 // just use whatever was in the sender. 1739 if (TextUtils.isEmpty(tokenizedAddress)) { 1740 tokenizedAddress = sender; 1741 } 1742 return tokenizedAddress; 1743 } 1744 1745 public static int getNotificationId(final android.accounts.Account account, 1746 final Folder folder) { 1747 return 1 ^ account.hashCode() ^ folder.hashCode(); 1748 } 1749 1750 private static int getNotificationId(int summaryNotificationId, int childHashCode) { 1751 return summaryNotificationId ^ childHashCode; 1752 } 1753 1754 private static class NotificationKey { 1755 public final Account account; 1756 public final Folder folder; 1757 1758 public NotificationKey(Account account, Folder folder) { 1759 this.account = account; 1760 this.folder = folder; 1761 } 1762 1763 @Override 1764 public boolean equals(Object other) { 1765 if (!(other instanceof NotificationKey)) { 1766 return false; 1767 } 1768 NotificationKey key = (NotificationKey) other; 1769 return account.getAccountManagerAccount().equals(key.account.getAccountManagerAccount()) 1770 && folder.equals(key.folder); 1771 } 1772 1773 @Override 1774 public String toString() { 1775 return account.getDisplayName() + " " + folder.name; 1776 } 1777 1778 @Override 1779 public int hashCode() { 1780 final int accountHashCode = account.getAccountManagerAccount().hashCode(); 1781 final int folderHashCode = folder.hashCode(); 1782 return accountHashCode ^ folderHashCode; 1783 } 1784 } 1785 1786 /** 1787 * Contains the logic for converting the contents of one HtmlTree into 1788 * plaintext. 1789 */ 1790 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter { 1791 // Strings for parsing html message bodies 1792 private static final String ELIDED_TEXT_ELEMENT_NAME = "div"; 1793 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class"; 1794 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text"; 1795 1796 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE = 1797 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE); 1798 1799 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE = 1800 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null); 1801 1802 private int mEndNodeElidedTextBlock = -1; 1803 1804 @Override 1805 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) { 1806 // If we are in the middle of an elided text block, don't add this node 1807 if (nodeNum < mEndNodeElidedTextBlock) { 1808 return; 1809 } else if (nodeNum == mEndNodeElidedTextBlock) { 1810 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum); 1811 return; 1812 } 1813 1814 // If this tag starts another elided text block, we want to remember the end 1815 if (n instanceof HtmlDocument.Tag) { 1816 boolean foundElidedTextTag = false; 1817 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n; 1818 final HTML.Element htmlElement = htmlTag.getElement(); 1819 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) { 1820 // Make sure that the class is what is expected 1821 final List<HtmlDocument.TagAttribute> attributes = 1822 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE); 1823 for (HtmlDocument.TagAttribute attribute : attributes) { 1824 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals( 1825 attribute.getValue())) { 1826 // Found an "elided-text" div. Remember information about this tag 1827 mEndNodeElidedTextBlock = endNum; 1828 foundElidedTextTag = true; 1829 break; 1830 } 1831 } 1832 } 1833 1834 if (foundElidedTextTag) { 1835 return; 1836 } 1837 } 1838 1839 super.addNode(n, nodeNum, endNum); 1840 } 1841 } 1842 1843 /** 1844 * During account setup in Email, we may not have an inbox yet, so the notification setting had 1845 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the 1846 * {@link FolderPreferences} now. 1847 */ 1848 public static void moveNotificationSetting(final AccountPreferences accountPreferences, 1849 final FolderPreferences folderPreferences) { 1850 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) { 1851 // If this setting has been changed some other way, don't overwrite it 1852 if (!folderPreferences.isNotificationsEnabledSet()) { 1853 final boolean notificationsEnabled = 1854 accountPreferences.getDefaultInboxNotificationsEnabled(); 1855 1856 folderPreferences.setNotificationsEnabled(notificationsEnabled); 1857 } 1858 1859 accountPreferences.clearDefaultInboxNotificationsEnabled(); 1860 } 1861 } 1862 1863 private static class NotificationBuilders { 1864 public final NotificationCompat.Builder notifBuilder; 1865 public final NotificationCompat.WearableExtender wearableNotifBuilder; 1866 1867 private NotificationBuilders(NotificationCompat.Builder notifBuilder, 1868 NotificationCompat.WearableExtender wearableNotifBuilder) { 1869 this.notifBuilder = notifBuilder; 1870 this.wearableNotifBuilder = wearableNotifBuilder; 1871 } 1872 1873 public static NotificationBuilders of(NotificationCompat.Builder notifBuilder, 1874 NotificationCompat.WearableExtender wearableNotifBuilder) { 1875 return new NotificationBuilders(notifBuilder, wearableNotifBuilder); 1876 } 1877 } 1878 1879 private static class ConfigResult { 1880 public String notificationTicker; 1881 public ContactIconInfo contactIconInfo; 1882 } 1883 1884 public static class ContactIconInfo { 1885 public Bitmap icon; 1886 public Bitmap wearableBg; 1887 } 1888} 1889