image-loader add scheme handling abstraction

This commit is contained in:
Dimitry Ivanov 2018-09-07 15:37:45 +03:00
parent 836cef28ed
commit fde9712454
9 changed files with 375 additions and 177 deletions

View File

@ -7,13 +7,7 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -23,13 +17,10 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import okhttp3.Call;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader { public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@ -44,31 +35,19 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return new Builder(); return new Builder();
} }
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String FILE_ANDROID_ASSETS = "android_asset";
private static final String SCHEME_FILE = "file";
private static final String SCHEME_DATA = "data";
private static final DataUriParser DATA_URI_PARSER = DataUriParser.create();
private static final DataUriDecoder DATA_URI_DECODER = DataUriDecoder.create();
private final OkHttpClient client;
private final Resources resources;
private final ExecutorService executorService; private final ExecutorService executorService;
private final Handler mainThread; private final Handler mainThread;
private final Drawable errorDrawable; private final Drawable errorDrawable;
private final Map<String, SchemeHandler> schemeHandlers;
private final List<MediaDecoder> mediaDecoders; private final List<MediaDecoder> mediaDecoders;
private final Map<String, Future<?>> requests; private final Map<String, Future<?>> requests;
AsyncDrawableLoader(Builder builder) { AsyncDrawableLoader(Builder builder) {
this.client = builder.client;
this.resources = builder.resources;
this.executorService = builder.executorService; this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper()); this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable; this.errorDrawable = builder.errorDrawable;
this.schemeHandlers = builder.schemeHandlers;
this.mediaDecoders = builder.mediaDecoders; this.mediaDecoders = builder.mediaDecoders;
this.requests = new HashMap<>(3); this.requests = new HashMap<>(3);
} }
@ -88,61 +67,58 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
request.cancel(true); request.cancel(true);
} }
final List<Call> calls = client.dispatcher().queuedCalls(); for (SchemeHandler schemeHandler : schemeHandlers.values()) {
if (calls != null) { schemeHandler.cancel(destination);
for (Call call : calls) {
if (!call.isCanceled()) {
if (destination.equals(call.request().tag())) {
call.cancel();
}
}
}
} }
} }
private Future<?> execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) { private Future<?> execute(@NonNull final String destination, @NonNull AsyncDrawable drawable) {
final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable); final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable);
// 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, if not a link -> show placeholder
return executorService.submit(new Runnable() { return executorService.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
final Item item; final ImageItem item;
final boolean isFromFile;
final Uri uri = Uri.parse(destination); final Uri uri = Uri.parse(destination);
final String scheme = uri.getScheme();
if (SCHEME_FILE.equals(scheme)) { final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme());
item = fromFile(uri); if (schemeHandler != null) {
isFromFile = true; item = schemeHandler.handle(destination, uri);
} else if (SCHEME_DATA.equals(scheme)) {
item = fromData(uri.getSchemeSpecificPart());
isFromFile = false;
} else { } else {
item = fromNetwork(destination); item = null;
isFromFile = false;
} }
final InputStream inputStream = item != null
? item.inputStream()
: null;
Drawable result = null; Drawable result = null;
if (item != null if (inputStream != null) {
&& item.inputStream != null) {
try { try {
final MediaDecoder mediaDecoder = isFromFile final String fileName = item.fileName();
? mediaDecoderFromFile(item.fileName) final MediaDecoder mediaDecoder = fileName != null
: mediaDecoderFromContentType(item.contentType); ? mediaDecoderFromFile(fileName)
: mediaDecoderFromContentType(item.contentType());
if (mediaDecoder != null) { if (mediaDecoder != null) {
result = mediaDecoder.decode(item.inputStream); result = mediaDecoder.decode(inputStream);
} }
} finally { } finally {
try { try {
item.inputStream.close(); inputStream.close();
} catch (IOException e) { } catch (IOException e) {
// no op // ignored
} }
} }
} }
@ -170,112 +146,6 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
}); });
} }
@Nullable
private Item fromFile(@NonNull Uri uri) {
final List<String> segments = uri.getPathSegments();
if (segments == null
|| segments.size() == 0) {
// pointing to file & having no path segments is no use
return null;
}
final Item out;
final InputStream inputStream;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String fileName = uri.getLastPathSegment();
if (assets) {
final StringBuilder path = new StringBuilder();
for (int i = 1, size = segments.size(); i < size; i++) {
if (i != 1) {
path.append('/');
}
path.append(segments.get(i));
}
// load assets
InputStream inner = null;
try {
inner = resources.getAssets().open(path.toString());
} catch (IOException e) {
e.printStackTrace();
}
inputStream = inner;
} else {
InputStream inner = null;
try {
inner = new BufferedInputStream(new FileInputStream(new File(uri.getPath())));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
inputStream = inner;
}
if (inputStream != null) {
out = new Item(fileName, null, inputStream);
} else {
out = null;
}
return out;
}
@Nullable
private Item fromData(@Nullable String part) {
if (TextUtils.isEmpty(part)) {
return null;
}
final DataUri dataUri = DATA_URI_PARSER.parse(part);
if (dataUri == null) {
return null;
}
final byte[] bytes = DATA_URI_DECODER.decode(dataUri);
if (bytes == null) {
return null;
}
return new Item(
null,
dataUri.contentType(),
new ByteArrayInputStream(bytes)
);
}
@Nullable
private Item fromNetwork(@NonNull String destination) {
Item out = null;
final Request request = new Request.Builder()
.url(destination)
.tag(destination)
.build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
if (response != null) {
final ResponseBody body = response.body();
if (body != null) {
final InputStream inputStream = body.byteStream();
if (inputStream != null) {
final String contentType = response.header(HEADER_CONTENT_TYPE);
out = new Item(null, contentType, inputStream);
}
}
}
return out;
}
@Nullable @Nullable
private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) { private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) {
@ -313,11 +183,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
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);
@NonNull @NonNull
@Deprecated
public Builder client(@NonNull OkHttpClient client) { public Builder client(@NonNull OkHttpClient client) {
this.client = client; this.client = client;
return this; return this;
@ -347,6 +221,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return this; return this;
} }
/**
* @since 2.0.0
*/
@NonNull
public Builder schemeHandler(@NonNull String scheme, @Nullable SchemeHandler schemeHandler) {
schemeHandlers.put(scheme, schemeHandler);
return this;
}
@NonNull @NonNull
public Builder mediaDecoders(@NonNull List<MediaDecoder> mediaDecoders) { public Builder mediaDecoders(@NonNull List<MediaDecoder> mediaDecoders) {
this.mediaDecoders.clear(); this.mediaDecoders.clear();
@ -367,17 +250,44 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@NonNull @NonNull
public AsyncDrawableLoader build() { public AsyncDrawableLoader build() {
if (client == null) { // I think we should deprecate this...
client = new OkHttpClient();
}
if (resources == null) { if (resources == null) {
resources = Resources.getSystem(); resources = Resources.getSystem();
} }
if (executorService == null) { if (executorService == null) {
// we will use executor from okHttp executorService = Executors.newCachedThreadPool();
executorService = client.dispatcher().executorService(); }
// @since 2.0.0
// put default scheme handlers (to mimic previous behavior)
{
final boolean hasHttp = schemeHandlers.containsKey("http");
final boolean hasHttps = schemeHandlers.containsKey("https");
if (!hasHttp || !hasHttps) {
if (client == null) {
client = new OkHttpClient();
}
final NetworkSchemeHandler handler = NetworkSchemeHandler.create(client);
if (!hasHttp) {
schemeHandlers.put("http", handler);
}
if (!hasHttps) {
schemeHandlers.put("https", handler);
}
}
if (!schemeHandlers.containsKey("file")) {
schemeHandlers.put("file", FileSchemeHandler.createWithAssets(resources.getAssets()));
}
if (!schemeHandlers.containsKey("data")) {
schemeHandlers.put("data", DataUriSchemeHandler.create());
}
} }
// add default media decoders if not specified // add default media decoders if not specified
@ -390,17 +300,4 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return new AsyncDrawableLoader(this); return new AsyncDrawableLoader(this);
} }
} }
private static class Item {
final String fileName;
final String contentType;
final InputStream inputStream;
Item(@Nullable String fileName, @Nullable String contentType, @Nullable InputStream inputStream) {
this.fileName = fileName;
this.contentType = contentType;
this.inputStream = inputStream;
}
}
} }

