Moved image loading into separate module

This commit is contained in:
Dimitry Ivanov 2019-05-28 18:50:03 +03:00
parent 661f72da0f
commit 5bf21bc940
56 changed files with 363 additions and 1521 deletions

2
_CHANGES.md Normal file
View File

@ -0,0 +1,2 @@
* `Markwon.builder` won't require CorePlugin registration (it is done automatically)
to create a builder without CorePlugin - use `Markwon#builderNoCore`

View File

@ -33,8 +33,7 @@ dependencies {
implementation project(':markwon-ext-tables') implementation project(':markwon-ext-tables')
implementation project(':markwon-ext-tasklist') implementation project(':markwon-ext-tasklist')
implementation project(':markwon-html') implementation project(':markwon-html')
implementation project(':markwon-image-gif') implementation project(':markwon-image')
implementation project(':markwon-image-svg')
implementation project(':markwon-syntax-highlight') implementation project(':markwon-syntax-highlight')
deps.with { deps.with {
@ -42,6 +41,8 @@ dependencies {
implementation it['prism4j'] implementation it['prism4j']
implementation it['debug'] implementation it['debug']
implementation it['dagger'] implementation it['dagger']
implementation it['android-svg']
implementation it['android-gif']
} }
deps['annotationProcessor'].with { deps['annotationProcessor'].with {

View File

@ -14,15 +14,18 @@ import java.util.concurrent.Future;
import javax.inject.Inject; import javax.inject.Inject;
import ru.noties.debug.Debug; import ru.noties.debug.Debug;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin; import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import ru.noties.markwon.ext.tables.TablePlugin; import ru.noties.markwon.ext.tables.TablePlugin;
import ru.noties.markwon.ext.tasklist.TaskListPlugin; import ru.noties.markwon.ext.tasklist.TaskListPlugin;
import ru.noties.markwon.gif.GifAwarePlugin; import ru.noties.markwon.gif.GifAwarePlugin;
import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.html.HtmlPlugin;
import ru.noties.markwon.image.DefaultImageMediaDecoder;
import ru.noties.markwon.image.ImagesPlugin; import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.gif.GifPlugin; import ru.noties.markwon.image.data.DataUriSchemeHandler;
import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.image.file.FileSchemeHandler;
import ru.noties.markwon.image.gif.GifMediaDecoder;
import ru.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
import ru.noties.markwon.image.svg.SvgMediaDecoder;
import ru.noties.markwon.syntax.Prism4jTheme; import ru.noties.markwon.syntax.Prism4jTheme;
import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDarkula;
import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.Prism4jThemeDefault;
@ -94,10 +97,18 @@ public class MarkdownRenderer {
: prism4JThemeDarkula; : prism4JThemeDarkula;
final Markwon markwon = Markwon.builder(context) final Markwon markwon = Markwon.builder(context)
.usePlugin(CorePlugin.create()) .usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() {
.usePlugin(ImagesPlugin.createWithAssets(context)) @Override
.usePlugin(SvgPlugin.create(context.getResources())) public void configureImages(@NonNull ImagesPlugin plugin) {
.usePlugin(GifPlugin.create(false)) plugin
.addSchemeHandler(DataUriSchemeHandler.create())
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()))
.addMediaDecoder(GifMediaDecoder.create(false))
.addMediaDecoder(SvgMediaDecoder.create())
.defaultMediaDecoder(DefaultImageMediaDecoder.create());
}
}))
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
.usePlugin(GifAwarePlugin.create(context)) .usePlugin(GifAwarePlugin.create(context))
.usePlugin(TablePlugin.create(context)) .usePlugin(TablePlugin.create(context))

View File

@ -14,8 +14,6 @@ import ru.noties.markwon.RenderProps;
import ru.noties.markwon.SpanFactory; import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.image.AsyncDrawableSpan; import ru.noties.markwon.image.AsyncDrawableSpan;
import ru.noties.markwon.image.ImageProps; import ru.noties.markwon.image.ImageProps;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority;
public class GifAwarePlugin extends AbstractMarkwonPlugin { public class GifAwarePlugin extends AbstractMarkwonPlugin {
@ -59,12 +57,6 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin {
}); });
} }
@NonNull
@Override
public Priority priority() {
return Priority.after(ImagesPlugin.class);
}
@Override @Override
public void afterSetText(@NonNull TextView textView) { public void afterSetText(@NonNull TextView textView) {
processor.process(textView); processor.process(textView);

View File

@ -7,11 +7,8 @@ import android.widget.TextView;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.Priority;
/** /**
* Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body)

View File

@ -40,7 +40,6 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory;
import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory;
import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; import ru.noties.markwon.core.factory.ThematicBreakSpanFactory;
import ru.noties.markwon.core.spans.OrderedListItemSpan; import ru.noties.markwon.core.spans.OrderedListItemSpan;
import ru.noties.markwon.priority.Priority;
/** /**
* @see CoreProps * @see CoreProps

View File

@ -40,7 +40,7 @@ public class AsyncDrawable extends Drawable {
this.imageSizeResolver = imageSizeResolver; this.imageSizeResolver = imageSizeResolver;
this.imageSize = imageSize; this.imageSize = imageSize;
final Drawable placeholder = loader.placeholder(); final Drawable placeholder = loader.placeholder(this);
if (placeholder != null) { if (placeholder != null) {
setPlaceholderResult(placeholder); setPlaceholderResult(placeholder);
} }

View File

@ -3,13 +3,6 @@ package ru.noties.markwon.image;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public abstract class AsyncDrawableLoader { public abstract class AsyncDrawableLoader {
@ -31,28 +24,9 @@ public abstract class AsyncDrawableLoader {
*/ */
public abstract void cancel(@NonNull AsyncDrawable drawable); public abstract void cancel(@NonNull AsyncDrawable drawable);
/**
* @see #load(AsyncDrawable)
* @deprecated 3.1.0-SNAPSHOT
*/
@Deprecated
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
load(drawable);
}
/**
* Method is deprecated because cancellation won\'t work for markdown input
* with multiple images with the same source
*
* @deprecated 3.1.0-SNAPSHOT
*/
@Deprecated
public void cancel(@NonNull String destination) {
Log.e("MARKWON-IL", "Image loading cancellation must be triggered " +
"by AsyncDrawable, please use #cancel(AsyncDrawable) method instead. " +
"No op, nothing is cancelled for destination: " + destination);
}
@Nullable @Nullable
public abstract Drawable placeholder(); public abstract Drawable placeholder(@NonNull AsyncDrawable drawable);
} }

View File

