Merge branch 'v2.0.0' into jlatexmath

This commit is contained in:
Dimitry Ivanov 2018-09-14 12:42:56 +03:00
commit 3cadd6c7e6
5 changed files with 282 additions and 18 deletions

View File

@ -77,6 +77,12 @@ AsyncDrawableLoader.builder()
If not provided explicitly, default `new OkHttpClient()` will be used 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 ### Resources
`android.content.res.Resources` to be used when obtaining an image `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 ### Executor service
`ExecutorService` to be used to download images in background thread `ExecutorService` to be used to download images in background thread
@ -113,7 +125,7 @@ AsyncDrawableLoader.builder()
.build(); .build();
``` ```
If not provided explicitly, default `okHttpClient.dispatcher().executorService()` will be used If not provided explicitly, default `Executors.newCachedThreadPool()` will be used
### Error drawable ### Error drawable
@ -134,8 +146,9 @@ of a specific image type.
```java ```java
AsyncDrawableLoader.builder() AsyncDrawableLoader.builder()
.mediaDecoders(MediaDecoder...) .addMediaDecoder(MediaDecoder)
.mediaDecoders(List<MediaDecoder>) .addMediaDecoders(MediaDecoder...)
.addMediaDecoders(Iterable<MediaDecoder>)
.build(); .build();
``` ```
@ -180,3 +193,51 @@ GifMediaDecoder.create(boolean)
```java ```java
ImageMediaDecoder.create(Resources) ImageMediaDecoder.create(Resources)
``` ```
### Scheme handler <Badge text="2.0.0" />
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 <Badge text="2.0.0" />
```java
NetworkSchemeHandler.create(OkHttpClient);
```
#### FileSchemeHandler <Badge text="2.0.0" />
Simple file handler
```java
FileSchemeHandler.create();
```
File handler that additionally allows access to Android `assets` folder
```java
FileSchemeHandler.createWithAssets(AssetManager);
```
#### DataUriSchemeHandler <Badge text="2.0.0" />
```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`
:::

View File

@ -12,7 +12,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -79,7 +78,13 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
// todo: should we cancel pending request for the same destination? // todo: should we cancel pending request for the same destination?
// we _could_ but there is possibility that one resource is request in multiple places // 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() { return executorService.submit(new Runnable() {
@Override @Override
@ -176,20 +181,36 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out; 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 { public static class Builder {
/**
* @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly
*/
@Deprecated
private OkHttpClient client; private OkHttpClient client;
/**
* @deprecated 2.0.0 construct {@link MediaDecoder} and {@link SchemeHandler} appropriately
*/
@Deprecated
private Resources resources; private Resources resources;
private ExecutorService executorService; private ExecutorService executorService;
private Drawable errorDrawable; private Drawable errorDrawable;
// @since 2.0.0
private final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
// @since 1.1.0 // @since 1.1.0
private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3); private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3);
// @since 2.0.0
private final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
/**
* @deprecated 2.0.0 add {@link NetworkSchemeHandler} directly
*/
@NonNull @NonNull
@Deprecated @Deprecated
public Builder client(@NonNull OkHttpClient client) { public Builder client(@NonNull OkHttpClient client) {
@ -224,6 +245,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
/** /**
* @since 2.0.0 * @since 2.0.0
*/ */
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
public Builder addSchemeHandler(@NonNull SchemeHandler schemeHandler) { public Builder addSchemeHandler(@NonNull SchemeHandler schemeHandler) {
@ -240,20 +262,97 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return this; return this;
} }
/**
* @see #addMediaDecoder(MediaDecoder)
* @see #addMediaDecoders(MediaDecoder...)
* @see #addMediaDecoders(Iterable)
* @since 1.1.0
* @deprecated 2.0.0
*/
@Deprecated
@NonNull @NonNull
public Builder mediaDecoders(@NonNull List<MediaDecoder> mediaDecoders) { public Builder mediaDecoders(@NonNull List<MediaDecoder> 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; return this;
} }
/**
* @see #addMediaDecoder(MediaDecoder)
* @see #addMediaDecoders(MediaDecoder...)
* @see #addMediaDecoders(Iterable)
* @since 1.1.0
* @deprecated 2.0.0
*/
@NonNull @NonNull
@Deprecated
public Builder mediaDecoders(MediaDecoder... mediaDecoders) { public Builder mediaDecoders(MediaDecoder... mediaDecoders) {
this.mediaDecoders.clear();
if (mediaDecoders != null // previously it was clearing before adding
&& mediaDecoders.length > 0) {
Collections.addAll(this.mediaDecoders, 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;
}
/**
* @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<MediaDecoder> 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; return this;
} }
@ -266,11 +365,14 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
if (executorService == null) { if (executorService == null) {
// @since 2.0.0 we are using newCachedThreadPool instead
// of `okHttpClient.dispatcher().executorService()`
executorService = Executors.newCachedThreadPool(); executorService = Executors.newCachedThreadPool();
} }
// @since 2.0.0 // @since 2.0.0
// put default scheme handlers (to mimic previous behavior) // put default scheme handlers (to mimic previous behavior)
// remove in 3.0.0 with plugins
if (schemeHandlers.size() == 0) { if (schemeHandlers.size() == 0) {
if (client == null) { if (client == null) {
client = new OkHttpClient(); client = new OkHttpClient();
@ -281,6 +383,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
// add default media decoders if not specified // add default media decoders if not specified
// remove in 3.0.0 with plugins
if (mediaDecoders.size() == 0) { if (mediaDecoders.size() == 0) {
mediaDecoders.add(SvgMediaDecoder.create(resources)); mediaDecoders.add(SvgMediaDecoder.create(resources));
mediaDecoders.add(GifMediaDecoder.create(true)); mediaDecoders.add(GifMediaDecoder.create(true));
@ -290,4 +393,13 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return new AsyncDrawableLoader(this); return new AsyncDrawableLoader(this);
} }
} }
// @since 2.0.0
@NonNull
private static <T> T requireNonNull(@Nullable T t) {
if (t == null) {
throw new NullPointerException();
}
return t;
}
} }

View File

@ -19,6 +19,8 @@ public class DataUriSchemeHandler extends SchemeHandler {
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
} }
private static final String START = "data://";
private final DataUriParser uriParser; private final DataUriParser uriParser;
private final DataUriDecoder uriDecoder; private final DataUriDecoder uriDecoder;
@ -32,12 +34,12 @@ public class DataUriSchemeHandler extends SchemeHandler {
@Override @Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final String part = uri.getSchemeSpecificPart(); if (!raw.startsWith(START)) {
if (TextUtils.isEmpty(part)) {
return null; return null;
} }
final String part = raw.substring(START.length());
final DataUri dataUri = uriParser.parse(part); final DataUri dataUri = uriParser.parse(part);
if (dataUri == null) { if (dataUri == null) {
return null; return null;

View File

@ -16,6 +16,10 @@ public abstract class SchemeHandler {
public abstract void cancel(@NonNull String raw); public abstract void cancel(@NonNull String raw);
/**
* Will be called only once during initialization, should return schemes that are
* handled by this handler
*/
@NonNull @NonNull
public abstract Collection<String> schemes(); public abstract Collection<String> schemes();
} }

View File

@ -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<String, Item> expected = new HashMap<String, Item>() {{
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<String, Item> 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);
}
}
}