aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--main/src/cgeo/geocaching/network/HtmlImage.java232
-rw-r--r--main/src/cgeo/geocaching/ui/ImagesList.java15
2 files changed, 150 insertions, 97 deletions
diff --git a/main/src/cgeo/geocaching/network/HtmlImage.java b/main/src/cgeo/geocaching/network/HtmlImage.java
index bebce42..7cff0d3 100644
--- a/main/src/cgeo/geocaching/network/HtmlImage.java
+++ b/main/src/cgeo/geocaching/network/HtmlImage.java
@@ -13,11 +13,11 @@ import cgeo.geocaching.utils.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.androidextra.Base64;
-
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.Nullable;
-
import rx.Observable;
import rx.Observable.OnSubscribeFunc;
import rx.Observer;
@@ -45,14 +45,23 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
-import java.util.HashSet;
-import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class HtmlImage implements Html.ImageGetter {
+ // 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.
+
private static final String[] BLOCKED = new String[] {
"gccounter.de",
"gccounter.com",
@@ -86,9 +95,8 @@ public class HtmlImage implements Html.ImageGetter {
final private PublishSubject<Observable<String>> loading = PublishSubject.create();
final Observable<String> waitForEnd = Observable.merge(loading).publish().refCount();
final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe());
- final private Scheduler downloadScheduler = Schedulers.executor(new ThreadPoolExecutor(5, 5, 5, TimeUnit.SECONDS,
+ final private Scheduler downloadScheduler = Schedulers.executor(new ThreadPoolExecutor(10, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>()));
- final private Set<String> downloading = new HashSet<String>();
public HtmlImage(final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) {
this.geocode = geocode;
@@ -105,34 +113,94 @@ public class HtmlImage implements Html.ImageGetter {
@Nullable
@Override
public BitmapDrawable getDrawable(final String url) {
- if (!onlySave) {
- return loadDrawable(url);
- }
- synchronized (downloading) {
- if (!downloading.contains(url)) {
- loading.onNext(fetchDrawable(url).map(new Func1<BitmapDrawable, String>() {
- @Override
- public String call(final BitmapDrawable bitmapDrawable) {
- return url;
- }
- }));
- downloading.add(url);
- }
+ final Observable<BitmapDrawable> drawable = fetchDrawable(url);
+ if (onlySave) {
+ loading.onNext(drawable.map(new Func1<BitmapDrawable, String>() {
+ @Override
+ public String call(final BitmapDrawable bitmapDrawable) {
+ return url;
+ }
+ }));
return null;
+ } else {
+ return drawable.toBlockingObservable().lastOrDefault(null);
}
}
public Observable<BitmapDrawable> fetchDrawable(final String url) {
- return Observable.create(new OnSubscribeFunc<BitmapDrawable>() {
+ final boolean shared = url.contains("/images/icons/icon_");
+ final String pseudoGeocode = shared ? SHARED : geocode;
+
+ final Observable<Pair<BitmapDrawable, Boolean>> loadFromDisk =
+ Observable.create(new OnSubscribeFunc<Pair<BitmapDrawable, Boolean>>() {
+ @Override
+ public Subscription onSubscribe(final Observer<? super Pair<BitmapDrawable, Boolean>> observer) {
+ final Pair<Bitmap, Boolean> loadResult = loadImageFromStorage(url, pseudoGeocode, shared);
+ final Bitmap bitmap = loadResult.getLeft();
+ observer.onNext(new ImmutablePair<BitmapDrawable, Boolean>(bitmap != null ?
+ ImageUtils.scaleBitmapToFitDisplay(bitmap) :
+ null,
+ loadResult.getRight()));
+ observer.onCompleted();
+ return Subscriptions.empty();
+ }
+ }).subscribeOn(Schedulers.computation());
+
+ final Observable<BitmapDrawable> downloadAndSave =
+ Observable.create(new OnSubscribeFunc<BitmapDrawable>() {
+ @Override
+ public Subscription onSubscribe(final Observer<? super BitmapDrawable> observer) {
+ final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, true);
+ if (url.startsWith("data:image/")) {
+ if (url.contains(";base64,")) {
+ saveBase64ToFile(url, file);
+ } else {
+ Log.e("HtmlImage.getDrawable: unable to decode non-base64 inline image");
+ observer.onCompleted();
+ return Subscriptions.empty();
+ }
+ } else {
+ if (subscription.isUnsubscribed() || downloadOrRefreshCopy(url, file)) {
+ // The existing copy was fresh enough or we were unsubscribed earlier.
+ observer.onCompleted();
+ return Subscriptions.empty();
+ }
+ }
+ if (onlySave) {
+ observer.onCompleted();
+ } else {
+ loadFromDisk.map(new Func1<Pair<BitmapDrawable, Boolean>, BitmapDrawable>() {
+ @Override
+ public BitmapDrawable call(final Pair<BitmapDrawable, Boolean> loadResult) {
+ final BitmapDrawable image = loadResult.getLeft();
+ if (image != null) {
+ return image;
+ } else {
+ return returnErrorImage ?
+ new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded)) :
+ getTransparent1x1Image(resources);
+ }
+ }
+ }).subscribe(observer);
+ }
+ return Subscriptions.empty();
+ }
+ }).subscribeOn(downloadScheduler);
+
+ if (StringUtils.isBlank(url) || isCounter(url)) {
+ return Observable.from(getTransparent1x1Image(resources));
+ }
+
+ return loadFromDisk.switchMap(new Func1<Pair<BitmapDrawable, Boolean>, Observable<? extends BitmapDrawable>>() {
@Override
- public Subscription onSubscribe(final Observer<? super BitmapDrawable> observer) {
- if (!subscription.isUnsubscribed()) {
- observer.onNext(loadDrawable(url));
+ public Observable<? extends BitmapDrawable> call(final Pair<BitmapDrawable, Boolean> loadResult) {
+ final BitmapDrawable bitmap = loadResult.getLeft();
+ if (loadResult.getRight()) {
+ return Observable.from(bitmap);
}
- observer.onCompleted();
- return Subscriptions.empty();
+ return bitmap != null && !onlySave ? downloadAndSave.startWith(bitmap) : downloadAndSave;
}
- }).subscribeOn(downloadScheduler);
+ });
}
public void waitForBackgroundLoading(@Nullable final CancellableHandler handler) {
@@ -143,54 +211,14 @@ public class HtmlImage implements Html.ImageGetter {
waitForEnd.toBlockingObservable().lastOrDefault(null);
}
- private BitmapDrawable loadDrawable(final String url) {
- // Reject empty and counter images URL
- if (StringUtils.isBlank(url) || isCounter(url)) {
- return getTransparent1x1Image(resources);
- }
-
- final boolean shared = url.contains("/images/icons/icon_");
- final String pseudoGeocode = shared ? SHARED : geocode;
-
- Bitmap image = loadImageFromStorage(url, pseudoGeocode, shared);
-
- // Download image and save it to the cache
- if (image == null) {
- final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, true);
- if (url.startsWith("data:image/")) {
- if (url.contains(";base64,")) {
- saveBase64ToFile(url, file);
- } else {
- Log.e("HtmlImage.getDrawable: unable to decode non-base64 inline image");
- return null;
- }
- } else {
- downloadOrRefreshCopy(url, file);
- }
- }
-
- if (onlySave) {
- return null;
- }
-
- // now load the newly downloaded image
- if (image == null) {
- image = loadImageFromStorage(url, pseudoGeocode, shared);
- }
-
- // get image and return
- if (image == null) {
- Log.d("HtmlImage.getDrawable: Failed to obtain image");
-
- return returnErrorImage
- ? new BitmapDrawable(resources, BitmapFactory.decodeResource(resources, R.drawable.image_not_loaded))
- : getTransparent1x1Image(resources);
- }
-
- return ImageUtils.scaleBitmapToFitDisplay(image);
- }
-
- private void downloadOrRefreshCopy(final String url, final File file) {
+ /**
+ * Download or refresh the copy of <code>url</code> in <code>file</code>.
+ *
+ * @param url the url of the document
+ * @param file the file to save the document in
+ * @return <code>true</code> if the existing file was up-to-date, <code>false</code> otherwise
+ */
+ private boolean downloadOrRefreshCopy(final String url, final File file) {
final String absoluteURL = makeAbsoluteURL(url);
if (absoluteURL != null) {
@@ -204,12 +232,14 @@ public class HtmlImage implements Html.ImageGetter {
if (!file.setLastModified(System.currentTimeMillis())) {
makeFreshCopy(file);
}
+ return true;
}
}
} catch (Exception e) {
Log.e("HtmlImage.downloadOrRefreshCopy", e);
}
}
+ return false;
}
private static void saveBase64ToFile(final String url, final File file) {
@@ -250,12 +280,20 @@ public class HtmlImage implements Html.ImageGetter {
return new BitmapDrawable(res, BitmapFactory.decodeResource(resources, R.drawable.image_no_placement));
}
+ /**
+ * Load an image from primary or secondary storage.
+ *
+ * @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
+ */
@Nullable
- private Bitmap loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) {
+ private Pair<Bitmap, Boolean> loadImageFromStorage(final String url, final String pseudoGeocode, final boolean forceKeep) {
try {
final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, false);
- final Bitmap image = loadCachedImage(file, forceKeep);
- if (image != null) {
+ final Pair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep);
+ if (image.getRight() || image.getLeft() != null) {
return image;
}
final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true);
@@ -263,7 +301,7 @@ public class HtmlImage implements Html.ImageGetter {
} catch (Exception e) {
Log.w("HtmlImage.loadImageFromStorage", e);
}
- return null;
+ return new ImmutablePair<Bitmap, Boolean>(null, false);
}
@Nullable
@@ -290,22 +328,36 @@ public class HtmlImage implements Html.ImageGetter {
return null;
}
+ /**
+ * Load a previously saved image.
+ *
+ * @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
+ * <code>true</code>)
+ */
@Nullable
- private Bitmap loadCachedImage(final File file, final boolean forceKeep) {
+ private Pair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) {
if (file.exists()) {
- if (listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep) {
- final BitmapFactory.Options bfOptions = new BitmapFactory.Options();
- bfOptions.inTempStorage = new byte[16 * 1024];
- bfOptions.inPreferredConfig = Bitmap.Config.RGB_565;
- setSampleSize(file, bfOptions);
- final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions);
- if (image == null) {
- Log.e("Cannot decode bitmap from " + file.getPath());
- }
- return image;
+ final boolean freshEnough = listId >= StoredList.STANDARD_LIST_ID || file.lastModified() > (new Date().getTime() - (24 * 60 * 60 * 1000)) || forceKeep;
+ if (onlySave) {
+ return new ImmutablePair<Bitmap, Boolean>(null, true);
+ }
+ final BitmapFactory.Options bfOptions = new BitmapFactory.Options();
+ bfOptions.inTempStorage = new byte[16 * 1024];
+ bfOptions.inPreferredConfig = Bitmap.Config.RGB_565;
+ setSampleSize(file, bfOptions);
+ final Bitmap image = BitmapFactory.decodeFile(file.getPath(), bfOptions);
+ if (image == null) {
+ Log.e("Cannot decode bitmap from " + file.getPath());
+ return new ImmutablePair<Bitmap, Boolean>(null, false);
}
+ return new ImmutablePair<Bitmap, Boolean>(image,
+ freshEnough);
}
- return null;
+ return new ImmutablePair<Bitmap, Boolean>(null, false);
}
private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) {
diff --git a/main/src/cgeo/geocaching/ui/ImagesList.java b/main/src/cgeo/geocaching/ui/ImagesList.java
index 1cd5a48..4058e15 100644
--- a/main/src/cgeo/geocaching/ui/ImagesList.java
+++ b/main/src/cgeo/geocaching/ui/ImagesList.java
@@ -27,7 +27,6 @@ import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
-import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -97,22 +96,23 @@ public class ImagesList {
descView.setVisibility(View.VISIBLE);
}
+ final ImageView imageView = (ImageView) inflater.inflate(R.layout.image_item, null);
+ assert(imageView != null);
subscriptions.add(AndroidObservable.fromActivity(activity, imgGetter.fetchDrawable(img.getUrl()))
.subscribe(new Action1<BitmapDrawable>() {
@Override
public void call(final BitmapDrawable image) {
- display(image, img, rowView);
+ display(imageView, image, img, rowView);
}
}));
+ rowView.addView(imageView);
imagesView.addView(rowView);
}
}
- private void display(final BitmapDrawable image, final Image img, final LinearLayout view) {
+ private void display(final ImageView imageView, final BitmapDrawable image, final Image img, final LinearLayout view) {
if (image != null) {
bitmaps.add(image.getBitmap());
- final ImageView imageView = (ImageView) inflater.inflate(R.layout.image_item, null);
- assert(imageView != null);
final Rect bounds = image.getBounds();
@@ -127,13 +127,14 @@ public class ImagesList {
activity.registerForContextMenu(imageView);
imageView.setImageDrawable(image);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
- imageView.setLayoutParams(new LayoutParams(bounds.width(), bounds.height()));
+ imageView.setLayoutParams(new LinearLayout.LayoutParams(bounds.width(), bounds.height()));
view.findViewById(R.id.progress_bar).setVisibility(View.GONE);
- view.addView(imageView);
imageView.setId(image.hashCode());
images.put(imageView.getId(), img);
+
+ view.invalidate();
}
}