Moving image loading functionality to standalone module
This commit is contained in:
		
							parent
							
								
									453880bd62
								
							
						
					
					
						commit
						661f72da0f
					
				| @ -22,6 +22,11 @@ import ru.noties.markwon.priority.Priority; | ||||
|  */ | ||||
| public abstract class AbstractMarkwonPlugin implements MarkwonPlugin { | ||||
| 
 | ||||
|     @Override | ||||
|     public void configure(@NonNull Registry registry) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     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 | ||||
|     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 | ||||
|     @Override | ||||
|     public String processMarkdown(@NonNull String markdown) { | ||||
|  | ||||
| @ -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 #builderNoCore(Context) | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -2,6 +2,7 @@ package ru.noties.markwon; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.VisibleForTesting; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| @ -9,19 +10,17 @@ import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashSet; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.html.MarkwonHtmlRenderer; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.priority.PriorityProcessor; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|     private final Context context; | ||||
| @ -30,8 +29,6 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|     private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; | ||||
| 
 | ||||
|     private PriorityProcessor priorityProcessor; | ||||
| 
 | ||||
|     MarkwonBuilderImpl(@NonNull Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| @ -69,13 +66,6 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("UnusedReturnValue") | ||||
|     @NonNull | ||||
|     public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { | ||||
|         this.priorityProcessor = priorityProcessor; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Markwon build() { | ||||
| @ -85,21 +75,12 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
|                     "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 | ||||
|         // 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 MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); | ||||
|         final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); | ||||
|         final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(); | ||||
|         final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl(); | ||||
|         final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); | ||||
| @ -108,7 +89,6 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             plugin.configureParser(parserBuilder); | ||||
|             plugin.configureTheme(themeBuilder); | ||||
|             plugin.configureImages(asyncDrawableLoaderBuilder); | ||||
|             plugin.configureConfiguration(configurationBuilder); | ||||
|             plugin.configureVisitor(visitorBuilder); | ||||
|             plugin.configureSpansFactory(spanFactoryBuilder); | ||||
| @ -117,7 +97,6 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|         final MarkwonConfiguration configuration = configurationBuilder.build( | ||||
|                 themeBuilder.build(), | ||||
|                 asyncDrawableLoaderBuilder.build(), | ||||
|                 htmlRendererBuilder.build(), | ||||
|                 spanFactoryBuilder.build()); | ||||
| 
 | ||||
| @ -133,62 +112,107 @@ class MarkwonBuilderImpl implements Markwon.Builder { | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     @NonNull | ||||
|     static List<MarkwonPlugin> preparePlugins( | ||||
|             @NonNull PriorityProcessor priorityProcessor, | ||||
|             @NonNull List<MarkwonPlugin> plugins) { | ||||
| 
 | ||||
|         // with this method we will ensure that CorePlugin is added IF and ONLY IF | ||||
|         // there are plugins that depend on it. If CorePlugin is added, or there are | ||||
|         // no plugins that require it, CorePlugin won't be added | ||||
|         final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins); | ||||
| 
 | ||||
|         return priorityProcessor.process(out); | ||||
|     static List<MarkwonPlugin> preparePlugins(@NonNull List<MarkwonPlugin> plugins) { | ||||
|         return new RegistryImpl(plugins).process(); | ||||
|     } | ||||
| 
 | ||||
|     // this method will _implicitly_ add CorePlugin if there is at least one plugin | ||||
|     // that depends on CorePlugin | ||||
|     @VisibleForTesting | ||||
|     @NonNull | ||||
|     static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) { | ||||
|         // loop over plugins -> if CorePlugin is found -> break; | ||||
|         // iterate over all plugins and check if CorePlugin is requested | ||||
|     // @since 4.0.0-SNAPSHOT | ||||
|     private static class RegistryImpl implements MarkwonPlugin.Registry { | ||||
| 
 | ||||
|         boolean hasCore = false; | ||||
|         boolean hasCoreDependents = false; | ||||
|         private final List<MarkwonPlugin> origin; | ||||
|         private final List<MarkwonPlugin> plugins; | ||||
|         private final Set<MarkwonPlugin> pending; | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|         RegistryImpl(@NonNull List<MarkwonPlugin> origin) { | ||||
|             this.origin = origin; | ||||
|             this.plugins = new ArrayList<>(origin.size()); | ||||
|             this.pending = new HashSet<>(3); | ||||
|         } | ||||
| 
 | ||||
|             // 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; | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public <P extends MarkwonPlugin> P require(@NonNull Class<P> plugin) { | ||||
|             return get(plugin); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public <P extends MarkwonPlugin> void require( | ||||
|                 @NonNull Class<P> plugin, | ||||
|                 @NonNull MarkwonPlugin.Action<? super P> action) { | ||||
|             action.apply(get(plugin)); | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         List<MarkwonPlugin> process() { | ||||
|             for (MarkwonPlugin plugin : origin) { | ||||
|                 configure(plugin); | ||||
|             } | ||||
|             return plugins; | ||||
|         } | ||||
| 
 | ||||
|             // if plugin has CorePlugin in dependencies -> mark for addition | ||||
|             if (!hasCoreDependents) { | ||||
|                 // here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass | ||||
|                 // or something -> ignore) | ||||
|                 if (plugin.priority().after().contains(CorePlugin.class)) { | ||||
|                     hasCoreDependents = true; | ||||
|         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); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // important thing here is to check if corePlugin is added | ||||
|         // add it _only_ if it's not present | ||||
|         if (hasCoreDependents && !hasCore) { | ||||
|             final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1); | ||||
|             // add default instance of CorePlugin | ||||
|             out.add(CorePlugin.create()); | ||||
|             out.addAll(plugins); | ||||
|             return out; | ||||
|         @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; | ||||
|         } | ||||
| 
 | ||||
|         return plugins; | ||||
|         @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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -113,6 +113,15 @@ public class MarkwonConfiguration { | ||||
|         Builder() { | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * @since 4.0.0-SNAPSHOT | ||||
|          */ | ||||
|         @NonNull | ||||
|         public Builder asyncDrawableLoader(@NonNull AsyncDrawableLoader asyncDrawableLoader) { | ||||
|             this.asyncDrawableLoader = asyncDrawableLoader; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) { | ||||
|             this.syntaxHighlight = syntaxHighlight; | ||||
| @ -149,15 +158,18 @@ public class MarkwonConfiguration { | ||||
|         @NonNull | ||||
|         public MarkwonConfiguration build( | ||||
|                 @NonNull MarkwonTheme theme, | ||||
|                 @NonNull AsyncDrawableLoader asyncDrawableLoader, | ||||
|                 @NonNull MarkwonHtmlRenderer htmlRenderer, | ||||
|                 @NonNull MarkwonSpansFactory spansFactory) { | ||||
| 
 | ||||
|             this.theme = theme; | ||||
|             this.asyncDrawableLoader = asyncDrawableLoader; | ||||
|             this.htmlRenderer = htmlRenderer; | ||||
|             this.spansFactory = spansFactory; | ||||
| 
 | ||||
|             // @since 4.0.0-SNAPSHOT | ||||
|             if (asyncDrawableLoader == null) { | ||||
|                 asyncDrawableLoader = AsyncDrawableLoader.noOp(); | ||||
|             } | ||||
| 
 | ||||
|             if (syntaxHighlight == null) { | ||||
|                 syntaxHighlight = new SyntaxHighlightNoOp(); | ||||
|             } | ||||
|  | ||||
| @ -9,10 +9,6 @@ import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| 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 | ||||
| @ -25,6 +21,35 @@ import ru.noties.markwon.priority.Priority; | ||||
|  */ | ||||
| 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 | ||||
|      * extension, etc). | ||||
| @ -39,17 +64,6 @@ public interface MarkwonPlugin { | ||||
|      */ | ||||
|     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} | ||||
|      * | ||||
| @ -82,9 +96,6 @@ public interface MarkwonPlugin { | ||||
|      */ | ||||
|     void configureHtmlRenderer(@NonNull MarkwonHtmlRenderer.Builder builder); | ||||
| 
 | ||||
|     @NonNull | ||||
|     Priority priority(); | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|  | ||||
| @ -95,12 +95,6 @@ public class CorePlugin extends AbstractMarkwonPlugin { | ||||
|                 .setFactory(ThematicBreak.class, new ThematicBreakSpanFactory()); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Priority priority() { | ||||
|         return Priority.none(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
|         OrderedListItemSpan.measure(textView, markdown); | ||||
|  | ||||
| @ -13,22 +13,6 @@ import java.util.concurrent.Executors; | ||||
| 
 | ||||
| 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 | ||||
|      */ | ||||
| @ -71,123 +55,4 @@ public abstract class AsyncDrawableLoader { | ||||
| 
 | ||||
|     @Nullable | ||||
|     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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| @ -7,25 +9,62 @@ import java.io.InputStream; | ||||
| /** | ||||
|  * @since 2.0.0 | ||||
|  */ | ||||
| public class ImageItem { | ||||
| public abstract class ImageItem { | ||||
| 
 | ||||
|     private final String contentType; | ||||
|     private final InputStream inputStream; | ||||
|     /** | ||||
|      * 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); | ||||
|     } | ||||
| 
 | ||||
|     public ImageItem( | ||||
|     @NonNull | ||||
|     public static ImageItem withDecodingNeeded( | ||||
|             @Nullable String contentType, | ||||
|             @Nullable InputStream inputStream) { | ||||
|         this.contentType = contentType; | ||||
|         this.inputStream = inputStream; | ||||
|         return new WithDecodingNeeded(contentType, inputStream); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String contentType() { | ||||
|         return contentType; | ||||
|     private ImageItem() { | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public InputStream inputStream() { | ||||
|         return inputStream; | ||||
|     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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -17,6 +17,7 @@ import ru.noties.markwon.MarkwonPlugin; | ||||
|  * @see MarkwonPlugin#priority() | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| @Deprecated | ||||
| public abstract class Priority { | ||||
| 
 | ||||
|     @NonNull | ||||
|  | ||||
							
								
								
									
										27
									
								
								markwon-image/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								markwon-image/build.gradle
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										4
									
								
								markwon-image/gradle.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								markwon-image/gradle.properties
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										1
									
								
								markwon-image/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								markwon-image/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <manifest package="ru.noties.markwon.image" /> | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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)); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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(); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
| @ -6,6 +6,7 @@ include ':app', ':sample', | ||||
|         ':markwon-ext-tables', | ||||
|         ':markwon-ext-tasklist', | ||||
|         ':markwon-html', | ||||
|         ':markwon-image', | ||||
|         ':markwon-image-gif', | ||||
|         ':markwon-image-okhttp', | ||||
|         ':markwon-image-svg', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov