2017-05-27 14:14:14 +03:00

394 lines
12 KiB
Java

package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.spans.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader {
public static AsyncDrawableLoader create() {
return builder().build();
}
public static AsyncDrawableLoader.Builder builder() {
return new Builder();
}
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String CONTENT_TYPE_GIF = "image/gif";
private static final String FILE_ANDROID_ASSETS = "android_asset";
private final OkHttpClient client;
private final Resources resources;
private final ExecutorService executorService;
private final Handler mainThread;
private final Drawable errorDrawable;
private final Map<String, Future<?>> requests;
AsyncDrawableLoader(Builder builder) {
this.client = builder.client;
this.resources = builder.resources;
this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable;
this.requests = new HashMap<>(3);
}
@Override
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
// if drawable is not a link -> show loading placeholder...
requests.put(destination, execute(destination, drawable));
}
@Override
public void cancel(@NonNull String destination) {
final Future<?> request = requests.remove(destination);
if (request != null) {
request.cancel(true);
}
final List<Call> calls = client.dispatcher().queuedCalls();
if (calls != null) {
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) {
final WeakReference<AsyncDrawable> reference = new WeakReference<AsyncDrawable>(drawable);
// todo, if not a link -> show placeholder
return executorService.submit(new Runnable() {
@Override
public void run() {
final Item item;
final Uri uri = Uri.parse(destination);
if ("file".equals(uri.getScheme())) {
item = fromFile(uri);
} else {
item = fromNetwork(destination);
}
Drawable result = null;
if (item != null
&& item.inputStream != null) {
try {
if (CONTENT_TYPE_SVG.equals(item.type)) {
result = handleSvg(item.inputStream);
} else if (CONTENT_TYPE_GIF.equals(item.type)) {
result = handleGif(item.inputStream);
} else {
result = handleSimple(item.inputStream);
}
} finally {
try {
item.inputStream.close();
} catch (IOException e) {
// no op
}
}
}
// if result is null, we assume it's an error
if (result == null) {
result = errorDrawable;
}
if (result != null) {
final Drawable out = result;
mainThread.post(new Runnable() {
@Override
public void run() {
final AsyncDrawable asyncDrawable = reference.get();
if (asyncDrawable != null && asyncDrawable.isAttached()) {
asyncDrawable.setResult(out);
}
}
});
}
requests.remove(destination);
}
});
}
private Item fromFile(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 String type;
final InputStream inputStream;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String lastSegment = uri.getLastPathSegment();
if (lastSegment.endsWith(".svg")) {
type = CONTENT_TYPE_SVG;
} else if (lastSegment.endsWith(".gif")) {
type = CONTENT_TYPE_GIF;
} else {
type = null;
}
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(type, inputStream);
} else {
out = null;
}
return out;
}
private Item fromNetwork(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 type;
final String contentType = response.header(HEADER_CONTENT_TYPE);
if (!TextUtils.isEmpty(contentType)
&& contentType.startsWith(CONTENT_TYPE_SVG)) {
type = CONTENT_TYPE_SVG;
} else {
type = contentType;
}
out = new Item(type, inputStream);
}
}
}
return out;
}
private Drawable handleSvg(InputStream stream) {
final Drawable out;
SVG svg = null;
try {
svg = SVG.getFromInputStream(stream);
} catch (SVGParseException e) {
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
private Drawable handleGif(InputStream stream) {
Drawable out = null;
final byte[] bytes = readBytes(stream);
if (bytes != null) {
try {
out = new GifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
} catch (IOException e) {
e.printStackTrace();
}
}
return out;
}
private Drawable handleSimple(InputStream stream) {
final Drawable out;
final Bitmap bitmap = BitmapFactory.decodeStream(stream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
} else {
out = null;
}
return out;
}
private static byte[] readBytes(InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return out;
}
public static class Builder {
private OkHttpClient client;
private Resources resources;
private ExecutorService executorService;
private Drawable errorDrawable;
public Builder client(@NonNull OkHttpClient client) {
this.client = client;
return this;
}
public Builder resources(@NonNull Resources resources) {
this.resources = resources;
return this;
}
public Builder executorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
}
public Builder errorDrawable(Drawable errorDrawable) {
this.errorDrawable = errorDrawable;
return this;
}
public AsyncDrawableLoader build() {
if (client == null) {
client = new OkHttpClient();
}
if (resources == null) {
resources = Resources.getSystem();
}
if (executorService == null) {
// we will use executor from okHttp
executorService = client.dispatcher().executorService();
}
return new AsyncDrawableLoader(this);
}
}
private static class Item {
final String type;
final InputStream inputStream;
Item(String type, InputStream inputStream) {
this.type = type;
this.inputStream = inputStream;
}
}
}