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