Load thumbnails using an asynchronous Glide target.

This commit is contained in:
Greyson Parrelli 2021-10-26 09:42:26 -04:00 committed by Alex Hart
parent 85551ca824
commit ae73601f52
3 changed files with 102 additions and 70 deletions

View file

@ -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<SpannableString> finalBody = recipientToStringAsync(thread.getRecipient().getId(), threadRecipient -> {
CharSequence bodyWithMediaIcon = createFinalBodyWithMediaIcon(context, body, thread, glideRequests);
if (threadRecipient.isGroup()) {
String body = removeNewlines(thread.getBody());
LiveData<SpannableString> 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<CharSequence> 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,

View file

@ -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<Bitmap> that can be observed.
*
* If the load is canceled or otherwise fails, it will post a null value.
*/
public class GlideLiveDataTarget extends CustomTarget<Bitmap> {
private final MutableLiveData<Bitmap> liveData = new MutableLiveData<>();
public GlideLiveDataTarget(int width, int height) {
super(width, height);
}
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> 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<Bitmap> getLiveData() {
return liveData;
}
}

View file

@ -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);