aboutsummaryrefslogtreecommitdiffstats
path: root/main/src/cgeo/geocaching/network/HtmlImage.java
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/cgeo/geocaching/network/HtmlImage.java')
-rw-r--r--main/src/cgeo/geocaching/network/HtmlImage.java202
1 files changed, 118 insertions, 84 deletions
diff --git a/main/src/cgeo/geocaching/network/HtmlImage.java b/main/src/cgeo/geocaching/network/HtmlImage.java
index 31edc9f..fe67af4 100644
--- a/main/src/cgeo/geocaching/network/HtmlImage.java
+++ b/main/src/cgeo/geocaching/network/HtmlImage.java
@@ -12,13 +12,13 @@ import cgeo.geocaching.utils.ImageUtils;
import cgeo.geocaching.utils.ImageUtils.ContainerDrawable;
import cgeo.geocaching.utils.Log;
import cgeo.geocaching.utils.RxUtils;
+import cgeo.geocaching.utils.RxUtils.ObservableCache;
import ch.boye.httpclientandroidlib.HttpResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
@@ -44,22 +44,14 @@ import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
-public class HtmlImage implements Html.ImageGetter {
+/**
+ * All-purpose image getter that can also be used as a ImageGetter interface when displaying caches.
+ */
- // This class implements an all-purpose image getter that can also be used as a ImageGetter interface
- // when displaying caches. An instance mainly has three possible use cases:
- // - If onlySave is true, getDrawable() will return null immediately and will queue the image retrieval
- // and saving in the loading subject. Downloads will start in parallel when the blocking
- // waitForBackgroundLoading() method is called, and they can be cancelled through the given handler.
- // - If onlySave is false and the instance is called through fetchDrawable(), then an observable for the
- // given URL will be returned. This observable will emit the local copy of the image if it is present,
- // regardless of its freshness, then if needed an updated fresher copy after retrieving it from the network.
- // - If onlySave is false and the instance is used as an ImageGetter, only the final version of the
- // image will be returned, unless a view has been provided. If it has, then a dummy drawable is returned
- // and is updated when the image is available, possibly several times if we had a stale copy of the image
- // and then got a new one from the network.
+public class HtmlImage implements Html.ImageGetter {
private static final String[] BLOCKED = new String[] {
"gccounter.de",
@@ -79,9 +71,9 @@ public class HtmlImage implements Html.ImageGetter {
};
public static final String SHARED = "shared";
- final private String geocode;
+ @NonNull final private String geocode;
/**
- * on error: return large error image, if <code>true</code>, otherwise empty 1x1 image
+ * on error: return large error image, if {@code true}, otherwise empty 1x1 image
*/
final private boolean returnErrorImage;
final private int listId;
@@ -89,25 +81,57 @@ public class HtmlImage implements Html.ImageGetter {
final private int maxWidth;
final private int maxHeight;
final private Resources resources;
- final private TextView view;
+ protected final TextView view;
+ final private Map<String, BitmapDrawable> cache = new HashMap<>();
+
+ final private ObservableCache<String, BitmapDrawable> observableCache = new ObservableCache<>(new Func1<String, Observable<BitmapDrawable>>() {
+ @Override
+ public Observable<BitmapDrawable> call(final String url) {
+ return fetchDrawableUncached(url);
+ }
+ });
// Background loading
final private PublishSubject<Observable<String>> loading = PublishSubject.create();
- final private Observable<String> waitForEnd = Observable.merge(loading).publish().refCount();
- final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe());
+ final private Observable<String> waitForEnd = Observable.merge(loading).cache();
+ final private CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe());
/**
- * Create a new HtmlImage object with different behaviours depending on <tt>onlySave</tt> and <tt>view</tt> values.
+ * Create a new HtmlImage object with different behaviors depending on <tt>onlySave</tt> and <tt>view</tt> values.
+ * There are the three possible use cases:
+ * <ul>
+ * <li>If onlySave is true, {@link #getDrawable(String)} will return <tt>null</tt> immediately and will queue the
+ * image retrieval and saving in the loading subject. Downloads will start in parallel when the blocking
+ * {@link #waitForEndObservable(cgeo.geocaching.utils.CancellableHandler)} method is called, and they can be
+ * cancelled through the given handler.</li>
+ * <li>If <tt>onlySave</tt> is <tt>false</tt> and the instance is called through {@link #fetchDrawable(String)},
+ * then an observable for the given URL will be returned. This observable will emit the local copy of the image if
+ * it is present regardless of its freshness, then if needed an updated fresher copy after retrieving it from the
+ * network.</li>
+ * <li>If <tt>onlySave</tt> is <tt>false</tt> and the instance is used as an {@link android.text.Html.ImageGetter},
+ * only the final version of the image will be returned, unless a view has been provided. If it has, then a dummy
+ * drawable is returned and is updated when the image is available, possibly several times if we had a stale copy of
+ * the image and then got a new one from the network.</li>
+ * </ul>
*
- * @param geocode the geocode of the item for which we are requesting the image
- * @param returnErrorImage set to <tt>true</tt> if an error image should be returned in case of a problem,
- * <tt>false</tt> to get a transparent 1x1 image instead
- * @param listId the list this cache belongs to, used to determine if an older image for the offline case can be used or not
- * @param onlySave if set to <tt>true</tt>, {@link #getDrawable(String)} will only fetch and store the image, not return it
- * @param view if non-null, {@link #getDrawable(String)} will return an initially empty drawable which will be redrawn when
- * the image is ready through an invalidation of the given view
+ * @param geocode
+ * the geocode of the item for which we are requesting the image, or {@link #SHARED} to use the shared
+ * cache directory
+ * @param returnErrorImage
+ * set to <tt>true</tt> if an error image should be returned in case of a problem, <tt>false</tt> to get
+ * a transparent 1x1 image instead
+ * @param listId
+ * the list this cache belongs to, used to determine if an older image for the offline case can be used
+ * or not
+ * @param onlySave
+ * if set to <tt>true</tt>, {@link #getDrawable(String)} will only fetch and store the image, not return
+ * it
+ * @param view
+ * if non-null, {@link #getDrawable(String)} will return an initially empty drawable which will be
+ * redrawn when
+ * the image is ready through an invalidation of the given view
*/
- public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave, final TextView view) {
+ public HtmlImage(@NonNull final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave, final TextView view) {
this.geocode = geocode;
this.returnErrorImage = returnErrorImage;
this.listId = listId;
@@ -121,12 +145,12 @@ public class HtmlImage implements Html.ImageGetter {
}
/**
- * Create a new HtmlImage object with different behaviours depending on <tt>onlySave</tt> value. No view object
+ * Create a new HtmlImage object with different behaviors depending on <tt>onlySave</tt> value. No view object
* will be tied to this HtmlImage.
*
* For documentation, see {@link #HtmlImage(String, boolean, int, boolean, TextView)}.
*/
- public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) {
+ public HtmlImage(@NonNull final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) {
this(geocode, returnErrorImage, listId, onlySave, null);
}
@@ -141,6 +165,9 @@ public class HtmlImage implements Html.ImageGetter {
@Nullable
@Override
public BitmapDrawable getDrawable(final String url) {
+ if (cache.containsKey(url)) {
+ return cache.get(url);
+ }
final Observable<BitmapDrawable> drawable = fetchDrawable(url);
if (onlySave) {
loading.onNext(drawable.map(new Func1<BitmapDrawable, String>() {
@@ -149,30 +176,37 @@ public class HtmlImage implements Html.ImageGetter {
return url;
}
}));
+ cache.put(url, null);
return null;
}
- if (view == null) {
- return drawable.toBlocking().lastOrDefault(null);
- }
+ final BitmapDrawable result = view == null ? drawable.toBlocking().lastOrDefault(null) : getContainerDrawable(drawable);
+ cache.put(url, result);
+ return result;
+ }
+
+ protected BitmapDrawable getContainerDrawable(final Observable<BitmapDrawable> drawable) {
return new ContainerDrawable(view, drawable);
}
- // Caches are loaded from disk on a computation scheduler to avoid using more threads than cores while decoding
- // the image. Downloads happen on downloadScheduler, in parallel with image decoding.
public Observable<BitmapDrawable> fetchDrawable(final String url) {
+ return observableCache.get(url);
+ }
+ // Caches are loaded from disk on a computation scheduler to avoid using more threads than cores while decoding
+ // the image. Downloads happen on downloadScheduler, in parallel with image decoding.
+ private Observable<BitmapDrawable> fetchDrawableUncached(final String url) {
if (StringUtils.isBlank(url) || ImageUtils.containsPattern(url, BLOCKED)) {
- return Observable.from(ImageUtils.getTransparent1x1Drawable(resources));
+ return Observable.just(ImageUtils.getTransparent1x1Drawable(resources));
}
// Explicit local file URLs are loaded from the filesystem regardless of their age. The IO part is short
// enough to make the whole operation on the computation scheduler.
if (FileUtils.isFileUrl(url)) {
- return Observable.defer(new Func0<Observable<? extends BitmapDrawable>>() {
+ return Observable.defer(new Func0<Observable<BitmapDrawable>>() {
@Override
- public Observable<? extends BitmapDrawable> call() {
- final Bitmap bitmap = loadCachedImage(FileUtils.urlToFile(url), true).getLeft();
- return bitmap != null ? Observable.from(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty();
+ public Observable<BitmapDrawable> call() {
+ final Bitmap bitmap = loadCachedImage(FileUtils.urlToFile(url), true).left;
+ return bitmap != null ? Observable.just(ImageUtils.scaleBitmapToFitDisplay(bitmap)) : Observable.<BitmapDrawable>empty();
}
}).subscribeOn(RxUtils.computationScheduler);
}
@@ -187,9 +221,9 @@ public class HtmlImage implements Html.ImageGetter {
subscriber.add(RxUtils.computationScheduler.createWorker().schedule(new Action0() {
@Override
public void call() {
- final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk();
- final BitmapDrawable bitmap = loaded.getLeft();
- if (loaded.getRight()) {
+ final ImmutablePair<BitmapDrawable, Boolean> loaded = loadFromDisk();
+ final BitmapDrawable bitmap = loaded.left;
+ if (loaded.right) {
subscriber.onNext(bitmap);
subscriber.onCompleted();
return;
@@ -206,14 +240,9 @@ public class HtmlImage implements Html.ImageGetter {
}));
}
- private Pair<BitmapDrawable, Boolean> loadFromDisk() {
- final Pair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared);
- final Bitmap bitmap = loadResult.getLeft();
- return new ImmutablePair<>(bitmap != null ?
- ImageUtils.scaleBitmapToFitDisplay(bitmap) :
- null,
- loadResult.getRight()
- );
+ private ImmutablePair<BitmapDrawable, Boolean> loadFromDisk() {
+ final ImmutablePair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared);
+ return scaleImage(loadResult);
}
private void downloadAndSave(final Subscriber<? super BitmapDrawable> subscriber) {
@@ -233,27 +262,33 @@ public class HtmlImage implements Html.ImageGetter {
}
if (onlySave) {
subscriber.onCompleted();
- } else {
- RxUtils.computationScheduler.createWorker().schedule(new Action0() {
- @Override
- public void call() {
- final Pair<BitmapDrawable, Boolean> loaded = loadFromDisk();
- final BitmapDrawable image = loaded.getLeft();
- if (image != null) {
- subscriber.onNext(image);
- } else {
- subscriber.onNext(returnErrorImage ?
- new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) :
- ImageUtils.getTransparent1x1Drawable(resources));
- }
- subscriber.onCompleted();
- }
- });
+ return;
}
+ RxUtils.computationScheduler.createWorker().schedule(new Action0() {
+ @Override
+ public void call() {
+ final ImmutablePair<BitmapDrawable, Boolean> loaded = loadFromDisk();
+ final BitmapDrawable image = loaded.left;
+ if (image != null) {
+ subscriber.onNext(image);
+ } else {
+ subscriber.onNext(returnErrorImage ?
+ new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) :
+ ImageUtils.getTransparent1x1Drawable(resources));
+ }
+ subscriber.onCompleted();
+ }
+ });
}
});
}
+ @SuppressWarnings("static-method")
+ protected ImmutablePair<BitmapDrawable, Boolean> scaleImage(final ImmutablePair<Bitmap, Boolean> loadResult) {
+ final Bitmap bitmap = loadResult.left;
+ return ImmutablePair.of(bitmap != null ? ImageUtils.scaleBitmapToFitDisplay(bitmap) : null, loadResult.right);
+ }
+
public Observable<String> waitForEndObservable(@Nullable final CancellableHandler handler) {
if (handler != null) {
handler.unsubscribeIfCancelled(subscription);
@@ -319,14 +354,14 @@ public class HtmlImage implements Html.ImageGetter {
* @param url the image URL
* @param pseudoGeocode the geocode or the shared name
* @param forceKeep keep the image if it is there, without checking its freshness
- * @return <code>true</code> if the image was there and is fresh enough, <code>false</code> otherwise
+ * @return A pair whose first element is the bitmap if available, and the second one is <code>true</code> if the image is present and fresh enough.
*/
@NonNull
- private Pair<Bitmap, Boolean> loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) {
+ private ImmutablePair<Bitmap, Boolean> loadImageFromStorage(final String url, @NonNull final String pseudoGeocode, final boolean forceKeep) {
try {
final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, false);
- final Pair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep);
- if (image.getRight() || image.getLeft() != null) {
+ final ImmutablePair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep);
+ if (image.right || image.left != null) {
return image;
}
final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true);
@@ -334,7 +369,7 @@ public class HtmlImage implements Html.ImageGetter {
} catch (final Exception e) {
Log.w("HtmlImage.loadImageFromStorage", e);
}
- return new ImmutablePair<>(null, false);
+ return ImmutablePair.of((Bitmap) null, false);
}
@Nullable
@@ -366,17 +401,17 @@ public class HtmlImage implements Html.ImageGetter {
*
* @param file the file on disk
* @param forceKeep keep the image if it is there, without checking its freshness
- * @return a pair with <code>true</code> if the image was there and is fresh enough or <code>false</code> otherwise,
- * and the image (possibly <code>null</code> if the first component is <code>false</code> and the image
- * could not be loaded, or if the first component is <code>true</code> and <code>onlySave</code> is also
+ * @return a pair with <code>true</code> in the second component if the image was there and is fresh enough or <code>false</code> otherwise,
+ * and the image (possibly <code>null</code> if the second component is <code>false</code> and the image
+ * could not be loaded, or if the second component is <code>true</code> and <code>onlySave</code> is also
* <code>true</code>)
*/
@NonNull
- private Pair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) {
+ private ImmutablePair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) {
if (file.exists()) {
- final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep;
- if (onlySave) {
- return new ImmutablePair<>(null, true);
+ final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (System.currentTimeMillis() - (24 * 60 * 60 * 1000)) || forceKeep;
+ if (freshEnough && onlySave) {
+ return ImmutablePair.of((Bitmap) null, true);
}
final BitmapFactory.Options bfOptions = new BitmapFactory.Options();
bfOptions.inTempStorage = new byte[16 * 1024];
@@ -385,12 +420,11 @@ public class HtmlImage implements Html.ImageGetter {
final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions);
if (image == null) {
Log.e("Cannot decode bitmap from " + file.getPath());
- return new ImmutablePair<>(null, false);
+ return ImmutablePair.of((Bitmap) null, false);
}
- return new ImmutablePair<>(image,
- freshEnough);
+ return ImmutablePair.of(image, freshEnough);
}
- return new ImmutablePair<>(null, false);
+ return ImmutablePair.of((Bitmap) null, false);
}
private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) {