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 Cursor participantsCursor = context.getContentResolver().query( 629 uri, ParticipantData.ParticipantsQuery.PROJECTION, null, null, null); 630 final ConversationParticipantsData participantsData = new ConversationParticipantsData(); 631 participantsData.bind(participantsCursor); 632 final Iterator<ParticipantData> iter = participantsData.iterator(); 633 634 final HashMap<String, Integer> firstNames = new HashMap<String, Integer>(); 635 boolean seenSelf = false; 636 while (iter.hasNext()) { 637 final ParticipantData participant = iter.next(); 638 // Make sure we only add the self participant once 639 if (participant.isSelf()) { 640 if (seenSelf) { 641 continue; 642 } else { 643 seenSelf = true; 644 } 645 } 646 647 final String firstName = participant.getFirstName(); 648 if (firstName == null) { 649 continue; 650 } 651 652 final int currentCount = firstNames.containsKey(firstName) 653 ? firstNames.get(firstName) 654 : 0; 655 firstNames.put(firstName, currentCount + 1); 656 } 657 return firstNames; 658 } 659 660 // Essentially, we're building a list of the past 20 messages for this conversation to display 661 // on the wearable. buildConversationPageForWearable(final String conversationId, int participantCount)662 public static Notification buildConversationPageForWearable(final String conversationId, 663 int participantCount) { 664 final Context context = Factory.get().getApplicationContext(); 665 666 // Limit the number of messages to show. We just want enough to provide context for the 667 // notification. Fetch one more than we need, so we can tell if there are more messages 668 // before the one we're showing. 669 // TODO: in the query, a multipart message will contain a row for each part. 670 // We might need a smarter GROUP_BY. On the other hand, we might want to show each of the 671 // parts as separate messages on the wearable. 672 final int limit = MAX_MESSAGES_IN_WEARABLE_PAGE + 1; 673 674 final List<CharSequence> messages = Lists.newArrayList(); 675 boolean hasSeenMessagesBeforeNotification = false; 676 Cursor convMessageCursor = null; 677 try { 678 final DatabaseWrapper db = DataModel.get().getDatabase(); 679 680 final String[] queryArgs = { conversationId }; 681 final String convPageSql = ConversationMessageData.getWearableQuerySql() + " LIMIT " + 682 limit; 683 convMessageCursor = db.rawQuery( 684 convPageSql, 685 queryArgs); 686 687 if (convMessageCursor == null || !convMessageCursor.moveToFirst()) { 688 return null; 689 } 690 final ConversationMessageData convMessageData = 691 new ConversationMessageData(); 692 693 final HashMap<String, Integer> firstNames = scanFirstNames(conversationId); 694 do { 695 convMessageData.bind(convMessageCursor); 696 697 final String authorFullName = convMessageData.getSenderFullName(); 698 final String authorFirstName = convMessageData.getSenderFirstName(); 699 String text = convMessageData.getText(); 700 701 final boolean isSmsPushNotification = convMessageData.getIsMmsNotification(); 702 703 // if auto-download was off to show a message to tap to download the message. We 704 // might need to get that working again. 705 if (isSmsPushNotification && text != null) { 706 text = convertHtmlAndStripUrls(text).toString(); 707 } 708 // Skip messages without any content 709 if (TextUtils.isEmpty(text) && !convMessageData.hasAttachments()) { 710 continue; 711 } 712 // Track whether there are messages prior to the one(s) shown in the notification. 713 if (convMessageData.getIsSeen()) { 714 hasSeenMessagesBeforeNotification = true; 715 } 716 717 final boolean usedMoreThanOnce = firstNameUsedMoreThanOnce( 718 firstNames, authorFirstName); 719 String displayName = usedMoreThanOnce ? authorFullName : authorFirstName; 720 if (TextUtils.isEmpty(displayName)) { 721 if (convMessageData.getIsIncoming()) { 722 displayName = convMessageData.getSenderDisplayDestination(); 723 if (TextUtils.isEmpty(displayName)) { 724 displayName = context.getString(R.string.unknown_sender); 725 } 726 } else { 727 displayName = context.getString(R.string.unknown_self_participant); 728 } 729 } 730 731 Uri attachmentUri = null; 732 String attachmentType = null; 733 final List<MessagePartData> attachments = convMessageData.getAttachments(); 734 for (final MessagePartData messagePartData : attachments) { 735 // Look for the first attachment that's not the text piece. 736 if (!messagePartData.isText()) { 737 attachmentUri = messagePartData.getContentUri(); 738 attachmentType = messagePartData.getContentType(); 739 break; 740 } 741 } 742 743 final CharSequence message = BugleNotifications.buildSpaceSeparatedMessage( 744 displayName, text, attachmentUri, attachmentType); 745 messages.add(message); 746 747 } while (convMessageCursor.moveToNext()); 748 } finally { 749 if (convMessageCursor != null) { 750 convMessageCursor.close(); 751 } 752 } 753 754 // If there is no conversation history prior to what is already visible in the main 755 // notification, there's no need to include the conversation log, too. 756 final int maxMessagesInNotification = getMaxMessagesInConversationNotification(); 757 if (!hasSeenMessagesBeforeNotification && messages.size() <= maxMessagesInNotification) { 758 return null; 759 } 760 761 final SpannableStringBuilder bigText = new SpannableStringBuilder(); 762 // There is at least 1 message prior to the first one that we're going to show. 763 // Indicate this by inserting an ellipsis at the beginning of the conversation log. 764 if (convMessageCursor.getCount() == limit) { 765 bigText.append(context.getString(R.string.ellipsis) + "\n\n"); 766 if (messages.size() > MAX_MESSAGES_IN_WEARABLE_PAGE) { 767 messages.remove(messages.size() - 1); 768 } 769 } 770 // Messages are sorted in descending timestamp order, so iterate backwards 771 // to get them back in ascending order for display purposes. 772 for (int i = messages.size() - 1; i >= 0; --i) { 773 bigText.append(messages.get(i)); 774 if (i > 0) { 775 bigText.append("\n\n"); 776 } 777 } 778 ++participantCount; // Add in myself 779 780 if (participantCount > 2) { 781 final SpannableString statusText = new SpannableString( 782 context.getResources().getQuantityString(R.plurals.wearable_participant_count, 783 participantCount, participantCount)); 784 statusText.setSpan(new ForegroundColorSpan(context.getResources().getColor( 785 R.color.wearable_notification_participants_count)), 0, statusText.length(), 786 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 787 bigText.append("\n\n").append(statusText); 788 } 789 790 final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(context); 791 final NotificationCompat.Style notifStyle = 792 new NotificationCompat.BigTextStyle(notifBuilder).bigText(bigText); 793 notifBuilder.setStyle(notifStyle); 794 795 final WearableExtender wearableExtender = new WearableExtender(); 796 wearableExtender.setStartScrollBottom(true); 797 notifBuilder.extend(wearableExtender); 798 799 return notifBuilder.build(); 800 } 801 802 /** 803 * Notification for one or more messages in a single conversation, which is bundled together 804 * with notifications for other conversations on a wearable device. 805 */ 806 public static class BundledMessageNotificationState extends MultiMessageNotificationState { 807 public int mGroupOrder; BundledMessageNotificationState(final ConversationInfoList convList, final int groupOrder)808 public BundledMessageNotificationState(final ConversationInfoList convList, 809 final int groupOrder) { 810 super(convList); 811 mGroupOrder = groupOrder; 812 } 813 } 814 815 /** 816 * Performs a query on the database. 817 */ createConversationInfoList()818 private static ConversationInfoList createConversationInfoList() { 819 // Map key is conversation id. We use LinkedHashMap to ensure that entries are iterated in 820 // the same order they were originally added. We scan unseen messages from newest to oldest, 821 // so the corresponding conversations are added in that order, too. 822 final Map<String, ConversationLineInfo> convLineInfos = new LinkedHashMap<>(); 823 int messageCount = 0; 824 825 Cursor convMessageCursor = null; 826 try { 827 final Context context = Factory.get().getApplicationContext(); 828 final DatabaseWrapper db = DataModel.get().getDatabase(); 829 830 convMessageCursor = db.rawQuery( 831 ConversationMessageData.getNotificationQuerySql(), 832 null); 833 834 if (convMessageCursor != null && convMessageCursor.moveToFirst()) { 835 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 836 LogUtil.v(TAG, "MessageNotificationState: Found unseen message notifications."); 837 } 838 final ConversationMessageData convMessageData = 839 new ConversationMessageData(); 840 841 HashMap<String, Integer> firstNames = null; 842 String conversationIdForFirstNames = null; 843 String groupConversationName = null; 844 final int maxMessages = getMaxMessagesInConversationNotification(); 845 846 do { 847 convMessageData.bind(convMessageCursor); 848 849 // First figure out if this is a valid message. 850 String authorFullName = convMessageData.getSenderFullName(); 851 String authorFirstName = convMessageData.getSenderFirstName(); 852 final String messageText = convMessageData.getText(); 853 854 final String convId = convMessageData.getConversationId(); 855 final String messageId = convMessageData.getMessageId(); 856 857 CharSequence text = messageText; 858 final boolean isManualDownloadNeeded = convMessageData.getIsMmsNotification(); 859 if (isManualDownloadNeeded) { 860 // Don't try and convert the text from html if it's sms and not a sms push 861 // notification. 862 Assert.equals(MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD, 863 convMessageData.getStatus()); 864 text = context.getResources().getString( 865 R.string.message_title_manual_download); 866 } 867 ConversationLineInfo currConvInfo = convLineInfos.get(convId); 868 if (currConvInfo == null) { 869 final ConversationListItemData convData = 870 ConversationListItemData.getExistingConversation(db, convId); 871 if (!convData.getNotificationEnabled()) { 872 // Skip conversations that have notifications disabled. 873 continue; 874 } 875 final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db, 876 convData.getSelfId()); 877 groupConversationName = convData.getName(); 878 final Uri avatarUri = AvatarUriUtil.createAvatarUri( 879 convMessageData.getSenderProfilePhotoUri(), 880 convMessageData.getSenderFullName(), 881 convMessageData.getSenderNormalizedDestination(), 882 convMessageData.getSenderContactLookupKey()); 883 currConvInfo = new ConversationLineInfo(convId, 884 convData.getIsGroup(), 885 groupConversationName, 886 convData.getIncludeEmailAddress(), 887 convMessageData.getReceivedTimeStamp(), 888 convData.getSelfId(), 889 convData.getNotificationSoundUri(), 890 convData.getNotificationEnabled(), 891 convData.getNotifiationVibrate(), 892 avatarUri, 893 convMessageData.getSenderContactLookupUri(), 894 subId, 895 convData.getParticipantCount()); 896 convLineInfos.put(convId, currConvInfo); 897 } 898 // Prepare the message line 899 if (currConvInfo.mTotalMessageCount < maxMessages) { 900 if (currConvInfo.mIsGroup) { 901 if (authorFirstName == null) { 902 // authorFullName might be null as well. In that case, we won't 903 // show an author. That is better than showing all the group 904 // names again on the 2nd line. 905 authorFirstName = authorFullName; 906 } 907 } else { 908 // don't recompute this if we don't need to 909 if (!TextUtils.equals(conversationIdForFirstNames, convId)) { 910 firstNames = scanFirstNames(convId); 911 conversationIdForFirstNames = convId; 912 } 913 if (firstNames != null) { 914 final Integer count = firstNames.get(authorFirstName); 915 if (count != null && count > 1) { 916 authorFirstName = authorFullName; 917 } 918 } 919 920 if (authorFullName == null) { 921 authorFullName = groupConversationName; 922 } 923 if (authorFirstName == null) { 924 authorFirstName = groupConversationName; 925 } 926 } 927 final String subjectText = MmsUtils.cleanseMmsSubject( 928 context.getResources(), 929 convMessageData.getMmsSubject()); 930 if (!TextUtils.isEmpty(subjectText)) { 931 final String subjectLabel = 932 context.getString(R.string.subject_label); 933 final SpannableStringBuilder spanBuilder = 934 new SpannableStringBuilder(); 935 936 spanBuilder.append(context.getString(R.string.notification_subject, 937 subjectLabel, subjectText)); 938 spanBuilder.setSpan(new TextAppearanceSpan( 939 context, R.style.NotificationSubjectText), 0, 940 subjectLabel.length(), 941 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 942 if (!TextUtils.isEmpty(text)) { 943 // Now add the actual message text below the subject header. 944 spanBuilder.append(System.getProperty("line.separator") + text); 945 } 946 text = spanBuilder; 947 } 948 // If we've got attachments, find the best one. If one of the messages is 949 // a photo, save the url so we'll display a big picture notification. 950 // Otherwise, show the first one we find. 951 Uri attachmentUri = null; 952 String attachmentType = null; 953 final MessagePartData messagePartData = 954 getMostInterestingAttachment(convMessageData); 955 if (messagePartData != null) { 956 attachmentUri = messagePartData.getContentUri(); 957 attachmentType = messagePartData.getContentType(); 958 } 959 currConvInfo.mLineInfos.add(new MessageLineInfo(currConvInfo.mIsGroup, 960 authorFullName, authorFirstName, text, 961 attachmentUri, attachmentType, isManualDownloadNeeded, messageId)); 962 } 963 messageCount++; 964 currConvInfo.mTotalMessageCount++; 965 } while (convMessageCursor.moveToNext()); 966 } 967 } finally { 968 if (convMessageCursor != null) { 969 convMessageCursor.close(); 970 } 971 } 972 if (convLineInfos.isEmpty()) { 973 return null; 974 } else { 975 return new ConversationInfoList(messageCount, 976 Lists.newLinkedList(convLineInfos.values())); 977 } 978 } 979 980 /** 981 * Scans all the attachments for a message and returns the most interesting one that we'll 982 * show in a notification. By order of importance, in case there are multiple attachments: 983 * 1- an image (because we can show the image as a BigPictureNotification) 984 * 2- a video (because we can show a video frame as a BigPictureNotification) 985 * 3- a vcard 986 * 4- an audio attachment 987 * @return MessagePartData for the most interesting part. Can be null. 988 */ getMostInterestingAttachment( final ConversationMessageData convMessageData)989 private static MessagePartData getMostInterestingAttachment( 990 final ConversationMessageData convMessageData) { 991 final List<MessagePartData> attachments = convMessageData.getAttachments(); 992 993 MessagePartData imagePart = null; 994 MessagePartData audioPart = null; 995 MessagePartData vcardPart = null; 996 MessagePartData videoPart = null; 997 998 // 99.99% of the time there will be 0 or 1 part, since receiving slideshows is so 999 // uncommon. 1000 1001 // Remember the first of each type of part. 1002 for (final MessagePartData messagePartData : attachments) { 1003 if (messagePartData.isImage() && imagePart == null) { 1004 imagePart = messagePartData; 1005 } 1006 if (messagePartData.isVideo() && videoPart == null) { 1007 videoPart = messagePartData; 1008 } 1009 if (messagePartData.isVCard() && vcardPart == null) { 1010 vcardPart = messagePartData; 1011 } 1012 if (messagePartData.isAudio() && audioPart == null) { 1013 audioPart = messagePartData; 1014 } 1015 } 1016 if (imagePart != null) { 1017 return imagePart; 1018 } else if (videoPart != null) { 1019 return videoPart; 1020 } else if (audioPart != null) { 1021 return audioPart; 1022 } else if (vcardPart != null) { 1023 return vcardPart; 1024 } 1025 return null; 1026 } 1027 getMaxMessagesInConversationNotification()1028 private static int getMaxMessagesInConversationNotification() { 1029 if (!BugleNotifications.isWearCompanionAppInstalled()) { 1030 return BugleGservices.get().getInt( 1031 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION, 1032 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_DEFAULT); 1033 } 1034 return BugleGservices.get().getInt( 1035 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE, 1036 BugleGservicesKeys.MAX_MESSAGES_IN_CONVERSATION_NOTIFICATION_WITH_WEARABLE_DEFAULT); 1037 } 1038 1039 /** 1040 * Scans the database for messages that need to go into notifications. Creates the appropriate 1041 * MessageNotificationState depending on if there are multiple senders, or 1042 * messages from one sender. 1043 * @return NotificationState for the notification created. 1044 */ getNotificationState()1045 public static NotificationState getNotificationState() { 1046 MessageNotificationState state = null; 1047 final ConversationInfoList convList = createConversationInfoList(); 1048 1049 if (convList == null || convList.mConvInfos.size() == 0) { 1050 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 1051 LogUtil.v(TAG, "MessageNotificationState: No unseen notifications"); 1052 } 1053 } else { 1054 final ConversationLineInfo convInfo = convList.mConvInfos.get(0); 1055 state = new MultiMessageNotificationState(convList); 1056 1057 if (convList.mConvInfos.size() > 1) { 1058 // We've got notifications across multiple conversations. Pass in the notification 1059 // we just built of the most recent notification so we can use that to show the 1060 // user the new message in the ticker. 1061 state = new MultiConversationNotificationState(convList, state); 1062 } else { 1063 // For now, only show avatars for notifications for a single conversation. 1064 if (convInfo.mAvatarUri != null) { 1065 if (state.mParticipantAvatarsUris == null) { 1066 state.mParticipantAvatarsUris = new ArrayList<Uri>(1); 1067 } 1068 state.mParticipantAvatarsUris.add(convInfo.mAvatarUri); 1069 } 1070 if (convInfo.mContactUri != null) { 1071 if (state.mParticipantContactUris == null) { 1072 state.mParticipantContactUris = new ArrayList<Uri>(1); 1073 } 1074 state.mParticipantContactUris.add(convInfo.mContactUri); 1075 } 1076 } 1077 } 1078 if (state != null && LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 1079 LogUtil.v(TAG, "MessageNotificationState: Notification state created" 1080 + ", title = " + LogUtil.sanitizePII(state.mTitle) 1081 + ", content = " + LogUtil.sanitizePII(state.mContent.toString())); 1082 } 1083 return state; 1084 } 1085 getTitle()1086 protected String getTitle() { 1087 return mTitle; 1088 } 1089 1090 @Override getLatestMessageNotificationType()1091 public int getLatestMessageNotificationType() { 1092 // This function is called to determine whether the most recent notification applies 1093 // to an sms conversation or a hangout conversation. We have different ringtone/vibrate 1094 // settings for both types of conversations. 1095 if (mConvList.mConvInfos.size() > 0) { 1096 final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0); 1097 return convInfo.getLatestMessageNotificationType(); 1098 } 1099 return BugleNotifications.LOCAL_SMS_NOTIFICATION; 1100 } 1101 1102 @Override getRingtoneUri()1103 public String getRingtoneUri() { 1104 if (mConvList.mConvInfos.size() > 0) { 1105 return mConvList.mConvInfos.get(0).mRingtoneUri; 1106 } 1107 return null; 1108 } 1109 1110 @Override getNotificationVibrate()1111 public boolean getNotificationVibrate() { 1112 if (mConvList.mConvInfos.size() > 0) { 1113 return mConvList.mConvInfos.get(0).mNotificationVibrate; 1114 } 1115 return false; 1116 } 1117 getTicker()1118 protected CharSequence getTicker() { 1119 return BugleNotifications.buildColonSeparatedMessage( 1120 mTickerSender != null ? mTickerSender : mTitle, 1121 mTickerText != null ? mTickerText : (mTickerNoContent ? null : mContent), null, 1122 null); 1123 } 1124 convertHtmlAndStripUrls(final String s)1125 private static CharSequence convertHtmlAndStripUrls(final String s) { 1126 final Spanned text = Html.fromHtml(s); 1127 if (text instanceof Spannable) { 1128 stripUrls((Spannable) text); 1129 } 1130 return text; 1131 } 1132 1133 // Since we don't want to show URLs in notifications, a function 1134 // to remove them in place. stripUrls(final Spannable text)1135 private static void stripUrls(final Spannable text) { 1136 final URLSpan[] spans = text.getSpans(0, text.length(), URLSpan.class); 1137 for (final URLSpan span : spans) { 1138 text.removeSpan(span); 1139 } 1140 } 1141 1142 /* 1143 private static void updateAlertStatusMessages(final long thresholdDeltaMs) { 1144 // TODO may need this when supporting error notifications 1145 final EsDatabaseHelper helper = EsDatabaseHelper.getDatabaseHelper(); 1146 final ContentValues values = new ContentValues(); 1147 final long nowMicros = System.currentTimeMillis() * 1000; 1148 values.put(MessageColumns.ALERT_STATUS, "1"); 1149 final String selection = 1150 MessageColumns.ALERT_STATUS + "=0 AND (" + 1151 MessageColumns.STATUS + "=" + EsProvider.MESSAGE_STATUS_FAILED_TO_SEND + " OR (" + 1152 MessageColumns.STATUS + "!=" + EsProvider.MESSAGE_STATUS_ON_SERVER + " AND " + 1153 MessageColumns.TIMESTAMP + "+" + thresholdDeltaMs*1000 + "<" + nowMicros + ")) "; 1154 1155 final int updateCount = helper.getWritableDatabaseWrapper().update( 1156 EsProvider.MESSAGES_TABLE, 1157 values, 1158 selection, 1159 null); 1160 if (updateCount > 0) { 1161 EsConversationsData.notifyConversationsChanged(); 1162 } 1163 }*/ 1164 applyWarningTextColor(final Context context, final CharSequence text)1165 static CharSequence applyWarningTextColor(final Context context, 1166 final CharSequence text) { 1167 if (text == null) { 1168 return null; 1169 } 1170 final SpannableStringBuilder spanBuilder = new SpannableStringBuilder(); 1171 spanBuilder.append(text); 1172 spanBuilder.setSpan(new ForegroundColorSpan(context.getResources().getColor( 1173 R.color.notification_warning_color)), 0, text.length(), 1174 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1175 return spanBuilder; 1176 } 1177 1178 /** 1179 * Check for failed messages and post notifications as needed. 1180 * TODO: Rewrite this as a NotificationState. 1181 */ checkFailedMessages()1182 public static void checkFailedMessages() { 1183 final DatabaseWrapper db = DataModel.get().getDatabase(); 1184 1185 final Cursor messageDataCursor = db.query(DatabaseHelper.MESSAGES_TABLE, 1186 MessageData.getProjection(), 1187 FailedMessageQuery.FAILED_MESSAGES_WHERE_CLAUSE, 1188 null /*selectionArgs*/, 1189 null /*groupBy*/, 1190 null /*having*/, 1191 FailedMessageQuery.FAILED_ORDER_BY); 1192 1193 try { 1194 final Context context = Factory.get().getApplicationContext(); 1195 final Resources resources = context.getResources(); 1196 final NotificationManagerCompat notificationManager = 1197 NotificationManagerCompat.from(context); 1198 if (messageDataCursor != null) { 1199 final MessageData messageData = new MessageData(); 1200 1201 final HashSet<String> conversationsWithFailedMessages = new HashSet<String>(); 1202 1203 // track row ids in case we want to display something that requires this 1204 // information 1205 final ArrayList<Integer> failedMessages = new ArrayList<Integer>(); 1206 1207 int cursorPosition = -1; 1208 final long when = 0; 1209 1210 messageDataCursor.moveToPosition(-1); 1211 while (messageDataCursor.moveToNext()) { 1212 messageData.bind(messageDataCursor); 1213 1214 final String conversationId = messageData.getConversationId(); 1215 if (DataModel.get().isNewMessageObservable(conversationId)) { 1216 // Don't post a system notification for an observable conversation 1217 // because we already show an angry red annotation in the conversation 1218 // itself or in the conversation preview snippet. 1219 continue; 1220 } 1221 1222 cursorPosition = messageDataCursor.getPosition(); 1223 failedMessages.add(cursorPosition); 1224 conversationsWithFailedMessages.add(conversationId); 1225 } 1226 1227 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 1228 LogUtil.d(TAG, "Found " + failedMessages.size() + " failed messages"); 1229 } 1230 if (failedMessages.size() > 0) { 1231 final NotificationCompat.Builder builder = 1232 new NotificationCompat.Builder(context); 1233 1234 CharSequence line1; 1235 CharSequence line2; 1236 final boolean isRichContent = false; 1237 ConversationIdSet conversationIds = null; 1238 PendingIntent destinationIntent; 1239 if (failedMessages.size() == 1) { 1240 messageDataCursor.moveToPosition(cursorPosition); 1241 messageData.bind(messageDataCursor); 1242 final String conversationId = messageData.getConversationId(); 1243 1244 // We have a single conversation, go directly to that conversation. 1245 destinationIntent = UIIntents.get() 1246 .getPendingIntentForConversationActivity(context, 1247 conversationId, 1248 null /*draft*/); 1249 1250 conversationIds = ConversationIdSet.createSet(conversationId); 1251 1252 final String failedMessgeSnippet = messageData.getMessageText(); 1253 int failureStringId; 1254 if (messageData.getStatus() == 1255 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) { 1256 failureStringId = 1257 R.string.notification_download_failures_line1_singular; 1258 } else { 1259 failureStringId = R.string.notification_send_failures_line1_singular; 1260 } 1261 line1 = resources.getString(failureStringId); 1262 line2 = failedMessgeSnippet; 1263 // Set rich text for non-SMS messages or MMS push notification messages 1264 // which we generate locally with rich text 1265 // TODO- fix this 1266 // if (messageData.isMmsInd()) { 1267 // isRichContent = true; 1268 // } 1269 } else { 1270 // We have notifications for multiple conversation, go to the conversation 1271 // list. 1272 destinationIntent = UIIntents.get() 1273 .getPendingIntentForConversationListActivity(context); 1274 1275 int line1StringId; 1276 int line2PluralsId; 1277 if (messageData.getStatus() == 1278 MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED) { 1279 line1StringId = 1280 R.string.notification_download_failures_line1_plural; 1281 line2PluralsId = R.plurals.notification_download_failures; 1282 } else { 1283 line1StringId = R.string.notification_send_failures_line1_plural; 1284 line2PluralsId = R.plurals.notification_send_failures; 1285 } 1286 line1 = resources.getString(line1StringId); 1287 line2 = resources.getQuantityString( 1288 line2PluralsId, 1289 conversationsWithFailedMessages.size(), 1290 failedMessages.size(), 1291 conversationsWithFailedMessages.size()); 1292 } 1293 line1 = applyWarningTextColor(context, line1); 1294 line2 = applyWarningTextColor(context, line2); 1295 1296 final PendingIntent pendingIntentForDelete = 1297 UIIntents.get().getPendingIntentForClearingNotifications( 1298 context, 1299 BugleNotifications.UPDATE_ERRORS, 1300 conversationIds, 1301 0); 1302 1303 builder 1304 .setContentTitle(line1) 1305 .setTicker(line1) 1306 .setWhen(when > 0 ? when : System.currentTimeMillis()) 1307 .setSmallIcon(R.drawable.ic_failed_light) 1308 .setDeleteIntent(pendingIntentForDelete) 1309 .setContentIntent(destinationIntent) 1310 .setSound(UriUtil.getUriForResourceId(context, R.raw.message_failure)); 1311 if (isRichContent && !TextUtils.isEmpty(line2)) { 1312 final NotificationCompat.InboxStyle inboxStyle = 1313 new NotificationCompat.InboxStyle(builder); 1314 if (line2 != null) { 1315 inboxStyle.addLine(Html.fromHtml(line2.toString())); 1316 } 1317 builder.setStyle(inboxStyle); 1318 } else { 1319 builder.setContentText(line2); 1320 } 1321 1322 if (builder != null) { 1323 notificationManager.notify( 1324 BugleNotifications.buildNotificationTag( 1325 PendingIntentConstants.MSG_SEND_ERROR, null), 1326 PendingIntentConstants.MSG_SEND_ERROR, 1327 builder.build()); 1328 } 1329 } else { 1330 notificationManager.cancel( 1331 BugleNotifications.buildNotificationTag( 1332 PendingIntentConstants.MSG_SEND_ERROR, null), 1333 PendingIntentConstants.MSG_SEND_ERROR); 1334 } 1335 } 1336 } finally { 1337 if (messageDataCursor != null) { 1338 messageDataCursor.close(); 1339 } 1340 } 1341 } 1342 } 1343