From f8bb065ffd0a76417c91e6df2ad8fcb49a9ee165 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Fri, 31 Jul 2015 16:46:17 -0700 Subject: [PATCH] Support for images in notifications. Closes #3859 Fixes #1858 // FREEBIE --- .../securesms/jobs/AttachmentDownloadJob.java | 3 + .../notifications/MessageNotifier.java | 25 +++--- .../notifications/NotificationItem.java | 25 ++++-- .../SingleRecipientNotificationBuilder.java | 78 ++++++++++++++++--- 4 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index dc02db32b2..1d40683ac2 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.database.PartDatabase.PartId; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; @@ -66,6 +67,8 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable retrievePart(masterSecret, part, messageId); Log.w(TAG, "Got part: " + part.getPartId()); } + + MessageNotifier.updateNotification(context, masterSecret); } @Override diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 285f49acea..ff32c9fdda 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -42,13 +42,17 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; import java.io.IOException; @@ -187,12 +191,12 @@ public class MessageNotifier { return; } - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); + SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, masterSecret, TextSecurePreferences.getNotificationPrivacy(context)); List notifications = notificationState.getNotifications(); builder.setSender(notifications.get(0).getIndividualRecipient()); builder.setMessageCount(notificationState.getMessageCount()); - builder.setPrimaryMessageBody(notifications.get(0).getText()); + builder.setPrimaryMessageBody(notifications.get(0).getText(), notifications.get(0).getSlideDeck()); builder.setContentIntent(notifications.get(0).getPendingIntent(context)); long timestamp = notifications.get(0).getTimestamp(); @@ -318,7 +322,7 @@ public class MessageNotifier { body.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (!recipients.isMuted()) { - notificationState.addNotification(new NotificationItem(recipient, recipients, null, threadId, body, 0)); + notificationState.addNotification(new NotificationItem(recipient, recipients, null, threadId, body, 0, null)); } } } finally { @@ -339,11 +343,12 @@ public class MessageNotifier { else reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor, masterSecret); while ((record = reader.getNext()) != null) { - Recipient recipient = record.getIndividualRecipient(); - Recipients recipients = record.getRecipients(); - long threadId = record.getThreadId(); - CharSequence body = record.getDisplayBody(); - Recipients threadRecipients = null; + Recipient recipient = record.getIndividualRecipient(); + Recipients recipients = record.getRecipients(); + long threadId = record.getThreadId(); + CharSequence body = record.getDisplayBody(); + Recipients threadRecipients = null; + ListenableFutureTask slideDeck = null; long timestamp; if (record.isPush()) timestamp = record.getDateSent(); @@ -357,14 +362,16 @@ public class MessageNotifier { body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); } else if (record.isMms() && TextUtils.isEmpty(body)) { body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); + slideDeck = ((MediaMmsMessageRecord)record).getSlideDeckFuture(); } else if (record.isMms() && !record.isMmsNotification()) { String message = context.getString(R.string.MessageNotifier_media_message_with_text, body); int italicLength = message.length() - body.length(); body = SpanUtil.italic(message, italicLength); + slideDeck = ((MediaMmsMessageRecord)record).getSlideDeckFuture(); } if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(recipient, recipients, threadRecipients, threadId, body, timestamp)); + notificationState.addNotification(new NotificationItem(recipient, recipients, threadRecipients, threadId, body, timestamp, slideDeck)); } } diff --git a/src/org/thoughtcrime/securesms/notifications/NotificationItem.java b/src/org/thoughtcrime/securesms/notifications/NotificationItem.java index 8c45fc9270..1b2fae2296 100644 --- a/src/org/thoughtcrime/securesms/notifications/NotificationItem.java +++ b/src/org/thoughtcrime/securesms/notifications/NotificationItem.java @@ -4,23 +4,29 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.support.annotation.Nullable; import org.thoughtcrime.securesms.ConversationActivity; +import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.ListenableFutureTask; +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; public class NotificationItem { - private final Recipients recipients; - private final Recipient individualRecipient; - private final Recipients threadRecipients; - private final long threadId; - private final CharSequence text; - private final long timestamp; + private final Recipients recipients; + private final Recipient individualRecipient; + private final Recipients threadRecipients; + private final long threadId; + private final CharSequence text; + private final long timestamp; + private final ListenableFutureTask slideDeck; public NotificationItem(Recipient individualRecipient, Recipients recipients, Recipients threadRecipients, long threadId, - CharSequence text, long timestamp) + CharSequence text, long timestamp, + @Nullable ListenableFutureTask slideDeck) { this.individualRecipient = individualRecipient; this.recipients = recipients; @@ -28,6 +34,7 @@ public class NotificationItem { this.text = text; this.threadId = threadId; this.timestamp = timestamp; + this.slideDeck = slideDeck; } public Recipients getRecipients() { @@ -50,6 +57,10 @@ public class NotificationItem { return threadId; } + public @Nullable ListenableFutureTask getSlideDeck() { + return slideDeck; + } + public PendingIntent getPendingIntent(Context context) { Intent intent = new Intent(context, ConversationActivity.class); Recipients notifyRecipients = threadRecipients != null ? threadRecipients : recipients; diff --git a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 8ac46008cf..d8a329ce7b 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -6,28 +6,46 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Action; import android.support.v4.app.RemoteInput; import android.text.SpannableStringBuilder; +import android.util.Log; + +import com.bumptech.glide.Glide; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; +import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.preferences.NotificationPrivacyPreference; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.ListenableFutureTask; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.ExecutionException; public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder { + private static final String TAG = SingleRecipientNotificationBuilder.class.getSimpleName(); + private final List messageBodies = new LinkedList<>(); - public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy) { + private ListenableFutureTask slideDeck; + private final MasterSecret masterSecret; + + public SingleRecipientNotificationBuilder(@NonNull Context context, + @Nullable MasterSecret masterSecret, + @NonNull NotificationPrivacyPreference privacy) + { super(context, privacy); + this.masterSecret = masterSecret; setSmallIcon(R.drawable.icon_notification); setColor(context.getResources().getColor(R.color.textsecure_primary)); @@ -62,9 +80,10 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil setNumber(messageCount); } - public void setPrimaryMessageBody(CharSequence message) { + public void setPrimaryMessageBody(CharSequence message, @Nullable ListenableFutureTask slideDeck) { if (privacy.isDisplayMessage()) { setContentText(message); + this.slideDeck = slideDeck; } else { setContentText(context.getString(R.string.SingleRecipientNotificationBuilder_contents_hidden)); } @@ -122,14 +141,14 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil @Override public Notification build() { if (privacy.isDisplayMessage()) { - SpannableStringBuilder content = new SpannableStringBuilder(); - - for (CharSequence message : messageBodies) { - content.append(message); - content.append('\n'); + if (messageBodies.size() == 1 && hasBigPictureSlide(slideDeck)) { + assert masterSecret != null; + setStyle(new NotificationCompat.BigPictureStyle() + .bigPicture(getBigPicture(masterSecret, slideDeck)) + .setSummaryText(getBigText(messageBodies))); + } else { + setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText(messageBodies))); } - - setStyle(new NotificationCompat.BigTextStyle().bigText(content)); } return super.build(); @@ -146,4 +165,45 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil } } + private boolean hasBigPictureSlide(@Nullable ListenableFutureTask slideDeck) { + try { + return masterSecret != null && + slideDeck != null && + Build.VERSION.SDK_INT >= 16 && + slideDeck.get().getThumbnailSlide(context).hasImage() && + !slideDeck.get().getThumbnailSlide(context).isInProgress() && + slideDeck.get().getThumbnailSlide(context).getThumbnailUri() != null; + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + return false; + } + } + + private Bitmap getBigPicture(@NonNull MasterSecret masterSecret, + @NonNull ListenableFutureTask slideDeck) + { + try { + Uri uri = slideDeck.get().getThumbnailSlide(context).getThumbnailUri(); + + return Glide.with(context) + .load(new DecryptableStreamUriLoader.DecryptableUri(masterSecret, uri)) + .asBitmap() + .into(500, 500) + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new AssertionError(e); + } + } + + private CharSequence getBigText(List messageBodies) { + SpannableStringBuilder content = new SpannableStringBuilder(); + + for (CharSequence message : messageBodies) { + content.append(message); + content.append('\n'); + } + + return content; + } + }