View File

@ -0,0 +1,60 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.ByteArrayInputStream;
/**
* @since 2.0.0
*/
public class DataUriSchemeHandler extends SchemeHandler {
@NonNull
public static DataUriSchemeHandler create() {
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
}
private final DataUriParser uriParser;
private final DataUriDecoder uriDecoder;
@SuppressWarnings("WeakerAccess")
DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) {
this.uriParser = uriParser;
this.uriDecoder = uriDecoder;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final String part = uri.getSchemeSpecificPart();
if (TextUtils.isEmpty(part)) {
return null;
}
final DataUri dataUri = uriParser.parse(part);
if (dataUri == null) {
return null;
}
final byte[] bytes = uriDecoder.decode(dataUri);
if (bytes == null) {
return null;
}
return new ImageItem(
dataUri.contentType(),
new ByteArrayInputStream(bytes),
null
);
}
@Override
public void cancel(@NonNull String raw) {
// no op
}
}

View File

@ -0,0 +1,101 @@
package ru.noties.markwon.il;
import android.content.res.AssetManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @since 2.0.0
*/
public class FileSchemeHandler extends SchemeHandler {
@NonNull
public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) {
return new FileSchemeHandler(assetManager);
}
@NonNull
public static FileSchemeHandler create() {
return new FileSchemeHandler(null);
}
private static final String FILE_ANDROID_ASSETS = "android_asset";
@Nullable
private final AssetManager assetManager;
@SuppressWarnings("WeakerAccess")
FileSchemeHandler(@Nullable AssetManager assetManager) {
this.assetManager = assetManager;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final List<String> segments = uri.getPathSegments();
if (segments == null
|| segments.size() == 0) {
// pointing to file & having no path segments is no use
return null;
}
final ImageItem out;
InputStream inputStream = null;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String fileName = uri.getLastPathSegment();
if (assets) {
// no handling of assets here if we have no assetsManager
if (assetManager != null) {
final StringBuilder path = new StringBuilder();
for (int i = 1, size = segments.size(); i < size; i++) {
if (i != 1) {
path.append('/');
}
path.append(segments.get(i));
}
// load assets
try {
inputStream = assetManager.open(path.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
try {
inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath())));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
out = new ImageItem(fileName, inputStream, fileName);
} else {
out = null;
}
return out;
}
@Override
public void cancel(@NonNull String raw) {
// no op
}
}

