Moving image loading functionality to standalone module

This commit is contained in:
Dimitry Ivanov 2019-05-28 16:15:25 +03:00
parent 453880bd62
commit 661f72da0f
28 changed files with 1637 additions and 255 deletions

View File

@ -22,6 +22,11 @@ import ru.noties.markwon.priority.Priority;
*/ */
public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
@Override
public void configure(@NonNull Registry registry) {
}
@Override @Override
public void configureParser(@NonNull Parser.Builder builder) { public void configureParser(@NonNull Parser.Builder builder) {
@ -32,11 +37,6 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
} }
@Override
public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
}
@Override @Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
@ -57,13 +57,6 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
} }
@NonNull
@Override
public Priority priority() {
// by default all come after CorePlugin
return Priority.after(CorePlugin.class);
}
@NonNull @NonNull
@Override @Override
public String processMarkdown(@NonNull String markdown) { public String processMarkdown(@NonNull String markdown) {

View File

@ -37,13 +37,26 @@ public abstract class Markwon {
} }
/** /**
* Factory method to obtain an instance of {@link Builder}. * Factory method to obtain an instance of {@link Builder} with {@link CorePlugin} added.
* *
* @see Builder * @see Builder
* @see #builderNoCore(Context)
* @since 3.0.0 * @since 3.0.0
*/ */
@NonNull @NonNull
public static Builder builder(@NonNull Context context) { public static Builder builder(@NonNull Context context) {
return new MarkwonBuilderImpl(context)
// @since 4.0.0-SNAPSHOT add CorePlugin
.usePlugin(CorePlugin.create());
}
/**
* Factory method to obtain an instance of {@link Builder} without {@link CorePlugin}.
*
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static Builder builderNoCore(@NonNull Context context) {
return new MarkwonBuilderImpl(context); return new MarkwonBuilderImpl(context);
} }

View File

@ -2,6 +2,7 @@ package ru.noties.markwon;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.widget.TextView; import android.widget.TextView;
@ -9,19 +10,17 @@ import org.commonmark.parser.Parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set;
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.PriorityProcessor;
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@SuppressWarnings("WeakerAccess")
class MarkwonBuilderImpl implements Markwon.Builder { class MarkwonBuilderImpl implements Markwon.Builder {
private final Context context; private final Context context;
@ -30,8 +29,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
private PriorityProcessor priorityProcessor;
MarkwonBuilderImpl(@NonNull Context context) { MarkwonBuilderImpl(@NonNull Context context) {
this.context = context; this.context = context;
} }
@ -69,13 +66,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
return this; return this;
} }
@SuppressWarnings("UnusedReturnValue")
@NonNull
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
this.priorityProcessor = priorityProcessor;
return this;
}
@NonNull @NonNull
@Override @Override
public Markwon build() { public Markwon build() {
@ -85,21 +75,12 @@ class MarkwonBuilderImpl implements Markwon.Builder {
"method to add them"); "method to add them");
} }
// this class will sort plugins to match a priority/dependency graph that we have
PriorityProcessor priorityProcessor = this.priorityProcessor;
if (priorityProcessor == null) {
// strictly speaking we do not need updating this field
// as we are not building this class to be reused between multiple `build` calls
priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
}
// please note that this method must not modify supplied collection // please note that this method must not modify supplied collection
// if nothing should be done -> the same collection can be returned // if nothing should be done -> the same collection can be returned
final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins); final List<MarkwonPlugin> plugins = preparePlugins(this.plugins);
final Parser.Builder parserBuilder = new Parser.Builder(); final Parser.Builder parserBuilder = new Parser.Builder();
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(); final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
@ -108,7 +89,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
plugin.configureParser(parserBuilder); plugin.configureParser(parserBuilder);
plugin.configureTheme(themeBuilder); plugin.configureTheme(themeBuilder);
plugin.configureImages(asyncDrawableLoaderBuilder);
plugin.configureConfiguration(configurationBuilder); plugin.configureConfiguration(configurationBuilder);
plugin.configureVisitor(visitorBuilder); plugin.configureVisitor(visitorBuilder);
plugin.configureSpansFactory(spanFactoryBuilder); plugin.configureSpansFactory(spanFactoryBuilder);
@ -117,7 +97,6 @@ class MarkwonBuilderImpl implements Markwon.Builder {
final MarkwonConfiguration configuration = configurationBuilder.build( final MarkwonConfiguration configuration = configurationBuilder.build(
themeBuilder.build(), themeBuilder.build(),
asyncDrawableLoaderBuilder.build(),
htmlRendererBuilder.build(), htmlRendererBuilder.build(),
spanFactoryBuilder.build()); spanFactoryBuilder.build());
@ -133,62 +112,107 @@ class MarkwonBuilderImpl implements Markwon.Builder {
@VisibleForTesting @VisibleForTesting
@NonNull @NonNull
static List<MarkwonPlugin> preparePlugins( static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) {
@NonNull PriorityProcessor priorityProcessor, return new RegistryImpl(plugins).process();
@NonNull List<MarkwonPlugin> plugins) { }
// with this method we will ensure that CorePlugin is added IF and ONLY IF // @since 4.0.0-SNAPSHOT
// there are plugins that depend on it. If CorePlugin is added, or there are private static class RegistryImpl implements MarkwonPlugin.Registry {
// no plugins that require it, CorePlugin won't be added
final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins); private final List<MarkwonPlugin> origin;
private final List<MarkwonPlugin> plugins;
return priorityProcessor.process(out); private final Set<MarkwonPlugin> pending;
RegistryImpl(@NonNull List<MarkwonPlugin> origin) {
this.origin = origin;
this.plugins = new ArrayList<>(origin.size());
this.pending = new HashSet<>(3);
} }
// this method will _implicitly_ add CorePlugin if there is at least one plugin
// that depends on CorePlugin
@VisibleForTesting
@NonNull @NonNull
static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) { @Override
// loop over plugins -> if CorePlugin is found -> break; public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) {
// iterate over all plugins and check if CorePlugin is requested return get(plugin);
boolean hasCore = false;
boolean hasCoreDependents = false;
for (MarkwonPlugin plugin : plugins) {
// here we do not check for exact match (a user could've subclasses CorePlugin
// and supplied it. In this case we DO NOT implicitly add CorePlugin
//
// if core is present already we do not need to iterate anymore -> as nothing
// will be changed (and we actually do not care if there are any dependents of Core
// as it's present anyway)
if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
hasCore = true;
break;
} }
// if plugin has CorePlugin in dependencies -> mark for addition @Override
if (!hasCoreDependents) { public <P extends MarkwonPlugin> void require(
// here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass @NonNull Class<P> plugin,
// or something -> ignore) @NonNull MarkwonPlugin.Action<? super P> action) {
if (plugin.priority().after().contains(CorePlugin.class)) { action.apply(get(plugin));
hasCoreDependents = true;
}
}
} }
// important thing here is to check if corePlugin is added @NonNull
// add it _only_ if it's not present List<MarkwonPlugin> process() {
if (hasCoreDependents && !hasCore) { for (MarkwonPlugin plugin : origin) {
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1); configure(plugin);
// add default instance of CorePlugin
out.add(CorePlugin.create());
out.addAll(plugins);
return out;
} }
return plugins; return plugins;
} }
private void configure(@NonNull MarkwonPlugin plugin) {
// important -> check if it's in plugins
// if it is -> no need to configure (already configured)
if (!plugins.contains(plugin)) {
if (pending.contains(plugin)) {
throw new IllegalStateException("Cyclic dependency chain found: " + pending);
}
// start tracking plugins that are pending for configuration
pending.add(plugin);
plugin.configure(this);
// stop pending tracking
pending.remove(plugin);
// check again if it's included (a child might've configured it already)
// add to out-collection if not already present
// this is a bit different from `find` method as it does check for exact instance
// and not a sub-type
if (!plugins.contains(plugin)) {
plugins.add(plugin);
}
}
}
@NonNull
private <P extends MarkwonPlugin> P get(@NonNull Class<P> type) {
// check if present already in plugins
// find in origin, if not found -> throw, else add to out-plugins
P plugin = find(plugins, type);
if (plugin == null) {
plugin = find(origin, type);
if (plugin == null) {
throw new IllegalStateException("Requested plugin is not added: " +
"" + type.getName() + ", plugins: " + origin);
}
configure(plugin);
}
return plugin;
}
@Nullable
private static <P extends MarkwonPlugin> P find(
@NonNull List<MarkwonPlugin> plugins,
@NonNull Class<P> type) {
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
//noinspection unchecked
return (P) plugin;
}
}
return null;
}
}
} }

View File

@ -113,6 +113,15 @@ public class MarkwonConfiguration {
Builder() { Builder() {
} }
/**
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public Builder asyncDrawableLoader(@NonNull AsyncDrawableLoader asyncDrawableLoader) {
this.asyncDrawableLoader = asyncDrawableLoader;
return this;
}
@NonNull @NonNull
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
this.syntaxHighlight = syntaxHighlight; this.syntaxHighlight = syntaxHighlight;
@ -149,15 +158,18 @@ public class MarkwonConfiguration {
@NonNull @NonNull
public MarkwonConfiguration build( public MarkwonConfiguration build(
@NonNull MarkwonTheme theme, @NonNull MarkwonTheme theme,
@NonNull AsyncDrawableLoader asyncDrawableLoader,
@NonNull MarkwonHtmlRenderer htmlRenderer, @NonNull MarkwonHtmlRenderer htmlRenderer,
@NonNull MarkwonSpansFactory spansFactory) { @NonNull MarkwonSpansFactory spansFactory) {
this.theme = theme; this.theme = theme;
this.asyncDrawableLoader = asyncDrawableLoader;
this.htmlRenderer = htmlRenderer; this.htmlRenderer = htmlRenderer;
this.spansFactory = spansFactory; this.spansFactory = spansFactory;
// @since 4.0.0-SNAPSHOT
if (asyncDrawableLoader == null) {
asyncDrawableLoader = AsyncDrawableLoader.noOp();
}
if (syntaxHighlight == null) { if (syntaxHighlight == null) {
syntaxHighlight = new SyntaxHighlightNoOp(); syntaxHighlight = new SyntaxHighlightNoOp();
} }

View File

@ -9,10 +9,6 @@ import org.commonmark.parser.Parser;
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.MediaDecoder;
import ru.noties.markwon.image.SchemeHandler;
import ru.noties.markwon.priority.Priority;
/** /**
* Class represents a plugin (extension) to Markwon to configure how parsing and rendering * Class represents a plugin (extension) to Markwon to configure how parsing and rendering
@ -25,6 +21,35 @@ import ru.noties.markwon.priority.Priority;
*/ */
public interface MarkwonPlugin { public interface MarkwonPlugin {
/**
* @see Registry#require(Class, Action)
* @since 4.0.0-SNAPSHOT
*/
interface Action<P extends MarkwonPlugin> {
void apply(@NonNull P p);
}
/**
* @see #configure(Registry)
* @since 4.0.0-SNAPSHOT
*/
interface Registry {
@NonNull
<P extends MarkwonPlugin> P require(@NonNull Class<P> plugin);
<P extends MarkwonPlugin> void require(
@NonNull Class<P> plugin,
@NonNull Action<? super P> action);
}
/**
* This method will be called before any other during {@link Markwon} instance construction.
*
* @since 4.0.0-SNAPSHOT
*/
void configure(@NonNull Registry registry);
/** /**
* Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom * Method to configure <code>org.commonmark.parser.Parser</code> (for example register custom
* extension, etc). * extension, etc).
@ -39,17 +64,6 @@ public interface MarkwonPlugin {
*/ */
void configureTheme(@NonNull MarkwonTheme.Builder builder); void configureTheme(@NonNull MarkwonTheme.Builder builder);
/**
* Configure image loading functionality. For example add new content-types
* {@link AsyncDrawableLoader.Builder#addMediaDecoder(String, MediaDecoder)}, a transport
* layer (network, file, etc) {@link AsyncDrawableLoader.Builder#addSchemeHandler(String, SchemeHandler)}
* or modify existing properties.
*
* @see AsyncDrawableLoader
* @see AsyncDrawableLoader.Builder
*/
void configureImages(@NonNull AsyncDrawableLoader.Builder builder);
/** /**
* Configure {@link MarkwonConfiguration} * Configure {@link MarkwonConfiguration}
* *
@ -82,9 +96,6 @@ public interface MarkwonPlugin {
*/ */
void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder); void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder);
@NonNull
Priority priority();
/** /**
* Process input markdown and return new string to be used in parsing stage further. * Process input markdown and return new string to be used in parsing stage further.
* Can be described as <code>pre-processing</code> of markdown String. * Can be described as <code>pre-processing</code> of markdown String.

View File

@ -95,12 +95,6 @@ public class CorePlugin extends AbstractMarkwonPlugin {
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
} }
@NonNull
@Override
public Priority priority() {
return Priority.none();
}
@Override @Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
OrderedListItemSpan.measure(textView, markdown); OrderedListItemSpan.measure(textView, markdown);

View File

@ -13,22 +13,6 @@ import java.util.concurrent.Executors;
public abstract class AsyncDrawableLoader { public abstract class AsyncDrawableLoader {
/**
* @since 3.0.0
*/
public interface DrawableProvider {
@Nullable
Drawable provide();
}
/**
* @since 3.0.0
*/
@NonNull
public static Builder builder() {
return new Builder();
}
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@ -71,123 +55,4 @@ public abstract class AsyncDrawableLoader {
@Nullable @Nullable
public abstract Drawable placeholder(); public abstract Drawable placeholder();
public static class Builder {
ExecutorService executorService;
final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3);
MediaDecoder defaultMediaDecoder;
DrawableProvider placeholderDrawableProvider;
DrawableProvider errorDrawableProvider;
AsyncDrawableLoader implementation;
@NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
@NonNull
public Builder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) {
schemeHandlers.put(scheme, schemeHandler);
return this;
}
@NonNull
public Builder addSchemeHandler(@NonNull Collection<String> schemes, @NonNull SchemeHandler schemeHandler) {
for (String scheme : schemes) {
schemeHandlers.put(scheme, schemeHandler);
}
return this;
}
@NonNull
public Builder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) {
mediaDecoders.put(contentType, mediaDecoder);
return this;
}
@NonNull
public Builder addMediaDecoder(@NonNull Collection<String> contentTypes, @NonNull MediaDecoder mediaDecoder) {
for (String contentType : contentTypes) {
mediaDecoders.put(contentType, mediaDecoder);
}
return this;
}
@NonNull
public Builder removeSchemeHandler(@NonNull String scheme) {
schemeHandlers.remove(scheme);
return this;
}
@NonNull
public Builder removeMediaDecoder(@NonNull String contentType) {
mediaDecoders.remove(contentType);
return this;
}
@NonNull
public Builder defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) {
this.defaultMediaDecoder = mediaDecoder;
return this;
}
/**
* @since 3.0.0
*/
@NonNull
public Builder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) {
this.placeholderDrawableProvider = placeholderDrawableProvider;
return this;
}
/**
* @since 3.0.0
*/
@NonNull
public Builder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) {
this.errorDrawableProvider = errorDrawableProvider;
return this;
}
/**
* Please note that if implementation is supplied, all configuration properties
* (scheme-handlers, media-decoders, placeholder, etc) of this builder instance
* will be ignored.
*
* @param implementation {@link AsyncDrawableLoader} implementation to be used.
* @since 3.0.1
*/
@NonNull
public Builder implementation(@NonNull AsyncDrawableLoader implementation) {
this.implementation = implementation;
return this;
}
@NonNull
public AsyncDrawableLoader build() {
// NB, all other configuration properties will be ignored if
// implementation is specified
if (implementation != null) {
return implementation;
}
// if we have no schemeHandlers -> we cannot show anything
// OR if we have no media decoders
if (schemeHandlers.size() == 0
|| (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) {
return new AsyncDrawableLoaderNoOp();
}
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}
return new AsyncDrawableLoaderImpl(this);
}
}
} }