@ -1,290 +0,0 @@
package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
private final ExecutorService executorService;
private final Map<String, SchemeHandler> schemeHandlers;
private final Map<String, MediaDecoder> mediaDecoders;
private final MediaDecoder defaultMediaDecoder;
private final DrawableProvider placeholderDrawableProvider;
private final DrawableProvider errorDrawableProvider;
private final Handler mainThread;
// @since 3.1.0-SNAPSHOT use a hash-map with a weak AsyncDrawable as key for multiple requests
// for the same destination
private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2);
AsyncDrawableLoaderImpl(@NonNull Builder builder) {
this.executorService = builder.executorService;
this.schemeHandlers = builder.schemeHandlers;
this.mediaDecoders = builder.mediaDecoders;
this.defaultMediaDecoder = builder.defaultMediaDecoder;
this.placeholderDrawableProvider = builder.placeholderDrawableProvider;
this.errorDrawableProvider = builder.errorDrawableProvider;
this.mainThread = new Handler(Looper.getMainLooper());
}
@Override
public void load(@NonNull final AsyncDrawable drawable) {
// primitive synchronization via main-thread
if (!isMainThread()) {
mainThread.post(new Runnable() {
@Override
public void run() {
load(drawable);
}
});
return;
}
// okay, if by some chance requested drawable already has a future associated -> no-op
// as AsyncDrawable cannot change `destination` (immutable field)
// @since 3.1.0-SNAPSHOT
if (hasTaskAssociated(drawable)) {
return;
}
final WeakReference<AsyncDrawable> reference = new WeakReference<>(drawable);
requests.put(reference, execute(drawable.getDestination(), reference));
}
@Override
public void cancel(@NonNull final AsyncDrawable drawable) {
if (!isMainThread()) {
mainThread.post(new Runnable() {
@Override
public void run() {
cancel(drawable);
}
});
return;
}
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
requests.entrySet().iterator();
AsyncDrawable key;
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
while (iterator.hasNext()) {
entry = iterator.next();
key = entry.getKey().get();
// if key is null or it contains requested AsyncDrawable -> cancel
if (shouldCleanUp(key) || key == drawable) {
entry.getValue().cancel(true);
iterator.remove();
}
}
}
private boolean hasTaskAssociated(@NonNull AsyncDrawable drawable) {
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
requests.entrySet().iterator();
boolean result = false;
AsyncDrawable key;
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
while (iterator.hasNext()) {
entry = iterator.next();
key = entry.getKey().get();
// clean-up
if (shouldCleanUp(key)) {
entry.getValue().cancel(true);
iterator.remove();
} else if (key == drawable) {
result = true;
// do not break, let iteration continue to possibly clean-up the rest references
}
}
return result;
}
private void cleanUp() {
final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator =
requests.entrySet().iterator();
AsyncDrawable key;
Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry;
while (iterator.hasNext()) {
entry = iterator.next();
key = entry.getKey().get();
// clean-up of already referenced or detached drawables
if (shouldCleanUp(key)) {
entry.getValue().cancel(true);
iterator.remove();
}
}
}
// @Override
// public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
//
// // todo: we cannot reliably identify request by the destination, as if
// // markdown input has multiple images with the same destination as source
// // we will be tracking only one of them (the one appears the last). We should
// // move to AsyncDrawable based identification. This method also _maybe_
// // should include the ImageSize (comment @since 3.1.0-SNAPSHOT)
//
// requests.put(destination, execute(destination, drawable));
// }
//
// @Override
// public void cancel(@NonNull String destination) {
//
// // todo: as we are moving away from a single request for a destination,
// // we should re-evaluate this cancellation logic, as if there are multiple images
// // in markdown input all of them will be cancelled (won't delivered), even if
// // only a single drawable is detached. Cancellation must also take
// // the AsyncDrawable argument (comment @since 3.1.0-SNAPSHOT)
//
// //
// final Future<?> request = requests.remove(destination);
// if (request != null) {
// request.cancel(true);
// }
// }
@Nullable
@Override
public Drawable placeholder() {
return placeholderDrawableProvider != null
? placeholderDrawableProvider.provide()
: null;
}
private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) {
// 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
public void run() {
final ImageItem item;
final Uri uri = Uri.parse(destination);
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme());
if (schemeHandler != null) {
item = schemeHandler.handle(destination, uri);
} else {
item = null;
}
final InputStream inputStream = item != null
? item.inputStream()
: null;
Drawable result = null;
if (inputStream != null) {
try {
MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType());
if (mediaDecoder == null) {
mediaDecoder = defaultMediaDecoder;
}
if (mediaDecoder != null) {
result = mediaDecoder.decode(inputStream);
}
} finally {
try {
inputStream.close();
} catch (IOException e) {
// ignored
}
}
}
// if result is null, we assume it's an error
if (result == null) {
result = errorDrawableProvider != null
? errorDrawableProvider.provide()
: null;
}
final Drawable out = result;
mainThread.post(new Runnable() {
@Override
public void run() {
if (out != null) {
// this doesn't work with markdown input with multiple images with the
// same source (comment @since 3.1.0-SNAPSHOT)
// final boolean canDeliver = requests.remove(destination) != null;
// if (canDeliver) {
// final AsyncDrawable asyncDrawable = reference.get();
// if (asyncDrawable != null && asyncDrawable.isAttached()) {
// asyncDrawable.setResult(out);
// }
// }
// todo: AsyncDrawable cannot change destination, so if it's
// attached and not garbage-collected, we can deliver the result.
// Note that there is no cache, so attach/detach of drawables
// will always request a new entry.. (comment @since 3.1.0-SNAPSHOT)
final AsyncDrawable asyncDrawable = reference.get();
if (asyncDrawable != null && asyncDrawable.isAttached()) {
asyncDrawable.setResult(out);
}
}
requests.remove(reference);
cleanUp();
}
});
}
});
}
private static boolean shouldCleanUp(@Nullable AsyncDrawable drawable) {
return drawable == null || !drawable.isAttached();
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
}

View File

@ -4,7 +4,7 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
@Override @Override
public void load(@NonNull AsyncDrawable drawable) { public void load(@NonNull AsyncDrawable drawable) {
@ -17,7 +17,7 @@ class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
@Nullable @Nullable
@Override @Override
public Drawable placeholder() { public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return null; return null;
} }
} }

View File

@ -1,70 +0,0 @@
package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 2.0.0
*/
public abstract class ImageItem {
/**
* Create an {@link ImageItem} with result, so no further decoding is required.
*
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static ImageItem withResult(@Nullable Drawable drawable) {
return new WithResult(drawable);
}
@NonNull
public static ImageItem withDecodingNeeded(
@Nullable String contentType,
@Nullable InputStream inputStream) {
return new WithDecodingNeeded(contentType, inputStream);
}
private ImageItem() {
}
public static class WithResult extends ImageItem {
private final Drawable result;
WithResult(@Nullable Drawable drawable) {
result = drawable;
}
@Nullable
public Drawable result() {
return result;
}
}
public static class WithDecodingNeeded extends ImageItem {
private final String contentType;
private final InputStream inputStream;
WithDecodingNeeded(
@Nullable String contentType,
@Nullable InputStream inputStream) {
this.contentType = contentType;
this.inputStream = inputStream;
}
@Nullable
public String contentType() {
return contentType;
}
@Nullable
public InputStream inputStream() {
return inputStream;
}
}
}

View File

@ -1,50 +0,0 @@
package ru.noties.markwon.image;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
* Here we just assume that supplied InputStream is of image type and try to decode it.
*
* @since 1.1.0
*/
public class ImageMediaDecoder extends MediaDecoder {
@NonNull
public static ImageMediaDecoder create(@NonNull Resources resources) {
return new ImageMediaDecoder(resources);
}
private final Resources resources;
@SuppressWarnings("WeakerAccess")
ImageMediaDecoder(Resources resources) {
this.resources = resources;
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
// absolutely not optimal... thing
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.applyIntrinsicBounds(out);
} else {
out = null;
}
return out;
}
}

