@Test(groups = "standalone") public void testObserveMultiple() { final TestSubscriber<Response> tester = new TestSubscriber<>(); try (AsyncHttpClient client = asyncHttpClient()) { Observable<Response> o1 = AsyncHttpObservable.observe(() -> client.prepareGet("http://gatling.io")); Observable<Response> o2 = AsyncHttpObservable.observe( () -> client.prepareGet("http://www.wisc.edu").setFollowRedirect(true)); Observable<Response> o3 = AsyncHttpObservable.observe( () -> client.prepareGet("http://www.umn.edu").setFollowRedirect(true)); Observable<Response> all = Observable.merge(o1, o2, o3); all.subscribe(tester); tester.awaitTerminalEvent(); tester.assertTerminalEvent(); tester.assertCompleted(); tester.assertNoErrors(); List<Response> responses = tester.getOnNextEvents(); assertNotNull(responses); assertEquals(responses.size(), 3); for (Response response : responses) { assertEquals(response.getStatusCode(), 200); } } catch (Exception e) { Thread.currentThread().interrupt(); } }
/** IO scheduler defaults to using CachedThreadScheduler */ @Test public final void testIOScheduler() { Observable<Integer> o1 = Observable.from(1, 2, 3, 4, 5); Observable<Integer> o2 = Observable.from(6, 7, 8, 9, 10); Observable<String> o = Observable.merge(o1, o2) .map( new Func1<Integer, String>() { @Override public String call(Integer t) { assertTrue( Thread.currentThread().getName().startsWith("RxCachedThreadScheduler")); return "Value_" + t + "_Thread_" + Thread.currentThread().getName(); } }); o.subscribeOn(Schedulers.io()) .toBlocking() .forEach( new Action1<String>() { @Override public void call(String t) { System.out.println("t: " + t); } }); }
public Observable<Pair<IAutomationMethod, Float>> getKeyEvents() { ArrayList<Observable<Pair<IAutomationMethod, Float>>> keyEvents = new ArrayList<Observable<Pair<IAutomationMethod, Float>>>(); for (Map.Entry<Component.Identifier, IAutomationMethod> entry : automationMethodHashMap.entrySet()) { IRxAutomationProjection projector = identifierToProjectionMap.get(entry.getKey()); Observable<Pair<IAutomationMethod, Float>> keyEvent = projector.map(entry.getValue(), this.rxGamePad.getComponentById(entry.getKey())); keyEvents.add(keyEvent); } return Observable.merge(keyEvents); }
private void bind(Photos photos) { mSwipeRefreshLayout.setRefreshing(false); mPhotos = photos; mPhotosAdapter = new PhotosAdapter(mPhotos.getPhotos()); mRecyclerView.setAdapter(mPhotosAdapter); Observable.merge( mPhotosAdapter .getThumbnailClickedSubject() .filter(PhotosValidator::isSourceValid) .map(Photo::getSourceUrl), mPhotosAdapter .getItemClickedSubject() .filter(photo -> StringUtils.isValidUrl(photo.getPermalink())) .map(Photo::getPermalink)) .compose(bindUntilEvent(ActivityEvent.DESTROY)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(OnlyNextObserver.forAction(this::view)); }
private void subscribeToChanges() { RxSharedPreferences prefs = RxSharedPreferences.create(PreferenceManager.getDefaultSharedPreferences(mAppContext)); Preference<Integer> lastPage = prefs.getInteger(Constants.PREF_LAST_PAGE); Observable.merge( mBookmarkModel.tagsObservable(), mBookmarkModel.bookmarksObservable(), lastPage.asObservable()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( new Action1<Object>() { @Override public void call(Object o) { if (mFragment != null) { requestData(false); } else { mCachedData = null; } } }); }
@Test public void testNumberOfThreadsOnScheduledMerge() { final ConcurrentHashMap<Long, Long> threads = new ConcurrentHashMap<Long, Long>(); // now we parallelMerge into 3 streams and observeOn for each // we expect 3 threads in the output Observable.merge(Observable.parallelMerge(getStreams(), 3, Schedulers.newThread())) .toBlocking() .forEach( new Action1<String>() { @Override public void call(String o) { System.out.println("o: " + o + " Thread: " + Thread.currentThread().getId()); threads.put(Thread.currentThread().getId(), Thread.currentThread().getId()); } }); assertTrue( threads.keySet().size() <= 3); // can be less than since merge doesn't block threads and may not use all of them }
/** * All-purpose image getter that can also be used as a ImageGetter interface when displaying caches. */ public class HtmlImage implements Html.ImageGetter { private static final String[] BLOCKED = { "gccounter.de", "gccounter.com", "cachercounter/?", "gccounter/imgcount.php", "flagcounter.com", "compteur-blog.net", "counter.digits.com", "andyhoppe", "besucherzaehler-homepage.de", "hitwebcounter.com", "kostenloser-counter.eu", "trendcounter.com", "hit-counter-download.com", "gcwetterau.de/counter" }; public static final String SHARED = "shared"; @NonNull private final String geocode; /** on error: return large error image, if {@code true}, otherwise empty 1x1 image */ private final boolean returnErrorImage; private final int listId; private final boolean onlySave; private final int maxWidth; private final int maxHeight; private final Resources resources; protected final TextView view; private final Map<String, BitmapDrawable> cache = new HashMap<>(); private final ObservableCache<String, BitmapDrawable> observableCache = new ObservableCache<>( new Func1<String, Observable<BitmapDrawable>>() { @Override public Observable<BitmapDrawable> call(final String url) { return fetchDrawableUncached(url); } }); // Background loading private final PublishSubject<Observable<String>> loading = PublishSubject.create(); private final Observable<String> waitForEnd = Observable.merge(loading).cache(); private final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe()); /** * 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>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>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. * </ul> * * @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( @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; this.onlySave = onlySave; this.view = view; final Point displaySize = Compatibility.getDisplaySize(); this.maxWidth = displaySize.x - 25; this.maxHeight = displaySize.y - 25; this.resources = CgeoApplication.getInstance().getResources(); } /** * Create a new HtmlImage object with different behaviors depending on <tt>onlySave</tt> value. No * view object will be tied to this HtmlImage. * * <p>For documentation, see {@link #HtmlImage(String, boolean, int, boolean, TextView)}. */ public HtmlImage( @NonNull final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) { this(geocode, returnErrorImage, listId, onlySave, null); } /** * Retrieve and optionally display an image. See {@link #HtmlImage(String, boolean, int, boolean, * TextView)} for the various behaviours. * * @param url the URL to fetch from cache or network * @return a drawable containing the image, or <tt>null</tt> if <tt>onlySave</tt> is <tt>true</tt> */ @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>() { @Override public String call(final BitmapDrawable bitmapDrawable) { return url; } })); cache.put(url, null); return 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); } 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.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<BitmapDrawable>>() { @Override 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(AndroidRxUtils.computationScheduler); } final boolean shared = url.contains("/images/icons/icon_"); final String pseudoGeocode = shared ? SHARED : geocode; return Observable.create( new OnSubscribe<BitmapDrawable>() { @Override public void call(final Subscriber<? super BitmapDrawable> subscriber) { subscription.add(subscriber); subscriber.add( AndroidRxUtils.computationScheduler .createWorker() .schedule( new Action0() { @Override public void call() { final ImmutablePair<BitmapDrawable, Boolean> loaded = loadFromDisk(); final BitmapDrawable bitmap = loaded.left; if (loaded.right) { subscriber.onNext(bitmap); subscriber.onCompleted(); return; } if (bitmap != null && !onlySave) { subscriber.onNext(bitmap); } AndroidRxUtils.networkScheduler .createWorker() .schedule( new Action0() { @Override public void call() { downloadAndSave(subscriber); } }); } })); } 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) { final File file = LocalStorage.getStorageFile(pseudoGeocode, url, true, true); if (url.startsWith("data:image/")) { if (url.contains(";base64,")) { ImageUtils.decodeBase64ToFile(StringUtils.substringAfter(url, ";base64,"), file); } else { Log.e("HtmlImage.getDrawable: unable to decode non-base64 inline image"); subscriber.onCompleted(); return; } } else if (subscriber.isUnsubscribed() || downloadOrRefreshCopy(url, file)) { // The existing copy was fresh enough or we were unsubscribed earlier. subscriber.onCompleted(); return; } if (onlySave) { subscriber.onCompleted(); return; } AndroidRxUtils.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); } loading.onCompleted(); return waitForEnd; } /** * 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) { try { final HttpResponse httpResponse = Network.getRequest(absoluteURL, null, file); if (httpResponse != null) { final int statusCode = httpResponse.getStatusLine().getStatusCode(); if (statusCode == 200) { LocalStorage.saveEntityToFile(httpResponse, file); } else if (statusCode == 304) { if (!file.setLastModified(System.currentTimeMillis())) { makeFreshCopy(file); } return true; } } } catch (final Exception e) { Log.e("HtmlImage.downloadOrRefreshCopy", e); } } return false; } /** * Make a fresh copy of the file to reset its timestamp. On some storage, it is impossible to * modify the modified time after the fact, in which case a brand new file must be created if we * want to be able to use the time as validity hint. * * <p>See Android issue 1699. * * @param file the file to refresh */ private static void makeFreshCopy(final File file) { final File tempFile = new File(file.getParentFile(), file.getName() + "-temp"); if (file.renameTo(tempFile)) { LocalStorage.copy(tempFile, file); FileUtils.deleteIgnoringFailure(tempFile); } else { Log.e("Could not reset timestamp of file " + file.getAbsolutePath()); } } /** * 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 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 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 ImmutablePair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep); if (image.right || image.left != null) { return image; } final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true); return loadCachedImage(fileSec, forceKeep); } catch (final Exception e) { Log.w("HtmlImage.loadImageFromStorage", e); } return ImmutablePair.of((Bitmap) null, false); } @Nullable private String makeAbsoluteURL(final String url) { // Check if uri is absolute or not, if not attach the connector hostname // FIXME: that should also include the scheme if (Uri.parse(url).isAbsolute()) { return url; } final String host = ConnectorFactory.getConnector(geocode).getHost(); if (StringUtils.isNotEmpty(host)) { final StringBuilder builder = new StringBuilder("http://"); builder.append(host); if (!StringUtils.startsWith(url, "/")) { // FIXME: explain why the result URL would be valid if the path does not start with // a '/', or signal an error. builder.append('/'); } builder.append(url); return builder.toString(); } 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> 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 ImmutablePair<Bitmap, Boolean> loadCachedImage(final File file, final boolean forceKeep) { if (file.exists()) { 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]; 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 ImmutablePair.of((Bitmap) null, false); } return ImmutablePair.of(image, freshEnough); } return ImmutablePair.of((Bitmap) null, false); } private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) { // Decode image size only final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BufferedInputStream stream = null; try { stream = new BufferedInputStream(new FileInputStream(file)); BitmapFactory.decodeStream(stream, null, options); } catch (final FileNotFoundException e) { Log.e("HtmlImage.setSampleSize", e); } finally { IOUtils.closeQuietly(stream); } int scale = 1; if (options.outHeight > maxHeight || options.outWidth > maxWidth) { scale = Math.max(options.outHeight / maxHeight, options.outWidth / maxWidth); } bfOptions.inSampleSize = scale; } }
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", "cachercounter/?", "gccounter/imgcount.php", "flagcounter.com", "compteur-blog.net", "counter.digits.com", "andyhoppe", "besucherzaehler-homepage.de", "hitwebcounter.com", "kostenloser-counter.eu", "trendcounter.com", "hit-counter-download.com", "gcwetterau.de/counter" }; public static final String SHARED = "shared"; private final String geocode; /** on error: return large error image, if <code>true</code>, otherwise empty 1x1 image */ private final boolean returnErrorImage; private final int listId; private final boolean onlySave; private final int maxWidth; private final int maxHeight; private final Resources resources; // Background loading private final PublishSubject<Observable<String>> loading = PublishSubject.create(); final Observable<String> waitForEnd = Observable.merge(loading).publish().refCount(); final CompositeSubscription subscription = new CompositeSubscription(waitForEnd.subscribe()); private final Scheduler downloadScheduler = Schedulers.executor( new ThreadPoolExecutor(10, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>())); public HtmlImage( final String geocode, final boolean returnErrorImage, final int listId, final boolean onlySave) { this.geocode = geocode; this.returnErrorImage = returnErrorImage; this.listId = listId; this.onlySave = onlySave; Point displaySize = Compatibility.getDisplaySize(); this.maxWidth = displaySize.x - 25; this.maxHeight = displaySize.y - 25; this.resources = CgeoApplication.getInstance().getResources(); } @Nullable @Override public BitmapDrawable getDrawable(final String 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) { 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 Observable<? extends BitmapDrawable> call( final Pair<BitmapDrawable, Boolean> loadResult) { final BitmapDrawable bitmap = loadResult.getLeft(); if (loadResult.getRight()) { return Observable.from(bitmap); } return bitmap != null && !onlySave ? downloadAndSave.startWith(bitmap) : downloadAndSave; } }); } public void waitForBackgroundLoading(@Nullable final CancellableHandler handler) { if (handler != null) { handler.unsubscribeIfCancelled(subscription); } loading.onCompleted(); waitForEnd.toBlockingObservable().lastOrDefault(null); } /** * 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) { try { final HttpResponse httpResponse = Network.getRequest(absoluteURL, null, file); if (httpResponse != null) { final int statusCode = httpResponse.getStatusLine().getStatusCode(); if (statusCode == 200) { LocalStorage.saveEntityToFile(httpResponse, file); } else if (statusCode == 304) { 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) { // TODO: when we use SDK level 8 or above, we can use the streaming version of the base64 // Android utilities. OutputStream out = null; try { out = new FileOutputStream(file); out.write(Base64.decode(StringUtils.substringAfter(url, ";base64,"), Base64.DEFAULT)); } catch (final IOException e) { Log.e("HtmlImage.saveBase64ToFile: cannot write file for decoded inline image", e); } finally { IOUtils.closeQuietly(out); } } /** * Make a fresh copy of the file to reset its timestamp. On some storage, it is impossible to * modify the modified time after the fact, in which case a brand new file must be created if we * want to be able to use the time as validity hint. * * <p>See Android issue 1699. * * @param file the file to refresh */ private static void makeFreshCopy(final File file) { final File tempFile = new File(file.getParentFile(), file.getName() + "-temp"); if (file.renameTo(tempFile)) { LocalStorage.copy(tempFile, file); FileUtils.deleteIgnoringFailure(tempFile); } else { Log.e("Could not reset timestamp of file " + file.getAbsolutePath()); } } private BitmapDrawable getTransparent1x1Image(final Resources res) { 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 */ @NonNull 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 Pair<Bitmap, Boolean> image = loadCachedImage(file, forceKeep); if (image.getRight() || image.getLeft() != null) { return image; } final File fileSec = LocalStorage.getStorageSecFile(pseudoGeocode, url, true); return loadCachedImage(fileSec, forceKeep); } catch (Exception e) { Log.w("HtmlImage.loadImageFromStorage", e); } return new ImmutablePair<Bitmap, Boolean>(null, false); } @Nullable private String makeAbsoluteURL(final String url) { // Check if uri is absolute or not, if not attach the connector hostname // FIXME: that should also include the scheme if (Uri.parse(url).isAbsolute()) { return url; } final String host = ConnectorFactory.getConnector(geocode).getHost(); if (StringUtils.isNotEmpty(host)) { final StringBuilder builder = new StringBuilder("http://"); builder.append(host); if (!StringUtils.startsWith(url, "/")) { // FIXME: explain why the result URL would be valid if the path does not start with // a '/', or signal an error. builder.append('/'); } builder.append(url); return builder.toString(); } 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>) */ @NonNull private Pair<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<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 new ImmutablePair<Bitmap, Boolean>(null, false); } private void setSampleSize(final File file, final BitmapFactory.Options bfOptions) { // Decode image size only BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BufferedInputStream stream = null; try { stream = new BufferedInputStream(new FileInputStream(file)); BitmapFactory.decodeStream(stream, null, options); } catch (FileNotFoundException e) { Log.e("HtmlImage.setSampleSize", e); } finally { IOUtils.closeQuietly(stream); } int scale = 1; if (options.outHeight > maxHeight || options.outWidth > maxWidth) { scale = Math.max(options.outHeight / maxHeight, options.outWidth / maxWidth); } bfOptions.inSampleSize = scale; } private static boolean isCounter(final String url) { for (String entry : BLOCKED) { if (StringUtils.containsIgnoreCase(url, entry)) { return true; } } return false; } }