View File

@ -13,6 +13,7 @@ import pl.droidsonroids.gif.GifDrawable;
/** /**
* @since 1.1.0 * @since 1.1.0
*/ */
@SuppressWarnings("WeakerAccess")
public class GifMediaDecoder extends MediaDecoder { public class GifMediaDecoder extends MediaDecoder {
protected static final String CONTENT_TYPE_GIF = "image/gif"; protected static final String CONTENT_TYPE_GIF = "image/gif";

View File

@ -0,0 +1,39 @@
package ru.noties.markwon.il;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 2.0.0
*/
public class ImageItem {
private final String contentType;
private final InputStream inputStream;
private final String fileName;
public ImageItem(
@Nullable String contentType,
@Nullable InputStream inputStream,
@Nullable String fileName) {
this.contentType = contentType;
this.inputStream = inputStream;
this.fileName = fileName;
}
@Nullable
public String contentType() {
return contentType;
}
@Nullable
public InputStream inputStream() {
return inputStream;
}
@Nullable
public String fileName() {
return fileName;
}
}

View File

@ -25,6 +25,7 @@ public class ImageMediaDecoder extends MediaDecoder {
private final Resources resources; private final Resources resources;
@SuppressWarnings("WeakerAccess")
ImageMediaDecoder(Resources resources) { ImageMediaDecoder(Resources resources) {
this.resources = resources; this.resources = resources;
} }
@ -45,6 +46,7 @@ public class ImageMediaDecoder extends MediaDecoder {
final Drawable out; final Drawable out;
// absolutely not optimal... thing
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) { if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap); out = new BitmapDrawable(resources, bitmap);

View File

@ -0,0 +1,81 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @since 2.0.0
*/
public class NetworkSchemeHandler extends SchemeHandler {
@NonNull
public static NetworkSchemeHandler create(@NonNull OkHttpClient client) {
return new NetworkSchemeHandler(client);
}
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private final OkHttpClient client;
@SuppressWarnings("WeakerAccess")
NetworkSchemeHandler(@NonNull OkHttpClient client) {
this.client = client;
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
ImageItem out = null;
final Request request = new Request.Builder()
.url(raw)
.tag(raw)
.build();
Response response = null;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
if (response != null) {
final ResponseBody body = response.body();
if (body != null) {
final InputStream inputStream = body.byteStream();
if (inputStream != null) {
final String contentType = response.header(HEADER_CONTENT_TYPE);
out = new ImageItem(contentType, inputStream, null);
}
}
}
return out;
}
@Override
public void cancel(@NonNull String raw) {
final List<Call> calls = client.dispatcher().queuedCalls();
if (calls != null) {
for (Call call : calls) {
if (!call.isCanceled()) {
if (raw.equals(call.request().tag())) {
call.cancel();
}
}
}
}
}
}

View File

@ -0,0 +1,16 @@
package ru.noties.markwon.il;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* @since 2.0.0
*/
public abstract class SchemeHandler {
@Nullable
public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri);
public abstract void cancel(@NonNull String raw);
}

View File

@ -28,6 +28,7 @@ public class SvgMediaDecoder extends MediaDecoder {
private final Resources resources; private final Resources resources;
@SuppressWarnings("WeakerAccess")
SvgMediaDecoder(Resources resources) { SvgMediaDecoder(Resources resources) {
this.resources = resources; this.resources = resources;
} }