View File

@ -1,130 +0,0 @@
package ru.noties.markwon.image;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.widget.TextView;
import org.commonmark.node.Image;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import java.util.Arrays;
import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.RenderProps;
import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.image.data.DataUriSchemeHandler;
import ru.noties.markwon.image.file.FileSchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler;
/**
* @since 3.0.0
*/
public class ImagesPlugin extends AbstractMarkwonPlugin {
@NonNull
public static ImagesPlugin create(@NonNull Context context) {
return new ImagesPlugin(context, false);
}
/**
* Special scheme that is used {@code file:///android_asset/}
*
* @param context
* @return
*/
@NonNull
public static ImagesPlugin createWithAssets(@NonNull Context context) {
return new ImagesPlugin(context, true);
}
private final Context context;
private final boolean useAssets;
protected ImagesPlugin(Context context, boolean useAssets) {
this.context = context;
this.useAssets = useAssets;
}
@Override
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
final FileSchemeHandler fileSchemeHandler = useAssets
? FileSchemeHandler.createWithAssets(context.getAssets())
: FileSchemeHandler.create();
builder
.addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create())
.addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler)
.addSchemeHandler(
Arrays.asList(
NetworkSchemeHandler.SCHEME_HTTP,
NetworkSchemeHandler.SCHEME_HTTPS),
NetworkSchemeHandler.create())
.defaultMediaDecoder(ImageMediaDecoder.create(context.getResources()));
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Image.class, new ImageSpanFactory());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
// if there is no image spanFactory, ignore
final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class);
if (spanFactory == null) {
visitor.visitChildren(image);
return;
}
final int length = visitor.length();
visitor.visitChildren(image);
// we must check if anything _was_ added, as we need at least one char to render
if (length == visitor.length()) {
visitor.builder().append('\uFFFC');
}
final MarkwonConfiguration configuration = visitor.configuration();
final Node parent = image.getParent();
final boolean link = parent instanceof Link;
final String destination = configuration
.urlProcessor()
.process(image.getDestination());
final RenderProps props = visitor.renderProps();
// apply image properties
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
// properties after we applied span (we could though)
ImageProps.DESTINATION.set(props, destination);
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link);
ImageProps.IMAGE_SIZE.set(props, null);
visitor.setSpans(length, spanFactory.getSpans(configuration, props));
}
});
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
AsyncDrawableScheduler.unschedule(textView);
}
@Override
public void afterSetText(@NonNull TextView textView) {
AsyncDrawableScheduler.schedule(textView);
}
}

View File

@ -1,16 +0,0 @@
package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 3.0.0
*/
public abstract class MediaDecoder {
@Nullable
public abstract Drawable decode(@NonNull InputStream inputStream);
}

View File

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

View File

@ -1,60 +0,0 @@
package ru.noties.markwon.image.data;
import android.support.annotation.Nullable;
public class DataUri {
private final String contentType;
private final boolean base64;
private final String data;
public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) {
this.contentType = contentType;
this.base64 = base64;
this.data = data;
}
@Nullable
public String contentType() {
return contentType;
}
public boolean base64() {
return base64;
}
@Nullable
public String data() {
return data;
}
@Override
public String toString() {
return "DataUri{" +
"contentType='" + contentType + '\'' +
", base64=" + base64 +
", data='" + data + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DataUri dataUri = (DataUri) o;
if (base64 != dataUri.base64) return false;
if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null)
return false;
return data != null ? data.equals(dataUri.data) : dataUri.data == null;
}
@Override
public int hashCode() {
int result = contentType != null ? contentType.hashCode() : 0;
result = 31 * result + (base64 ? 1 : 0);
result = 31 * result + (data != null ? data.hashCode() : 0);
return result;
}
}

View File

@ -1,41 +0,0 @@
package ru.noties.markwon.image.data;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
public abstract class DataUriDecoder {
@Nullable
public abstract byte[] decode(@NonNull DataUri dataUri);
@NonNull
public static DataUriDecoder create() {
return new Impl();
}
static class Impl extends DataUriDecoder {
@Nullable
@Override
public byte[] decode(@NonNull DataUri dataUri) {
final String data = dataUri.data();
if (!TextUtils.isEmpty(data)) {
try {
if (dataUri.base64()) {
return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT);
} else {
return data.getBytes("UTF-8");
}
} catch (Throwable t) {
return null;
}
} else {
return null;
}
}
}
}

View File

@ -1,79 +0,0 @@
package ru.noties.markwon.image.data;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public abstract class DataUriParser {
@Nullable
public abstract DataUri parse(@NonNull String input);
@NonNull
public static DataUriParser create() {
return new Impl();
}
static class Impl extends DataUriParser {
@Nullable
@Override
public DataUri parse(@NonNull String input) {
final int index = input.indexOf(',');
// we expect exactly one comma
if (index < 0) {
return null;
}
final String contentType;
final boolean base64;
if (index > 0) {
final String part = input.substring(0, index);
final String[] parts = part.split(";");
final int length = parts.length;
if (length > 0) {
// if one: either content-type or base64
if (length == 1) {
final String value = parts[0];
if ("base64".equals(value)) {
contentType = null;
base64 = true;
} else {
contentType = value.indexOf('/') > -1
? value
: null;
base64 = false;
}
} else {
contentType = parts[0].indexOf('/') > -1
? parts[0]
: null;
base64 = "base64".equals(parts[length - 1]);
}
} else {
contentType = null;
base64 = false;
}
} else {
contentType = null;
base64 = false;
}
final String data;
if (index < input.length()) {
final String value = input.substring(index + 1, input.length()).replaceAll("\n", "");
if (value.length() == 0) {
data = null;
} else {
data = value;
}
} else {
data = null;
}
return new DataUri(contentType, base64, data);
}
}
}

View File

@ -1,65 +0,0 @@
package ru.noties.markwon.image.data;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayInputStream;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler;
/**
* @since 2.0.0
*/
public class DataUriSchemeHandler extends SchemeHandler {
public static final String SCHEME = "data";
@NonNull
public static DataUriSchemeHandler create() {
return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create());
}
private static final String START = "data:";
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) {
if (!raw.startsWith(START)) {
return null;
}
String part = raw.substring(START.length());
// this part is added to support `data://` with which this functionality was released
if (part.startsWith("//")) {
part = part.substring(2);
}
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)
);
}
}

View File

