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