View File

@ -1,5 +1,7 @@
package ru.noties.markwon.image; package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import java.io.InputStream; import java.io.InputStream;
@ -7,12 +9,48 @@ import java.io.InputStream;
/** /**
* @since 2.0.0 * @since 2.0.0
*/ */
public class ImageItem { 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 String contentType;
private final InputStream inputStream; private final InputStream inputStream;
public ImageItem( WithDecodingNeeded(
@Nullable String contentType, @Nullable String contentType,
@Nullable InputStream inputStream) { @Nullable InputStream inputStream) {
this.contentType = contentType; this.contentType = contentType;
@ -28,4 +66,5 @@ public class ImageItem {
public InputStream inputStream() { public InputStream inputStream() {
return inputStream; return inputStream;
} }
}
} }

View File

@ -17,6 +17,7 @@ import ru.noties.markwon.MarkwonPlugin;
* @see MarkwonPlugin#priority() * @see MarkwonPlugin#priority()
* @since 3.0.0 * @since 3.0.0
*/ */
@Deprecated
public abstract class Priority { public abstract class Priority {
@NonNull @NonNull

View File

@ -0,0 +1,27 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
}
dependencies {
api project(':markwon-core')
deps.with {
compileOnly it['android-gif']
compileOnly it['android-svg']
compileOnly it['okhttp']
}
}
registerArtifact(this)

View File

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

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.image" />

View File

@ -0,0 +1,110 @@
package ru.noties.markwon.image;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncDrawableLoaderBuilder {
ExecutorService executorService;
final Map<String, SchemeHandler> schemeHandlers = new HashMap<>(3);
final Map<String, MediaDecoder> mediaDecoders = new HashMap<>(3);
MediaDecoder defaultMediaDecoder;
DrawableProvider placeholderDrawableProvider;
DrawableProvider errorDrawableProvider;
@NonNull
public AsyncDrawableLoaderBuilder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder addSchemeHandler(@NonNull String scheme, @NonNull SchemeHandler schemeHandler) {
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);
}
return this;
}
@NonNull
public AsyncDrawableLoaderBuilder addMediaDecoder(@NonNull String contentType, @NonNull MediaDecoder mediaDecoder) {
mediaDecoders.put(contentType, mediaDecoder);
return this;
}
@NonNull
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;
return this;
}
/**
* @since 3.0.0
*/
@NonNull
public AsyncDrawableLoaderBuilder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) {
this.placeholderDrawableProvider = placeholderDrawableProvider;
return this;
}
/**
* @since 3.0.0
*/
@NonNull
public AsyncDrawableLoaderBuilder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) {
this.errorDrawableProvider = errorDrawableProvider;
return this;
}
@NonNull
public AsyncDrawableLoader build() {
// if we have no schemeHandlers -> we cannot show anything
// OR if we have no media decoders
if (schemeHandlers.size() == 0
|| (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) {
return new AsyncDrawableLoaderNoOp();
}
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}
return new AsyncDrawableLoaderImpl(this);
}
}

