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