@SuppressWarnings("PMD.NPathComplexity")
    private Observable<ImageData> fetchDrawableUncached(final String url) {
        if (StringUtils.isBlank(url) || ImageUtils.containsPattern(url, BLOCKED)) {
            return Observable.just(new ImageData(ImageUtils.getTransparent1x1Drawable(resources), null, null));
        }

        // 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(() -> {
                final ImmutableTriple<Bitmap, Metadata, Boolean> data = loadCachedImage(FileUtils.urlToFile(url), true);
                return data != null && data.left != null ?
                    Observable.just(new ImageData(ImageUtils.scaleBitmapToDisplay(data.left), data.middle, Uri.parse(url))) :
                        Observable.just(IMAGE_ERROR_DATA);
            }).subscribeOn(AndroidRxUtils.computationScheduler);
        }
        // Content Uris are also loaded regardless of their age (needed for spoiler images)
        final Uri uri = Uri.parse(url);
        if (UriUtils.isContentUri(uri)) {
            return Observable.defer(() -> {
                delayForTest();

                final ImmutableTriple<Bitmap, Metadata, Boolean> data = loadCachedImage(uri, true, -1);
                return data != null && data.left != null ?
                    Observable.just(new ImageData(ImageUtils.scaleBitmapToDisplay(data.left), data.middle, uri)) :
                        Observable.just(IMAGE_ERROR_DATA);
            }).subscribeOn(AndroidRxUtils.computationScheduler);
        }

        final boolean shared = url.contains("/images/icons/icon_");
        final String pseudoGeocode = shared ? SHARED : geocode;

        return Observable.create(new ObservableOnSubscribe<ImageData>() {
            @Override
            public void subscribe(final ObservableEmitter<ImageData> emitter) {
                // Canceling disposable must sever this connection
                final CancellableDisposable aborter = new CancellableDisposable(emitter::onComplete);
                disposable.add(aborter);
                // Canceling this subscription must dispose the data retrieval
                emitter.setDisposable(AndroidRxUtils.computationScheduler.scheduleDirect(() -> {
                    delayForTest();
                    final InternalImageData loaded = loadFromDisk();
                    final BitmapDrawable bitmap = loaded.bitmapDrawable;
                    if (loaded.isFresh) {
                        if (!onlySave) {
                            emitter.onNext(new ImageData(bitmap, loaded.metadata, loaded.localUri));
                        }
                        emitter.onComplete();
                        return;
                    }
                    if (bitmap != null && !onlySave) {
                        emitter.onNext(new ImageData(bitmap, loaded.metadata, loaded.localUri));
                    }
                    AndroidRxUtils.networkScheduler.scheduleDirect(() -> downloadAndSave(emitter, aborter));
                }));
            }

            private InternalImageData loadFromDisk() {
                final InternalImageData loadResult = loadImageFromStorage(url, pseudoGeocode, shared);
                loadResult.bitmapDrawable =  scaleImage(loadResult.bitmap);
                return loadResult;
            }

            private void downloadAndSave(final ObservableEmitter<ImageData> emitter, final Disposable disposable) {
                final File file = LocalStorage.getGeocacheDataFile(pseudoGeocode, url, true, true);
                if (url.startsWith("data:image/")) {
                    if (url.contains(";base64,")) {
                        ImageUtils.decodeBase64ToFile(StringUtils.substringAfter(url, ";base64,"), file);
                    } else {
                        Log.e("HtmlImage.fetchDrawableUncached: unable to decode non-base64 inline image");
                        emitter.onComplete();
                        return;
                    }
                } else if (disposable.isDisposed() || downloadOrRefreshCopy(url, file)) {
                    // The existing copy was fresh enough or we were unsubscribed earlier.
                    emitter.onComplete();
                    return;
                }
                if (onlySave) {
                    emitter.onComplete();
                    return;
                }
                AndroidRxUtils.computationScheduler.scheduleDirect(() -> {
                    final InternalImageData loaded = loadFromDisk();
                    final BitmapDrawable image = loaded.bitmapDrawable;
                    if (image != null) {
                        emitter.onNext(new ImageData(image, loaded.metadata, loaded.localUri));
                    } else {
                        emitter.onNext(new ImageData(getErrorImage(), null, null));
                    }
                    emitter.onComplete();
                });
            }
        });
    }
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.
main/src/main/java/cgeo/geocaching/network/HtmlImage.java