View File

@ -0,0 +1,290 @@
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

@ -0,0 +1,154 @@
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.
*
* @see #withDecodingNeeded(String, InputStream)
* @see WithResult
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static ImageItem withResult(@NonNull Drawable drawable) {
return new WithResult(drawable);
}
/**
* Create an {@link ImageItem} that requires further decoding of InputStream.
*
* @see #withResult(Drawable)
* @see WithDecodingNeeded
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static ImageItem withDecodingNeeded(
@Nullable String contentType,
@NonNull InputStream inputStream) {
return new WithDecodingNeeded(contentType, inputStream);
}
private ImageItem() {
}
/**
* @since 4.0.0-SNAPSHOT
*/
public abstract boolean hasResult();
/**
* @since 4.0.0-SNAPSHOT
*/
public abstract boolean hasDecodingNeeded();
/**
* @see #hasResult()
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public abstract WithResult getAsWithResult();
/**
* @see #hasDecodingNeeded()
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public abstract WithDecodingNeeded getAsWithDecodingNeeded();
/**
* @since 4.0.0-SNAPSHOT
*/
public static class WithResult extends ImageItem {
private final Drawable result;
private WithResult(@NonNull Drawable drawable) {
result = drawable;
}
@NonNull
public Drawable result() {
return result;
}
@Override
public boolean hasResult() {
return true;
}
@Override
public boolean hasDecodingNeeded() {
return false;
}
@NonNull
@Override
public WithResult getAsWithResult() {
return this;
}
@NonNull
@Override
public WithDecodingNeeded getAsWithDecodingNeeded() {
throw new IllegalStateException();
}
}
/**
* @since 4.0.0-SNAPSHOT
*/
public static class WithDecodingNeeded extends ImageItem {
private final String contentType;
private final InputStream inputStream;
private WithDecodingNeeded(
@Nullable String contentType,
@NonNull InputStream inputStream) {
this.contentType = contentType;
this.inputStream = inputStream;
}
@Nullable
public String contentType() {
return contentType;
}
@NonNull
public InputStream inputStream() {
return inputStream;
}
@Override
public boolean hasResult() {
return false;
}
@Override
public boolean hasDecodingNeeded() {
return true;
}
@NonNull
@Override
public WithResult getAsWithResult() {
throw new IllegalStateException();
}
@NonNull
@Override
public WithDecodingNeeded getAsWithDecodingNeeded() {
return this;
}
}
}

