diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index c06d9ee4f8..ba41fb6527 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -17,7 +17,6 @@ package org.thoughtcrime.securesms.conversationlist; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.Typeface; import android.text.Spannable; import android.text.SpannableString; @@ -32,16 +31,13 @@ import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; -import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import com.makeramen.roundedimageview.RoundedDrawable; import org.signal.core.util.DimensionUnit; @@ -64,6 +60,7 @@ import org.thoughtcrime.securesms.database.model.LiveUpdateMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; +import org.thoughtcrime.securesms.glide.GlideLiveDataTarget; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.LiveRecipient; @@ -82,9 +79,6 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.Collections; import java.util.Locale; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync; @@ -519,17 +513,16 @@ public final class ConversationListItem extends ConstraintLayout } else if (extra != null && extra.isRemoteDelete()) { return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint); } else { - String body = removeNewlines(thread.getBody()); - - LiveData finalBody = recipientToStringAsync(thread.getRecipient().getId(), threadRecipient -> { - CharSequence bodyWithMediaIcon = createFinalBodyWithMediaIcon(context, body, thread, glideRequests); - if (threadRecipient.isGroup()) { + String body = removeNewlines(thread.getBody()); + LiveData finalBody = LiveDataUtil.mapAsync(createFinalBodyWithMediaIcon(context, body, thread, glideRequests), updatedBody -> { + if (thread.getRecipient().isGroup()) { RecipientId groupMessageSender = thread.getGroupMessageSender(); if (!groupMessageSender.isUnknown()) { - return createGroupMessageUpdateString(context, bodyWithMediaIcon, Recipient.resolved(groupMessageSender), thread.isRead()); + return createGroupMessageUpdateString(context, updatedBody, Recipient.resolved(groupMessageSender), thread.isRead()); } } - return new SpannableString(bodyWithMediaIcon); + + return new SpannableString(updatedBody); }); return whileLoadingShow(body, finalBody); @@ -537,63 +530,57 @@ public final class ConversationListItem extends ConstraintLayout } } - @WorkerThread - private static CharSequence createFinalBodyWithMediaIcon(@NonNull Context context, - @NonNull String body, - @NonNull ThreadRecord thread, - @NonNull GlideRequests glideRequests) + private static LiveData createFinalBodyWithMediaIcon(@NonNull Context context, + @NonNull String body, + @NonNull ThreadRecord thread, + @NonNull GlideRequests glideRequests) { - if (thread.getSnippetUri() != null) { - try { - int thumbSize = (int) DimensionUnit.SP.toPixels(20f); - Bitmap thumb = glideRequests.asBitmap() - .load(new DecryptableStreamUriLoader.DecryptableUri(thread.getSnippetUri())) - .override(thumbSize, thumbSize) - .transform( - new OverlayTransformation(ContextCompat.getColor(context, R.color.transparent_black_08)), - new CenterCrop() - ) - .submit() - .get(1, TimeUnit.SECONDS); - - RoundedDrawable drawable = RoundedDrawable.fromBitmap(thumb); - drawable.setBounds(0, 0, thumbSize, thumbSize); - drawable.setCornerRadius(DimensionUnit.DP.toPixels(4)); - drawable.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - - CharSequence span = SpanUtil.buildCenteredImageSpan(drawable); - - final String withoutPrefix; - - if (body.startsWith(EmojiStrings.GIF)) { - withoutPrefix = body.replace(EmojiStrings.GIF, ""); - } else if (body.startsWith(EmojiStrings.VIDEO)) { - withoutPrefix = body.replace(EmojiStrings.VIDEO, ""); - } else if (body.startsWith(EmojiStrings.PHOTO)) { - withoutPrefix = body.replace(EmojiStrings.PHOTO, ""); - } else if (thread.getExtra() != null && thread.getExtra().getStickerEmoji() != null && body.startsWith(thread.getExtra().getStickerEmoji())) { - withoutPrefix = body.replace(thread.getExtra().getStickerEmoji(), ""); - } else { - withoutPrefix = null; - } - - if (withoutPrefix != null) { - return new SpannableStringBuilder() - .append(span) - .append(withoutPrefix); - } else { - return body; - } - - } catch (ExecutionException | InterruptedException e) { - return new SpannableString(body); - } catch (TimeoutException e) { - Log.w(TAG, "Hit a timeout when generating a thumbnail! " + thread.getSnippetUri()); - return new SpannableString(body); - } - } else { - return new SpannableString(body); + if (thread.getSnippetUri() == null) { + return LiveDataUtil.just(body); } + + final String bodyWithoutMediaPrefix; + + if (body.startsWith(EmojiStrings.GIF)) { + bodyWithoutMediaPrefix = body.replace(EmojiStrings.GIF, ""); + } else if (body.startsWith(EmojiStrings.VIDEO)) { + bodyWithoutMediaPrefix = body.replace(EmojiStrings.VIDEO, ""); + } else if (body.startsWith(EmojiStrings.PHOTO)) { + bodyWithoutMediaPrefix = body.replace(EmojiStrings.PHOTO, ""); + } else if (thread.getExtra() != null && thread.getExtra().getStickerEmoji() != null && body.startsWith(thread.getExtra().getStickerEmoji())) { + bodyWithoutMediaPrefix = body.replace(thread.getExtra().getStickerEmoji(), ""); + } else { + return LiveDataUtil.just(body); + } + + int thumbSize = (int) DimensionUnit.SP.toPixels(20f); + GlideLiveDataTarget target = new GlideLiveDataTarget(thumbSize, thumbSize); + + glideRequests.asBitmap() + .load(new DecryptableStreamUriLoader.DecryptableUri(thread.getSnippetUri())) + .override(thumbSize, thumbSize) + .transform( + new OverlayTransformation(ContextCompat.getColor(context, R.color.transparent_black_08)), + new CenterCrop() + ) + .into(target); + + return Transformations.map(target.getLiveData(), bitmap -> { + if (bitmap == null) { + return body; + } + + RoundedDrawable drawable = RoundedDrawable.fromBitmap(bitmap); + drawable.setBounds(0, 0, thumbSize, thumbSize); + drawable.setCornerRadius(DimensionUnit.DP.toPixels(4)); + drawable.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + + CharSequence thumbnailSpan = SpanUtil.buildCenteredImageSpan(drawable); + + return new SpannableStringBuilder() + .append(thumbnailSpan) + .append(bodyWithoutMediaPrefix); + }); } private static SpannableString createGroupMessageUpdateString(@NonNull Context context, diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/GlideLiveDataTarget.java b/app/src/main/java/org/thoughtcrime/securesms/glide/GlideLiveDataTarget.java new file mode 100644 index 0000000000..889fe87a2d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/GlideLiveDataTarget.java @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.glide; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; + +/** + * A Glide target that exposes a LiveData that can be observed. + * + * If the load is canceled or otherwise fails, it will post a null value. + */ +public class GlideLiveDataTarget extends CustomTarget { + + private final MutableLiveData liveData = new MutableLiveData<>(); + + public GlideLiveDataTarget(int width, int height) { + super(width, height); + } + + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { + liveData.postValue(resource); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + liveData.postValue(null); + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + liveData.postValue(null); + } + + public @NonNull LiveData getLiveData() { + return liveData; + } +} diff --git a/core-util/src/main/java/org/signal/core/util/concurrent/SignalExecutors.java b/core-util/src/main/java/org/signal/core/util/concurrent/SignalExecutors.java index f267128306..91d4efe969 100644 --- a/core-util/src/main/java/org/signal/core/util/concurrent/SignalExecutors.java +++ b/core-util/src/main/java/org/signal/core/util/concurrent/SignalExecutors.java @@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger; public final class SignalExecutors { public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded")); - public static final ExecutorService BOUNDED = newCachedBoundedExecutor("signal-bounded", 4, 12, 5); + public static final ExecutorService BOUNDED = Executors.newFixedThreadPool(4, new NumberedThreadFactory("signal-bounded")); public static final ExecutorService SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial")); public static final ExecutorService BOUNDED_IO = newCachedBoundedExecutor("signal-io-bounded", 1, 32, 30);