From 89c57fca2d73f9384e06f36bd4552b902bb3b0a0 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 4 Jun 2017 10:34:51 +0300 Subject: [PATCH] functional progressbars for glide-loaded images in headlines buffer --- org.fox.ttrss/build.gradle | 6 +- org.fox.ttrss/src/main/AndroidManifest.xml | 9 +- .../java/org/fox/ttrss/HeadlinesFragment.java | 50 +++++- .../ttrss/util/OkHttpProgressGlideModule.java | 160 ++++++++++++++++++ .../org/fox/ttrss/util/ProgressTarget.java | 110 ++++++++++++ .../org/fox/ttrss/util/WrappingTarget.java | 52 ++++++ 6 files changed, 377 insertions(+), 10 deletions(-) create mode 100644 org.fox.ttrss/src/main/java/org/fox/ttrss/util/OkHttpProgressGlideModule.java create mode 100644 org.fox.ttrss/src/main/java/org/fox/ttrss/util/ProgressTarget.java create mode 100644 org.fox.ttrss/src/main/java/org/fox/ttrss/util/WrappingTarget.java diff --git a/org.fox.ttrss/build.gradle b/org.fox.ttrss/build.gradle index 7f48ed19..cabd2045 100755 --- a/org.fox.ttrss/build.gradle +++ b/org.fox.ttrss/build.gradle @@ -28,6 +28,10 @@ android { dependencies { compile project(':taskerlocaleapi') compile files('libs/dashclock-api-r1.1.jar') + compile 'com.squareup.okhttp3:okhttp:3.8.0' + compile('com.github.bumptech.glide:okhttp3-integration:1.5.0') { + exclude group: 'glide-parent' + } compile 'org.jsoup:jsoup:1.10.2' compile 'com.bogdwellers:pinchtozoom:0.1' compile 'com.github.bumptech.glide:glide:3.8.0' @@ -44,8 +48,6 @@ dependencies { compile 'com.ToxicBakery.viewpager.transforms:view-pager-transforms:1.2.32@aar' compile 'me.relex:circleindicator:1.2.2@aar' compile 'com.viewpagerindicator:library:2.4.1' - //compile 'com.nhaarman.listviewanimations:lib-core:3.1.0@aar' - //compile 'com.nhaarman.listviewanimations:lib-manipulation:3.1.0@aar' compile 'com.nineoldandroids:library:2.4.0' compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' compile files('libs/YouTubeAndroidPlayerApi.jar') diff --git a/org.fox.ttrss/src/main/AndroidManifest.xml b/org.fox.ttrss/src/main/AndroidManifest.xml index 8e73e336..fd76ca34 100755 --- a/org.fox.ttrss/src/main/AndroidManifest.xml +++ b/org.fox.ttrss/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + android:versionName="1.202"> @@ -18,6 +19,12 @@ android:hardwareAccelerated="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" > + + + + flavorProgressTarget; public ArticleViewHolder(View v) { super(v); @@ -804,14 +808,44 @@ public class HeadlinesFragment extends Fragment { topChangedMessage = v.findViewById(R.id.headlines_row_top_changed); flavorImageOverflow = v.findViewById(R.id.gallery_overflow); flavorVideoView = (TextureView) v.findViewById(R.id.flavor_video); + + if (flavorImageView != null && flavorImageLoadingBar != null) { + flavorProgressTarget = new FlavorProgressTarget<>(new GlideDrawableImageViewTarget(flavorImageView), flavorImageLoadingBar); + } } public void clearAnimation() { view.clearAnimation(); } - } - + + private static class FlavorProgressTarget extends ProgressTarget { + private final ProgressBar progress; + public FlavorProgressTarget(Target target, ProgressBar progress) { + super(target); + this.progress = progress; + } + + @Override public float getGranualityPercentage() { + return 0.1f; // this matches the format string for #text below + } + + @Override protected void onConnecting() { + progress.setIndeterminate(true); + progress.setVisibility(View.VISIBLE); + } + @Override protected void onDownloading(long bytesRead, long expectedLength) { + progress.setIndeterminate(false); + progress.setProgress((int)(100 * bytesRead / expectedLength)); + } + @Override protected void onDownloaded() { + progress.setIndeterminate(true); + } + @Override protected void onDelivered() { + progress.setVisibility(View.INVISIBLE); + } + } + private class ArticleListAdapter extends RecyclerView.Adapter { private ArrayList
items; @@ -1072,6 +1106,7 @@ public class HeadlinesFragment extends Fragment { /* reset to default in case of convertview */ holder.flavorImageLoadingBar.setVisibility(View.GONE); + holder.flavorImageLoadingBar.setIndeterminate(false); holder.flavorImageView.setVisibility(View.GONE); holder.flavorVideoKindView.setVisibility(View.GONE); holder.flavorImageOverflow.setVisibility(View.GONE); @@ -1155,17 +1190,18 @@ public class HeadlinesFragment extends Fragment { //Log.d(TAG, "TAG:" + holder.flavorImageOverflow.getTag()); if (!article.flavorImageUri.equals(holder.flavorImageOverflow.getTag())) { - holder.flavorImageLoadingBar.setVisibility(View.VISIBLE); - holder.flavorImageLoadingBar.setIndeterminate(true); + //holder.flavorImageLoadingBar.setVisibility(View.VISIBLE); + //holder.flavorImageLoadingBar.setIndeterminate(true); holder.flavorImageView.setMaxHeight((int)(m_screenHeight * 0.8f)); + holder.flavorProgressTarget.setModel(article.flavorImageUri); Glide.with(HeadlinesFragment.this) .load(article.flavorImageUri) .dontAnimate() .dontTransform() - .diskCacheStrategy(DiskCacheStrategy.ALL) - .skipMemoryCache(false) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) .listener(new RequestListener() { @Override public boolean onException(Exception e, String model, Target target, boolean isFirstResource) { @@ -1201,7 +1237,7 @@ public class HeadlinesFragment extends Fragment { } } }) - .into(holder.flavorImageView); + .into(holder.flavorProgressTarget); } else { holder.flavorImageOverflow.setVisibility(View.VISIBLE); diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/OkHttpProgressGlideModule.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/OkHttpProgressGlideModule.java new file mode 100644 index 00000000..b333807e --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/OkHttpProgressGlideModule.java @@ -0,0 +1,160 @@ +package org.fox.ttrss.util; + +import java.io.*; +import java.util.*; + +import android.content.Context; +import android.os.*; + +import com.bumptech.glide.*; +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.GlideModule; + +import okhttp3.*; +import okio.*; + +public class OkHttpProgressGlideModule implements GlideModule { + @Override public void applyOptions(Context context, GlideBuilder builder) { + + } + @Override public void registerComponents(Context context, Glide glide) { + OkHttpClient client = new OkHttpClient.Builder() + .addNetworkInterceptor(createInterceptor(new DispatchingProgressListener())) + .build(); + glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client)); + } + + private static Interceptor createInterceptor(final ResponseProgressListener listener) { + return new Interceptor() { + @Override public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Response response = chain.proceed(request); + return response.newBuilder() + .body(new OkHttpProgressResponseBody(request.url(), response.body(), listener)) + .build(); + } + }; + } + + public interface UIProgressListener { + void onProgress(long bytesRead, long expectedLength); + /** + * Control how often the listener needs an update. 0% and 100% will always be dispatched. + * @return in percentage (0.2 = call {@link #onProgress} around every 0.2 percent of progress) + */ + float getGranualityPercentage(); + } + + public static void forget(String url) { + DispatchingProgressListener.forget(url); + } + public static void expect(String url, UIProgressListener listener) { + DispatchingProgressListener.expect(url, listener); + } + + private interface ResponseProgressListener { + void update(HttpUrl url, long bytesRead, long contentLength); + } + + private static class DispatchingProgressListener implements ResponseProgressListener { + private static final Map LISTENERS = new HashMap<>(); + private static final Map PROGRESSES = new HashMap<>(); + + private final Handler handler; + DispatchingProgressListener() { + this.handler = new Handler(Looper.getMainLooper()); + } + + static void forget(String url) { + LISTENERS.remove(url); + PROGRESSES.remove(url); + } + static void expect(String url, UIProgressListener listener) { + LISTENERS.put(url, listener); + } + + @Override public void update(HttpUrl url, final long bytesRead, final long contentLength) { + //System.out.printf("%s: %d/%d = %.2f%%%n", url, bytesRead, contentLength, (100f * bytesRead) / contentLength); + + String key = url.toString(); + final UIProgressListener listener = LISTENERS.get(key); + + if (listener == null) { + return; + } + if (contentLength <= bytesRead) { + forget(key); + } + if (needsDispatch(key, bytesRead, contentLength, listener.getGranualityPercentage())) { + handler.post(new Runnable() { + @Override public void run() { + listener.onProgress(bytesRead, contentLength); + } + }); + } + } + + private boolean needsDispatch(String key, long current, long total, float granularity) { + if (granularity == 0 || current == 0 || total == current) { + return true; + } + float percent = 100f * current / total; + long currentProgress = (long)(percent / granularity); + Long lastProgress = PROGRESSES.get(key); + if (lastProgress == null || currentProgress != lastProgress) { + PROGRESSES.put(key, currentProgress); + return true; + } else { + return false; + } + } + } + + private static class OkHttpProgressResponseBody extends ResponseBody { + private final HttpUrl url; + private final ResponseBody responseBody; + private final ResponseProgressListener progressListener; + private BufferedSource bufferedSource; + + OkHttpProgressResponseBody(HttpUrl url, ResponseBody responseBody, + ResponseProgressListener progressListener) { + + this.url = url; + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override public MediaType contentType() { + return responseBody.contentType(); + } + + @Override public long contentLength() { + return responseBody.contentLength(); + } + + @Override public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long totalBytesRead = 0L; + @Override public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + long fullLength = responseBody.contentLength(); + if (bytesRead == -1) { // this source is exhausted + totalBytesRead = fullLength; + } else { + totalBytesRead += bytesRead; + } + progressListener.update(url, totalBytesRead, fullLength); + return bytesRead; + } + }; + } + } +} \ No newline at end of file diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ProgressTarget.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ProgressTarget.java new file mode 100644 index 00000000..df02865c --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/ProgressTarget.java @@ -0,0 +1,110 @@ +package org.fox.ttrss.util; + + +import android.graphics.drawable.Drawable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.Target; + +public abstract class ProgressTarget extends WrappingTarget implements OkHttpProgressGlideModule.UIProgressListener { + private T model; + private boolean ignoreProgress = true; + public ProgressTarget(Target target) { + this(null, target); + } + public ProgressTarget(T model, Target target) { + super(target); + this.model = model; + } + + public final T getModel() { + return model; + } + public final void setModel(T model) { + Glide.clear(this); // indirectly calls cleanup + this.model = model; + } + /** + * Convert a model into an Url string that is used to match up the OkHttp requests. For explicit + * {@link com.bumptech.glide.load.model.GlideUrl GlideUrl} loads this needs to return + * {@link com.bumptech.glide.load.model.GlideUrl#toStringUrl toStringUrl}. For custom models do the same as your + * {@link com.bumptech.glide.load.model.stream.BaseGlideUrlLoader BaseGlideUrlLoader} does. + * @param model return the representation of the given model, DO NOT use {@link #getModel()} inside this method. + * @return a stable Url representation of the model, otherwise the progress reporting won't work + */ + protected String toUrlString(T model) { + return String.valueOf(model); + } + + @Override public float getGranualityPercentage() { + return 1.0f; + } + + @Override public void onProgress(long bytesRead, long expectedLength) { + if (ignoreProgress) { + return; + } + if (expectedLength == Long.MAX_VALUE) { + onConnecting(); + } else if (bytesRead == expectedLength) { + onDownloaded(); + } else { + onDownloading(bytesRead, expectedLength); + } + } + + /** + * Called when the Glide load has started. + * At this time it is not known if the Glide will even go and use the network to fetch the image. + */ + protected abstract void onConnecting(); + /** + * Called when there's any progress on the download; not called when loading from cache. + * At this time we know how many bytes have been transferred through the wire. + */ + protected abstract void onDownloading(long bytesRead, long expectedLength); + /** + * Called when the bytes downloaded reach the length reported by the server; not called when loading from cache. + * At this time it is fairly certain, that Glide either finished reading the stream. + * This means that the image was either already decoded or saved the network stream to cache. + * In the latter case there's more work to do: decode the image from cache and transform. + * These cannot be listened to for progress so it's unsure how fast they'll be, best to show indeterminate progress. + */ + protected abstract void onDownloaded(); + /** + * Called when the Glide load has finished either by successfully loading the image or failing to load or cancelled. + * In any case the best is to hide/reset any progress displays. + */ + protected abstract void onDelivered(); + + private void start() { + OkHttpProgressGlideModule.expect(toUrlString(model), this); + ignoreProgress = false; + onProgress(0, Long.MAX_VALUE); + } + private void cleanup() { + ignoreProgress = true; + T model = this.model; // save in case it gets modified + onDelivered(); + OkHttpProgressGlideModule.forget(toUrlString(model)); + this.model = null; + } + + @Override public void onLoadStarted(Drawable placeholder) { + super.onLoadStarted(placeholder); + start(); + } + @Override public void onResourceReady(Z resource, GlideAnimation animation) { + cleanup(); + super.onResourceReady(resource, animation); + } + @Override public void onLoadFailed(Exception e, Drawable errorDrawable) { + cleanup(); + super.onLoadFailed(e, errorDrawable); + } + @Override public void onLoadCleared(Drawable placeholder) { + cleanup(); + super.onLoadCleared(placeholder); + } +} \ No newline at end of file diff --git a/org.fox.ttrss/src/main/java/org/fox/ttrss/util/WrappingTarget.java b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/WrappingTarget.java new file mode 100644 index 00000000..5dcfa7ec --- /dev/null +++ b/org.fox.ttrss/src/main/java/org/fox/ttrss/util/WrappingTarget.java @@ -0,0 +1,52 @@ +package org.fox.ttrss.util; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +import com.bumptech.glide.request.Request; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.*; + +public class WrappingTarget implements Target { + protected final @NonNull Target target; + public WrappingTarget(@NonNull Target target) { + this.target = target; + } + public @NonNull Target getWrappedTarget() { + return target; + } + @Override public void getSize(SizeReadyCallback cb) { + target.getSize(cb); + } + + @Override public void onLoadStarted(Drawable placeholder) { + target.onLoadStarted(placeholder); + } + @Override public void onLoadFailed(Exception e, Drawable errorDrawable) { + target.onLoadFailed(e, errorDrawable); + } + @SuppressWarnings("unchecked") + @Override public void onResourceReady(Z resource, GlideAnimation glideAnimation) { + target.onResourceReady(resource, (GlideAnimation)glideAnimation); + } + @Override public void onLoadCleared(Drawable placeholder) { + target.onLoadCleared(placeholder); + } + + @Override public Request getRequest() { + return target.getRequest(); + } + @Override public void setRequest(Request request) { + target.setRequest(request); + } + + @Override public void onStart() { + target.onStart(); + } + @Override public void onStop() { + target.onStop(); + } + @Override public void onDestroy() { + target.onDestroy(); + } +} \ No newline at end of file