View File

@ -0,0 +1,135 @@
package ru.noties.markwon.image;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;
import org.commonmark.node.Image;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
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;
public class ImagesPlugin extends AbstractMarkwonPlugin {
/**
* @since 4.0.0-SNAPSHOT
*/
public interface DrawableProvider {
@Nullable
Drawable provide();
}
@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;
}
// we must expose scheme handling... so it's available during construction and via `require`
// @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

@ -0,0 +1,29 @@
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 {
/**
* Changes since 4.0.0-SNAPSHOT:
* <ul>
* <li>Returns `non-null` drawable</li>
* <li>Added `contentType` method parameter</li>
* <li>Added `throws Exception` to method signature</li>
* </ul>
*
* @throws Exception since 4.0.0-SNAPSHOT
*/
@NonNull
public abstract Drawable decode(
@Nullable String contentType,
@NonNull InputStream inputStream
) throws Exception;
}

View File

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

View File

@ -0,0 +1,60 @@
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

@ -0,0 +1,39 @@
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) throws Throwable;
@NonNull
public static DataUriDecoder create() {
return new Impl();
}
static class Impl extends DataUriDecoder {
private static final String CHARSET = "UTF-8";
@Nullable
@Override
public byte[] decode(@NonNull DataUri dataUri) throws Throwable {
final String data = dataUri.data();
if (!TextUtils.isEmpty(data)) {
if (dataUri.base64()) {
return Base64.decode(data.getBytes(CHARSET), Base64.DEFAULT);
} else {
return data.getBytes(CHARSET);
}
} else {
return null;
}
}
}
}

