From e056bd1aa29c378adea05b514634d21515bac250 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 11 Oct 2017 17:47:12 -0700 Subject: [PATCH] Update giphy connectivity strategy for glide // FREEBIE --- .../securesms/giph/model/GiphyImage.java | 12 + .../securesms/giph/model/GiphyPaddedUrl.java | 50 +++ .../securesms/giph/ui/GiphyAdapter.java | 17 +- .../glide/GiphyPaddedUrlFetcher.java | 285 ++++++++++++++++++ .../securesms/glide/GiphyPaddedUrlLoader.java | 52 ++++ .../securesms/mms/SignalGlideModule.java | 3 + 6 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/giph/model/GiphyPaddedUrl.java create mode 100644 src/org/thoughtcrime/securesms/glide/GiphyPaddedUrlFetcher.java create mode 100644 src/org/thoughtcrime/securesms/glide/GiphyPaddedUrlLoader.java diff --git a/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java index 8efc46b8d9..ace6e16053 100644 --- a/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java +++ b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java @@ -12,10 +12,18 @@ public class GiphyImage { return images.downsized.url; } + public long getGifSize() { + return images.downsized.size; + } + public String getGifMmsUrl() { return images.fixed_height_downsampled.url; } + public long getMmsGifSize() { + return images.fixed_height_downsampled.size; + } + public float getGifAspectRatio() { return (float)images.downsized.width / (float)images.downsized.height; } @@ -24,6 +32,10 @@ public class GiphyImage { return images.downsized_still.url; } + public long getStillSize() { + return images.downsized_still.size; + } + public static class ImageTypes { @JsonProperty private ImageData fixed_height; diff --git a/src/org/thoughtcrime/securesms/giph/model/GiphyPaddedUrl.java b/src/org/thoughtcrime/securesms/giph/model/GiphyPaddedUrl.java new file mode 100644 index 0000000000..c00268446d --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/model/GiphyPaddedUrl.java @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.giph.model; + + +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.Key; + +import org.thoughtcrime.securesms.util.Conversions; + +import java.security.MessageDigest; + +public class GiphyPaddedUrl implements Key { + + private final String target; + private final long size; + + public GiphyPaddedUrl(@NonNull String target, long size) { + this.target = target; + this.size = size; + } + + public String getTarget() { + return target; + } + + public long getSize() { + return size; + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) { + messageDigest.update(target.getBytes()); + messageDigest.update(Conversions.longToByteArray(size)); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof GiphyPaddedUrl)) return false; + + GiphyPaddedUrl that = (GiphyPaddedUrl)other; + + return this.target.equals(that.target) && this.size == that.size; + } + + @Override + public int hashCode() { + return target.hashCode() ^ (int)size; + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java index 144100df0b..8761ae087b 100644 --- a/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java @@ -24,12 +24,15 @@ import com.bumptech.glide.request.target.Target; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.PrintWriter; import java.util.List; import java.util.concurrent.ExecutionException; @@ -69,7 +72,7 @@ class GiphyAdapter extends RecyclerView.Adapter { Log.w(TAG, e); synchronized (this) { - if (image.getGifUrl().equals(model)) { + if (new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()).equals(model)) { this.modelReady = true; notifyAll(); } @@ -81,7 +84,7 @@ class GiphyAdapter extends RecyclerView.Adapter { @Override public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { synchronized (this) { - if (image.getGifUrl().equals(model)) { + if (new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize()).equals(model)) { this.modelReady = true; notifyAll(); } @@ -99,7 +102,8 @@ class GiphyAdapter extends RecyclerView.Adapter { } return Glide.with(context) - .load(forMms ? image.getGifMmsUrl() : image.getGifUrl()) + .load(forMms ? new GiphyPaddedUrl(image.getGifMmsUrl(), image.getMmsGifSize()) : + new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize())) .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .get(); } @@ -144,18 +148,19 @@ class GiphyAdapter extends RecyclerView.Adapter { holder.gifProgress.setVisibility(View.GONE); RequestBuilder thumbnailRequest = GlideApp.with(context) - .load(image.getStillUrl()) + .load(new GiphyPaddedUrl(image.getStillUrl(), image.getStillSize())) .diskCacheStrategy(DiskCacheStrategy.ALL); if (Util.isLowMemory(context)) { - glideRequests.load(image.getStillUrl()) + glideRequests.load(new GiphyPaddedUrl(image.getStillUrl(), image.getStillSize())) .placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context))) .diskCacheStrategy(DiskCacheStrategy.ALL) + .listener(holder) .into(holder.thumbnail); holder.setModelReady(); } else { - glideRequests.load(image.getGifUrl()) + glideRequests.load(new GiphyPaddedUrl(image.getGifUrl(), image.getGifSize())) .thumbnail(thumbnailRequest) .placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context))) .diskCacheStrategy(DiskCacheStrategy.ALL) diff --git a/src/org/thoughtcrime/securesms/glide/GiphyPaddedUrlFetcher.java b/src/org/thoughtcrime/securesms/glide/GiphyPaddedUrlFetcher.java new file mode 100644 index 0000000000..05ed981160 --- /dev/null +++ b/src/org/thoughtcrime/securesms/glide/GiphyPaddedUrlFetcher.java @@ -0,0 +1,285 @@ +package org.thoughtcrime.securesms.glide; + + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.util.ContentLengthInputStream; + +import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl; +import org.thoughtcrime.securesms.util.Util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +class GiphyPaddedUrlFetcher implements DataFetcher { + + private static final String TAG = GiphyPaddedUrlFetcher.class.getSimpleName(); + + private static final long MB = 1024 * 1024; + private static final long KB = 1024; + + private final OkHttpClient client; + private final GiphyPaddedUrl url; + + private List bodies; + private List rangeStreams; + private InputStream stream; + + GiphyPaddedUrlFetcher(@NonNull OkHttpClient client, + @NonNull GiphyPaddedUrl url) + { + this.client = client; + this.url = url; + } + + @Override + public void loadData(Priority priority, DataCallback callback) { + bodies = new LinkedList<>(); + rangeStreams = new LinkedList<>(); + stream = null; + + try { + List requestPattern = getRequestPattern(url.getSize()); + + for (ByteRange range : requestPattern) { + Request request = new Request.Builder() + .addHeader("Range", "bytes=" + range.start + "-" + range.end) + .addHeader("Accept-Encoding", "identity") + .url(url.getTarget()) + .get() + .build(); + + Response response = client.newCall(request).execute(); + + if (!response.isSuccessful()) { + throw new IOException("Bad response: " + response.code() + " - " + response.message()); + } + + ResponseBody responseBody = response.body(); + + if (responseBody == null) throw new IOException("Response body was null"); + else bodies.add(responseBody); + + rangeStreams.add(new SkippingInputStream(ContentLengthInputStream.obtain(responseBody.byteStream(), responseBody.contentLength()), range.ignoreFirst)); + } + + stream = new InputStreamList(rangeStreams); + callback.onDataReady(stream); + } catch (IOException e) { + Log.w(TAG, e); + callback.onLoadFailed(e); + } + } + + @Override + public void cleanup() { + if (rangeStreams != null) { + for (InputStream rangeStream : rangeStreams) { + try { + if (rangeStream != null) rangeStream.close(); + } catch (IOException ignored) {} + } + } + + if (bodies != null) { + for (ResponseBody body : bodies) { + if (body != null) body.close(); + } + } + + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) {} + } + } + + @Override + public void cancel() { + + } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.REMOTE; + } + + private List getRequestPattern(long size) throws IOException { + if (size > MB) return getRequestPattern(size, MB); + else if (size > 500 * KB) return getRequestPattern(size, 500 * KB); + else if (size > 100 * KB) return getRequestPattern(size, 100 * KB); + else if (size > 50 * KB) return getRequestPattern(size, 50 * KB); + else if (size > KB) return getRequestPattern(size, KB); + + throw new IOException("Unsupported size: " + size); + } + + private List getRequestPattern(long size, long increment) { + List results = new LinkedList<>(); + + long offset = 0; + + while (size - offset > increment) { + results.add(new ByteRange(offset, offset + increment - 1, 0)); + offset += increment; + } + + if (size - offset > 0) { + results.add(new ByteRange(size - increment, size-1, increment - (size - offset))); + } + + return results; + } + + private static class ByteRange { + private final long start; + private final long end; + private final long ignoreFirst; + + private ByteRange(long start, long end, long ignoreFirst) { + this.start = start; + this.end = end; + this.ignoreFirst = ignoreFirst; + } + } + + private static class SkippingInputStream extends FilterInputStream { + + private long skip; + + SkippingInputStream(InputStream in, long skip) { + super(in); + this.skip = skip; + } + + @Override + public int read() throws IOException { + if (skip != 0) { + skipFully(skip); + skip = 0; + } + + return super.read(); + } + + @Override + public int read(@NonNull byte[] buffer) throws IOException { + if (skip != 0) { + skipFully(skip); + skip = 0; + } + + return super.read(buffer); + } + + @Override + public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { + if (skip != 0) { + skipFully(skip); + skip = 0; + } + + return super.read(buffer, offset, length); + } + + @Override + public int available() throws IOException { + return Util.toIntExact(super.available() - skip); + } + + private void skipFully(long amount) throws IOException { + byte[] buffer = new byte[4096]; + + while (amount > 0) { + int read = super.read(buffer, 0, Math.min(buffer.length, Util.toIntExact(amount))); + + if (read != -1) amount -= read; + else return; + } + } + } + + private static class InputStreamList extends InputStream { + + private final List inputStreams; + + private int currentStreamIndex = 0; + + InputStreamList(List inputStreams) { + this.inputStreams = inputStreams; + } + + @Override + public int read() throws IOException { + while (currentStreamIndex < inputStreams.size()) { + int result = inputStreams.get(currentStreamIndex).read(); + + if (result == -1) currentStreamIndex++; + else return result; + } + + return -1; + } + + @Override + public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { + while (currentStreamIndex < inputStreams.size()) { + int result = inputStreams.get(currentStreamIndex).read(buffer, offset, length); + + if (result == -1) currentStreamIndex++; + else return result; + } + + return -1; + } + + @Override + public int read(@NonNull byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public void close() throws IOException { + for (InputStream stream : inputStreams) { + try { + stream.close(); + } catch (IOException ignored) {} + } + } + + @Override + public int available() { + int total = 0; + + for (int i=currentStreamIndex;i { + + private final OkHttpClient client; + + private GiphyPaddedUrlLoader(OkHttpClient client) { + this.client = client; + } + + @Nullable + @Override + public LoadData buildLoadData(GiphyPaddedUrl url, int width, int height, Options options) { + return new LoadData<>(url, new GiphyPaddedUrlFetcher(client, url)); + } + + @Override + public boolean handles(GiphyPaddedUrl url) { + return true; + } + + public static class Factory implements ModelLoaderFactory { + + private final OkHttpClient client; + + public Factory() { + this.client = new OkHttpClient.Builder().proxySelector(new GiphyProxySelector()).build(); + } + + @Override + public ModelLoader build(MultiModelLoaderFactory multiFactory) { + return new GiphyPaddedUrlLoader(client); + } + + @Override + public void teardown() {} + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 8637d7536b..76006d3bc9 100644 --- a/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -13,7 +13,9 @@ import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.AppGlideModule; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; +import org.thoughtcrime.securesms.giph.model.GiphyPaddedUrl; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; +import org.thoughtcrime.securesms.glide.GiphyPaddedUrlLoader; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -39,6 +41,7 @@ public class SignalGlideModule extends AppGlideModule { registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); + registry.append(GiphyPaddedUrl.class, InputStream.class, new GiphyPaddedUrlLoader.Factory()); registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); }