@ -1,105 +0,0 @@
package ru.noties.markwon.image.file;
import android.content.res.AssetManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
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;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler;
/**
* @since 3.0.0
*/
public class FileSchemeHandler extends SchemeHandler {
public static final String SCHEME = "file";
@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) {
final String contentType = MimeTypeMap
.getSingleton()
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName));
out = new ImageItem(contentType, inputStream);
} else {
out = null;
}
return out;
}
}

View File

@ -1,70 +0,0 @@
package ru.noties.markwon.image.network;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler;
/**
* A simple network scheme handler that is not dependent on any external libraries.
*
* @see #create()
* @since 3.0.0
*/
public class NetworkSchemeHandler extends SchemeHandler {
public static final String SCHEME_HTTP = "http";
public static final String SCHEME_HTTPS = "https";
@NonNull
public static NetworkSchemeHandler create() {
return new NetworkSchemeHandler();
}
@Nullable
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
try {
final URL url = new URL(raw);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
final int responseCode = connection.getResponseCode();
if (responseCode >= 200 && responseCode < 300) {
final String contentType = contentType(connection.getHeaderField("Content-Type"));
final InputStream inputStream = new BufferedInputStream(connection.getInputStream());
return new ImageItem(contentType, inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Nullable
static String contentType(@Nullable String contentType) {
if (contentType == null) {
return null;
}
final int index = contentType.indexOf(';');
if (index > -1) {
return contentType.substring(0, index);
}
return contentType;
}
}

View File

@ -1,97 +0,0 @@
package ru.noties.markwon.priority;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import ru.noties.markwon.MarkwonPlugin;
// a small dependency graph also
// what if plugins cannot be constructed into a graph? for example they depend on something
// but not overlap? then it would be hard to sort them (but this doesn't make sense, if
// they do not care about other components, just put them in whatever order, no?)
/**
* @see MarkwonPlugin#priority()
* @since 3.0.0
*/
@Deprecated
public abstract class Priority {
@NonNull
public static Priority none() {
return builder().build();
}
@NonNull
public static Priority after(@NonNull Class<? extends MarkwonPlugin> plugin) {
return builder().after(plugin).build();
}
@NonNull
public static Priority after(
@NonNull Class<? extends MarkwonPlugin> plugin1,
@NonNull Class<? extends MarkwonPlugin> plugin2) {
return builder().after(plugin1).after(plugin2).build();
}
@NonNull
public static Builder builder() {
return new Impl.BuilderImpl();
}
public interface Builder {
@NonNull
Builder after(@NonNull Class<? extends MarkwonPlugin> plugin);
@NonNull
Priority build();
}
@NonNull
public abstract List<Class<? extends MarkwonPlugin>> after();
static class Impl extends Priority {
private final List<Class<? extends MarkwonPlugin>> after;
Impl(@NonNull List<Class<? extends MarkwonPlugin>> after) {
this.after = after;
}
@NonNull
@Override
public List<Class<? extends MarkwonPlugin>> after() {
return after;
}
@Override
public String toString() {
return "Priority{" +
"after=" + after +
'}';
}
static class BuilderImpl implements Builder {
private final List<Class<? extends MarkwonPlugin>> after = new ArrayList<>(0);
@NonNull
@Override
public Builder after(@NonNull Class<? extends MarkwonPlugin> plugin) {
after.add(plugin);
return this;
}
@NonNull
@Override
public Priority build() {
return new Impl(Collections.unmodifiableList(after));
}
}
}
}

View File

@ -1,18 +0,0 @@
package ru.noties.markwon.priority;
import android.support.annotation.NonNull;
import java.util.List;
import ru.noties.markwon.MarkwonPlugin;
public abstract class PriorityProcessor {
@NonNull
public static PriorityProcessor create() {
return new PriorityProcessorImpl();
}
@NonNull
public abstract List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> plugins);
}

View File

@ -1,132 +0,0 @@
package ru.noties.markwon.priority;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import ru.noties.markwon.MarkwonPlugin;
import static java.lang.Math.max;
class PriorityProcessorImpl extends PriorityProcessor {
@NonNull
@Override
public List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> in) {
// create new collection based on supplied argument
final List<MarkwonPlugin> plugins = new ArrayList<>(in);
final int size = plugins.size();
final Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map =
new HashMap<>(size);
for (MarkwonPlugin plugin : plugins) {
if (map.put(plugin.getClass(), new HashSet<>(plugin.priority().after())) != null) {
throw new IllegalStateException(String.format("Markwon duplicate plugin " +
"found `%s`: %s", plugin.getClass().getName(), plugin));
}
}
final Map<MarkwonPlugin, Integer> cache = new HashMap<>(size);
for (MarkwonPlugin plugin : plugins) {
cache.put(plugin, eval(plugin, map));
}
Collections.sort(plugins, new PriorityComparator(cache));
return plugins;
}
private static int eval(
@NonNull MarkwonPlugin plugin,
@NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {
final Set<Class<? extends MarkwonPlugin>> set = map.get(plugin.getClass());
// no dependencies
if (set.isEmpty()) {
return 0;
}
final Class<? extends MarkwonPlugin> who = plugin.getClass();
int max = 0;
for (Class<? extends MarkwonPlugin> dependency : set) {
max = max(max, eval(who, dependency, map));
}
return 1 + max;
}
// we need to count the number of steps to a root node (which has no parents)
private static int eval(
@NonNull Class<? extends MarkwonPlugin> who,
@NonNull Class<? extends MarkwonPlugin> plugin,
@NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {
// exact match
Set<Class<? extends MarkwonPlugin>> set = map.get(plugin);
if (set == null) {
// let's try to find inexact type (overridden/subclassed)
for (Map.Entry<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> entry : map.entrySet()) {
if (plugin.isAssignableFrom(entry.getKey())) {
set = entry.getValue();
break;
}
}
if (set == null) {
// unsatisfied dependency
throw new IllegalStateException(String.format("Markwon unsatisfied dependency found. " +
"Plugin `%s` comes after `%s` but it is not added.",
who.getName(), plugin.getName()));
}
}
if (set.isEmpty()) {
return 0;
}
int value = 1;
for (Class<? extends MarkwonPlugin> dependency : set) {
// a case when a plugin defines `Priority.after(getClass)` or being
// referenced by own dependency (even indirect)
if (who.equals(dependency)) {
throw new IllegalStateException(String.format("Markwon plugin `%s` defined self " +
"as a dependency or being referenced by own dependency (cycle)", who.getName()));
}
value += eval(who, dependency, map);
}
return value;
}
private static class PriorityComparator implements Comparator<MarkwonPlugin> {
private final Map<MarkwonPlugin, Integer> map;
PriorityComparator(@NonNull Map<MarkwonPlugin, Integer> map) {
this.map = map;
}
@Override
public int compare(MarkwonPlugin o1, MarkwonPlugin o2) {
return map.get(o1).compareTo(map.get(o2));
}
}
}

View File

@ -8,7 +8,6 @@ import org.robolectric.annotation.Config;
import java.util.List; import java.util.List;
import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.priority.Priority;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;

View File

@ -23,8 +23,6 @@ import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.html.MarkwonHtmlRenderer; import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.Priority;
import ru.noties.markwon.priority.PriorityProcessor;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;

View File

@ -14,8 +14,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import ru.noties.markwon.image.ImageItem;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;

View File

@ -18,7 +18,6 @@ import java.util.List;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonPlugin; import ru.noties.markwon.MarkwonPlugin;
import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.image.ImagesPlugin;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;

View File

@ -19,13 +19,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.RenderProps; import ru.noties.markwon.RenderProps;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.ImageProps;
import ru.noties.markwon.image.ImageSize; import ru.noties.markwon.image.ImageSize;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.MediaDecoder;
import ru.noties.markwon.image.SchemeHandler;
import ru.noties.markwon.priority.Priority;
/** /**
* @since 3.0.0 * @since 3.0.0

View File

@ -10,7 +10,6 @@ import java.io.InputStream;
import pl.droidsonroids.gif.GifDrawable; import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.image.DrawableUtils; import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.MediaDecoder;
/** /**
* @since 1.1.0 * @since 1.1.0

View File

@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority; import ru.noties.markwon.priority.Priority;
public class GifPlugin extends AbstractMarkwonPlugin { public class GifPlugin extends AbstractMarkwonPlugin {

View File

@ -7,8 +7,6 @@ import java.util.Arrays;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.network.NetworkSchemeHandler;
import ru.noties.markwon.priority.Priority; import ru.noties.markwon.priority.Priority;
/** /**

View File

@ -11,8 +11,6 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler;
class OkHttpSchemeHandler extends SchemeHandler { class OkHttpSchemeHandler extends SchemeHandler {

View File

@ -14,7 +14,6 @@ import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream; import java.io.InputStream;
import ru.noties.markwon.image.DrawableUtils; import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.MediaDecoder;
/** /**
* @since 1.1.0 * @since 1.1.0

View File

@ -5,7 +5,6 @@ import android.support.annotation.NonNull;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority; import ru.noties.markwon.priority.Priority;
public class SvgPlugin extends AbstractMarkwonPlugin { public class SvgPlugin extends AbstractMarkwonPlugin {

View File

@ -1,4 +1,4 @@
POM_NAME=Image POM_NAME=Image
POM_ARTIFACT_ID=image POM_ARTIFACT_ID=image
POM_DESCRIPTION=Markwon image loading module (with GIF and SVG support) POM_DESCRIPTION=Markwon image loading module (with optional GIF and SVG support)
POM_PACKAGING=aar POM_PACKAGING=aar

View File

@ -1,6 +1,5 @@
package ru.noties.markwon.image; package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -10,88 +9,69 @@ import java.util.Map;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class AsyncDrawableLoaderBuilder { class AsyncDrawableLoaderBuilder {
ExecutorService executorService; ExecutorService executorService;
final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3); final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3); final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3);
MediaDecoder defaultMediaDecoder; MediaDecoder defaultMediaDecoder;
DrawableProvider placeholderDrawableProvider; ImagesPlugin.PlaceholderProvider placeholderProvider;
DrawableProvider errorDrawableProvider; ImagesPlugin.ErrorHandler errorHandler;
@NonNull boolean isBuilt;
public AsyncDrawableLoaderBuilder executorService(@NonNull ExecutorService executorService) {
void executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService; this.executorService = executorService;
return this;
} }
@NonNull void addSchemeHandler(@NonNull SchemeHandler schemeHandler) {
public AsyncDrawableLoaderBuilder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) { for (String scheme : schemeHandler.supportedSchemes()) {
schemeHandlers.put(scheme, schemeHandler);
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder addSchemeHandler(@NonNull Collection<String> schemes, @NonNull SchemeHandler schemeHandler) {
for (String scheme : schemes) {
schemeHandlers.put(scheme, schemeHandler); schemeHandlers.put(scheme, schemeHandler);
} }
return this;
} }
@NonNull void addMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
public AsyncDrawableLoaderBuilder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) { final Collection<String> supportedTypes = mediaDecoder.supportedTypes();
mediaDecoders.put(contentType, mediaDecoder); if (supportedTypes.isEmpty()) {
return this; // todo: we should think about this little _side-effect_... does it worth it?
defaultMediaDecoder = mediaDecoder;
} else {
for (String type : supportedTypes) {
mediaDecoders.put(type, mediaDecoder);
}
}
} }
@NonNull void defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
public AsyncDrawableLoaderBuilder addMediaDecoder(@NonNull Collection<String> contentTypes, @NonNull MediaDecoder mediaDecoder) {
for (String contentType : contentTypes) {
mediaDecoders.put(contentType, mediaDecoder);
}
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder removeSchemeHandler(@NonNull String scheme) {
schemeHandlers.remove(scheme);
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder removeMediaDecoder(@NonNull String contentType) {
mediaDecoders.remove(contentType);
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
this.defaultMediaDecoder = mediaDecoder; this.defaultMediaDecoder = mediaDecoder;
return this; }
void removeSchemeHandler(@NonNull String scheme) {
schemeHandlers.remove(scheme);
}
void removeMediaDecoder(@NonNull String contentType) {
mediaDecoders.remove(contentType);
} }
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@NonNull void placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) {
public AsyncDrawableLoaderBuilder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) { this.placeholderProvider = placeholderDrawableProvider;
this.placeholderDrawableProvider = placeholderDrawableProvider;
return this;
} }
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@NonNull void errorHandler(@NonNull ImagesPlugin.ErrorHandler errorHandler) {
public AsyncDrawableLoaderBuilder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) { this.errorHandler = errorHandler;
this.errorDrawableProvider = errorDrawableProvider;
return this;
} }
@NonNull @NonNull
public AsyncDrawableLoader build() { AsyncDrawableLoader build() {
isBuilt = true;
// if we have no schemeHandlers -> we cannot show anything // if we have no schemeHandlers -> we cannot show anything
// OR if we have no media decoders // OR if we have no media decoders

View File

@ -6,9 +6,8 @@ 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.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -22,8 +21,8 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
private final Map<String, SchemeHandler> schemeHandlers; private final Map<String, SchemeHandler> schemeHandlers;
private final Map<String, MediaDecoder> mediaDecoders; private final Map<String, MediaDecoder> mediaDecoders;
private final MediaDecoder defaultMediaDecoder; private final MediaDecoder defaultMediaDecoder;
private final DrawableProvider placeholderDrawableProvider; private final ImagesPlugin.PlaceholderProvider placeholderProvider;
private final DrawableProvider errorDrawableProvider; private final ImagesPlugin.ErrorHandler errorHandler;
private final Handler mainThread; private final Handler mainThread;
@ -31,13 +30,13 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
// for the same destination // for the same destination
private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2); private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2);
AsyncDrawableLoaderImpl(@NonNull Builder builder) { AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder builder) {
this.executorService = builder.executorService; this.executorService = builder.executorService;
this.schemeHandlers = builder.schemeHandlers; this.schemeHandlers = builder.schemeHandlers;
this.mediaDecoders = builder.mediaDecoders; this.mediaDecoders = builder.mediaDecoders;
this.defaultMediaDecoder = builder.defaultMediaDecoder; this.defaultMediaDecoder = builder.defaultMediaDecoder;
this.placeholderDrawableProvider = builder.placeholderDrawableProvider; this.placeholderProvider = builder.placeholderProvider;
this.errorDrawableProvider = builder.errorDrawableProvider; this.errorHandler = builder.errorHandler;
this.mainThread = new Handler(Looper.getMainLooper()); this.mainThread = new Handler(Looper.getMainLooper());
} }
@ -147,48 +146,18 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
} }
} }
// @Override
// public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
//
// // todo: we cannot reliably identify request by the destination, as if
// // markdown input has multiple images with the same destination as source
// // we will be tracking only one of them (the one appears the last). We should
// // move to AsyncDrawable based identification. This method also _maybe_
// // should include the ImageSize (comment @since 3.1.0-SNAPSHOT)
//
// requests.put(destination, execute(destination, drawable));
// }
//
// @Override
// public void cancel(@NonNull String destination) {
//
// // todo: as we are moving away from a single request for a destination,
// // we should re-evaluate this cancellation logic, as if there are multiple images
// // in markdown input all of them will be cancelled (won't delivered), even if
// // only a single drawable is detached. Cancellation must also take
// // the AsyncDrawable argument (comment @since 3.1.0-SNAPSHOT)
//
// //
// final Future<?> request = requests.remove(destination);
// if (request != null) {
// request.cancel(true);
// }
// }
@Nullable @Nullable
@Override @Override
public Drawable placeholder() { public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return placeholderDrawableProvider != null return placeholderProvider != null
? placeholderDrawableProvider.provide() ? placeholderProvider.providePlaceholder(drawable)
: null; : null;
} }
@NonNull
private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) { private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) {
// todo: error handing (simply applying errorDrawable is not a good solution // todo: more efficient DefaultImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal
// 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 // 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 // image bounds (but we will need to cache inputStream in order to inspect and optimize
// input image...) // input image...)
@ -197,71 +166,60 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader {
@Override @Override
public void run() { public void run() {
final ImageItem item;
final Uri uri = Uri.parse(destination); final Uri uri = Uri.parse(destination);
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); Drawable drawable = null;
if (schemeHandler != null) {
item = schemeHandler.handle(destination, uri);
} else {
item = null;
}
final InputStream inputStream = item != null
? item.inputStream()
: null;
Drawable result = null;
if (inputStream != null) {
try { try {
// obtain scheme handler
final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme());
if (schemeHandler != null) {
// handle scheme
final ImageItem imageItem = schemeHandler.handle(destination, uri);
// if resulting imageItem needs further decoding -> proceed
if (imageItem.hasDecodingNeeded()) {
final ImageItem.WithDecodingNeeded withDecodingNeeded = imageItem.getAsWithDecodingNeeded();
MediaDecoder mediaDecoder = mediaDecoders.get(withDecodingNeeded.contentType());
MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType());
if (mediaDecoder == null) { if (mediaDecoder == null) {
mediaDecoder = defaultMediaDecoder; mediaDecoder = defaultMediaDecoder;
} }
if (mediaDecoder != null) { if (mediaDecoder != null) {
result = mediaDecoder.decode(inputStream); drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream());
} else {
// throw that no media decoder is found
throw new IllegalStateException("No media-decoder is found: " + destination);
}
} else {
drawable = imageItem.getAsWithResult().result();
}
} else {
// throw no scheme handler is available
throw new IllegalStateException("No scheme-handler is found: " + destination);
} }
} finally { } catch (Throwable t) {
try { if (errorHandler != null) {
inputStream.close(); drawable = errorHandler.handleError(destination, t);
} catch (IOException e) { } else {
// ignored // else simply log the error
} Log.e("MARKWON-IMAGE", "Error loading image: " + destination, t);
} }
} }
// if result is null, we assume it's an error final Drawable out = drawable;
if (result == null) {
result = errorDrawableProvider != null
? errorDrawableProvider.provide()
: null;
}
final Drawable out = result;
mainThread.post(new Runnable() { mainThread.post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (out != null) { if (out != null) {
// AsyncDrawable cannot change destination, so if it's
// this doesn't work with markdown input with multiple images with the
// same source (comment @since 3.1.0-SNAPSHOT)
// final boolean canDeliver = requests.remove(destination) != null;
// if (canDeliver) {
// final AsyncDrawable asyncDrawable = reference.get();
// if (asyncDrawable != null && asyncDrawable.isAttached()) {
// asyncDrawable.setResult(out);
// }
// }
// todo: AsyncDrawable cannot change destination, so if it's
// attached and not garbage-collected, we can deliver the result. // attached and not garbage-collected, we can deliver the result.
// Note that there is no cache, so attach/detach of drawables // Note that there is no cache, so attach/detach of drawables
// will always request a new entry.. (comment @since 3.1.0-SNAPSHOT) // will always request a new entry.. (comment @since 3.1.0-SNAPSHOT)

View File

@ -0,0 +1,62 @@
package ru.noties.markwon.image;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
/**
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
* Here we just assume that supplied InputStream is of image type and try to decode it.
*
* @since 1.1.0
*/
public class DefaultImageMediaDecoder extends MediaDecoder {
@NonNull
public static DefaultImageMediaDecoder create() {
return new DefaultImageMediaDecoder(Resources.getSystem());
}
@NonNull
public static DefaultImageMediaDecoder create(@NonNull Resources resources) {
return new DefaultImageMediaDecoder(resources);
}
private final Resources resources;
@SuppressWarnings("WeakerAccess")
DefaultImageMediaDecoder(Resources resources) {
this.resources = resources;
}
@NonNull
@Override
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
final Bitmap bitmap;
try {
// absolutely not optimal... thing
bitmap = BitmapFactory.decodeStream(inputStream);
} catch (Throwable t) {
throw new IllegalStateException("Exception decoding input-stream", t);
}
final Drawable drawable = new BitmapDrawable(resources, bitmap);
DrawableUtils.applyIntrinsicBounds(drawable);
return drawable;
}
@NonNull
@Override
public Collection<String> supportedTypes() {
return Collections.emptySet();
}
}

View File

@ -1,6 +1,5 @@
package ru.noties.markwon.image; package ru.noties.markwon.image;
import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -11,6 +10,8 @@ import org.commonmark.node.Image;
import org.commonmark.node.Link; import org.commonmark.node.Link;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import java.util.concurrent.ExecutorService;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration; import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory; import ru.noties.markwon.MarkwonSpansFactory;
@ -23,55 +24,123 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
/** /**
* @since 4.0.0-SNAPSHOT * @since 4.0.0-SNAPSHOT
*/ */
public interface DrawableProvider { public interface ImagesConfigure {
@Nullable void configureImages(@NonNull ImagesPlugin plugin);
Drawable provide();
}
@NonNull
public static ImagesPlugin create(@NonNull Context context) {
return new ImagesPlugin(context, false);
} }
/** /**
* Special scheme that is used {@code file:///android_asset/} * @since 4.0.0-SNAPSHOT
* */
* @param context public interface PlaceholderProvider {
* @return @Nullable
Drawable providePlaceholder(@NonNull AsyncDrawable drawable);
}
/**
* @since 4.0.0-SNAPSHOT
*/
public interface ErrorHandler {
/**
* Can optionally return a Drawable that will be displayed in case of an error
*/
@Nullable
Drawable handleError(@NonNull String url, @NonNull Throwable throwable);
}
/**
* Factory method to create an empty {@link ImagesPlugin} instance with no {@link SchemeHandler}s
* nor {@link MediaDecoder}s registered. Can be used to further configure via instance methods or
* via {@link ru.noties.markwon.MarkwonPlugin#configure(Registry)}
*/ */
@NonNull @NonNull
public static ImagesPlugin createWithAssets(@NonNull Context context) { public static ImagesPlugin createEmpty() {
return new ImagesPlugin(context, true); return new ImagesPlugin();
} }
private final Context context; @NonNull
private final boolean useAssets; public static ImagesPlugin create(@NonNull ImagesConfigure configure) {
final ImagesPlugin plugin = new ImagesPlugin();
protected ImagesPlugin(Context context, boolean useAssets) { configure.configureImages(plugin);
this.context = context; return plugin;
this.useAssets = useAssets;
} }
// we must expose scheme handling... so it's available during construction and via `require` private final AsyncDrawableLoaderBuilder builder = new AsyncDrawableLoaderBuilder();
// @Override /**
// public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { * Optional (by default new cached thread executor will be used)
// *
// final FileSchemeHandler fileSchemeHandler = useAssets * @since 4.0.0-SNAPSHOT
// ? FileSchemeHandler.createWithAssets(context.getAssets()) */
// : FileSchemeHandler.create(); @NonNull
// public ImagesPlugin executorService(@NonNull ExecutorService executorService) {
// builder checkBuilderState();
// .addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create()) builder.executorService(executorService);
// .addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler) return this;
// .addSchemeHandler( }
// Arrays.asList(
// NetworkSchemeHandler.SCHEME_HTTP, /**
// NetworkSchemeHandler.SCHEME_HTTPS), * @see SchemeHandler
// NetworkSchemeHandler.create()) * @see ru.noties.markwon.image.data.DataUriSchemeHandler
// .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources())); * @see ru.noties.markwon.image.file.FileSchemeHandler
// } * @see ru.noties.markwon.image.network.NetworkSchemeHandler
* @see ru.noties.markwon.image.network.OkHttpNetworkSchemeHandler
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public ImagesPlugin addSchemeHandler(@NonNull SchemeHandler schemeHandler) {
checkBuilderState();
builder.addSchemeHandler(schemeHandler);
return this;
}
@NonNull
public ImagesPlugin addMediaDecoder(@NonNull MediaDecoder mediaDecoder) {
checkBuilderState();
builder.addMediaDecoder(mediaDecoder);
return this;
}
@NonNull
public ImagesPlugin defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
checkBuilderState();
builder.defaultMediaDecoder(mediaDecoder);
return this;
}
@NonNull
public ImagesPlugin removeSchemeHandler(@NonNull String scheme) {
checkBuilderState();
builder.removeSchemeHandler(scheme);
return this;
}
@NonNull
public ImagesPlugin removeMediaDecoder(@NonNull String contentType) {
checkBuilderState();
builder.removeMediaDecoder(contentType);
return this;
}
@NonNull
public ImagesPlugin placeholderProvider(@NonNull PlaceholderProvider placeholderProvider) {
checkBuilderState();
builder.placeholderProvider(placeholderProvider);
return this;
}
@NonNull
public ImagesPlugin errorHandler(@NonNull ErrorHandler errorHandler) {
checkBuilderState();
builder.errorHandler(errorHandler);
return this;
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
checkBuilderState();
builder.asyncDrawableLoader(this.builder.build());
}
@Override @Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
@ -132,4 +201,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
public void afterSetText(@NonNull TextView textView) { public void afterSetText(@NonNull TextView textView) {
AsyncDrawableScheduler.schedule(textView); AsyncDrawableScheduler.schedule(textView);
} }
private void checkBuilderState() {
if (builder.isBuilt) {
throw new IllegalStateException("ImagesPlugin has already been configured " +
"and cannot be modified any further");
}
}
} }

