1 /* 2 * Copyright (C) 2015 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 */ 16 package com.android.messaging.datamodel; 17 18 import android.app.Notification; 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.database.Cursor; 23 import android.graphics.Typeface; 24 import android.net.Uri; 25 import androidx.core.app.NotificationCompat; 26 import androidx.core.app.NotificationCompat.Builder; 27 import androidx.core.app.NotificationCompat.WearableExtender; 28 import androidx.core.app.NotificationManagerCompat; 29 import android.text.Html; 30 import android.text.Spannable; 31 import android.text.SpannableString; 32 import android.text.SpannableStringBuilder; 33 import android.text.Spanned; 34 import android.text.TextUtils; 35 import android.text.style.ForegroundColorSpan; 36 import android.text.style.StyleSpan; 37 import android.text.style.TextAppearanceSpan; 38 import android.text.style.URLSpan; 39 40 import com.android.messaging.Factory; 41 import com.android.messaging.R; 42 import com.android.messaging.datamodel.data.ConversationListItemData; 43 import com.android.messaging.datamodel.data.ConversationMessageData; 44 import com.android.messaging.datamodel.data.ConversationParticipantsData; 45 import com.android.messaging.datamodel.data.MessageData; 46 import com.android.messaging.datamodel.data.MessagePartData; 47 import com.android.messaging.datamodel.data.ParticipantData; 48 import com.android.messaging.datamodel.media.VideoThumbnailRequest; 49 import com.android.messaging.sms.MmsUtils; 50 import com.android.messaging.ui.UIIntents; 51 import com.android.messaging.util.Assert; 52 import com.android.messaging.util.AvatarUriUtil; 53 import com.android.messaging.util.BugleGservices; 54 import com.android.messaging.util.BugleGservicesKeys; 55 import com.android.messaging.util.ContentType; 56 import com.android.messaging.util.ConversationIdSet; 57 import com.android.messaging.util.LogUtil; 58 import com.android.messaging.util.PendingIntentConstants; 59 import com.android.messaging.util.UriUtil; 60 import com.google.common.collect.Lists; 61 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.HashSet; 65 import java.util.Iterator; 66 import java.util.LinkedHashMap; 67 import java.util.List; 68 import java.util.Map; 69 70 /** 71 * Notification building class for conversation messages. 72 * 73 * Message Notifications are built in several stages with several utility classes. 74 * 1) Perform a database query and fill a data structure with information on messages and 75 * conversations which need to be notified. 76 * 2) Based on the data structure choose an appropriate NotificationState subclass to 77 * represent all the notifications. 78 * -- For one or more messages in one conversation: MultiMessageNotificationState. 79 * -- For multiple messages in multiple conversations: MultiConversationNotificationState 80 * 81 * A three level structure is used to coalesce the data from the database. From bottom to top: 82 * 1) NotificationLineInfo - A single message that needs to be notified. 83 * 2) ConversationLineInfo - A list of NotificationLineInfo in a single conversation. 84 * 3) ConversationInfoList - A list of ConversationLineInfo and the total number of messages. 85 * 86 * The createConversationInfoList function performs the query and creates the data structure. 87 */ 88 public abstract class MessageNotificationState extends NotificationState { 89 // Logging 90 static final String TAG = LogUtil.BUGLE_NOTIFICATIONS_TAG; 91 private static final int MAX_MESSAGES_IN_WEARABLE_PAGE = 20; 92 93 private static final int MAX_CHARACTERS_IN_GROUP_NAME = 30; 94 95 private static final int REPLY_INTENT_REQUEST_CODE_OFFSET = 0; 96 private static final int NUM_EXTRA_REQUEST_CODES_NEEDED = 1; 97 protected String mTickerSender = null; 98 protected CharSequence mTickerText = null; 99 protected String mTitle = null; 100 protected CharSequence mContent = null; 101 protected Uri mAttachmentUri = null; 102 protected String mAttachmentType = null; 103 104 @Override getAttachmentUri()105 protected Uri getAttachmentUri() { 106 return mAttachmentUri; 107 } 108 109 @Override getAttachmentType()110 protected String getAttachmentType() { 111 return mAttachmentType; 112 } 113 114 @Override getIcon()115 public int getIcon() { 116 return R.drawable.ic_sms_light; 117 } 118 119 @Override getPriority()120 public int getPriority() { 121 // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker 122 // isn't displayed. 123 return Notification.PRIORITY_HIGH; 124 } 125 126 /** 127 * Base class for single notification events for messages. Multiple of these 128 * may be grouped into a single conversation. 129 */ 130 static class NotificationLineInfo { 131 132 final int mNotificationType; 133 NotificationLineInfo()134 NotificationLineInfo() { 135 mNotificationType = BugleNotifications.LOCAL_SMS_NOTIFICATION; 136 } 137 NotificationLineInfo(final int notificationType)138 NotificationLineInfo(final int notificationType) { 139 mNotificationType = notificationType; 140 } 141 } 142 143 /** 144 * Information on a single chat message which should be shown in a notification. 145 */ 146 static class MessageLineInfo extends NotificationLineInfo { 147 final CharSequence mText; 148 Uri mAttachmentUri; 149 String mAttachmentType; 150 final String mAuthorFullName; 151 final String mAuthorFirstName; 152 boolean mIsManualDownloadNeeded; 153 final String mMessageId; 154 MessageLineInfo(final boolean isGroup, final String authorFullName, final String authorFirstName, final CharSequence text, final Uri attachmentUrl, final String attachmentType, final boolean isManualDownloadNeeded, final String messageId)155 MessageLineInfo(final boolean isGroup, final String authorFullName, 156 final String authorFirstName, final CharSequence text, final Uri attachmentUrl, 157 final String attachmentType, final boolean isManualDownloadNeeded, 158 final String messageId) { 159 super(BugleNotifications.LOCAL_SMS_NOTIFICATION); 160 mAuthorFullName = authorFullName; 161 mAuthorFirstName = authorFirstName; 162 mText = text; 163 mAttachmentUri = attachmentUrl; 164 mAttachmentType = attachmentType; 165 mIsManualDownloadNeeded = isManualDownloadNeeded; 166 mMessageId = messageId; 167 } 168 } 169 170 /** 171 * Information on all the notification messages within a single conversation. 172 */ 173 static class ConversationLineInfo { 174 // Conversation id of the latest message in the notification for this merged conversation. 175 final String mConversationId; 176 177 // True if this represents a group conversation. 178 final boolean mIsGroup; 179 180 // Name of the group conversation if available. 181 final String mGroupConversationName; 182 183 // True if this conversation's recipients includes one or more email address(es) 184 // (see ConversationColumns.INCLUDE_EMAIL_ADDRESS) 185 final boolean mIncludeEmailAddress; 186 187 // Timestamp of the latest message 188 final long mReceivedTimestamp; 189 190 // Self participant id. 191 final String mSelfParticipantId; 192 193 // List of individual line notifications to be parsed later. 194 final List<NotificationLineInfo> mLineInfos; 195 196 // Total number of messages. Might be different that mLineInfos.size() as the number of 197 // line infos is capped. 198 int mTotalMessageCount; 199 200 // Custom ringtone if set 201 final String mRingtoneUri; 202 203 // Should notification be enabled for this conversation? 204 final boolean mNotificationEnabled; 205 206 // Should notifications vibrate for this conversation? 207 final boolean mNotificationVibrate; 208 209 // Avatar uri of sender 210 final Uri mAvatarUri; 211 212 // Contact uri of sender 213 final Uri mContactUri; 214 215 // Subscription id. 216 final int mSubId; 217 218 // Number of participants 219 final int mParticipantCount; 220 ConversationLineInfo(final String conversationId, final boolean isGroup, final String groupConversationName, final boolean includeEmailAddress, final long receivedTimestamp, final String selfParticipantId, final String ringtoneUri, final boolean notificationEnabled, final boolean notificationVibrate, final Uri avatarUri, final Uri contactUri, final int subId, final int participantCount)221 public ConversationLineInfo(final String conversationId, 222 final boolean isGroup, 223 final String groupConversationName, 224 final boolean includeEmailAddress, 225 final long receivedTimestamp, 226 final String selfParticipantId, 227 final String ringtoneUri, 228 final boolean notificationEnabled, 229 final boolean notificationVibrate, 230 final Uri avatarUri, 231 final Uri contactUri, 232 final int subId, 233 final int participantCount) { 234 mConversationId = conversationId; 235 mIsGroup = isGroup; 236 mGroupConversationName = groupConversationName; 237 mIncludeEmailAddress = includeEmailAddress; 238 mReceivedTimestamp = receivedTimestamp; 239 mSelfParticipantId = selfParticipantId; 240 mLineInfos = new ArrayList<NotificationLineInfo>(); 241 mTotalMessageCount = 0; 242 mRingtoneUri = ringtoneUri; 243 mAvatarUri = avatarUri; 244 mContactUri = contactUri; 245 mNotificationEnabled = notificationEnabled; 246 mNotificationVibrate = notificationVibrate; 247 mSubId = subId; 248 mParticipantCount = participantCount; 249 } 250 getLatestMessageNotificationType()251 public int getLatestMessageNotificationType() { 252 final MessageLineInfo messageLineInfo = getLatestMessageLineInfo(); 253 if (messageLineInfo == null) { 254 return BugleNotifications.LOCAL_SMS_NOTIFICATION; 255 } 256 return messageLineInfo.mNotificationType; 257 } 258 getLatestMessageId()259 public String getLatestMessageId() { 260 final MessageLineInfo messageLineInfo = getLatestMessageLineInfo(); 261 if (messageLineInfo == null) { 262 return null; 263 } 264 return messageLineInfo.mMessageId; 265 } 266 getDoesLatestMessageNeedDownload()267 public boolean getDoesLatestMessageNeedDownload() { 268 final MessageLineInfo messageLineInfo = getLatestMessageLineInfo(); 269 if (messageLineInfo == null) { 270 return false; 271 } 272 return messageLineInfo.mIsManualDownloadNeeded; 273 } 274 getLatestMessageLineInfo()275 private MessageLineInfo getLatestMessageLineInfo() { 276 // The latest message is stored at index zero of the message line infos. 277 if (mLineInfos.size() > 0 && mLineInfos.get(0) instanceof MessageLineInfo) { 278 return (MessageLineInfo) mLineInfos.get(0); 279 } 280 return null; 281 } 282 } 283 284 /** 285 * Information on all the notification messages across all conversations. 286 */ 287 public static class ConversationInfoList { 288 final int mMessageCount; 289 final List<ConversationLineInfo> mConvInfos; ConversationInfoList(final int count, final List<ConversationLineInfo> infos)290 public ConversationInfoList(final int count, final List<ConversationLineInfo> infos) { 291 mMessageCount = count; 292 mConvInfos = infos; 293 } 294 } 295 296 final ConversationInfoList mConvList; 297 private long mLatestReceivedTimestamp; 298 makeConversationIdSet(final ConversationInfoList convList)299 private static ConversationIdSet makeConversationIdSet(final ConversationInfoList convList) { 300 ConversationIdSet set = null; 301 if (convList != null && convList.mConvInfos != null && convList.mConvInfos.size() > 0) { 302 set = new ConversationIdSet(); 303 for (final ConversationLineInfo info : convList.mConvInfos) { 304 set.add(info.mConversationId); 305 } 306 } 307 return set; 308 } 309 MessageNotificationState(final ConversationInfoList convList)310 protected MessageNotificationState(final ConversationInfoList convList) { 311 super(makeConversationIdSet(convList)); 312 mConvList = convList; 313 mType = PendingIntentConstants.SMS_NOTIFICATION_ID; 314 mLatestReceivedTimestamp = Long.MIN_VALUE; 315 if (convList != null) { 316 for (final ConversationLineInfo info : convList.mConvInfos) { 317 mLatestReceivedTimestamp = Math.max(mLatestReceivedTimestamp, 318 info.mReceivedTimestamp); 319 } 320 } 321 } 322 323 @Override getLatestReceivedTimestamp()324 public long getLatestReceivedTimestamp() { 325 return mLatestReceivedTimestamp; 326 } 327 328 @Override getNumRequestCodesNeeded()329 public int getNumRequestCodesNeeded() { 330 // Get additional request codes for the Reply PendingIntent (wearables only) 331 // and the DND PendingIntent. 332 return super.getNumRequestCodesNeeded() + NUM_EXTRA_REQUEST_CODES_NEEDED; 333 } 334 getBaseExtraRequestCode()335 private int getBaseExtraRequestCode() { 336 return mBaseRequestCode + super.getNumRequestCodesNeeded(); 337 } 338 getReplyIntentRequestCode()339 public int getReplyIntentRequestCode() { 340 return getBaseExtraRequestCode() + REPLY_INTENT_REQUEST_CODE_OFFSET; 341 } 342 343 @Override getClearIntent()344 public PendingIntent getClearIntent() { 345 return UIIntents.get().getPendingIntentForClearingNotifications( 346 Factory.get().getApplicationContext(), 347 BugleNotifications.UPDATE_MESSAGES, 348 mConversationIds, 349 getClearIntentRequestCode()); 350 } 351 352 /** 353 * Notification for multiple messages in at least 2 different conversations. 354 */ 355 public static class MultiConversationNotificationState extends MessageNotificationState { 356 357 public final List<MessageNotificationState> 358 mChildren = new ArrayList<MessageNotificationState>(); 359 MultiConversationNotificationState( final ConversationInfoList convList, final MessageNotificationState state)360 public MultiConversationNotificationState( 361 final ConversationInfoList convList, final MessageNotificationState state) { 362 super(convList); 363 mAttachmentUri = null; 364 mAttachmentType = null; 365 366 // Pull the ticker title/text from the single notification 367 mTickerSender = state.getTitle(); 368 mTitle = Factory.get().getApplicationContext().getResources().getQuantityString( 369 R.plurals.notification_new_messages, 370 convList.mMessageCount, convList.mMessageCount); 371 mTickerText = state.mContent; 372 373 // Create child notifications for each conversation, 374 // which will be displayed (only) on a wearable device. 375 for (int i = 0; i < convList.mConvInfos.size(); i++) { 376 final ConversationLineInfo convInfo = convList.mConvInfos.get(i); 377 if (!(convInfo.mLineInfos.get(0) instanceof MessageLineInfo)) { 378 continue; 379 } 380 setPeopleForConversation(convInfo.mConversationId); 381 final ConversationInfoList list = new ConversationInfoList( 382 convInfo.mTotalMessageCount, Lists.newArrayList(convInfo)); 383 mChildren.add(new BundledMessageNotificationState(list, i)); 384 } 385 } 386 387 @Override getIcon()388 public int getIcon() { 389 return R.drawable.ic_sms_multi_light; 390 } 391 392 @Override build(final Builder builder)393 protected NotificationCompat.Style build(final Builder builder) { 394 builder.setContentTitle(mTitle); 395 NotificationCompat.InboxStyle inboxStyle = null; 396 inboxStyle = new NotificationCompat.InboxStyle(builder); 397 398 final Context context = Factory.get().getApplicationContext(); 399 // enumeration_comma is defined as ", " 400 final String separator = context.getString(R.string.enumeration_comma); 401 final StringBuilder senders = new StringBuilder(); 402 long when = 0; 403 for (int i = 0; i < mConvList.mConvInfos.size(); i++) { 404 final ConversationLineInfo convInfo = mConvList.mConvInfos.get(i); 405 if (convInfo.mReceivedTimestamp > when) { 406 when = convInfo.mReceivedTimestamp; 407 } 408 String sender; 409 CharSequence text; 410 final NotificationLineInfo lineInfo = convInfo.mLineInfos.get(0); 411 final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfo; 412 if (convInfo.mIsGroup) { 413 sender = (convInfo.mGroupConversationName.length() > 414 MAX_CHARACTERS_IN_GROUP_NAME) ? 415 truncateGroupMessageName(convInfo.mGroupConversationName) 416 : convInfo.mGroupConversationName; 417 } else { 418 sender = messageLineInfo.mAuthorFullName; 419 } 420 text = messageLineInfo.mText; 421 mAttachmentUri = messageLineInfo.mAttachmentUri; 422 mAttachmentType = messageLineInfo.mAttachmentType; 423 424 inboxStyle.addLine(BugleNotifications.formatInboxMessage( 425 sender, text, mAttachmentUri, mAttachmentType)); 426 if (sender != null) { 427 if (senders.length() > 0) { 428 senders.append(separator); 429 } 430 senders.append(sender); 431 } 432 } 433 // for collapsed state 434 mContent = senders; 435 builder.setContentText(senders) 436 .setTicker(getTicker()) 437 .setWhen(when); 438 439 return inboxStyle; 440 } 441 } 442 443 /** 444 * Truncate group conversation name to be displayed in the notifications. This either truncates 445 * the entire group name or finds the last comma in the available length and truncates the name 446 * at that point 447 */ truncateGroupMessageName(final String conversationName)448 private static String truncateGroupMessageName(final String conversationName) { 449 int endIndex = MAX_CHARACTERS_IN_GROUP_NAME; 450 for (int i = MAX_CHARACTERS_IN_GROUP_NAME; i >= 0; i--) { 451 // The dividing marker should stay consistent with ConversationListItemData.DIVIDER_TEXT 452 if (conversationName.charAt(i) == ',') { 453 endIndex = i; 454 break; 455 } 456 } 457 return conversationName.substring(0, endIndex) + '\u2026'; 458 } 459 460 /** 461 * Notification for multiple messages in a single conversation. Also used if there is a single 462 * message in a single conversation. 463 */ 464 public static class MultiMessageNotificationState extends MessageNotificationState { 465 MultiMessageNotificationState(final ConversationInfoList convList)466 public MultiMessageNotificationState(final ConversationInfoList convList) { 467 super(convList); 468 // This conversation has been accepted. 469 final ConversationLineInfo convInfo = convList.mConvInfos.get(0); 470 setAvatarUrlsForConversation(convInfo.mConversationId); 471 setPeopleForConversation(convInfo.mConversationId); 472 473 final Context context = Factory.get().getApplicationContext(); 474 MessageLineInfo messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0); 475 // attached photo 476 mAttachmentUri = messageInfo.mAttachmentUri; 477 mAttachmentType = messageInfo.mAttachmentType; 478 mContent = messageInfo.mText; 479 480 if (mAttachmentUri != null) { 481 // The default attachment type is an image, since that's what was originally 482 // supported. When there's no content type, assume it's an image. 483 int message = R.string.notification_picture; 484 if (ContentType.isAudioType(mAttachmentType)) { 485 message = R.string.notification_audio; 486 } else if (ContentType.isVideoType(mAttachmentType)) { 487 message = R.string.notification_video; 488 } else if (ContentType.isVCardType(mAttachmentType)) { 489 message = R.string.notification_vcard; 490 } 491 final String attachment = context.getString(message); 492 final SpannableStringBuilder spanBuilder = new SpannableStringBuilder(); 493 if (!TextUtils.isEmpty(mContent)) { 494 spanBuilder.append(mContent).append(System.getProperty("line.separator")); 495 } 496 final int start = spanBuilder.length(); 497 spanBuilder.append(attachment); 498 spanBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, spanBuilder.length(), 499 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 500 mContent = spanBuilder; 501 } 502 if (convInfo.mIsGroup) { 503 // When the message is part of a group, the sender's first name 504 // is prepended to the message, but not for the ticker message. 505 mTickerText = mContent; 506 mTickerSender = messageInfo.mAuthorFullName; 507 // append the bold name to the front of the message 508 mContent = BugleNotifications.buildSpaceSeparatedMessage( 509 messageInfo.mAuthorFullName, mContent, mAttachmentUri, 510 mAttachmentType); 511 mTitle = convInfo.mGroupConversationName; 512 } else { 513 // No matter how many messages there are, since this is a 1:1, just 514 // get the author full name from the first one. 515 messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0); 516 mTitle = messageInfo.mAuthorFullName; 517 } 518 } 519 520 @Override build(final Builder builder)521 protected NotificationCompat.Style build(final Builder builder) { 522 builder.setContentTitle(mTitle) 523 .setTicker(getTicker()); 524 525 NotificationCompat.Style notifStyle = null; 526 final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0); 527 final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos; 528 final int messageCount = lineInfos.size(); 529 // At this point, all the messages come from the same conversation. We need to load 530 // the sender's avatar and then finish building the notification on a callback. 531 532 builder.setContentText(mContent); // for collapsed state 533 534 if (messageCount == 1) { 535 final boolean shouldShowImage = ContentType.isImageType(mAttachmentType) 536 || (ContentType.isVideoType(mAttachmentType) 537 && VideoThumbnailRequest.shouldShowIncomingVideoThumbnails()); 538 if (mAttachmentUri != null && shouldShowImage) { 539 // Show "Picture" as the content 540 final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0); 541 String authorFirstName = messageLineInfo.mAuthorFirstName; 542 543 // For the collapsed state, just show "picture" unless this is a 544 // group conversation. If it's a group, show the sender name and 545 // "picture". 546 final CharSequence tickerTag = 547 BugleNotifications.formatAttachmentTag(authorFirstName, 548 mAttachmentType); 549 // For 1:1 notifications don't show first name in the notification, but 550 // do show it in the ticker text 551 CharSequence pictureTag = tickerTag; 552 if (!convInfo.mIsGroup) { 553 authorFirstName = null; 554 pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName, 555 mAttachmentType); 556 } 557 builder.setContentText(pictureTag); 558 builder.setTicker(tickerTag); 559 560 notifStyle = new NotificationCompat.BigPictureStyle(builder) 561 .setSummaryText(BugleNotifications.formatInboxMessage( 562 authorFirstName, 563 null, null, 564 null)); // expanded state, just show sender 565 } else { 566 notifStyle = new NotificationCompat.BigTextStyle(builder) 567 .bigText(mContent); 568 } 569 } else { 570 // We've got multiple messages for the same sender. 571 // Starting with the oldest new message, display the full text of each message. 572 // Begin a line for each subsequent message. 573 final SpannableStringBuilder buf = new SpannableStringBuilder(); 574 575 for (int i = lineInfos.size() - 1; i >= 0; --i) { 576 final NotificationLineInfo info = lineInfos.get(i); 577 final MessageLineInfo messageLineInfo = (MessageLineInfo) info; 578 mAttachmentUri = messageLineInfo.mAttachmentUri; 579 mAttachmentType = messageLineInfo.mAttachmentType; 580 CharSequence text = messageLineInfo.mText; 581 if (!TextUtils.isEmpty(text) || mAttachmentUri != null) { 582 if (convInfo.mIsGroup) { 583 // append the bold name to the front of the message 584 text = BugleNotifications.buildSpaceSeparatedMessage( 585 messageLineInfo.mAuthorFullName, text, mAttachmentUri, 586 mAttachmentType); 587 } else { 588 text = BugleNotifications.buildSpaceSeparatedMessage( 589 null, text, mAttachmentUri, mAttachmentType); 590 } 591 buf.append(text); 592 if (i > 0) { 593 buf.append('\n'); 594 } 595 } 596 } 597 598 // Show a single notification -- big style with the text of all the messages 599 notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf); 600 } 601 builder.setWhen(convInfo.mReceivedTimestamp); 602 return notifStyle; 603 } 604 605 } 606 firstNameUsedMoreThanOnce( final HashMap<String, Integer> map, final String firstName)607 private static boolean firstNameUsedMoreThanOnce( 608 final HashMap<String, Integer> map, final String firstName) { 609 if (map == null) { 610 return false; 611 } 612 if (firstName == null) { 613 return false; 614 } 615 final Integer count = map.get(firstName); 616 if (count != null) { 617 return count > 1; 618 } else { 619 return false; 620 } 621 } 622 scanFirstNames(final String conversationId)623 private static HashMap<String, Integer> scanFirstNames(final String conversationId) { 624 final Context context = Factory.get().getApplicationContext(); 625 final Uri uri = 626 MessagingContentProvider.buildConversationParticipantsUri(conversationId); 627 final ConversationParticipantsData participantsData = new ConversationParticipantsData(); 628 629 try (final Cursor participantsCursor = context.getContentResolver().query( 630 uri, ParticipantData.ParticipantsQuery.PROJECTION, null, null, null)) { 631 participantsData.bind(participantsCursor); 632 } 633 634 final Iterator<ParticipantData> iter = participantsData.iterator(); 635 636 final HashMap<String, Integer> firstNames = new HashMap<String, Integer>(); 637 boolean seenSelf = false; 638 while (iter.hasNext()) { 639 final ParticipantData participant = iter.next(); 640 // Make sure we only add the self participant once 641 if (participant.isSelf()) { 642 if (seenSelf) { 643 continue; 644 } else { 645 seenSelf = true; 646 } 647 } 648 649 final String firstName = participant.getFirstName(); 650 if (firstName == null) { 651 continue; 652 } 653 654 final int currentCount = firstNames.containsKey(firstName) 655 ? firstNames.get(firstName) 656 : 0; 657 firstNames.put(firstName, currentCount + 1); 658 } 659 return firstNames; 660 } 661 662 // Essentially, we're building a list of the past 20 messages for this conversation to display 663 // on the wearable. buildConversationPageForWearable(final String conversationId, int participantCount)664 public static Notification buildConversationPageForWearable(final String conversationId, 665 int participantCount) { 666 final Context context = Factory.get().getApplicationContext(); 667 668 // Limit the number of messages to show. We just want enough to provide context for the 669 // notification. Fetch one more than we need, so we can tell if there are more messages 670 // before the one we're showing. 671 // TODO: in the query, a multipart message will contain a row for each part. 672 // We might need a smarter GROUP_BY. On the other hand, we might want to show each of the 673 // parts as separate messages on the wearable. 674 final int limit = MAX_MESSAGES_IN_WEARABLE_PAGE + 1; 675 676 final List<CharSequence> messages = Lists.newArrayList(); 677 boolean hasSeenMessagesBeforeNotification = false; 678 Cursor convMessageCursor = null; 679 try { 680 final DatabaseWrapper db = DataModel.get().getDatabase(); 681 682 final String[] queryArgs = { conversationId }; 683 final String convPageSql = ConversationMessageData.getWearableQuerySql() + " LIMIT " + 684 limit; 685 convMessageCursor = db.rawQuery( 686 convPageSql, 687 queryArgs); 688 689 if (convMessageCursor == null || !convMessageCursor.moveToFirst()) { 690 return null; 691 } 692 final ConversationMessageData convMessageData = 693 new ConversationMessageData(); 694 695 final HashMap<String, Integer> firstNames = scanFirstNames(conversationId); 696 do { 697 convMessageData.bind(convMessageCursor); 698 699 final String authorFullName = convMessageData.getSenderFullName(); 700 final String authorFirstName = convMessageData.getSenderFirstName(); 701 String text = convMessageData.getText(); 702 703 final boolean isSmsPushNotification = convMessageData.getIsMmsNotification(); 704 705 // if auto-download was off to show a message to tap to download the message. We 706 // might need to get that working again. 707 if (isSmsPushNotification && text != null) { 708 text = convertHtmlAndStripUrls(text).toString(); 709 } 710 // Skip messages without any content 711 if (TextUtils.isEmpty(text) && !convMessageData.hasAttachments()) { 712 continue; 713 } 714 // Track whether there are messages prior to the one(s) shown in the notification. 715 if (convMessageData.getIsSeen()) { 716 hasSeenMessagesBeforeNotification = true; 717 } 718 719 final boolean usedMoreThanOnce = firstNameUsedMoreThanOnce( 720 firstNames, authorFirstName); 721 String displayName = usedMoreThanOnce ? authorFullName : authorFirstName; 722 if (TextUtils.isEmpty(displayName)) { 723 if (convMessageData.getIsIncoming()) { 724 displayName = convMessageData.getSenderDisplayDestination(); 725 if (TextUtils.isEmpty(displayName)) { 726 displayName = context.getString(R.string.unknown_sender); 727 } 728 } else { 729 displayName = context.getString(R.string.unknown_self_participant); 730 } 731 } 732 733 Uri attachmentUri = null; 734 String attachmentType = null; 735 final List<MessagePartData> attachments = convMessageData.getAttachments(); 736 for (final MessagePartData messagePartData : attachments) { 737 // Look for the first attachment that's not the text piece. 738 if (!messagePartData.isText()) { 739 attachmentUri = messagePartData.getContentUri(); 740 attachmentType = messagePartData.getContentType(); 741 break; 742 } 743 } 744 745 final CharSequence message = BugleNotifications.buildSpaceSeparatedMessage( 746 displayName, text, attachmentUri, attachmentType); 747 messages.add(message); 748 749 } while (convMessageCursor.moveToNext()); 750 } finally { 751 if (convMessageCursor != null) { 752 convMessageCursor.close(); 753 } 754 } 755 756 // If there is no conversation history prior to what is already visible in the main 757 // notification, there's no need to include the conversation log, too. 758 final int maxMessagesInNotification = getMaxMessagesInConversationNotification(); 759 if (!hasSeenMessagesBeforeNotification && messages.size() <= maxMessagesInNotification) { 760 return null; 761 } 762 763 final SpannableStringBuilder bigText = new SpannableStringBuilder(); 764 // There is at least 1 message prior to the first one that we're going to show. 765 // Indicate this by inserting an ellipsis at the beginning of the conversation log. 766 if (convMessageCursor.getCount() == limit) { 767 bigText.append(context.getString(R.string.ellipsis) + "\n\n"); 768 if (messages.size() > MAX_MESSAGES_IN_WEARABLE_PAGE) { 769 messages.remove(messages.size() - 1); 770 } 771 } 772 // Messages are sorted in descending timestamp order, so iterate backwards 773 // to get them back in ascending order for display purposes. 774 for (int i = messages.size() - 1; i >= 0; --i) { 775 bigText.append(messages.get(i)); 776 if (i > 0) { 777 bigText.append("\n\n"); 778 } 779 } 780 ++participantCount; // Add in myself 781 782 if (participantCount > 2) { 783 final SpannableString statusText = new SpannableString( 784 context.getResources().getQuantityString(R.plurals.wearable_participant_count, 785 participantCount, participantCount)); 786 statusText.setSpan(new ForegroundColorSpan(context.getResources().getColor( 787 R.color.wearable_notification_participants_count)), 0, statusText.length(), 788 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 789 bigText.append("\n\n").append(statusText); 790 } 791 792 final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(context); 793 final NotificationCompat.Style notifStyle = 794 new NotificationCompat.BigTextStyle(notifBuilder).bigText(bigText); 795 notifBuilder.setStyle(notifStyle); 796 797 final WearableExtender wearableExtender = new WearableExtender(); 798 wearableExtender.setStartScrollBottom(true); 799 notifBuilder.extend(wearableExtender); 800 801 return notifBuilder.build(); 802 } 803 804 /** 805 * Notification for one or more messages in a single conversation, which is bundled together 806 * with notifications for other conversations on a wearable device. 807 */ 808 public static class BundledMessageNotificationState extends MultiMessageNotificationState { 809 public int mGroupOrder; BundledMessageNotificationState(final ConversationInfoList convList, final int groupOrder)810 public BundledMessageNotificationState(final ConversationInfoList convList, 811 final int groupOrder) { 812 super(convList); 813 mGroupOrder = groupOrder; 814 } 815 } 816 817 /** 818 * Performs a query on the database. 819 */ createConversationInfoList()820 private static ConversationInfoList createConversationInfoList() { 821 // Map key is conversation id. We use LinkedHashMap to ensure that entries are iterated in 822 // the same order they were originally added. We scan unseen messages from newest to oldest, 823 // so the corresponding conversations are added in that order, too. 824 final Map<String, ConversationLineInfo> convLineInfos = new LinkedHashMap<>(); 825 int messageCount = 0; 826 827 Cursor convMessageCursor = null; 828 try { 829 final Context context = Factory.get().getApplicationContext(); 830 final DatabaseWrapper db = DataModel.get().getDatabase(); 831 832 convMessageCursor = db.rawQuery( 833 ConversationMessageData.getNotificationQuerySql(), 834 null); 835 836 if (convMessageCursor != null && convMessageCursor.moveToFirst()) { 837 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 838 LogUtil.v(TAG, "MessageNotificationState: Found unseen message notifications."); 839 } 840 final ConversationMessageData convMessageData = 841 new ConversationMessageData(); 842 843 HashMap<String, Integer> firstNames = null; 844 String conversationIdForFirstNames = null; 845 String groupConversationName = null; 846 final int maxMessages = getMaxMessagesInConversationNotification(); 847 848 do { 849 convMessageData.bind(convMessageCursor); 850 851 // First figure out if this is a valid message. 852 String authorFullName = convMessageData.getSenderFullName(); 853 String authorFirstName = convMessageData.getSenderFirstName(); 854 final String messageText = convMessageData.getText(); 855 856 final String convId = convMessageData.getConversationId(); 857 final String messageId = convMessageData.getMessageId(); 858 859 CharSequence text = messageText; 860 final boolean isManualDownloadNeeded = convMessageData.getIsMmsNotification(); 861 if (isManualDownloadNeeded) { 862 // Don't try and convert the text from html if it's sms and not a sms push 863 // notification. 864 Assert.equals(MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD, 865 convMessageData.getStatus()); 866 text = context.getResources().getString( 867 R.string.message_title_manual_download); 868 } 869 ConversationLineInfo currConvInfo = convLineInfos.get(convId); 870 if (currConvInfo == null) { 871 final ConversationListItemData convData = 872 ConversationListItemData.getExistingConversation(db, convId); 873 if (!convData.getNotificationEnabled()) { 874 // Skip conversations that have notifications disabled. 875 continue; 876 } 877 final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db, 878 convData.getSelfId()); 879 groupConversationName = convData.getName(); 880 final Uri avatarUri = AvatarUriUtil.createAvatarUri( 881 convMessageData.getSenderProfilePhotoUri(), 882 convMessageData.getSenderFullName(), 883 convMessageData.getSenderNormalizedDestination(), 884 convMessageData.getSenderContactLookupKey()); 885 currConvInfo = new ConversationLineInfo(convId, 886 convData.getIsGroup(), 887 groupConversationName, 888 convData.getIncludeEmailAddress(), 889 convMessageData.getReceivedTimeStamp(), 890 convData.getSelfId(), 891 convData.getNotificationSoundUri(), 892 convData.getNotificationEnabled(), 893 convData.getNotifiationVibrate(), 894 avatarUri, 895 convMessageData.getSenderContactLookupUri(), 896 subId, 897 convData.getParticipantCount()); 898 convLineInfos.put(convId, currConvInfo); 899 } 900 // Prepare the message line 901 if (currConvInfo.mTotalMessageCount < maxMessages) { 902 if (currConvInfo.mIsGroup) { 903 if (authorFirstName == null) { 904 // authorFullName might be null as well. In that case, we won't 905 // show an author. That is better than showing all the group 906 // names again on the 2nd line. 907 authorFirstName = authorFullName; 908 } 909 } else { 910 // don't recompute this if we don't need to 911 if (!TextUtils.equals(conversationIdForFirstNames, convId)) { 912 firstNames = scanFirstNames(convId); 913 conversationIdForFirstNames = convId; 914 } 915 if (firstNames != null) { 916 final Integer count = firstNames.get(authorFirstName); 917 if (count != null && count > 1) { 918 authorFirstName = authorFullName; 919 } 920 } 921 922 if (authorFullName == null) { 923 authorFullName = groupConversationName; 924 } 925 if (authorFirstName == null) { 926 authorFirstName = groupConversationName; 927 } 928 } 929 final String subjectText = MmsUtils.cleanseMmsSubject( 930 context.getResources(), 931 convMessageData.getMmsSubject()); 932 if (!TextUtils.isEmpty(subjectText)) { 933 final String subjectLabel = 934 context.getString(R.string.subject_label); 935 final SpannableStringBuilder spanBuilder = 936 new SpannableStringBuilder(); 937 938 spanBuilder.append(context.getString(R.string.notification_subject, 939 subjectLabel, subjectText)); 940 spanBuilder.setSpan(new TextAppearanceSpan( 941 context, R.style.NotificationSubjectText), 0, 942 subjectLabel.length(), 943 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 944 if (!TextUtils.isEmpty(text)) { 945 // Now add the actual message text below the subject header. 946 spanBuilder.append(System.getProperty("line.separator") + text); 947 } 948 text = spanBuilder; 949 } 950 // If we've got attachments, find the best one. If one of the messages is 951 // a photo, save the url so we'll display a big picture notification. 952 // Otherwise, show the first one we find. 953 Uri attachmentUri = null; 954 String attachmentType = null; 955 final MessagePartData messagePartData = 956 getMostInterestingAttachment(convMessageData); 957 if (messagePartData != null) { 958 attachmentUri = messagePartData.getContentUri(); 959 attachmentType = messagePartData.getContentType(); 960 } 961 currConvInfo.mLineInfos.add(new MessageLineInfo(currConvInfo.mIsGroup, 962 authorFullName, authorFirstName, text, 963 attachmentUri, attachmentType, isManualDownloadNeeded, messageId)); 964 } 965 messageCount++; 966 currConvInfo.mTotalMessageCount++; 967 } while (convMessageCursor.moveToNext()); 968 } 969 } finally { 970 if (convMessageCursor != null) { 971 convMessageCursor.close(); 972 } 973 } 974 if (convLineInfos.isEmpty()) { 975 return null; 976 } else { 977 return new ConversationInfoList(messageCount, 978 Lists.newLinkedList(convLineInfos.values())); 979 } 980 } 981 982 /** 983 * Scans all the attachments for a message and returns the most interesting one that we'll 984 * show in a notification. By order of importance, in case there are multiple attachments: 985 * 1- an image (because we can show the image as a BigPictureNotification) 986 * 2- a video (because we can show a video frame as a BigPictureNotification) 987 * 3- a vcard 988 * 4- an audio attachment 989 * @return MessagePartData for the most interesting part. Can be null. 990 */ getMostInterestingAttachment( final ConversationMessageData convMessageData)991 private static MessagePartData getMostInterestingAttachment( 992 final ConversationMessageData convMessageData) { 993 final List<MessagePartData> attachments = convMessageData.getAttachments(); 994 995 MessagePartData imagePart = null; 996 MessagePartData audioPart = null; 997 MessagePartData vcardPart = null; 998 MessagePartData videoPart = null; 999 1000 // 99.99% of the time there will be 0 or 1 part, since receiving slideshows is so 1001 // uncommon. 1002 1003 // Remember the first of each type of part. 1004 for (final MessagePartData messagePartData : attachments) { 1005 if (messagePartData.isImage() && imagePart == null) { 1006 imagePart = messagePartData; 1007 } 1008 if (messagePartData.isVideo() && videoPart == null) { 1009 videoPart = messagePartData; 1010 } 1011 if (messagePartData.isVCard() && vcardPart == null) { 1012 vcardPart = messagePartData; 1013 } 1014 if (messagePartData.isAudio() && audioPart == null) { 1015 audioPart = messagePartData; 1016 } 1017 } 1018 if (imagePart != null) { 1019 return imagePart; 1020 } else if (videoPart != null) { 1021 return videoPart; 1022 } else if (audioPart != null) { 1023 return audioPart; 1024 } else if (vcardPart != null) { 1025 return vcardPart; 1026 } 1027 return null; 1028 } 1029 getMaxMessagesInConversationNotification()1030 private static int getMaxMessagesInConversationNotification() { 1031 if (!BugleNotifications.isWearCompanionAppInstalled()) { 1032 return BugleGservices.get().getInt( 1033 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION, 1034 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_DEFAULT); 1035 } 1036 return BugleGservices.get().getInt( 1037 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE, 1038 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE_DEFAULT); 1039 } 1040 1041 /** 1042 * Scans the database for messages that need to go into notifications. Creates the appropriate 1043 * MessageNotificationState depending on if there are multiple senders, or 1044 * messages from one sender. 1045 * @return NotificationState for the notification created. 1046 */ getNotificationState()1047 public static NotificationState getNotificationState() { 1048 MessageNotificationState state = null; 1049 final ConversationInfoList convList = createConversationInfoList(); 1050 1051 if (convList == null || convList.mConvInfos.size() == 0) { 1052 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 1053 LogUtil.v(TAG, "MessageNotificationState: No unseen notifications"); 1054 } 1055 } else { 1056 final ConversationLineInfo convInfo = convList.mConvInfos.get(0); 1057 state = new MultiMessageNotificationState(convList); 1058 1059 if (convList.mConvInfos.size() > 1) { 1060 // We've got notifications across multiple conversations. Pass in the notification 1061 // we just built of the most recent notification so we can use that to show the 1062 // user the new message in the ticker. 1063 state = new MultiConversationNotificationState(convList, state); 1064 } else { 1065 // For now, only show avatars for notifications for a single conversation. 1066 if (convInfo.mAvatarUri != null) { 1067 if (state.mParticipantAvatarsUris == null) { 1068 state.mParticipantAvatarsUris = new ArrayList<Uri>(1); 1069 } 1070 state.mParticipantAvatarsUris.add(convInfo.mAvatarUri); 1071 } 1072 if (convInfo.mContactUri != null) { 1073 if (state.mParticipantContactUris == null) { 1074 state.mParticipantContactUris = new ArrayList<Uri>(1); 1075 } 1076 state.mParticipantContactUris.add(convInfo.mContactUri); 1077 } 1078 } 1079 } 1080 if (state != null && LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 1081 LogUtil.v(TAG, "MessageNotificationState: Notification state created" 1082 + ", title = " 1083 + (state.mTickerSender != null ? state.mTickerSender : state.mTitle) 1084 + ", content = " 1085 + (state.mTickerText != null ? state.mTickerText : state.mContent)); 1086 } 1087 return state; 1088 } 1089 getTitle()1090 protected String getTitle() { 1091 return mTitle; 1092 } 1093 1094 @Override getLatestMessageNotificationType()1095 public int getLatestMessageNotificationType() { 1096 // This function is called to determine whether the most recent notification applies 1097 // to an sms conversation or a hangout conversation. We have different ringtone/vibrate 1098 // settings for both types of conversations. 1099 if (mConvList.mConvInfos.size() > 0) { 1100 final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0); 1101 return convInfo.getLatestMessageNotificationType(); 1102 } 1103 return BugleNotifications.LOCAL_SMS_NOTIFICATION; 1104 } 1105 1106 @Override getRingtoneUri()1107 public String getRingtoneUri() { 1108 if (mConvList.mConvInfos.size() > 0) { 1109 return mConvList.mConvInfos.get(0).mRingtoneUri; 1110 } 1111 return null; 1112 } 1113 1114 @Override getNotificationVibrate()1115 public boolean getNotificationVibrate() { 1116 if (mConvList.mConvInfos.size() > 0) { 1117 return mConvList.mConvInfos.get(0).mNotificationVibrate; 1118 } 1119 return false; 1120 } 1121 getTicker()1122 protected CharSequence getTicker() { 1123 return BugleNotifications.buildColonSeparatedMessage( 1124 mTickerSender != null ? mTickerSender : mTitle, 1125 mTickerText != null ? mTickerText : mContent, 1126 null, 1127 null); 1128 } 1129 convertHtmlAndStripUrls(final String s)1130 private static CharSequence convertHtmlAndStripUrls(final String s) { 1131 final Spanned text = Html.fromHtml(s); 1132 if (text instanceof Spannable) { 1133 stripUrls((Spannable) text); 1134 } 1135 return text; 1136 } 1137 1138 // Since we don't want to show URLs in notifications, a function 1139 // to remove them in place. stripUrls(final Spannable text)1140 private static void stripUrls(final Spannable text) { 1141 final URLSpan[] spans = text.getSpans(0, text.length(), URLSpan.class); 1142 for (final URLSpan span : spans) { 1143 text.removeSpan(span); 1144 } 1145 } 1146 1147 /* 1148 private static void updateAlertStatusMessages(final long thresholdDeltaMs) { 1149 // TODO may need this when supporting error notifications 1150 final EsDatabaseHelper helper = EsDatabaseHelper.getDatabaseHelper(); 1151 final ContentValues values = new ContentValues(); 1152 final long nowMicros = System.currentTimeMillis() * 1000; 1153 values.put(MessageColumns.ALERT_STATUS, "1"); 1154 final String selection = 1155 MessageColumns.ALERT_STATUS + "=0 AND (" + 1156 MessageColumns.STATUS + "=" + EsProvider.MESSAGE_STATUS_FAILED_TO_SEND + " OR (" + 1157 MessageColumns.STATUS + "!=" + EsProvider.MESSAGE_STATUS_ON_SERVER + " AND " + 1158 MessageColumns.TIMESTAMP + "+" + thresholdDeltaMs*1000 + "<" + nowMicros + ")) "; 1159 1160 final int updateCount = helper.getWritableDatabaseWrapper().update( 1161 EsProvider.MESSAGES_TABLE, 1162 values, 1163 selection, 1164 null); 1165 if (updateCount > 0) { 1166 EsConversationsData.notifyConversationsChanged(); 1167 } 1168 }*/ 1169 applyWarningTextColor(final Context context, final CharSequence text)1170 static CharSequence applyWarningTextColor(final Context context, 1171 final CharSequence text) { 1172 if (text == null) { 1173 return null; 1174 } 1175 final SpannableStringBuilder spanBuilder = new SpannableStringBuilder(); 1176 spanBuilder.append(text); 1177 spanBuilder.setSpan(new ForegroundColorSpan(context.getResources().getColor( 1178 R.color.notification_warning_color)), 0, text.length(), 1179 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1180 return spanBuilder; 1181 } 1182 1183 /** 1184 * Check for failed messages and post notifications as needed. 1185 * TODO: Rewrite this as a NotificationState. 1186 */ checkFailedMessages()1187 public static void checkFailedMessages() { 1188 final DatabaseWrapper db = DataModel.get().getDatabase(); 1189 1190 final Cursor messageDataCursor = db.query(DatabaseHelper.MESSAGES_TABLE, 1191 MessageData.getProjection(), 1192 FailedMessageQuery.FAILED_MESSAGES_WHERE_CLAUSE, 1193 null /*selectionArgs*/, 1194 null /*groupBy*/, 1195 null /*having*/, 1196 FailedMessageQuery.FAILED_ORDER_BY); 1197 1198 try { 1199 final Context context = Factory.get().getApplicationContext(); 1200 final Resources resources = context.getResources(); 1201 final NotificationManagerCompat notificationManager = 1202 NotificationManagerCompat.from(context); 1203 if (messageDataCursor != null) { 1204 final MessageData messageData = new MessageData(); 1205 1206 final HashSet<String> conversationsWithFailedMessages = new HashSet<String>(); 1207 1208 // track row ids in case we want to display something that requires this 1209 // information 1210 final ArrayList<Integer> failedMessages = new ArrayList<Integer>(); 1211 1212 int cursorPosition = -1; 1213 final long when = 0; 1214 1215 messageDataCursor.moveToPosition(-1); 1216 while (messageDataCursor.moveToNext()) { 1217 messageData.bind(messageDataCursor); 1218 1219 final String conversationId = messageData.getConversationId(); 1220 if (DataModel.get().isNewMessageObservable(conversationId)) { 1221 // Don't post a system notification for an observable conversation 1222 // because we already show an angry red annotation in the conversation 1223 // itself or in the conversation preview snippet. 1224 continue; 1225 } 1226 1227 cursorPosition = messageDataCursor.getPosition(); 1228 failedMessages.add(cursorPosition); 1229 conversationsWithFailedMessages.add(conversationId); 1230 } 1231 1232 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 1233 LogUtil.d(TAG, "Found " + failedMessages.size() + " failed messages"); 1234 } 1235 if (failedMessages.size() > 0) { 1236 final NotificationCompat.Builder builder = 1237 new NotificationCompat.Builder(context); 1238 1239 CharSequence line1; 1240 CharSequence line2; 1241 final boolean isRichContent = false; 1242 ConversationIdSet conversationIds = null; 1243 PendingIntent destinationIntent; 1244 if (failedMessages.size() == 1) { 1245 messageDataCursor.moveToPosition(cursorPosition); 1246 messageData.bind(messageDataCursor); 1247 final String conversationId = messageData.getConversationId(); 1248 1249 // We have a single conversation, go directly to that conversation. 1250 destinationIntent = UIIntents.get() 1251 .getPendingIntentForConversationActivity(context, 1252 conversationId, 1253 null /*draft*/); 1254 1255 conversationIds = ConversationIdSet.createSet(conversationId); 1256 1257 final String failedMessgeSnippet = messageData.getMessageText(); 1258 int failureStringId; 1259 if (messageData.getStatus() == 1260 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) { 1261 failureStringId = 1262 R.string.notification_download_failures_line1_singular; 1263 } else { 1264 failureStringId = R.string.notification_send_failures_line1_singular; 1265 } 1266 line1 = resources.getString(failureStringId); 1267 line2 = failedMessgeSnippet; 1268 // Set rich text for non-SMS messages or MMS push notification messages 1269 // which we generate locally with rich text 1270 // TODO- fix this 1271 // if (messageData.isMmsInd()) { 1272 // isRichContent = true; 1273 // } 1274 } else { 1275 // We have notifications for multiple conversation, go to the conversation 1276 // list. 1277 destinationIntent = UIIntents.get() 1278 .getPendingIntentForConversationListActivity(context); 1279 1280 int line1StringId; 1281 int line2PluralsId; 1282 if (messageData.getStatus() == 1283 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) { 1284 line1StringId = 1285 R.string.notification_download_failures_line1_plural; 1286 line2PluralsId = R.plurals.notification_download_failures; 1287 } else { 1288 line1StringId = R.string.notification_send_failures_line1_plural; 1289 line2PluralsId = R.plurals.notification_send_failures; 1290 } 1291 line1 = resources.getString(line1StringId); 1292 line2 = resources.getQuantityString( 1293 line2PluralsId, 1294 conversationsWithFailedMessages.size(), 1295 failedMessages.size(), 1296 conversationsWithFailedMessages.size()); 1297 } 1298 line1 = applyWarningTextColor(context, line1); 1299 line2 = applyWarningTextColor(context, line2); 1300 1301 final PendingIntent pendingIntentForDelete = 1302 UIIntents.get().getPendingIntentForClearingNotifications( 1303 context, 1304 BugleNotifications.UPDATE_ERRORS, 1305 conversationIds, 1306 0); 1307 1308 builder 1309 .setContentTitle(line1) 1310 .setTicker(line1) 1311 .setWhen(when > 0 ? when : System.currentTimeMillis()) 1312 .setSmallIcon(R.drawable.ic_failed_light) 1313 .setDeleteIntent(pendingIntentForDelete) 1314 .setContentIntent(destinationIntent) 1315 .setSound(UriUtil.getUriForResourceId(context, R.raw.message_failure)); 1316 if (isRichContent && !TextUtils.isEmpty(line2)) { 1317 final NotificationCompat.InboxStyle inboxStyle = 1318 new NotificationCompat.InboxStyle(builder); 1319 if (line2 != null) { 1320 inboxStyle.addLine(Html.fromHtml(line2.toString())); 1321 } 1322 builder.setStyle(inboxStyle); 1323 } else { 1324 builder.setContentText(line2); 1325 } 1326 1327 if (builder != null) { 1328 notificationManager.notify( 1329 BugleNotifications.buildNotificationTag( 1330 PendingIntentConstants.MSG_SEND_ERROR, null), 1331 PendingIntentConstants.MSG_SEND_ERROR, 1332 builder.build()); 1333 } 1334 } else { 1335 notificationManager.cancel( 1336 BugleNotifications.buildNotificationTag( 1337 PendingIntentConstants.MSG_SEND_ERROR, null), 1338 PendingIntentConstants.MSG_SEND_ERROR); 1339 } 1340 } 1341 } finally { 1342 if (messageDataCursor != null) { 1343 messageDataCursor.close(); 1344 } 1345 } 1346 } 1347 } 1348