View File

@ -0,0 +1,79 @@
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

@ -0,0 +1,64 @@
package ru.noties.markwon.image.data;
import android.net.Uri;
import android.support.annotation.NonNull;
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;
}
@NonNull
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
if (!raw.startsWith(START)) {
throw new IllegalStateException("Invalid data-uri: " + raw);
}
final String part = raw.substring(START.length());
final DataUri dataUri = uriParser.parse(part);
if (dataUri == null) {
throw new IllegalStateException("Invalid data-uri: " + raw);
}
final byte[] bytes;
try {
bytes = uriDecoder.decode(dataUri);
} catch (Throwable t) {
throw new IllegalStateException("Cannot decode data-uri: " + raw, t);
}
if (bytes == null) {
throw new IllegalStateException("Decoding data-uri failed: " + raw);
}
return ImageItem.withDecodingNeeded(
dataUri.contentType(),
new ByteArrayInputStream(bytes));
}
}

View File

@ -0,0 +1,114 @@
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.text.TextUtils;
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";
/**
* @see ru.noties.markwon.urlprocessor.UrlProcessorAndroidAssets
*/
@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;
}
@NonNull
@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
throw new IllegalStateException("Invalid file path: " + raw);
}
final InputStream inputStream;
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) {
throw new IllegalStateException("Exception obtaining asset file: " +
"" + raw + ", path: " + path.toString(), e);
}
} else {
throw new IllegalStateException("Supplied file path points to assets, " +
"but FileSchemeHandler was not supplied with AssetsManager. " +
"Use `#createWithAssets` factory method to create FileSchemeHandler " +
"that can handle android assets");
}
} else {
final String path = uri.getPath();
if (TextUtils.isEmpty(path)) {
throw new IllegalStateException("Invalid file path: " + raw + ", " + path);
}
try {
inputStream = new BufferedInputStream(new FileInputStream(new File(path)));
} catch (FileNotFoundException e) {
throw new IllegalStateException("Exception reading file: " + raw, e);
}
}
final String contentType = MimeTypeMap
.getSingleton()
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName));
return ImageItem.withDecodingNeeded(contentType, inputStream);
}
}

