From 3bd8aa8a86cfb69331af2b79b544f24fb648cd62 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 19 Dec 2019 11:42:10 -0400 Subject: [PATCH] Apply MessageStyle and fix chronology issues. --- .../notifications/MessageNotifier.java | 78 ++++++--- .../SingleRecipientNotificationBuilder.java | 159 +++++++++++++----- 2 files changed, 173 insertions(+), 64 deletions(-) diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index f3c419d46a..0e938e7bfd 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -37,7 +37,6 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import androidx.core.text.HtmlCompat; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -71,11 +70,9 @@ import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.whispersystems.signalservice.internal.util.Util; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -170,6 +167,30 @@ public class MessageNotifier { } } + private static boolean isDisplayingSummaryNotification(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= 23) { + try { + NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); + StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); + + for (StatusBarNotification activeNotification : activeNotifications) { + if (activeNotification.getId() == SUMMARY_NOTIFICATION_ID) { + return true; + } + } + + return false; + + } catch (Throwable e) { + // XXX Android ROM Bug, see #6043 + Log.w(TAG, e); + return false; + } + } else { + return false; + } + } + private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { if (Build.VERSION.SDK_INT >= 23) { try { @@ -209,7 +230,7 @@ public class MessageNotifier { return; } - updateNotification(context, false, 0); + updateNotification(context, -1, false, 0); } public static void updateNotification(@NonNull Context context, long threadId) @@ -226,7 +247,7 @@ public class MessageNotifier { long threadId, boolean signal) { - boolean isVisible = visibleThread == threadId; + boolean isVisible = visibleThread == threadId; ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); Recipient recipients = DatabaseFactory.getThreadDatabase(context) @@ -246,11 +267,12 @@ public class MessageNotifier { if (isVisible) { sendInThreadNotification(context, threads.getRecipientForThreadId(threadId)); } else { - updateNotification(context, signal, 0); + updateNotification(context, threadId, signal, 0); } } private static void updateNotification(@NonNull Context context, + long targetThread, boolean signal, int reminderCount) { @@ -281,13 +303,22 @@ public class MessageNotifier { if (notificationState.hasMultipleThreads()) { if (Build.VERSION.SDK_INT >= 23) { for (long threadId : notificationState.getThreads()) { - sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true); + if (targetThread < 1 || targetThread == threadId) { + sendSingleThreadNotification(context, + new NotificationState(notificationState.getNotificationsForThread(threadId)), + signal && (threadId == targetThread), + true); + } } } - sendMultipleThreadNotification(context, notificationState, signal); + sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23)); } else { sendSingleThreadNotification(context, notificationState, signal, false); + + if (isDisplayingSummaryNotification(context)) { + sendMultipleThreadNotification(context, notificationState, false); + } } cancelOrphanedNotifications(context, notificationState); @@ -302,9 +333,10 @@ public class MessageNotifier { } } - private static void sendSingleThreadNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal, boolean bundled) + private static void sendSingleThreadNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal, + boolean bundled) { Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled); @@ -317,8 +349,13 @@ public class MessageNotifier { SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); List notifications = notificationState.getNotifications(); Recipient recipient = notifications.get(0).getRecipient(); - int notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0)); + int notificationId; + if (Build.VERSION.SDK_INT >= 23) { + notificationId = (int) (SUMMARY_NOTIFICATION_ID + notifications.get(0).getThreadId()); + } else { + notificationId = SUMMARY_NOTIFICATION_ID; + } builder.setThread(notifications.get(0).getRecipient()); builder.setMessageCount(notificationState.getMessageCount()); @@ -327,7 +364,7 @@ public class MessageNotifier { builder.setContentIntent(notifications.get(0).getPendingIntent(context)); builder.setDeleteIntent(notificationState.getDeleteIntent(context)); builder.setOnlyAlertOnce(!signal); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp())); long timestamp = notifications.get(0).getTimestamp(); if (timestamp != 0) builder.setWhen(timestamp); @@ -348,7 +385,7 @@ public class MessageNotifier { while(iterator.hasPrevious()) { NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText()); + builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp()); } if (signal) { @@ -357,9 +394,9 @@ public class MessageNotifier { notifications.get(0).getText()); } - if (bundled) { + if (Build.VERSION.SDK_INT >= 23) { builder.setGroup(NOTIFICATION_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); } Notification notification = builder.build(); @@ -378,10 +415,13 @@ public class MessageNotifier { builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount()); builder.setMostRecentSender(notifications.get(0).getIndividualRecipient()); - builder.setGroup(NOTIFICATION_GROUP); builder.setDeleteIntent(notificationState.getDeleteIntent(context)); builder.setOnlyAlertOnce(!signal); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + + if (Build.VERSION.SDK_INT >= 23) { + builder.setGroup(NOTIFICATION_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + } long timestamp = notifications.get(0).getTimestamp(); if (timestamp != 0) builder.setWhen(timestamp); @@ -599,7 +639,7 @@ public class MessageNotifier { @Override protected Void doInBackground(Void... params) { int reminderCount = intent.getIntExtra("reminder_count", 0); - MessageNotifier.updateNotification(context, true, reminderCount + 1); + MessageNotifier.updateNotification(context, -1, true, reminderCount + 1); return null; } diff --git a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 0e904d7680..5821a4e958 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -13,9 +13,13 @@ import androidx.annotation.StringRes; import androidx.appcompat.view.ContextThemeWrapper; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Action; +import androidx.core.app.Person; import androidx.core.app.RemoteInput; +import androidx.core.graphics.drawable.IconCompat; + import android.text.SpannableStringBuilder; +import com.annimon.stream.Stream; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; @@ -46,11 +50,12 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private static final int BIG_PICTURE_DIMEN = 500; private static final int LARGE_ICON_DIMEN = 250; - private final List messageBodies = new LinkedList<>(); + private final List messages = new LinkedList<>(); private SlideDeck slideDeck; private CharSequence contentTitle; private CharSequence contentText; + private Recipient threadRecipient; public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) { @@ -76,25 +81,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil addPerson(recipient.getContactUri().toString()); } - ContactPhoto contactPhoto = recipient.getContactPhoto(); - FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto(); - - if (contactPhoto != null) { - try { - setLargeIcon(GlideApp.with(context.getApplicationContext()) - .load(contactPhoto) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .circleCrop() - .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), - context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) - .get()); - } catch (InterruptedException | ExecutionException e) { - Log.w(TAG, e); - setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context))); - } - } else { - setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context))); - } + setLargeIcon(getContactDrawable(recipient)); } else { setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); @@ -102,6 +89,27 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } } + private Drawable getContactDrawable(@NonNull Recipient recipient) { + ContactPhoto contactPhoto = recipient.getContactPhoto(); + FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto(); + + if (contactPhoto != null) { + try { + return GlideApp.with(context.getApplicationContext()) + .load(contactPhoto) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .circleCrop() + .submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width), + context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height)) + .get(); + } catch (InterruptedException | ExecutionException e) { + return fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context)); + } + } else { + return fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context)); + } + } + public void setMessageCount(int messageCount) { setContentInfo(String.valueOf(messageCount)); setNumber(messageCount); @@ -139,10 +147,10 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder = new NotificationCompat.CarExtender.UnreadConversation.Builder(contentTitle.toString()) - .addMessage(contentText.toString()) - .setLatestTimestamp(timestamp) - .setReadPendingIntent(androidAutoHeardIntent) - .setReplyAction(androidAutoReplyIntent, remoteInput); + .addMessage(contentText.toString()) + .setLatestTimestamp(timestamp) + .setReadPendingIntent(androidAutoHeardIntent) + .setReplyAction(androidAutoReplyIntent, remoteInput); extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build())); } @@ -202,19 +210,35 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil public void addMessageBody(@NonNull Recipient threadRecipient, @NonNull Recipient individualRecipient, - @Nullable CharSequence messageBody) + @Nullable CharSequence messageBody, + long timestamp) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); + Person.Builder personBuilder = new Person.Builder() + .setKey(individualRecipient.getId().serialize()) + .setBot(false); - if (privacy.isDisplayContact() && threadRecipient.isGroup()) { - stringBuilder.append(Util.getBoldedString(individualRecipient.toShortString(context) + ": ")); - } + this.threadRecipient = threadRecipient; - if (privacy.isDisplayMessage()) { - messageBodies.add(stringBuilder.append(messageBody == null ? "" : messageBody)); + if (privacy.isDisplayContact()) { + personBuilder.setName(individualRecipient.getDisplayName(context)); + + Bitmap bitmap = getLargeBitmap(getContactDrawable(individualRecipient)); + if (bitmap != null) { + personBuilder.setIcon(IconCompat.createWithBitmap(bitmap)); + } } else { - messageBodies.add(stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message))); + personBuilder.setName(""); } + + final CharSequence text; + if (privacy.isDisplayMessage()) { + text = messageBody == null ? "" : messageBody; + } else { + text = stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message)); + } + + messages.add(new NotificationCompat.MessagingStyle.Message(text, timestamp, personBuilder.build())); } @Override @@ -223,33 +247,68 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil Optional largeIconUri = getLargeIconUri(slideDeck); Optional bigPictureUri = getBigPictureUri(slideDeck); - if (messageBodies.size() == 1 && largeIconUri.isPresent()) { + if (messages.size() == 1 && largeIconUri.isPresent()) { setLargeIcon(getNotificationPicture(largeIconUri.get(), LARGE_ICON_DIMEN)); } - if (messageBodies.size() == 1 && bigPictureUri.isPresent()) { + if (messages.size() == 1 && bigPictureUri.isPresent()) { setStyle(new NotificationCompat.BigPictureStyle() .bigPicture(getNotificationPicture(bigPictureUri.get(), BIG_PICTURE_DIMEN)) - .setSummaryText(getBigText(messageBodies))); + .setSummaryText(getBigText())); } else { - setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText(messageBodies))); + if (Build.VERSION.SDK_INT >= 24) { + applyMessageStyle(); + } else { + applyLegacy(); + } } } return super.build(); } + private void applyMessageStyle() { + NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle( + new Person.Builder() + .setBot(false) + .setName(Recipient.self().getDisplayName(context)) + .setKey(Recipient.self().getId().serialize()) + .build()); + + if (threadRecipient.isGroup()) { + if (privacy.isDisplayContact()) { + messagingStyle.setConversationTitle(threadRecipient.getDisplayName(context)); + } else { + messagingStyle.setConversationTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal)); + } + + messagingStyle.setGroupConversation(true); + } + + Stream.of(messages).forEach(messagingStyle::addMessage); + setStyle(messagingStyle); + } + + private void applyLegacy() { + setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText())); + } + private void setLargeIcon(@Nullable Drawable drawable) { if (drawable != null) { - int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize); - - if (recipientPhotoBitmap != null) { - setLargeIcon(recipientPhotoBitmap); - } + setLargeIcon(getLargeBitmap(drawable)); } } + private @Nullable Bitmap getLargeBitmap(@Nullable Drawable drawable) { + if (drawable != null) { + int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); + + return BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize); + } + + return null; + } + private static Optional getLargeIconUri(@Nullable SlideDeck slideDeck) { if (slideDeck == null) { return Optional.absent(); @@ -302,12 +361,12 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil return super.setContentText(this.contentText); } - private CharSequence getBigText(List messageBodies) { + private CharSequence getBigText() { SpannableStringBuilder content = new SpannableStringBuilder(); - for (int i = 0; i < messageBodies.size(); i++) { - content.append(trimToDisplayLength(messageBodies.get(i))); - if (i < messageBodies.size() - 1) { + for (int i = 0; i < messages.size(); i++) { + content.append(getBigTextFor(messages.get(i))); + if (i < messages.size() - 1) { content.append('\n'); } } @@ -315,4 +374,14 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil return content; } + private CharSequence getBigTextFor(NotificationCompat.MessagingStyle.Message message) { + SpannableStringBuilder content = new SpannableStringBuilder(); + + if (message.getPerson() != null && message.getPerson().getName() != null && threadRecipient.isGroup()) { + content.append(Util.getBoldedString(message.getPerson().getName().toString())).append(": "); + } + + return trimToDisplayLength(content.append(message.getText())); + } + }