View File

@ -5,6 +5,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
/** /**
* @since 3.0.0 * @since 3.0.0
@ -16,14 +17,17 @@ public abstract class MediaDecoder {
* <ul> * <ul>
* <li>Returns `non-null` drawable</li> * <li>Returns `non-null` drawable</li>
* <li>Added `contentType` method parameter</li> * <li>Added `contentType` method parameter</li>
* <li>Added `throws Exception` to method signature</li>
* </ul> * </ul>
*
* @throws Exception since 4.0.0-SNAPSHOT
*/ */
@NonNull @NonNull
public abstract Drawable decode( public abstract Drawable decode(
@Nullable String contentType, @Nullable String contentType,
@NonNull InputStream inputStream @NonNull InputStream inputStream
) throws Exception; );
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public abstract Collection<String> supportedTypes();
} }

View File

@ -3,6 +3,8 @@ package ru.noties.markwon.image;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.util.Collection;
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@ -12,13 +14,17 @@ public abstract class SchemeHandler {
* Changes since 4.0.0-SNAPSHOT: * Changes since 4.0.0-SNAPSHOT:
* <ul> * <ul>
* <li>Returns `non-null` image-item</li> * <li>Returns `non-null` image-item</li>
* <li>added `throws Exception` to method signature</li>
* </ul> * </ul>
* *
* @throws Exception since 4.0.0-SNAPSHOT
* @see ImageItem#withResult(android.graphics.drawable.Drawable) * @see ImageItem#withResult(android.graphics.drawable.Drawable)
* @see ImageItem#withDecodingNeeded(String, java.io.InputStream) * @see ImageItem#withDecodingNeeded(String, java.io.InputStream)
*/ */
@NonNull @NonNull
public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri) throws Exception; public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri);
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public abstract Collection<String> supportedSchemes();
} }

