mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Load thumbnails using an asynchronous Glide target.
This commit is contained in:
committed by
Alex Hart
parent
85551ca824
commit
ae73601f52
@@ -17,7 +17,6 @@
|
|||||||
package org.thoughtcrime.securesms.conversationlist;
|
package org.thoughtcrime.securesms.conversationlist;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
@@ -32,16 +31,13 @@ import androidx.annotation.ColorInt;
|
|||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.Transformations;
|
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.CenterCrop;
|
||||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
|
||||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||||
|
|
||||||
import org.signal.core.util.DimensionUnit;
|
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.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||||
|
import org.thoughtcrime.securesms.glide.GlideLiveDataTarget;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
@@ -82,9 +79,6 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
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;
|
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()) {
|
} 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);
|
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
|
||||||
} else {
|
} else {
|
||||||
String body = removeNewlines(thread.getBody());
|
String body = removeNewlines(thread.getBody());
|
||||||
|
LiveData<SpannableString> finalBody = LiveDataUtil.mapAsync(createFinalBodyWithMediaIcon(context, body, thread, glideRequests), updatedBody -> {
|
||||||
LiveData<SpannableString> finalBody = recipientToStringAsync(thread.getRecipient().getId(), threadRecipient -> {
|
if (thread.getRecipient().isGroup()) {
|
||||||
CharSequence bodyWithMediaIcon = createFinalBodyWithMediaIcon(context, body, thread, glideRequests);
|
|
||||||
if (threadRecipient.isGroup()) {
|
|
||||||
RecipientId groupMessageSender = thread.getGroupMessageSender();
|
RecipientId groupMessageSender = thread.getGroupMessageSender();
|
||||||
if (!groupMessageSender.isUnknown()) {
|
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);
|
return whileLoadingShow(body, finalBody);
|
||||||
@@ -537,63 +530,57 @@ public final class ConversationListItem extends ConstraintLayout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
private static LiveData<CharSequence> createFinalBodyWithMediaIcon(@NonNull Context context,
|
||||||
private static CharSequence createFinalBodyWithMediaIcon(@NonNull Context context,
|
@NonNull String body,
|
||||||
@NonNull String body,
|
@NonNull ThreadRecord thread,
|
||||||
@NonNull ThreadRecord thread,
|
@NonNull GlideRequests glideRequests)
|
||||||
@NonNull GlideRequests glideRequests)
|
|
||||||
{
|
{
|
||||||
if (thread.getSnippetUri() != null) {
|
if (thread.getSnippetUri() == null) {
|
||||||
try {
|
return LiveDataUtil.just(body);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
private static SpannableString createGroupMessageUpdateString(@NonNull Context context,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
public final class SignalExecutors {
|
public final class SignalExecutors {
|
||||||
|
|
||||||
public static final ExecutorService UNBOUNDED = Executors.newCachedThreadPool(new NumberedThreadFactory("signal-unbounded"));
|
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 SERIAL = Executors.newSingleThreadExecutor(new NumberedThreadFactory("signal-serial"));
|
||||||
public static final ExecutorService BOUNDED_IO = newCachedBoundedExecutor("signal-io-bounded", 1, 32, 30);
|
public static final ExecutorService BOUNDED_IO = newCachedBoundedExecutor("signal-io-bounded", 1, 32, 30);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user