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