View File

@ -4,6 +4,8 @@ import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Collections;
import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler; import ru.noties.markwon.image.SchemeHandler;
@ -61,4 +63,10 @@ public class DataUriSchemeHandler extends SchemeHandler {
dataUri.contentType(), dataUri.contentType(),
new ByteArrayInputStream(bytes)); new ByteArrayInputStream(bytes));
} }
@NonNull
@Override
public Collection<String> supportedSchemes() {
return Collections.singleton(SCHEME);
}
} }

View File

@ -13,6 +13,8 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageItem;
@ -111,4 +113,10 @@ public class FileSchemeHandler extends SchemeHandler {
return ImageItem.withDecodingNeeded(contentType, inputStream); return ImageItem.withDecodingNeeded(contentType, inputStream);
} }
@NonNull
@Override
public Collection<String> supportedSchemes() {
return Collections.singleton(SCHEME);
}
} }

View File

@ -7,6 +7,8 @@ import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import pl.droidsonroids.gif.GifDrawable; import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.image.DrawableUtils; import ru.noties.markwon.image.DrawableUtils;
@ -58,6 +60,12 @@ public class GifMediaDecoder extends MediaDecoder {
return drawable; return drawable;
} }
@NonNull
@Override
public Collection<String> supportedTypes() {
return Collections.singleton(CONTENT_TYPE);
}
@NonNull @NonNull
protected GifDrawable newGifDrawable(@NonNull byte[] bytes) throws IOException { protected GifDrawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
return new GifDrawable(bytes); return new GifDrawable(bytes);

View File

@ -9,6 +9,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import ru.noties.markwon.image.ImageItem; import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler; import ru.noties.markwon.image.SchemeHandler;
@ -56,6 +58,12 @@ public class NetworkSchemeHandler extends SchemeHandler {
return imageItem; return imageItem;
} }
@NonNull
@Override
public Collection<String> supportedSchemes() {
return Arrays.asList(SCHEME_HTTP, SCHEME_HTTPS);
}
@Nullable @Nullable
static String contentType(@Nullable String contentType) { static String contentType(@Nullable String contentType) {

View File

@ -3,8 +3,9 @@ package ru.noties.markwon.image.network;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
@ -16,7 +17,7 @@ import ru.noties.markwon.image.SchemeHandler;
/** /**
* @since 4.0.0-SNAPSHOT * @since 4.0.0-SNAPSHOT
*/ */
class OkHttpNetworkSchemeHandler extends SchemeHandler { public class OkHttpNetworkSchemeHandler extends SchemeHandler {
/** /**
* @see #create(OkHttpClient) * @see #create(OkHttpClient)
@ -51,8 +52,8 @@ class OkHttpNetworkSchemeHandler extends SchemeHandler {
final Response response; final Response response;
try { try {
response = client.newCall(request).execute(); response = client.newCall(request).execute();
} catch (IOException e) { } catch (Throwable t) {
throw new IllegalStateException("Exception obtaining network resource: " + raw, e); throw new IllegalStateException("Exception obtaining network resource: " + raw, t);
} }
if (response == null) { if (response == null) {
@ -68,8 +69,18 @@ class OkHttpNetworkSchemeHandler extends SchemeHandler {
throw new IllegalStateException("Response does not contain body: " + raw); throw new IllegalStateException("Response does not contain body: " + raw);
} }
final String contentType = response.header(HEADER_CONTENT_TYPE); // important to process content-type as it can have encoding specified (which we should remove)
final String contentType =
NetworkSchemeHandler.contentType(response.header(HEADER_CONTENT_TYPE));
return ImageItem.withDecodingNeeded(contentType, inputStream); return ImageItem.withDecodingNeeded(contentType, inputStream);
} }
@NonNull
@Override
public Collection<String> supportedSchemes() {
return Arrays.asList(
NetworkSchemeHandler.SCHEME_HTTP,
NetworkSchemeHandler.SCHEME_HTTPS);
}
} }

View File

@ -12,6 +12,8 @@ import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException; import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import ru.noties.markwon.image.DrawableUtils; import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.MediaDecoder; import ru.noties.markwon.image.MediaDecoder;
@ -71,4 +73,10 @@ public class SvgMediaDecoder extends MediaDecoder {
DrawableUtils.applyIntrinsicBounds(drawable); DrawableUtils.applyIntrinsicBounds(drawable);
return drawable; return drawable;
} }
@NonNull
@Override
public Collection<String> supportedTypes() {
return Collections.singleton(CONTENT_TYPE);
}
} }

View File

@ -39,9 +39,7 @@ dependencies {
implementation project(':markwon-ext-tables') implementation project(':markwon-ext-tables')
implementation project(':markwon-ext-tasklist') implementation project(':markwon-ext-tasklist')
implementation project(':markwon-html') implementation project(':markwon-html')
implementation project(':markwon-image-gif') implementation project(':markwon-image')
implementation project(':markwon-image-okhttp')
implementation project(':markwon-image-svg')
implementation project(':markwon-syntax-highlight') implementation project(':markwon-syntax-highlight')
implementation project(':markwon-recycler') implementation project(':markwon-recycler')
implementation project(':markwon-recycler-table') implementation project(':markwon-recycler-table')

View File

@ -22,10 +22,6 @@ import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.movement.MovementMethodPlugin; import ru.noties.markwon.movement.MovementMethodPlugin;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.SchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler;
public class BasicPluginsActivity extends Activity { public class BasicPluginsActivity extends Activity {

View File

@ -6,7 +6,6 @@ import android.support.annotation.Nullable;
import android.widget.TextView; import android.widget.TextView;
import ru.noties.markwon.Markwon; import ru.noties.markwon.Markwon;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.sample.R; import ru.noties.markwon.sample.R;
public class CustomExtensionActivity extends Activity { public class CustomExtensionActivity extends Activity {

View File

@ -7,8 +7,6 @@ import org.commonmark.parser.Parser;
import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.priority.Priority;
public class IconPlugin extends AbstractMarkwonPlugin { public class IconPlugin extends AbstractMarkwonPlugin {

View File

@ -7,7 +7,6 @@ import android.widget.TextView;
import ru.noties.markwon.Markwon; import ru.noties.markwon.Markwon;
import ru.noties.markwon.ext.latex.JLatexMathPlugin; import ru.noties.markwon.ext.latex.JLatexMathPlugin;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.sample.R; import ru.noties.markwon.sample.R;
public class LatexActivity extends Activity { public class LatexActivity extends Activity {

View File

@ -26,7 +26,6 @@ import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.core.CorePlugin; import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.html.HtmlPlugin; import ru.noties.markwon.html.HtmlPlugin;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.svg.SvgPlugin; import ru.noties.markwon.image.svg.SvgPlugin;
import ru.noties.markwon.recycler.MarkwonAdapter; import ru.noties.markwon.recycler.MarkwonAdapter;
import ru.noties.markwon.recycler.SimpleEntry; import ru.noties.markwon.recycler.SimpleEntry;

View File

@ -7,9 +7,6 @@ include ':app', ':sample',
':markwon-ext-tasklist', ':markwon-ext-tasklist',
':markwon-html', ':markwon-html',
':markwon-image', ':markwon-image',
':markwon-image-gif',
':markwon-image-okhttp',
':markwon-image-svg',
':markwon-recycler', ':markwon-recycler',
':markwon-recycler-table', ':markwon-recycler-table',
':markwon-syntax-highlight', ':markwon-syntax-highlight',