View File

@ -0,0 +1,77 @@
package ru.noties.markwon.image.gif;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.MediaDecoder;
/**
* @since 1.1.0
*/
@SuppressWarnings("WeakerAccess")
public class GifMediaDecoder extends MediaDecoder {
public static final String CONTENT_TYPE = "image/gif";
@NonNull
public static GifMediaDecoder create(boolean autoPlayGif) {
return new GifMediaDecoder(autoPlayGif);
}
private final boolean autoPlayGif;
protected GifMediaDecoder(boolean autoPlayGif) {
this.autoPlayGif = autoPlayGif;
}
@NonNull
@Override
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
final byte[] bytes;
try {
bytes = readBytes(inputStream);
} catch (IOException e) {
throw new IllegalStateException("Cannot read GIF input-stream", e);
}
final GifDrawable drawable;
try {
drawable = newGifDrawable(bytes);
} catch (IOException e) {
throw new IllegalStateException("Exception creating GifDrawable", e);
}
DrawableUtils.applyIntrinsicBounds(drawable);
if (!autoPlayGif) {
drawable.pause();
}
return drawable;
}
@NonNull
protected GifDrawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
return new GifDrawable(bytes);
}
@NonNull
protected static byte[] readBytes(@NonNull InputStream stream) throws IOException {
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);
}
return outputStream.toByteArray();
}
}

