Fix for media thumbnails flickering on model updates.

Only update ImageView contents when they have changed.

Fixes #1004
Fixes #2663
Closes #3184

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-05-18 08:38:48 -07:00
parent 636b11abea
commit 082985276f
8 changed files with 74 additions and 56 deletions

View File

@ -200,7 +200,7 @@ public class ConversationItem extends LinearLayout {
}
bubbleContainer.setState(transportationState, mediaCaptionState);
}
}
private void setSelectionBackgroundDrawables(MessageRecord messageRecord) {
int[] attributes = new int[]{R.attr.conversation_list_item_background_selected,
@ -354,7 +354,9 @@ public class ConversationItem extends LinearLayout {
private void resolveMedia(MediaMmsMessageRecord messageRecord) {
if (hasMedia(messageRecord)) {
mediaThumbnail.setImageResource(messageRecord.getSlideDeckFuture(), masterSecret);
mediaThumbnail.setImageResource(masterSecret, messageRecord.getId(),
messageRecord.getDateReceived(),
messageRecord.getSlideDeckFuture());
}
}

View File

@ -131,10 +131,8 @@ public abstract class BubbleContainer extends RelativeLayout {
}
private void setMediaVisibility(@MediaState int mediaState) {
media.reset();
if (!isMediaPresent(mediaState)) {
media.hide();
}
if (!isMediaPresent(mediaState)) media.setVisibility(View.GONE);
else media.setVisibility(View.VISIBLE);
}
private void setMediaPendingMask(@TransportState int transportState) {

View File

@ -122,15 +122,6 @@ public class ForegroundImageView extends RoundedImageView {
return ActivityOptions.makeScaleUpAnimation(this, 0, 0, getWidth(), getHeight());
}
public void reset() {
setImageDrawable(null);
setVisibility(View.VISIBLE);
}
public void hide() {
setVisibility(View.GONE);
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == mForeground);

View File

@ -4,6 +4,7 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
@ -27,13 +28,17 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.ThumbnailTransform;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import ws.com.google.android.mms.pdu.PduPart;
public class ThumbnailView extends ForegroundImageView {
private ListenableFutureTask<SlideDeck> slideDeckFuture = null;
private SlideDeckListener slideDeckListener = null;
private ThumbnailClickListener thumbnailClickListener = null;
private String slideId = null;
private Slide slide = null;
private Handler handler = new Handler();
public ThumbnailView(Context context) {
@ -53,31 +58,41 @@ public class ThumbnailView extends ForegroundImageView {
super.onDetachedFromWindow();
}
public void setImageResource(@NonNull ListenableFutureTask<SlideDeck> slideDeckFuture,
@Nullable MasterSecret masterSecret)
public void setImageResource(@Nullable MasterSecret masterSecret,
long id, long timestamp,
@NonNull ListenableFutureTask<SlideDeck> slideDeckFuture)
{
if (this.slideDeckFuture != null && this.slideDeckListener != null) {
this.slideDeckFuture.removeListener(this.slideDeckListener);
}
String slideId = id + "::" + timestamp;
if (!slideId.equals(this.slideId)) {
setImageDrawable(null);
this.slide = null;
this.slideId = slideId;
}
this.slideDeckListener = new SlideDeckListener(masterSecret);
this.slideDeckFuture = slideDeckFuture;
this.slideDeckFuture.addListener(this.slideDeckListener);
}
public void setImageResource(@NonNull Slide slide) {
setImageResource(slide, null);
}
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
if (isContextValid()) {
buildGlideRequest(slide, masterSecret).into(ThumbnailView.this);
if (!Util.equals(slide, this.slide)) buildGlideRequest(slide, masterSecret).into(this);
this.slide = slide;
setOnClickListener(new ThumbnailClickDispatcher(thumbnailClickListener, slide));
} else {
Log.w(TAG, "Not going to load resource, context is invalid");
}
}
public void setImageResource(@NonNull Slide slide) {
setImageResource(slide, null);
}
public void setThumbnailClickListener(ThumbnailClickListener listener) {
this.thumbnailClickListener = listener;
}
@ -131,7 +146,7 @@ public class ThumbnailView extends ForegroundImageView {
}
return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
.crossFade().transform(new ThumbnailTransform(getContext()));
.transform(new ThumbnailTransform(getContext()));
}
private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) {
@ -163,7 +178,8 @@ public class ThumbnailView extends ForegroundImageView {
handler.post(new Runnable() {
@Override
public void run() {
hide();
Log.w(TAG, "Resolved slide was null!");
setVisibility(View.GONE);
}
});
}
@ -175,7 +191,8 @@ public class ThumbnailView extends ForegroundImageView {
handler.post(new Runnable() {
@Override
public void run() {
hide();
Log.w(TAG, "onFailure!");
setVisibility(View.GONE);
}
});
}

View File

@ -42,12 +42,12 @@ public class AudioSlide extends Slide {
}
@Override
public boolean hasImage() {
public boolean hasImage() {
return true;
}
@Override
public boolean hasAudio() {
public boolean hasAudio() {
return true;
}

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.Resources.Theme;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -36,28 +37,16 @@ public abstract class Slide {
protected final Context context;
protected MasterSecret masterSecret;
public Slide(Context context, PduPart part) {
public Slide(Context context, @NonNull PduPart part) {
this.part = part;
this.context = context;
}
public Slide(Context context, MasterSecret masterSecret, PduPart part) {
public Slide(Context context, @NonNull MasterSecret masterSecret, @NonNull PduPart part) {
this(context, part);
this.masterSecret = masterSecret;
}
protected byte[] getPartData() {
try {
if (part.getData() != null)
return part.getData();
return Util.readFully(PartAuthority.getPartStream(context, masterSecret, part.getDataUri()));
} catch (IOException e) {
Log.w("Slide", e);
return new byte[0];
}
}
public String getContentType() {
return new String(part.getContentType());
}
@ -107,4 +96,28 @@ public abstract class Slide {
if (size > MmsMediaConstraints.MAX_MESSAGE_SIZE) throw new MediaTooLargeException("Media exceeds maximum message size.");
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Slide)) return false;
Slide that = (Slide)other;
return Util.equals(this.getContentType(), that.getContentType()) &&
this.hasAudio() == that.hasAudio() &&
this.hasImage() == that.hasImage() &&
this.hasVideo() == that.hasVideo() &&
this.isDraft() == that.isDraft() &&
Util.equals(this.getUri(), that.getUri()) &&
Util.equals(this.getThumbnailUri(), that.getThumbnailUri());
}
@Override
public int hashCode() {
return Util.hashCode(getContentType(), hasAudio(), hasImage(),
hasVideo(), isDraft(), getUri(), getThumbnailUri());
}
}

View File

@ -17,16 +17,9 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.LRUCache;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.Map;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.CharacterSets;
@ -34,14 +27,6 @@ import ws.com.google.android.mms.pdu.PduPart;
public class TextSlide extends Slide {
private static final int MAX_CACHE_SIZE = 10;
private static final Map<Uri, SoftReference<String>> textCache =
Collections.synchronizedMap(new LRUCache<Uri, SoftReference<String>>(MAX_CACHE_SIZE));
public TextSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
}
public TextSlide(Context context, String message) {
super(context, getPartForMessage(message));
}

View File

@ -21,11 +21,13 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Looper;
import android.provider.Telephony;
import android.support.annotation.Nullable;
import android.telephony.TelephonyManager;
import android.text.Spannable;
import android.text.SpannableString;
@ -45,6 +47,7 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
@ -303,4 +306,13 @@ public class Util {
throw new AssertionError("Main-thread assertion failed.");
}
}
public static boolean equals(@Nullable Object a, @Nullable Object b) {
return a == b || (a != null && a.equals(b));
}
public static int hashCode(@Nullable Object... objects) {
return Arrays.hashCode(objects);
}
}