diff --git a/docs/docs/image-loader.md b/docs/docs/image-loader.md index a5716c80..f9642215 100644 --- a/docs/docs/image-loader.md +++ b/docs/docs/image-loader.md @@ -77,6 +77,12 @@ AsyncDrawableLoader.builder() If not provided explicitly, default `new OkHttpClient()` will be used +:::warning +This configuration option is scheduled to be removed in `3.0.0` version, +use `NetworkSchemeHandler.create(OkHttpClient)` directly by calling +`build.addSchemeHandler()` +::: + ### Resources `android.content.res.Resources` to be used when obtaining an image @@ -103,6 +109,12 @@ To quote Android documentation for `#getSystem` method: ::: +:::warning +This configuration option is scheduled to be removed in `3.0.0`. Construct +your `MediaDecoder`s and `SchemeHandler`s appropriately and add them via +`build.addMediaDecoder()` and `builder.addSchemeHandler` +::: + ### Executor service `ExecutorService` to be used to download images in background thread @@ -113,7 +125,7 @@ AsyncDrawableLoader.builder() .build(); ``` -If not provided explicitly, default `okHttpClient.dispatcher().executorService()` will be used +If not provided explicitly, default `Executors.newCachedThreadPool()` will be used ### Error drawable @@ -134,8 +146,9 @@ of a specific image type. ```java AsyncDrawableLoader.builder() - .mediaDecoders(MediaDecoder...) - .mediaDecoders(List) + .addMediaDecoder(MediaDecoder) + .addMediaDecoders(MediaDecoder...) + .addMediaDecoders(Iterable) .build(); ``` @@ -179,4 +192,52 @@ GifMediaDecoder.create(boolean) ```java ImageMediaDecoder.create(Resources) -``` \ No newline at end of file +``` + +### Scheme handler + +Starting with `2.0.0` `image-loader` module introduced +`SchemeHandler` abstraction + +```java +AsyncDrawableLoader.builder() + .addSchemeHandler(SchemeHandler) + .build() +``` + +Currently there are 3 `SchemeHandler`s that are bundled with this module: +* `NetworkSchemeHandler` (`http` and `https`) +* `FileSchemeHandler` (`file`) +* `DataUriSchemeHandler` (`data`) + +#### NetworkSchemeHandler + +```java +NetworkSchemeHandler.create(OkHttpClient); +``` + +#### FileSchemeHandler + +Simple file handler +```java +FileSchemeHandler.create(); +``` + +File handler that additionally allows access to Android `assets` folder +```java +FileSchemeHandler.createWithAssets(AssetManager); +``` + +#### DataUriSchemeHandler + +```java +DataUriSchemeHandler.create(); +``` + +--- + +::: warning +Note that currently if no `SchemeHandler`s were provided via `builder.addSchemeHandler()` +call then all 3 default scheme handlers will be added. The same goes for `MediaDecoder`s +(`builder.addMediaDecoder`). This behavior is scheduled to be removed in `3.0.0` +::: \ No newline at end of file diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java index 0009c855..432e8797 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/AsyncDrawableLoader.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -79,7 +78,13 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { // todo: should we cancel pending request for the same destination? // we _could_ but there is possibility that one resource is request in multiple places - // todo, if not a link -> show placeholder + // todo: error handing (simply applying errorDrawable is not a good solution + // as reason for an error is unclear (no scheme handler, no input data, error decoding, etc) + + // todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal + // for big images for sure. We _could_ introduce internal Drawable that will check for + // image bounds (but we will need to cache inputStream in order to inspect and optimize + // input image...) return executorService.submit(new Runnable() { @Override @@ -176,20 +181,36 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { return out; } + // todo: as now we have different layers of abstraction (for scheme handling and media decoding) + // we no longer should add dependencies implicitly, it would be way better to allow adding + // multiple artifacts (file, data, network, svg, gif)... at least, maybe we can extract API + // for this module (without implementations), but keep _all-in_ (fat) artifact with all of these. public static class Builder { + /** + * @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly + */ + @Deprecated private OkHttpClient client; + + /** + * @deprecated 2.0.0 construct {@link MediaDecoder} and {@link SchemeHandler} appropriately + */ + @Deprecated private Resources resources; + private ExecutorService executorService; private Drawable errorDrawable; - // @since 2.0.0 - private final Map schemeHandlers = new HashMap<>(3); - // @since 1.1.0 private final List mediaDecoders = new ArrayList<>(3); + // @since 2.0.0 + private final Map schemeHandlers = new HashMap<>(3); + /** + * @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly + */ @NonNull @Deprecated public Builder client(@NonNull OkHttpClient client) { @@ -224,6 +245,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { /** * @since 2.0.0 */ + @SuppressWarnings("UnusedReturnValue") @NonNull public Builder addSchemeHandler(@NonNull SchemeHandler schemeHandler) { @@ -240,20 +262,97 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { return this; } + /** + * @see #addMediaDecoder(MediaDecoder) + * @see #addMediaDecoders(MediaDecoder...) + * @see #addMediaDecoders(Iterable) + * @since 1.1.0 + * @deprecated 2.0.0 + */ + @Deprecated @NonNull public Builder mediaDecoders(@NonNull List mediaDecoders) { - this.mediaDecoders.clear(); - this.mediaDecoders.addAll(mediaDecoders); + + // previously it was clearing before adding + + for (MediaDecoder mediaDecoder : mediaDecoders) { + this.mediaDecoders.add(requireNonNull(mediaDecoder)); + } + return this; } + /** + * @see #addMediaDecoder(MediaDecoder) + * @see #addMediaDecoders(MediaDecoder...) + * @see #addMediaDecoders(Iterable) + * @since 1.1.0 + * @deprecated 2.0.0 + */ @NonNull + @Deprecated public Builder mediaDecoders(MediaDecoder... mediaDecoders) { - this.mediaDecoders.clear(); - if (mediaDecoders != null - && mediaDecoders.length > 0) { - Collections.addAll(this.mediaDecoders, mediaDecoders); + + // previously it was clearing before adding + + final int length = mediaDecoders != null + ? mediaDecoders.length + : 0; + + if (length > 0) { + for (int i = 0; i < length; i++) { + this.mediaDecoders.add(requireNonNull(mediaDecoders[i])); + } } + + return this; + } + + /** + * @see SvgMediaDecoder + * @see GifMediaDecoder + * @see ImageMediaDecoder + * @since 2.0.0 + */ + @NonNull + public Builder addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { + mediaDecoders.add(mediaDecoder); + return this; + } + + /** + * @see SvgMediaDecoder + * @see GifMediaDecoder + * @see ImageMediaDecoder + * @since 2.0.0 + */ + @NonNull + public Builder addMediaDecoders(@NonNull Iterable mediaDecoders) { + for (MediaDecoder mediaDecoder : mediaDecoders) { + this.mediaDecoders.add(requireNonNull(mediaDecoder)); + } + return this; + } + + /** + * @see SvgMediaDecoder + * @see GifMediaDecoder + * @see ImageMediaDecoder + * @since 2.0.0 + */ + @NonNull + public Builder addMediaDecoders(MediaDecoder... mediaDecoders) { + + final int length = mediaDecoders != null + ? mediaDecoders.length + : 0; + + if (length > 0) { + for (int i = 0; i < length; i++) { + this.mediaDecoders.add(requireNonNull(mediaDecoders[i])); + } + } + return this; } @@ -266,11 +365,14 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { } if (executorService == null) { + // @since 2.0.0 we are using newCachedThreadPool instead + // of `okHttpClient.dispatcher().executorService()` executorService = Executors.newCachedThreadPool(); } // @since 2.0.0 // put default scheme handlers (to mimic previous behavior) + // remove in 3.0.0 with plugins if (schemeHandlers.size() == 0) { if (client == null) { client = new OkHttpClient(); @@ -281,6 +383,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { } // add default media decoders if not specified + // remove in 3.0.0 with plugins if (mediaDecoders.size() == 0) { mediaDecoders.add(SvgMediaDecoder.create(resources)); mediaDecoders.add(GifMediaDecoder.create(true)); @@ -290,4 +393,13 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader { return new AsyncDrawableLoader(this); } } + + // @since 2.0.0 + @NonNull + private static T requireNonNull(@Nullable T t) { + if (t == null) { + throw new NullPointerException(); + } + return t; + } } diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java index e93aa256..73f415af 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/DataUriSchemeHandler.java @@ -19,6 +19,8 @@ public class DataUriSchemeHandler extends SchemeHandler { return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); } + private static final String START = "data://"; + private final DataUriParser uriParser; private final DataUriDecoder uriDecoder; @@ -32,12 +34,12 @@ public class DataUriSchemeHandler extends SchemeHandler { @Override public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { - final String part = uri.getSchemeSpecificPart(); - - if (TextUtils.isEmpty(part)) { + if (!raw.startsWith(START)) { return null; } + final String part = raw.substring(START.length()); + final DataUri dataUri = uriParser.parse(part); if (dataUri == null) { return null; diff --git a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java b/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java index 3a7f9fc7..6d8a44d1 100644 --- a/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java +++ b/markwon-image-loader/src/main/java/ru/noties/markwon/il/SchemeHandler.java @@ -16,6 +16,10 @@ public abstract class SchemeHandler { public abstract void cancel(@NonNull String raw); + /** + * Will be called only once during initialization, should return schemes that are + * handled by this handler + */ @NonNull public abstract Collection schemes(); } diff --git a/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java new file mode 100644 index 00000000..5274c5fb --- /dev/null +++ b/markwon-image-loader/src/test/java/ru/noties/markwon/il/DataUriSchemeHandlerTest.java @@ -0,0 +1,85 @@ +package ru.noties.markwon.il; + +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class DataUriSchemeHandlerTest { + + private DataUriSchemeHandler handler; + + @Before + public void before() { + handler = DataUriSchemeHandler.create(); + } + + @Test + public void scheme_specific_part_is_empty() { + assertNull(handler.handle("data:", Uri.parse("data:"))); + } + + @Test + public void data_uri_is_empty() { + assertNull(handler.handle("data://whatever", Uri.parse("data://whatever"))); + } + + @Test + public void no_data() { + assertNull(handler.handle("data://,", Uri.parse("data://,"))); + } + + @Test + public void correct() { + + final class Item { + + final String contentType; + final String data; + + Item(String contentType, String data) { + this.contentType = contentType; + this.data = data; + } + } + + final Map expected = new HashMap() {{ + put("data://text/plain;,123", new Item("text/plain", "123")); + put("data://image/svg+xml;base64,MTIz", new Item("image/svg+xml", "123")); + }}; + + for (Map.Entry entry : expected.entrySet()) { + final ImageItem item = handler.handle(entry.getKey(), Uri.parse(entry.getKey())); + assertNotNull(entry.getKey(), item); + assertEquals(entry.getKey(), entry.getValue().contentType, item.contentType()); + assertEquals(entry.getKey(), entry.getValue().data, readStream(item.inputStream())); + } + } + + @NonNull + private static String readStream(@NonNull InputStream stream) { + try { + final Scanner scanner = new Scanner(stream, "UTF-8").useDelimiter("\\A"); + return scanner.hasNext() + ? scanner.next() + : ""; + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} \ No newline at end of file