View File

@ -0,0 +1,73 @@
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();
}
@NonNull
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final ImageItem imageItem;
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());
imageItem = ImageItem.withDecodingNeeded(contentType, inputStream);
} else {
throw new IOException("Bad response code: " + responseCode + ", url: " + raw);
}
} catch (IOException e) {
throw new IllegalStateException("Exception obtaining network resource: " + raw, e);
}
return imageItem;
}
@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

@ -0,0 +1,75 @@
package ru.noties.markwon.image.network;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.SchemeHandler;
/**
* @since 4.0.0-SNAPSHOT
*/
class OkHttpNetworkSchemeHandler extends SchemeHandler {
/**
* @see #create(OkHttpClient)
*/
@NonNull
public static OkHttpNetworkSchemeHandler create() {
return new OkHttpNetworkSchemeHandler(new OkHttpClient());
}
@NonNull
public static OkHttpNetworkSchemeHandler create(@NonNull OkHttpClient client) {
return new OkHttpNetworkSchemeHandler(client);
}
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private final OkHttpClient client;
OkHttpNetworkSchemeHandler(@NonNull OkHttpClient client) {
this.client = client;
}
@NonNull
@Override
public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
final Request request = new Request.Builder()
.url(raw)
.tag(raw)
.build();
final Response response;
try {
response = client.newCall(request).execute();
} catch (IOException e) {
throw new IllegalStateException("Exception obtaining network resource: " + raw, e);
}
if (response == null) {
throw new IllegalStateException("Could not obtain network response: " + raw);
}
final ResponseBody body = response.body();
final InputStream inputStream = body != null
? body.byteStream()
: null;
if (inputStream == null) {
throw new IllegalStateException("Response does not contain body: " + raw);
}
final String contentType = response.header(HEADER_CONTENT_TYPE);
return ImageItem.withDecodingNeeded(contentType, inputStream);
}
}

View File

@ -0,0 +1,74 @@
package ru.noties.markwon.image.svg;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream;
import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.MediaDecoder;
/**
* @since 1.1.0
*/
public class SvgMediaDecoder extends MediaDecoder {
public static final String CONTENT_TYPE = "image/svg+xml";
/**
* @see #create(Resources)
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static SvgMediaDecoder create() {
return new SvgMediaDecoder(Resources.getSystem());
}
@NonNull
public static SvgMediaDecoder create(@NonNull Resources resources) {
return new SvgMediaDecoder(resources);
}
private final Resources resources;
@SuppressWarnings("WeakerAccess")
SvgMediaDecoder(Resources resources) {
this.resources = resources;
}
@NonNull
@Override
public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) {
final SVG svg;
try {
svg = SVG.getFromInputStream(inputStream);
} catch (SVGParseException e) {
throw new IllegalStateException("Exception decoding SVG", e);
}
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);
final Drawable drawable = new BitmapDrawable(resources, bitmap);
DrawableUtils.applyIntrinsicBounds(drawable);
return drawable;
}
}

View File

@ -6,6 +6,7 @@ include ':app', ':sample',
':markwon-ext-tables', ':markwon-ext-tables',
':markwon-ext-tasklist', ':markwon-ext-tasklist',
':markwon-html', ':markwon-html',
':markwon-image',
':markwon-image-gif', ':markwon-image-gif',
':markwon-image-okhttp', ':markwon-image-okhttp',
':markwon-image-svg', ':markwon-image-svg',