Moved image loading into separate module
This commit is contained in:
		
							parent
							
								
									661f72da0f
								
							
						
					
					
						commit
						5bf21bc940
					
				
							
								
								
									
										2
									
								
								_CHANGES.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								_CHANGES.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| * `Markwon.builder` won't require CorePlugin registration (it is done automatically) | ||||
|   to create a builder without CorePlugin - use `Markwon#builderNoCore` | ||||
| @ -33,8 +33,7 @@ dependencies { | ||||
|     implementation project(':markwon-ext-tables') | ||||
|     implementation project(':markwon-ext-tasklist') | ||||
|     implementation project(':markwon-html') | ||||
|     implementation project(':markwon-image-gif') | ||||
|     implementation project(':markwon-image-svg') | ||||
|     implementation project(':markwon-image') | ||||
|     implementation project(':markwon-syntax-highlight') | ||||
| 
 | ||||
|     deps.with { | ||||
| @ -42,6 +41,8 @@ dependencies { | ||||
|         implementation it['prism4j'] | ||||
|         implementation it['debug'] | ||||
|         implementation it['dagger'] | ||||
|         implementation it['android-svg'] | ||||
|         implementation it['android-gif'] | ||||
|     } | ||||
| 
 | ||||
|     deps['annotationProcessor'].with { | ||||
|  | ||||
| @ -14,15 +14,18 @@ import java.util.concurrent.Future; | ||||
| import javax.inject.Inject; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.ext.strikethrough.StrikethroughPlugin; | ||||
| import ru.noties.markwon.ext.tables.TablePlugin; | ||||
| import ru.noties.markwon.ext.tasklist.TaskListPlugin; | ||||
| import ru.noties.markwon.gif.GifAwarePlugin; | ||||
| import ru.noties.markwon.html.HtmlPlugin; | ||||
| import ru.noties.markwon.image.DefaultImageMediaDecoder; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.gif.GifPlugin; | ||||
| import ru.noties.markwon.image.svg.SvgPlugin; | ||||
| import ru.noties.markwon.image.data.DataUriSchemeHandler; | ||||
| import ru.noties.markwon.image.file.FileSchemeHandler; | ||||
| import ru.noties.markwon.image.gif.GifMediaDecoder; | ||||
| import ru.noties.markwon.image.network.OkHttpNetworkSchemeHandler; | ||||
| import ru.noties.markwon.image.svg.SvgMediaDecoder; | ||||
| import ru.noties.markwon.syntax.Prism4jTheme; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDarkula; | ||||
| import ru.noties.markwon.syntax.Prism4jThemeDefault; | ||||
| @ -94,10 +97,18 @@ public class MarkdownRenderer { | ||||
|                         : prism4JThemeDarkula; | ||||
| 
 | ||||
|                 final Markwon markwon = Markwon.builder(context) | ||||
|                         .usePlugin(CorePlugin.create()) | ||||
|                         .usePlugin(ImagesPlugin.createWithAssets(context)) | ||||
|                         .usePlugin(SvgPlugin.create(context.getResources())) | ||||
|                         .usePlugin(GifPlugin.create(false)) | ||||
|                         .usePlugin(ImagesPlugin.create(new ImagesPlugin.ImagesConfigure() { | ||||
|                             @Override | ||||
|                             public void configureImages(@NonNull ImagesPlugin plugin) { | ||||
|                                 plugin | ||||
|                                         .addSchemeHandler(DataUriSchemeHandler.create()) | ||||
|                                         .addSchemeHandler(OkHttpNetworkSchemeHandler.create()) | ||||
|                                         .addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets())) | ||||
|                                         .addMediaDecoder(GifMediaDecoder.create(false)) | ||||
|                                         .addMediaDecoder(SvgMediaDecoder.create()) | ||||
|                                         .defaultMediaDecoder(DefaultImageMediaDecoder.create()); | ||||
|                             } | ||||
|                         })) | ||||
|                         .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)) | ||||
|                         .usePlugin(GifAwarePlugin.create(context)) | ||||
|                         .usePlugin(TablePlugin.create(context)) | ||||
|  | ||||
| @ -14,8 +14,6 @@ import ru.noties.markwon.RenderProps; | ||||
| import ru.noties.markwon.SpanFactory; | ||||
| import ru.noties.markwon.image.AsyncDrawableSpan; | ||||
| import ru.noties.markwon.image.ImageProps; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| public class GifAwarePlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
| @ -59,12 +57,6 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Priority priority() { | ||||
|         return Priority.after(ImagesPlugin.class); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterSetText(@NonNull TextView textView) { | ||||
|         processor.process(textView); | ||||
|  | ||||
| @ -7,11 +7,8 @@ import android.widget.TextView; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| 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.Priority; | ||||
| 
 | ||||
| /** | ||||
|  * Class that extends {@link MarkwonPlugin} with all methods implemented (empty body) | ||||
|  | ||||
| @ -40,7 +40,6 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory; | ||||
| import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory; | ||||
| import ru.noties.markwon.core.factory.ThematicBreakSpanFactory; | ||||
| import ru.noties.markwon.core.spans.OrderedListItemSpan; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| /** | ||||
|  * @see CoreProps | ||||
|  | ||||
| @ -40,7 +40,7 @@ public class AsyncDrawable extends Drawable { | ||||
|         this.imageSizeResolver = imageSizeResolver; | ||||
|         this.imageSize = imageSize; | ||||
| 
 | ||||
|         final Drawable placeholder = loader.placeholder(); | ||||
|         final Drawable placeholder = loader.placeholder(this); | ||||
|         if (placeholder != null) { | ||||
|             setPlaceholderResult(placeholder); | ||||
|         } | ||||
|  | ||||
| @ -3,13 +3,6 @@ package ru.noties.markwon.image; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| public abstract class AsyncDrawableLoader { | ||||
| 
 | ||||
| @ -31,28 +24,9 @@ public abstract class AsyncDrawableLoader { | ||||
|      */ | ||||
|     public abstract void cancel(@NonNull AsyncDrawable drawable); | ||||
| 
 | ||||
|     /** | ||||
|      * @see #load(AsyncDrawable) | ||||
|      * @deprecated 3.1.0-SNAPSHOT | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { | ||||
|         load(drawable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Method is deprecated because cancellation won\'t work for markdown input | ||||
|      * with multiple images with the same source | ||||
|      * | ||||
|      * @deprecated 3.1.0-SNAPSHOT | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void cancel(@NonNull String destination) { | ||||
|         Log.e("MARKWON-IL", "Image loading cancellation must be triggered " + | ||||
|                 "by AsyncDrawable, please use #cancel(AsyncDrawable) method instead. " + | ||||
|                 "No op, nothing is cancelled for destination: " + destination); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract Drawable placeholder(); | ||||
|     public abstract Drawable placeholder(@NonNull AsyncDrawable drawable); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,290 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.net.Uri; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Future; | ||||
| 
 | ||||
| class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
| 
 | ||||
|     private final ExecutorService executorService; | ||||
|     private final Map<String, SchemeHandler> schemeHandlers; | ||||
|     private final Map<String, MediaDecoder> mediaDecoders; | ||||
|     private final MediaDecoder defaultMediaDecoder; | ||||
|     private final DrawableProvider placeholderDrawableProvider; | ||||
|     private final DrawableProvider errorDrawableProvider; | ||||
| 
 | ||||
|     private final Handler mainThread; | ||||
| 
 | ||||
|     // @since 3.1.0-SNAPSHOT use a hash-map with a weak AsyncDrawable as key for multiple requests | ||||
|     //  for the same destination | ||||
|     private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2); | ||||
| 
 | ||||
|     AsyncDrawableLoaderImpl(@NonNull Builder builder) { | ||||
|         this.executorService = builder.executorService; | ||||
|         this.schemeHandlers = builder.schemeHandlers; | ||||
|         this.mediaDecoders = builder.mediaDecoders; | ||||
|         this.defaultMediaDecoder = builder.defaultMediaDecoder; | ||||
|         this.placeholderDrawableProvider = builder.placeholderDrawableProvider; | ||||
|         this.errorDrawableProvider = builder.errorDrawableProvider; | ||||
|         this.mainThread = new Handler(Looper.getMainLooper()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void load(@NonNull final AsyncDrawable drawable) { | ||||
| 
 | ||||
|         // primitive synchronization via main-thread | ||||
|         if (!isMainThread()) { | ||||
|             mainThread.post(new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     load(drawable); | ||||
|                 } | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // okay, if by some chance requested drawable already has a future associated -> no-op | ||||
|         // as AsyncDrawable cannot change `destination` (immutable field) | ||||
|         // @since 3.1.0-SNAPSHOT | ||||
|         if (hasTaskAssociated(drawable)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final WeakReference<AsyncDrawable> reference = new WeakReference<>(drawable); | ||||
|         requests.put(reference, execute(drawable.getDestination(), reference)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void cancel(@NonNull final AsyncDrawable drawable) { | ||||
| 
 | ||||
|         if (!isMainThread()) { | ||||
|             mainThread.post(new Runnable() { | ||||
|                 @Override | ||||
|                 public void run() { | ||||
|                     cancel(drawable); | ||||
|                 } | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator = | ||||
|                 requests.entrySet().iterator(); | ||||
| 
 | ||||
|         AsyncDrawable key; | ||||
|         Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry; | ||||
| 
 | ||||
|         while (iterator.hasNext()) { | ||||
| 
 | ||||
|             entry = iterator.next(); | ||||
|             key = entry.getKey().get(); | ||||
| 
 | ||||
|             // if key is null or it contains requested AsyncDrawable -> cancel | ||||
|             if (shouldCleanUp(key) || key == drawable) { | ||||
|                 entry.getValue().cancel(true); | ||||
|                 iterator.remove(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean hasTaskAssociated(@NonNull AsyncDrawable drawable) { | ||||
| 
 | ||||
|         final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator = | ||||
|                 requests.entrySet().iterator(); | ||||
| 
 | ||||
|         boolean result = false; | ||||
| 
 | ||||
|         AsyncDrawable key; | ||||
|         Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry; | ||||
| 
 | ||||
|         while (iterator.hasNext()) { | ||||
| 
 | ||||
|             entry = iterator.next(); | ||||
|             key = entry.getKey().get(); | ||||
| 
 | ||||
|             // clean-up | ||||
|             if (shouldCleanUp(key)) { | ||||
|                 entry.getValue().cancel(true); | ||||
|                 iterator.remove(); | ||||
|             } else if (key == drawable) { | ||||
|                 result = true; | ||||
|                 // do not break, let iteration continue to possibly clean-up the rest references | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     private void cleanUp() { | ||||
| 
 | ||||
|         final Iterator<Map.Entry<WeakReference<AsyncDrawable>, Future<?>>> iterator = | ||||
|                 requests.entrySet().iterator(); | ||||
| 
 | ||||
|         AsyncDrawable key; | ||||
|         Map.Entry<WeakReference<AsyncDrawable>, Future<?>> entry; | ||||
| 
 | ||||
|         while (iterator.hasNext()) { | ||||
| 
 | ||||
|             entry = iterator.next(); | ||||
|             key = entry.getKey().get(); | ||||
| 
 | ||||
|             // clean-up of already referenced or detached drawables | ||||
|             if (shouldCleanUp(key)) { | ||||
|                 entry.getValue().cancel(true); | ||||
|                 iterator.remove(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| //    @Override | ||||
| //    public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { | ||||
| // | ||||
| //        // todo: we cannot reliably identify request by the destination, as if | ||||
| //        //  markdown input has multiple images with the same destination as source | ||||
| //        //  we will be tracking only one of them (the one appears the last). We should | ||||
| //        //  move to AsyncDrawable based identification. This method also _maybe_ | ||||
| //        //  should include the ImageSize (comment @since 3.1.0-SNAPSHOT) | ||||
| // | ||||
| //        requests.put(destination, execute(destination, drawable)); | ||||
| //    } | ||||
| // | ||||
| //    @Override | ||||
| //    public void cancel(@NonNull String destination) { | ||||
| // | ||||
| //        // todo: as we are moving away from a single request for a destination, | ||||
| //        //  we should re-evaluate this cancellation logic, as if there are multiple images | ||||
| //        //  in markdown input all of them will be cancelled (won't delivered), even if | ||||
| //        //  only a single drawable is detached. Cancellation must also take | ||||
| //        //  the AsyncDrawable argument (comment @since 3.1.0-SNAPSHOT) | ||||
| // | ||||
| //        // | ||||
| //        final Future<?> request = requests.remove(destination); | ||||
| //        if (request != null) { | ||||
| //            request.cancel(true); | ||||
| //        } | ||||
| //    } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Drawable placeholder() { | ||||
|         return placeholderDrawableProvider != null | ||||
|                 ? placeholderDrawableProvider.provide() | ||||
|                 : null; | ||||
|     } | ||||
| 
 | ||||
|     private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) { | ||||
| 
 | ||||
|         // todo: error handing (simply applying errorDrawable is not a good solution | ||||
|         //      as reason for an error is unclear (no scheme handler, no input data, error decoding, etc) | ||||
| 
 | ||||
|         // todo: more efficient ImageMediaDecoder... BitmapFactory.decodeStream is a bit not optimal | ||||
|         //      for big images for sure. We _could_ introduce internal Drawable that will check for | ||||
|         //      image bounds (but we will need to cache inputStream in order to inspect and optimize | ||||
|         //      input image...) | ||||
| 
 | ||||
|         return executorService.submit(new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
| 
 | ||||
|                 final ImageItem item; | ||||
| 
 | ||||
|                 final Uri uri = Uri.parse(destination); | ||||
| 
 | ||||
|                 final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); | ||||
| 
 | ||||
|                 if (schemeHandler != null) { | ||||
|                     item = schemeHandler.handle(destination, uri); | ||||
|                 } else { | ||||
|                     item = null; | ||||
|                 } | ||||
| 
 | ||||
|                 final InputStream inputStream = item != null | ||||
|                         ? item.inputStream() | ||||
|                         : null; | ||||
| 
 | ||||
|                 Drawable result = null; | ||||
| 
 | ||||
|                 if (inputStream != null) { | ||||
|                     try { | ||||
| 
 | ||||
|                         MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType()); | ||||
|                         if (mediaDecoder == null) { | ||||
|                             mediaDecoder = defaultMediaDecoder; | ||||
|                         } | ||||
| 
 | ||||
|                         if (mediaDecoder != null) { | ||||
|                             result = mediaDecoder.decode(inputStream); | ||||
|                         } | ||||
| 
 | ||||
|                     } finally { | ||||
|                         try { | ||||
|                             inputStream.close(); | ||||
|                         } catch (IOException e) { | ||||
|                             // ignored | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // if result is null, we assume it's an error | ||||
|                 if (result == null) { | ||||
|                     result = errorDrawableProvider != null | ||||
|                             ? errorDrawableProvider.provide() | ||||
|                             : null; | ||||
|                 } | ||||
| 
 | ||||
|                 final Drawable out = result; | ||||
| 
 | ||||
|                 mainThread.post(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
| 
 | ||||
|                         if (out != null) { | ||||
| 
 | ||||
|                             // this doesn't work with markdown input with multiple images with the | ||||
|                             // same source (comment @since 3.1.0-SNAPSHOT) | ||||
| //                            final boolean canDeliver = requests.remove(destination) != null; | ||||
| //                            if (canDeliver) { | ||||
| //                                final AsyncDrawable asyncDrawable = reference.get(); | ||||
| //                                if (asyncDrawable != null && asyncDrawable.isAttached()) { | ||||
| //                                    asyncDrawable.setResult(out); | ||||
| //                                } | ||||
| //                            } | ||||
| 
 | ||||
|                             // todo: AsyncDrawable cannot change destination, so if it's | ||||
|                             //  attached and not garbage-collected, we can deliver the result. | ||||
|                             //  Note that there is no cache, so attach/detach of drawables | ||||
|                             //  will always request a new entry.. (comment @since 3.1.0-SNAPSHOT) | ||||
|                             final AsyncDrawable asyncDrawable = reference.get(); | ||||
|                             if (asyncDrawable != null && asyncDrawable.isAttached()) { | ||||
|                                 asyncDrawable.setResult(out); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         requests.remove(reference); | ||||
|                         cleanUp(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean shouldCleanUp(@Nullable AsyncDrawable drawable) { | ||||
|         return drawable == null || !drawable.isAttached(); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||||
|     private static boolean isMainThread() { | ||||
|         return Looper.myLooper() == Looper.getMainLooper(); | ||||
|     } | ||||
| } | ||||
| @ -4,7 +4,7 @@ import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { | ||||
| public class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { | ||||
|     @Override | ||||
|     public void load(@NonNull AsyncDrawable drawable) { | ||||
| 
 | ||||
| @ -17,7 +17,7 @@ class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader { | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Drawable placeholder() { | ||||
|     public Drawable placeholder(@NonNull AsyncDrawable drawable) { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,70 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| /** | ||||
|  * @since 2.0.0 | ||||
|  */ | ||||
| public abstract class ImageItem { | ||||
| 
 | ||||
|     /** | ||||
|      * Create an {@link ImageItem} with result, so no further decoding is required. | ||||
|      * | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static ImageItem withResult(@Nullable Drawable drawable) { | ||||
|         return new WithResult(drawable); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static ImageItem withDecodingNeeded( | ||||
|             @Nullable String contentType, | ||||
|             @Nullable InputStream inputStream) { | ||||
|         return new WithDecodingNeeded(contentType, inputStream); | ||||
|     } | ||||
| 
 | ||||
|     private ImageItem() { | ||||
|     } | ||||
| 
 | ||||
|     public static class WithResult extends ImageItem { | ||||
| 
 | ||||
|         private final Drawable result; | ||||
| 
 | ||||
|         WithResult(@Nullable Drawable drawable) { | ||||
|             result = drawable; | ||||
|         } | ||||
| 
 | ||||
|         @Nullable | ||||
|         public Drawable result() { | ||||
|             return result; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static class WithDecodingNeeded extends ImageItem { | ||||
| 
 | ||||
|         private final String contentType; | ||||
|         private final InputStream inputStream; | ||||
| 
 | ||||
|         WithDecodingNeeded( | ||||
|                 @Nullable String contentType, | ||||
|                 @Nullable InputStream inputStream) { | ||||
|             this.contentType = contentType; | ||||
|             this.inputStream = inputStream; | ||||
|         } | ||||
| 
 | ||||
|         @Nullable | ||||
|         public String contentType() { | ||||
|             return contentType; | ||||
|         } | ||||
| 
 | ||||
|         @Nullable | ||||
|         public InputStream inputStream() { | ||||
|             return inputStream; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,50 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.graphics.drawable.BitmapDrawable; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| /** | ||||
|  * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. | ||||
|  * Here we just assume that supplied InputStream is of image type and try to decode it. | ||||
|  * | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| public class ImageMediaDecoder extends MediaDecoder { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static ImageMediaDecoder create(@NonNull Resources resources) { | ||||
|         return new ImageMediaDecoder(resources); | ||||
|     } | ||||
| 
 | ||||
|     private final Resources resources; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     ImageMediaDecoder(Resources resources) { | ||||
|         this.resources = resources; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Drawable decode(@NonNull InputStream inputStream) { | ||||
| 
 | ||||
|         final Drawable out; | ||||
| 
 | ||||
|         // absolutely not optimal... thing | ||||
|         final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); | ||||
|         if (bitmap != null) { | ||||
|             out = new BitmapDrawable(resources, bitmap); | ||||
|             DrawableUtils.applyIntrinsicBounds(out); | ||||
|         } else { | ||||
|             out = null; | ||||
|         } | ||||
| 
 | ||||
|         return out; | ||||
|     } | ||||
| } | ||||
| @ -1,130 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Spanned; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Image; | ||||
| import org.commonmark.node.Link; | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonConfiguration; | ||||
| import ru.noties.markwon.MarkwonSpansFactory; | ||||
| import ru.noties.markwon.MarkwonVisitor; | ||||
| import ru.noties.markwon.RenderProps; | ||||
| import ru.noties.markwon.SpanFactory; | ||||
| import ru.noties.markwon.image.data.DataUriSchemeHandler; | ||||
| import ru.noties.markwon.image.file.FileSchemeHandler; | ||||
| import ru.noties.markwon.image.network.NetworkSchemeHandler; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public class ImagesPlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static ImagesPlugin create(@NonNull Context context) { | ||||
|         return new ImagesPlugin(context, false); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Special scheme that is used {@code file:///android_asset/} | ||||
|      * | ||||
|      * @param context | ||||
|      * @return | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static ImagesPlugin createWithAssets(@NonNull Context context) { | ||||
|         return new ImagesPlugin(context, true); | ||||
|     } | ||||
| 
 | ||||
|     private final Context context; | ||||
|     private final boolean useAssets; | ||||
| 
 | ||||
|     protected ImagesPlugin(Context context, boolean useAssets) { | ||||
|         this.context = context; | ||||
|         this.useAssets = useAssets; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
| 
 | ||||
|         final FileSchemeHandler fileSchemeHandler = useAssets | ||||
|                 ? FileSchemeHandler.createWithAssets(context.getAssets()) | ||||
|                 : FileSchemeHandler.create(); | ||||
| 
 | ||||
|         builder | ||||
|                 .addSchemeHandler(DataUriSchemeHandler.SCHEME, DataUriSchemeHandler.create()) | ||||
|                 .addSchemeHandler(FileSchemeHandler.SCHEME, fileSchemeHandler) | ||||
|                 .addSchemeHandler( | ||||
|                         Arrays.asList( | ||||
|                                 NetworkSchemeHandler.SCHEME_HTTP, | ||||
|                                 NetworkSchemeHandler.SCHEME_HTTPS), | ||||
|                         NetworkSchemeHandler.create()) | ||||
|                 .defaultMediaDecoder(ImageMediaDecoder.create(context.getResources())); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
|         builder.setFactory(Image.class, new ImageSpanFactory()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { | ||||
|         builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() { | ||||
|             @Override | ||||
|             public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) { | ||||
| 
 | ||||
|                 // if there is no image spanFactory, ignore | ||||
|                 final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class); | ||||
|                 if (spanFactory == null) { | ||||
|                     visitor.visitChildren(image); | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 final int length = visitor.length(); | ||||
| 
 | ||||
|                 visitor.visitChildren(image); | ||||
| 
 | ||||
|                 // we must check if anything _was_ added, as we need at least one char to render | ||||
|                 if (length == visitor.length()) { | ||||
|                     visitor.builder().append('\uFFFC'); | ||||
|                 } | ||||
| 
 | ||||
|                 final MarkwonConfiguration configuration = visitor.configuration(); | ||||
| 
 | ||||
|                 final Node parent = image.getParent(); | ||||
|                 final boolean link = parent instanceof Link; | ||||
| 
 | ||||
|                 final String destination = configuration | ||||
|                         .urlProcessor() | ||||
|                         .process(image.getDestination()); | ||||
| 
 | ||||
|                 final RenderProps props = visitor.renderProps(); | ||||
| 
 | ||||
|                 // apply image properties | ||||
|                 // Please note that we explicitly set IMAGE_SIZE to null as we do not clear | ||||
|                 // properties after we applied span (we could though) | ||||
|                 ImageProps.DESTINATION.set(props, destination); | ||||
|                 ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link); | ||||
|                 ImageProps.IMAGE_SIZE.set(props, null); | ||||
| 
 | ||||
|                 visitor.setSpans(length, spanFactory.getSpans(configuration, props)); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { | ||||
|         AsyncDrawableScheduler.unschedule(textView); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void afterSetText(@NonNull TextView textView) { | ||||
|         AsyncDrawableScheduler.schedule(textView); | ||||
|     } | ||||
| } | ||||
| @ -1,16 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public abstract class MediaDecoder { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract Drawable decode(@NonNull InputStream inputStream); | ||||
| } | ||||
| @ -1,14 +0,0 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public abstract class SchemeHandler { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| package ru.noties.markwon.image.data; | ||||
| 
 | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| public class DataUri { | ||||
| 
 | ||||
|     private final String contentType; | ||||
|     private final boolean base64; | ||||
|     private final String data; | ||||
| 
 | ||||
|     public DataUri(@Nullable String contentType, boolean base64, @Nullable String data) { | ||||
|         this.contentType = contentType; | ||||
|         this.base64 = base64; | ||||
|         this.data = data; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String contentType() { | ||||
|         return contentType; | ||||
|     } | ||||
| 
 | ||||
|     public boolean base64() { | ||||
|         return base64; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String data() { | ||||
|         return data; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return "DataUri{" + | ||||
|                 "contentType='" + contentType + '\'' + | ||||
|                 ", base64=" + base64 + | ||||
|                 ", data='" + data + '\'' + | ||||
|                 '}'; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean equals(Object o) { | ||||
|         if (this == o) return true; | ||||
|         if (o == null || getClass() != o.getClass()) return false; | ||||
| 
 | ||||
|         DataUri dataUri = (DataUri) o; | ||||
| 
 | ||||
|         if (base64 != dataUri.base64) return false; | ||||
|         if (contentType != null ? !contentType.equals(dataUri.contentType) : dataUri.contentType != null) | ||||
|             return false; | ||||
|         return data != null ? data.equals(dataUri.data) : dataUri.data == null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         int result = contentType != null ? contentType.hashCode() : 0; | ||||
|         result = 31 * result + (base64 ? 1 : 0); | ||||
|         result = 31 * result + (data != null ? data.hashCode() : 0); | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| @ -1,41 +0,0 @@ | ||||
| package ru.noties.markwon.image.data; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.TextUtils; | ||||
| import android.util.Base64; | ||||
| 
 | ||||
| public abstract class DataUriDecoder { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract byte[] decode(@NonNull DataUri dataUri); | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static DataUriDecoder create() { | ||||
|         return new Impl(); | ||||
|     } | ||||
| 
 | ||||
|     static class Impl extends DataUriDecoder { | ||||
| 
 | ||||
|         @Nullable | ||||
|         @Override | ||||
|         public byte[] decode(@NonNull DataUri dataUri) { | ||||
| 
 | ||||
|             final String data = dataUri.data(); | ||||
| 
 | ||||
|             if (!TextUtils.isEmpty(data)) { | ||||
|                 try { | ||||
|                     if (dataUri.base64()) { | ||||
|                         return Base64.decode(data.getBytes("UTF-8"), Base64.DEFAULT); | ||||
|                     } else { | ||||
|                         return data.getBytes("UTF-8"); | ||||
|                     } | ||||
|                 } catch (Throwable t) { | ||||
|                     return null; | ||||
|                 } | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,79 +0,0 @@ | ||||
| package ru.noties.markwon.image.data; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| public abstract class DataUriParser { | ||||
| 
 | ||||
|     @Nullable | ||||
|     public abstract DataUri parse(@NonNull String input); | ||||
| 
 | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static DataUriParser create() { | ||||
|         return new Impl(); | ||||
|     } | ||||
| 
 | ||||
|     static class Impl extends DataUriParser { | ||||
| 
 | ||||
|         @Nullable | ||||
|         @Override | ||||
|         public DataUri parse(@NonNull String input) { | ||||
| 
 | ||||
|             final int index = input.indexOf(','); | ||||
|             // we expect exactly one comma | ||||
|             if (index < 0) { | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             final String contentType; | ||||
|             final boolean base64; | ||||
| 
 | ||||
|             if (index > 0) { | ||||
|                 final String part = input.substring(0, index); | ||||
|                 final String[] parts = part.split(";"); | ||||
|                 final int length = parts.length; | ||||
|                 if (length > 0) { | ||||
|                     // if one: either content-type or base64 | ||||
|                     if (length == 1) { | ||||
|                         final String value = parts[0]; | ||||
|                         if ("base64".equals(value)) { | ||||
|                             contentType = null; | ||||
|                             base64 = true; | ||||
|                         } else { | ||||
|                             contentType = value.indexOf('/') > -1 | ||||
|                                     ? value | ||||
|                                     : null; | ||||
|                             base64 = false; | ||||
|                         } | ||||
|                     } else { | ||||
|                         contentType = parts[0].indexOf('/') > -1 | ||||
|                                 ? parts[0] | ||||
|                                 : null; | ||||
|                         base64 = "base64".equals(parts[length - 1]); | ||||
|                     } | ||||
|                 } else { | ||||
|                     contentType = null; | ||||
|                     base64 = false; | ||||
|                 } | ||||
|             } else { | ||||
|                 contentType = null; | ||||
|                 base64 = false; | ||||
|             } | ||||
| 
 | ||||
|             final String data; | ||||
|             if (index < input.length()) { | ||||
|                 final String value = input.substring(index + 1, input.length()).replaceAll("\n", ""); | ||||
|                 if (value.length() == 0) { | ||||
|                     data = null; | ||||
|                 } else { | ||||
|                     data = value; | ||||
|                 } | ||||
|             } else { | ||||
|                 data = null; | ||||
|             } | ||||
| 
 | ||||
|             return new DataUri(contentType, base64, data); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,65 +0,0 @@ | ||||
| package ru.noties.markwon.image.data; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.ByteArrayInputStream; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| 
 | ||||
| /** | ||||
|  * @since 2.0.0 | ||||
|  */ | ||||
| public class DataUriSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|     public static final String SCHEME = "data"; | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static DataUriSchemeHandler create() { | ||||
|         return new DataUriSchemeHandler(DataUriParser.create(), DataUriDecoder.create()); | ||||
|     } | ||||
| 
 | ||||
|     private static final String START = "data:"; | ||||
| 
 | ||||
|     private final DataUriParser uriParser; | ||||
|     private final DataUriDecoder uriDecoder; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     DataUriSchemeHandler(@NonNull DataUriParser uriParser, @NonNull DataUriDecoder uriDecoder) { | ||||
|         this.uriParser = uriParser; | ||||
|         this.uriDecoder = uriDecoder; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { | ||||
| 
 | ||||
|         if (!raw.startsWith(START)) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         String part = raw.substring(START.length()); | ||||
| 
 | ||||
|         // this part is added to support `data://` with which this functionality was released | ||||
|         if (part.startsWith("//")) { | ||||
|             part = part.substring(2); | ||||
|         } | ||||
| 
 | ||||
|         final DataUri dataUri = uriParser.parse(part); | ||||
|         if (dataUri == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         final byte[] bytes = uriDecoder.decode(dataUri); | ||||
|         if (bytes == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return new ImageItem( | ||||
|                 dataUri.contentType(), | ||||
|                 new ByteArrayInputStream(bytes) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @ -1,105 +0,0 @@ | ||||
| package ru.noties.markwon.image.file; | ||||
| 
 | ||||
| import android.content.res.AssetManager; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.webkit.MimeTypeMap; | ||||
| 
 | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public class FileSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|     public static final String SCHEME = "file"; | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static FileSchemeHandler createWithAssets(@NonNull AssetManager assetManager) { | ||||
|         return new FileSchemeHandler(assetManager); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static FileSchemeHandler create() { | ||||
|         return new FileSchemeHandler(null); | ||||
|     } | ||||
| 
 | ||||
|     private static final String FILE_ANDROID_ASSETS = "android_asset"; | ||||
| 
 | ||||
|     @Nullable | ||||
|     private final AssetManager assetManager; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     FileSchemeHandler(@Nullable AssetManager assetManager) { | ||||
|         this.assetManager = assetManager; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { | ||||
| 
 | ||||
|         final List<String> segments = uri.getPathSegments(); | ||||
|         if (segments == null | ||||
|                 || segments.size() == 0) { | ||||
|             // pointing to file & having no path segments is no use | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         final ImageItem out; | ||||
| 
 | ||||
|         InputStream inputStream = null; | ||||
| 
 | ||||
|         final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); | ||||
|         final String fileName = uri.getLastPathSegment(); | ||||
| 
 | ||||
|         if (assets) { | ||||
| 
 | ||||
|             // no handling of assets here if we have no assetsManager | ||||
|             if (assetManager != null) { | ||||
| 
 | ||||
|                 final StringBuilder path = new StringBuilder(); | ||||
|                 for (int i = 1, size = segments.size(); i < size; i++) { | ||||
|                     if (i != 1) { | ||||
|                         path.append('/'); | ||||
|                     } | ||||
|                     path.append(segments.get(i)); | ||||
|                 } | ||||
|                 // load assets | ||||
| 
 | ||||
|                 try { | ||||
|                     inputStream = assetManager.open(path.toString()); | ||||
|                 } catch (IOException e) { | ||||
|                     e.printStackTrace(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         } else { | ||||
|             try { | ||||
|                 inputStream = new BufferedInputStream(new FileInputStream(new File(uri.getPath()))); | ||||
|             } catch (FileNotFoundException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (inputStream != null) { | ||||
|             final String contentType = MimeTypeMap | ||||
|                     .getSingleton() | ||||
|                     .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(fileName)); | ||||
|             out = new ImageItem(contentType, inputStream); | ||||
|         } else { | ||||
|             out = null; | ||||
|         } | ||||
| 
 | ||||
|         return out; | ||||
|     } | ||||
| } | ||||
| @ -1,70 +0,0 @@ | ||||
| package ru.noties.markwon.image.network; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| 
 | ||||
| /** | ||||
|  * A simple network scheme handler that is not dependent on any external libraries. | ||||
|  * | ||||
|  * @see #create() | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public class NetworkSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|     public static final String SCHEME_HTTP = "http"; | ||||
|     public static final String SCHEME_HTTPS = "https"; | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static NetworkSchemeHandler create() { | ||||
|         return new NetworkSchemeHandler(); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { | ||||
| 
 | ||||
|         try { | ||||
| 
 | ||||
|             final URL url = new URL(raw); | ||||
|             final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||||
|             connection.connect(); | ||||
| 
 | ||||
|             final int responseCode = connection.getResponseCode(); | ||||
|             if (responseCode >= 200 && responseCode < 300) { | ||||
|                 final String contentType = contentType(connection.getHeaderField("Content-Type")); | ||||
|                 final InputStream inputStream = new BufferedInputStream(connection.getInputStream()); | ||||
|                 return new ImageItem(contentType, inputStream); | ||||
|             } | ||||
| 
 | ||||
|         } catch (IOException e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     static String contentType(@Nullable String contentType) { | ||||
| 
 | ||||
|         if (contentType == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         final int index = contentType.indexOf(';'); | ||||
|         if (index > -1) { | ||||
|             return contentType.substring(0, index); | ||||
|         } | ||||
| 
 | ||||
|         return contentType; | ||||
|     } | ||||
| } | ||||
| @ -1,97 +0,0 @@ | ||||
| package ru.noties.markwon.priority; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.MarkwonPlugin; | ||||
| 
 | ||||
| // a small dependency graph also | ||||
| // what if plugins cannot be constructed into a graph? for example they depend on something | ||||
| // but not overlap? then it would be hard to sort them (but this doesn't make sense, if | ||||
| // they do not care about other components, just put them in whatever order, no?) | ||||
| 
 | ||||
| /** | ||||
|  * @see MarkwonPlugin#priority() | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| @Deprecated | ||||
| public abstract class Priority { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Priority none() { | ||||
|         return builder().build(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Priority after(@NonNull Class<? extends MarkwonPlugin> plugin) { | ||||
|         return builder().after(plugin).build(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Priority after( | ||||
|             @NonNull Class<? extends MarkwonPlugin> plugin1, | ||||
|             @NonNull Class<? extends MarkwonPlugin> plugin2) { | ||||
|         return builder().after(plugin1).after(plugin2).build(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static Builder builder() { | ||||
|         return new Impl.BuilderImpl(); | ||||
|     } | ||||
| 
 | ||||
|     public interface Builder { | ||||
| 
 | ||||
|         @NonNull | ||||
|         Builder after(@NonNull Class<? extends MarkwonPlugin> plugin); | ||||
| 
 | ||||
|         @NonNull | ||||
|         Priority build(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract List<Class<? extends MarkwonPlugin>> after(); | ||||
| 
 | ||||
| 
 | ||||
|     static class Impl extends Priority { | ||||
| 
 | ||||
|         private final List<Class<? extends MarkwonPlugin>> after; | ||||
| 
 | ||||
|         Impl(@NonNull List<Class<? extends MarkwonPlugin>> after) { | ||||
|             this.after = after; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         @Override | ||||
|         public List<Class<? extends MarkwonPlugin>> after() { | ||||
|             return after; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return "Priority{" + | ||||
|                     "after=" + after + | ||||
|                     '}'; | ||||
|         } | ||||
| 
 | ||||
|         static class BuilderImpl implements Builder { | ||||
| 
 | ||||
|             private final List<Class<? extends MarkwonPlugin>> after = new ArrayList<>(0); | ||||
| 
 | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public Builder after(@NonNull Class<? extends MarkwonPlugin> plugin) { | ||||
|                 after.add(plugin); | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public Priority build() { | ||||
|                 return new Impl(Collections.unmodifiableList(after)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| package ru.noties.markwon.priority; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.MarkwonPlugin; | ||||
| 
 | ||||
| public abstract class PriorityProcessor { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static PriorityProcessor create() { | ||||
|         return new PriorityProcessorImpl(); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public abstract List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> plugins); | ||||
| } | ||||
| @ -1,132 +0,0 @@ | ||||
| package ru.noties.markwon.priority; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import ru.noties.markwon.MarkwonPlugin; | ||||
| 
 | ||||
| import static java.lang.Math.max; | ||||
| 
 | ||||
| class PriorityProcessorImpl extends PriorityProcessor { | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> in) { | ||||
| 
 | ||||
|         // create new collection based on supplied argument | ||||
|         final List<MarkwonPlugin> plugins = new ArrayList<>(in); | ||||
| 
 | ||||
|         final int size = plugins.size(); | ||||
| 
 | ||||
|         final Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map = | ||||
|                 new HashMap<>(size); | ||||
| 
 | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             if (map.put(plugin.getClass(), new HashSet<>(plugin.priority().after())) != null) { | ||||
|                 throw new IllegalStateException(String.format("Markwon duplicate plugin " + | ||||
|                         "found `%s`: %s", plugin.getClass().getName(), plugin)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         final Map<MarkwonPlugin, Integer> cache = new HashMap<>(size); | ||||
|         for (MarkwonPlugin plugin : plugins) { | ||||
|             cache.put(plugin, eval(plugin, map)); | ||||
|         } | ||||
| 
 | ||||
|         Collections.sort(plugins, new PriorityComparator(cache)); | ||||
| 
 | ||||
|         return plugins; | ||||
|     } | ||||
| 
 | ||||
|     private static int eval( | ||||
|             @NonNull MarkwonPlugin plugin, | ||||
|             @NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) { | ||||
| 
 | ||||
|         final Set<Class<? extends MarkwonPlugin>> set = map.get(plugin.getClass()); | ||||
| 
 | ||||
|         // no dependencies | ||||
|         if (set.isEmpty()) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         final Class<? extends MarkwonPlugin> who = plugin.getClass(); | ||||
| 
 | ||||
|         int max = 0; | ||||
| 
 | ||||
|         for (Class<? extends MarkwonPlugin> dependency : set) { | ||||
|             max = max(max, eval(who, dependency, map)); | ||||
|         } | ||||
| 
 | ||||
|         return 1 + max; | ||||
|     } | ||||
| 
 | ||||
|     // we need to count the number of steps to a root node (which has no parents) | ||||
|     private static int eval( | ||||
|             @NonNull Class<? extends MarkwonPlugin> who, | ||||
|             @NonNull Class<? extends MarkwonPlugin> plugin, | ||||
|             @NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) { | ||||
| 
 | ||||
|         // exact match | ||||
|         Set<Class<? extends MarkwonPlugin>> set = map.get(plugin); | ||||
| 
 | ||||
|         if (set == null) { | ||||
| 
 | ||||
|             // let's try to find inexact type (overridden/subclassed) | ||||
|             for (Map.Entry<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> entry : map.entrySet()) { | ||||
|                 if (plugin.isAssignableFrom(entry.getKey())) { | ||||
|                     set = entry.getValue(); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (set == null) { | ||||
|                 // unsatisfied dependency | ||||
|                 throw new IllegalStateException(String.format("Markwon unsatisfied dependency found. " + | ||||
|                                 "Plugin `%s` comes after `%s` but it is not added.", | ||||
|                         who.getName(), plugin.getName())); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (set.isEmpty()) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         int value = 1; | ||||
| 
 | ||||
|         for (Class<? extends MarkwonPlugin> dependency : set) { | ||||
| 
 | ||||
|             // a case when a plugin defines `Priority.after(getClass)` or being | ||||
|             // referenced by own dependency (even indirect) | ||||
|             if (who.equals(dependency)) { | ||||
|                 throw new IllegalStateException(String.format("Markwon plugin `%s` defined self " + | ||||
|                         "as a dependency or being referenced by own dependency (cycle)", who.getName())); | ||||
|             } | ||||
| 
 | ||||
|             value += eval(who, dependency, map); | ||||
|         } | ||||
| 
 | ||||
|         return value; | ||||
|     } | ||||
| 
 | ||||
|     private static class PriorityComparator implements Comparator<MarkwonPlugin> { | ||||
| 
 | ||||
|         private final Map<MarkwonPlugin, Integer> map; | ||||
| 
 | ||||
|         PriorityComparator(@NonNull Map<MarkwonPlugin, Integer> map) { | ||||
|             this.map = map; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int compare(MarkwonPlugin o1, MarkwonPlugin o2) { | ||||
|             return map.get(o1).compareTo(map.get(o2)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -8,7 +8,6 @@ import org.robolectric.annotation.Config; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| 
 | ||||
|  | ||||
| @ -23,8 +23,6 @@ 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.Priority; | ||||
| import ru.noties.markwon.priority.PriorityProcessor; | ||||
| 
 | ||||
| import static org.hamcrest.Matchers.containsString; | ||||
| import static org.hamcrest.Matchers.hasItem; | ||||
|  | ||||
| @ -14,8 +14,6 @@ import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.Scanner; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNotNull; | ||||
| import static org.junit.Assert.assertNull; | ||||
|  | ||||
| @ -18,7 +18,6 @@ import java.util.List; | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonPlugin; | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| 
 | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertTrue; | ||||
|  | ||||
| @ -19,13 +19,7 @@ import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonVisitor; | ||||
| import ru.noties.markwon.RenderProps; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.ImageProps; | ||||
| import ru.noties.markwon.image.ImageSize; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.MediaDecoder; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  | ||||
| @ -10,7 +10,6 @@ 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 | ||||
|  | ||||
| @ -4,7 +4,6 @@ import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| public class GifPlugin extends AbstractMarkwonPlugin { | ||||
|  | ||||
| @ -7,8 +7,6 @@ import java.util.Arrays; | ||||
| import okhttp3.OkHttpClient; | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.network.NetworkSchemeHandler; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -11,8 +11,6 @@ import okhttp3.OkHttpClient; | ||||
| import okhttp3.Request; | ||||
| import okhttp3.Response; | ||||
| import okhttp3.ResponseBody; | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| 
 | ||||
| class OkHttpSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|  | ||||
| @ -14,7 +14,6 @@ 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 | ||||
|  | ||||
| @ -5,7 +5,6 @@ import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| public class SvgPlugin extends AbstractMarkwonPlugin { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| POM_NAME=Image | ||||
| POM_ARTIFACT_ID=image | ||||
| POM_DESCRIPTION=Markwon image loading module (with GIF and SVG support) | ||||
| POM_DESCRIPTION=Markwon image loading module (with optional GIF and SVG support) | ||||
| POM_PACKAGING=aar | ||||
| @ -1,6 +1,5 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| @ -10,88 +9,69 @@ import java.util.Map; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| public class AsyncDrawableLoaderBuilder { | ||||
| 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; | ||||
|     ImagesPlugin.PlaceholderProvider placeholderProvider; | ||||
|     ImagesPlugin.ErrorHandler errorHandler; | ||||
| 
 | ||||
|     @NonNull | ||||
|     public AsyncDrawableLoaderBuilder executorService(@NonNull ExecutorService executorService) { | ||||
|     boolean isBuilt; | ||||
| 
 | ||||
|     void 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) { | ||||
|     void addSchemeHandler(@NonNull SchemeHandler schemeHandler) { | ||||
|         for (String scheme : schemeHandler.supportedSchemes()) { | ||||
|             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); | ||||
|     void addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { | ||||
|         final Collection<String> supportedTypes = mediaDecoder.supportedTypes(); | ||||
|         if (supportedTypes.isEmpty()) { | ||||
|             // todo: we should think about this little _side-effect_... does it worth it? | ||||
|             defaultMediaDecoder = mediaDecoder; | ||||
|         } else { | ||||
|             for (String type : supportedTypes) { | ||||
|                 mediaDecoders.put(type, 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) { | ||||
|     void defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { | ||||
|         this.defaultMediaDecoder = mediaDecoder; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     void removeSchemeHandler(@NonNull String scheme) { | ||||
|         schemeHandlers.remove(scheme); | ||||
|     } | ||||
| 
 | ||||
|     void removeMediaDecoder(@NonNull String contentType) { | ||||
|         mediaDecoders.remove(contentType); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public AsyncDrawableLoaderBuilder placeholderDrawableProvider(@NonNull DrawableProvider placeholderDrawableProvider) { | ||||
|         this.placeholderDrawableProvider = placeholderDrawableProvider; | ||||
|         return this; | ||||
|     void placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) { | ||||
|         this.placeholderProvider = placeholderDrawableProvider; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 3.0.0 | ||||
|      */ | ||||
|     @NonNull | ||||
|     public AsyncDrawableLoaderBuilder errorDrawableProvider(@NonNull DrawableProvider errorDrawableProvider) { | ||||
|         this.errorDrawableProvider = errorDrawableProvider; | ||||
|         return this; | ||||
|     void errorHandler(@NonNull ImagesPlugin.ErrorHandler errorHandler) { | ||||
|         this.errorHandler = errorHandler; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @NonNull | ||||
|     public AsyncDrawableLoader build() { | ||||
|     AsyncDrawableLoader build() { | ||||
| 
 | ||||
|         isBuilt = true; | ||||
| 
 | ||||
|         // if we have no schemeHandlers -> we cannot show anything | ||||
|         // OR if we have no media decoders | ||||
|  | ||||
| @ -6,9 +6,8 @@ import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| @ -22,8 +21,8 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|     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 ImagesPlugin.PlaceholderProvider placeholderProvider; | ||||
|     private final ImagesPlugin.ErrorHandler errorHandler; | ||||
| 
 | ||||
|     private final Handler mainThread; | ||||
| 
 | ||||
| @ -31,13 +30,13 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|     //  for the same destination | ||||
|     private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2); | ||||
| 
 | ||||
|     AsyncDrawableLoaderImpl(@NonNull Builder builder) { | ||||
|     AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder 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.placeholderProvider = builder.placeholderProvider; | ||||
|         this.errorHandler = builder.errorHandler; | ||||
|         this.mainThread = new Handler(Looper.getMainLooper()); | ||||
|     } | ||||
| 
 | ||||
| @ -147,48 +146,18 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| //    @Override | ||||
| //    public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { | ||||
| // | ||||
| //        // todo: we cannot reliably identify request by the destination, as if | ||||
| //        //  markdown input has multiple images with the same destination as source | ||||
| //        //  we will be tracking only one of them (the one appears the last). We should | ||||
| //        //  move to AsyncDrawable based identification. This method also _maybe_ | ||||
| //        //  should include the ImageSize (comment @since 3.1.0-SNAPSHOT) | ||||
| // | ||||
| //        requests.put(destination, execute(destination, drawable)); | ||||
| //    } | ||||
| // | ||||
| //    @Override | ||||
| //    public void cancel(@NonNull String destination) { | ||||
| // | ||||
| //        // todo: as we are moving away from a single request for a destination, | ||||
| //        //  we should re-evaluate this cancellation logic, as if there are multiple images | ||||
| //        //  in markdown input all of them will be cancelled (won't delivered), even if | ||||
| //        //  only a single drawable is detached. Cancellation must also take | ||||
| //        //  the AsyncDrawable argument (comment @since 3.1.0-SNAPSHOT) | ||||
| // | ||||
| //        // | ||||
| //        final Future<?> request = requests.remove(destination); | ||||
| //        if (request != null) { | ||||
| //            request.cancel(true); | ||||
| //        } | ||||
| //    } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Drawable placeholder() { | ||||
|         return placeholderDrawableProvider != null | ||||
|                 ? placeholderDrawableProvider.provide() | ||||
|     public Drawable placeholder(@NonNull AsyncDrawable drawable) { | ||||
|         return placeholderProvider != null | ||||
|                 ? placeholderProvider.providePlaceholder(drawable) | ||||
|                 : null; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     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 | ||||
|         // todo: more efficient DefaultImageMediaDecoder... 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...) | ||||
| @ -197,71 +166,60 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|             @Override | ||||
|             public void run() { | ||||
| 
 | ||||
|                 final ImageItem item; | ||||
| 
 | ||||
|                 final Uri uri = Uri.parse(destination); | ||||
| 
 | ||||
|                 final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); | ||||
|                 Drawable drawable = null; | ||||
| 
 | ||||
|                 if (schemeHandler != null) { | ||||
|                     item = schemeHandler.handle(destination, uri); | ||||
|                 } else { | ||||
|                     item = null; | ||||
|                 } | ||||
|                 try { | ||||
|                     // obtain scheme handler | ||||
|                     final SchemeHandler schemeHandler = schemeHandlers.get(uri.getScheme()); | ||||
|                     if (schemeHandler != null) { | ||||
| 
 | ||||
|                 final InputStream inputStream = item != null | ||||
|                         ? item.inputStream() | ||||
|                         : null; | ||||
|                         // handle scheme | ||||
|                         final ImageItem imageItem = schemeHandler.handle(destination, uri); | ||||
| 
 | ||||
|                 Drawable result = null; | ||||
|                         // if resulting imageItem needs further decoding -> proceed | ||||
|                         if (imageItem.hasDecodingNeeded()) { | ||||
| 
 | ||||
|                 if (inputStream != null) { | ||||
|                     try { | ||||
|                             final ImageItem.WithDecodingNeeded withDecodingNeeded = imageItem.getAsWithDecodingNeeded(); | ||||
| 
 | ||||
|                         MediaDecoder mediaDecoder = mediaDecoders.get(item.contentType()); | ||||
|                         if (mediaDecoder == null) { | ||||
|                             mediaDecoder = defaultMediaDecoder; | ||||
|                             MediaDecoder mediaDecoder = mediaDecoders.get(withDecodingNeeded.contentType()); | ||||
| 
 | ||||
|                             if (mediaDecoder == null) { | ||||
|                                 mediaDecoder = defaultMediaDecoder; | ||||
|                             } | ||||
| 
 | ||||
|                             if (mediaDecoder != null) { | ||||
|                                 drawable = mediaDecoder.decode(withDecodingNeeded.contentType(), withDecodingNeeded.inputStream()); | ||||
|                             } else { | ||||
|                                 // throw that no media decoder is found | ||||
|                                 throw new IllegalStateException("No media-decoder is found: " + destination); | ||||
|                             } | ||||
|                         } else { | ||||
|                             drawable = imageItem.getAsWithResult().result(); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // throw no scheme handler is available | ||||
|                         throw new IllegalStateException("No scheme-handler is found: " + destination); | ||||
|                     } | ||||
| 
 | ||||
|                         if (mediaDecoder != null) { | ||||
|                             result = mediaDecoder.decode(inputStream); | ||||
|                         } | ||||
| 
 | ||||
|                     } finally { | ||||
|                         try { | ||||
|                             inputStream.close(); | ||||
|                         } catch (IOException e) { | ||||
|                             // ignored | ||||
|                         } | ||||
|                 } catch (Throwable t) { | ||||
|                     if (errorHandler != null) { | ||||
|                         drawable = errorHandler.handleError(destination, t); | ||||
|                     } else { | ||||
|                         // else simply log the error | ||||
|                         Log.e("MARKWON-IMAGE", "Error loading image: " + destination, t); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // if result is null, we assume it's an error | ||||
|                 if (result == null) { | ||||
|                     result = errorDrawableProvider != null | ||||
|                             ? errorDrawableProvider.provide() | ||||
|                             : null; | ||||
|                 } | ||||
| 
 | ||||
|                 final Drawable out = result; | ||||
|                 final Drawable out = drawable; | ||||
| 
 | ||||
|                 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 | ||||
|                             // 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) | ||||
|  | ||||
| @ -0,0 +1,62 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.graphics.drawable.BitmapDrawable; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| /** | ||||
|  * This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases. | ||||
|  * Here we just assume that supplied InputStream is of image type and try to decode it. | ||||
|  * | ||||
|  * @since 1.1.0 | ||||
|  */ | ||||
| public class DefaultImageMediaDecoder extends MediaDecoder { | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static DefaultImageMediaDecoder create() { | ||||
|         return new DefaultImageMediaDecoder(Resources.getSystem()); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static DefaultImageMediaDecoder create(@NonNull Resources resources) { | ||||
|         return new DefaultImageMediaDecoder(resources); | ||||
|     } | ||||
| 
 | ||||
|     private final Resources resources; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     DefaultImageMediaDecoder(Resources resources) { | ||||
|         this.resources = resources; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Drawable decode(@Nullable String contentType, @NonNull InputStream inputStream) { | ||||
| 
 | ||||
|         final Bitmap bitmap; | ||||
|         try { | ||||
|             // absolutely not optimal... thing | ||||
|             bitmap = BitmapFactory.decodeStream(inputStream); | ||||
|         } catch (Throwable t) { | ||||
|             throw new IllegalStateException("Exception decoding input-stream", t); | ||||
|         } | ||||
| 
 | ||||
|         final Drawable drawable = new BitmapDrawable(resources, bitmap); | ||||
|         DrawableUtils.applyIntrinsicBounds(drawable); | ||||
|         return drawable; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedTypes() { | ||||
|         return Collections.emptySet(); | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,5 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| @ -11,6 +10,8 @@ import org.commonmark.node.Image; | ||||
| import org.commonmark.node.Link; | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| import java.util.concurrent.ExecutorService; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonConfiguration; | ||||
| import ru.noties.markwon.MarkwonSpansFactory; | ||||
| @ -23,55 +24,123 @@ 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); | ||||
|     public interface ImagesConfigure { | ||||
|         void configureImages(@NonNull ImagesPlugin plugin); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Special scheme that is used {@code file:///android_asset/} | ||||
|      * | ||||
|      * @param context | ||||
|      * @return | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     public interface PlaceholderProvider { | ||||
|         @Nullable | ||||
|         Drawable providePlaceholder(@NonNull AsyncDrawable drawable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     public interface ErrorHandler { | ||||
| 
 | ||||
|         /** | ||||
|          * Can optionally return a Drawable that will be displayed in case of an error | ||||
|          */ | ||||
|         @Nullable | ||||
|         Drawable handleError(@NonNull String url, @NonNull Throwable throwable); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Factory method to create an empty {@link ImagesPlugin} instance with no {@link SchemeHandler}s | ||||
|      * nor {@link MediaDecoder}s registered. Can be used to further configure via instance methods or | ||||
|      * via {@link ru.noties.markwon.MarkwonPlugin#configure(Registry)} | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static ImagesPlugin createWithAssets(@NonNull Context context) { | ||||
|         return new ImagesPlugin(context, true); | ||||
|     public static ImagesPlugin createEmpty() { | ||||
|         return new ImagesPlugin(); | ||||
|     } | ||||
| 
 | ||||
|     private final Context context; | ||||
|     private final boolean useAssets; | ||||
| 
 | ||||
|     protected ImagesPlugin(Context context, boolean useAssets) { | ||||
|         this.context = context; | ||||
|         this.useAssets = useAssets; | ||||
|     @NonNull | ||||
|     public static ImagesPlugin create(@NonNull ImagesConfigure configure) { | ||||
|         final ImagesPlugin plugin = new ImagesPlugin(); | ||||
|         configure.configureImages(plugin); | ||||
|         return plugin; | ||||
|     } | ||||
| 
 | ||||
|     // we must expose scheme handling... so it's available during construction and via `require` | ||||
|     private final AsyncDrawableLoaderBuilder builder = new AsyncDrawableLoaderBuilder(); | ||||
| 
 | ||||
| //    @Override | ||||
| //    public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { | ||||
| // | ||||
| //        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())); | ||||
| //    } | ||||
|     /** | ||||
|      * Optional (by default new cached thread executor will be used) | ||||
|      * | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public ImagesPlugin executorService(@NonNull ExecutorService executorService) { | ||||
|         checkBuilderState(); | ||||
|         builder.executorService(executorService); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @see SchemeHandler | ||||
|      * @see ru.noties.markwon.image.data.DataUriSchemeHandler | ||||
|      * @see ru.noties.markwon.image.file.FileSchemeHandler | ||||
|      * @see ru.noties.markwon.image.network.NetworkSchemeHandler | ||||
|      * @see ru.noties.markwon.image.network.OkHttpNetworkSchemeHandler | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public ImagesPlugin addSchemeHandler(@NonNull SchemeHandler schemeHandler) { | ||||
|         checkBuilderState(); | ||||
|         builder.addSchemeHandler(schemeHandler); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { | ||||
|         checkBuilderState(); | ||||
|         builder.addMediaDecoder(mediaDecoder); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { | ||||
|         checkBuilderState(); | ||||
|         builder.defaultMediaDecoder(mediaDecoder); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin removeSchemeHandler(@NonNull String scheme) { | ||||
|         checkBuilderState(); | ||||
|         builder.removeSchemeHandler(scheme); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin removeMediaDecoder(@NonNull String contentType) { | ||||
|         checkBuilderState(); | ||||
|         builder.removeMediaDecoder(contentType); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin placeholderProvider(@NonNull PlaceholderProvider placeholderProvider) { | ||||
|         checkBuilderState(); | ||||
|         builder.placeholderProvider(placeholderProvider); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public ImagesPlugin errorHandler(@NonNull ErrorHandler errorHandler) { | ||||
|         checkBuilderState(); | ||||
|         builder.errorHandler(errorHandler); | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { | ||||
|         checkBuilderState(); | ||||
|         builder.asyncDrawableLoader(this.builder.build()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { | ||||
| @ -132,4 +201,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { | ||||
|     public void afterSetText(@NonNull TextView textView) { | ||||
|         AsyncDrawableScheduler.schedule(textView); | ||||
|     } | ||||
| 
 | ||||
|     private void checkBuilderState() { | ||||
|         if (builder.isBuilt) { | ||||
|             throw new IllegalStateException("ImagesPlugin has already been configured " + | ||||
|                     "and cannot be modified any further"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,7 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
| @ -16,14 +17,17 @@ public abstract class MediaDecoder { | ||||
|      * <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; | ||||
|     ); | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract Collection<String> supportedTypes(); | ||||
| } | ||||
|  | ||||
| @ -3,6 +3,8 @@ package ru.noties.markwon.image; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| /** | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| @ -12,13 +14,17 @@ 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; | ||||
|     public abstract ImageItem handle(@NonNull String raw, @NonNull Uri uri); | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public abstract Collection<String> supportedSchemes(); | ||||
| } | ||||
|  | ||||
| @ -4,6 +4,8 @@ import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| @ -61,4 +63,10 @@ public class DataUriSchemeHandler extends SchemeHandler { | ||||
|                 dataUri.contentType(), | ||||
|                 new ByteArrayInputStream(bytes)); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedSchemes() { | ||||
|         return Collections.singleton(SCHEME); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,6 +13,8 @@ import java.io.FileInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| @ -111,4 +113,10 @@ public class FileSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|         return ImageItem.withDecodingNeeded(contentType, inputStream); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedSchemes() { | ||||
|         return Collections.singleton(SCHEME); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -7,6 +7,8 @@ import android.support.annotation.Nullable; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| import pl.droidsonroids.gif.GifDrawable; | ||||
| import ru.noties.markwon.image.DrawableUtils; | ||||
| @ -58,6 +60,12 @@ public class GifMediaDecoder extends MediaDecoder { | ||||
|         return drawable; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedTypes() { | ||||
|         return Collections.singleton(CONTENT_TYPE); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     protected GifDrawable newGifDrawable(@NonNull byte[] bytes) throws IOException { | ||||
|         return new GifDrawable(bytes); | ||||
|  | ||||
| @ -9,6 +9,8 @@ import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| @ -56,6 +58,12 @@ public class NetworkSchemeHandler extends SchemeHandler { | ||||
|         return imageItem; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedSchemes() { | ||||
|         return Arrays.asList(SCHEME_HTTP, SCHEME_HTTPS); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     static String contentType(@Nullable String contentType) { | ||||
| 
 | ||||
|  | ||||
| @ -3,8 +3,9 @@ package ru.noties.markwon.image.network; | ||||
| import android.net.Uri; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| import okhttp3.OkHttpClient; | ||||
| import okhttp3.Request; | ||||
| @ -16,7 +17,7 @@ import ru.noties.markwon.image.SchemeHandler; | ||||
| /** | ||||
|  * @since 4.0.0-SNAPSHOT | ||||
|  */ | ||||
| class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
| public class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|     /** | ||||
|      * @see #create(OkHttpClient) | ||||
| @ -51,8 +52,8 @@ class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
|         final Response response; | ||||
|         try { | ||||
|             response = client.newCall(request).execute(); | ||||
|         } catch (IOException e) { | ||||
|             throw new IllegalStateException("Exception obtaining network resource: " + raw, e); | ||||
|         } catch (Throwable t) { | ||||
|             throw new IllegalStateException("Exception obtaining network resource: " + raw, t); | ||||
|         } | ||||
| 
 | ||||
|         if (response == null) { | ||||
| @ -68,8 +69,18 @@ class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
|             throw new IllegalStateException("Response does not contain body: " + raw); | ||||
|         } | ||||
| 
 | ||||
|         final String contentType = response.header(HEADER_CONTENT_TYPE); | ||||
|         // important to process content-type as it can have encoding specified (which we should remove) | ||||
|         final String contentType = | ||||
|                 NetworkSchemeHandler.contentType(response.header(HEADER_CONTENT_TYPE)); | ||||
| 
 | ||||
|         return ImageItem.withDecodingNeeded(contentType, inputStream); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedSchemes() { | ||||
|         return Arrays.asList( | ||||
|                 NetworkSchemeHandler.SCHEME_HTTP, | ||||
|                 NetworkSchemeHandler.SCHEME_HTTPS); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,8 @@ import com.caverock.androidsvg.SVG; | ||||
| import com.caverock.androidsvg.SVGParseException; | ||||
| 
 | ||||
| import java.io.InputStream; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| import ru.noties.markwon.image.DrawableUtils; | ||||
| import ru.noties.markwon.image.MediaDecoder; | ||||
| @ -71,4 +73,10 @@ public class SvgMediaDecoder extends MediaDecoder { | ||||
|         DrawableUtils.applyIntrinsicBounds(drawable); | ||||
|         return drawable; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Collection<String> supportedTypes() { | ||||
|         return Collections.singleton(CONTENT_TYPE); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -39,9 +39,7 @@ dependencies { | ||||
|     implementation project(':markwon-ext-tables') | ||||
|     implementation project(':markwon-ext-tasklist') | ||||
|     implementation project(':markwon-html') | ||||
|     implementation project(':markwon-image-gif') | ||||
|     implementation project(':markwon-image-okhttp') | ||||
|     implementation project(':markwon-image-svg') | ||||
|     implementation project(':markwon-image') | ||||
|     implementation project(':markwon-syntax-highlight') | ||||
|     implementation project(':markwon-recycler') | ||||
|     implementation project(':markwon-recycler-table') | ||||
|  | ||||
| @ -22,10 +22,6 @@ import ru.noties.markwon.MarkwonVisitor; | ||||
| import ru.noties.markwon.movement.MovementMethodPlugin; | ||||
| import ru.noties.markwon.core.MarkwonTheme; | ||||
| import ru.noties.markwon.image.AsyncDrawableLoader; | ||||
| import ru.noties.markwon.image.ImageItem; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.SchemeHandler; | ||||
| import ru.noties.markwon.image.network.NetworkSchemeHandler; | ||||
| 
 | ||||
| public class BasicPluginsActivity extends Activity { | ||||
| 
 | ||||
|  | ||||
| @ -6,7 +6,6 @@ import android.support.annotation.Nullable; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import ru.noties.markwon.Markwon; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.sample.R; | ||||
| 
 | ||||
| public class CustomExtensionActivity extends Activity { | ||||
|  | ||||
| @ -7,8 +7,6 @@ import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import ru.noties.markwon.AbstractMarkwonPlugin; | ||||
| import ru.noties.markwon.MarkwonVisitor; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.priority.Priority; | ||||
| 
 | ||||
| public class IconPlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,6 @@ import android.widget.TextView; | ||||
| 
 | ||||
| import ru.noties.markwon.Markwon; | ||||
| import ru.noties.markwon.ext.latex.JLatexMathPlugin; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.sample.R; | ||||
| 
 | ||||
| public class LatexActivity extends Activity { | ||||
|  | ||||
| @ -26,7 +26,6 @@ import ru.noties.markwon.MarkwonConfiguration; | ||||
| import ru.noties.markwon.MarkwonVisitor; | ||||
| import ru.noties.markwon.core.CorePlugin; | ||||
| import ru.noties.markwon.html.HtmlPlugin; | ||||
| import ru.noties.markwon.image.ImagesPlugin; | ||||
| import ru.noties.markwon.image.svg.SvgPlugin; | ||||
| import ru.noties.markwon.recycler.MarkwonAdapter; | ||||
| import ru.noties.markwon.recycler.SimpleEntry; | ||||
|  | ||||
| @ -7,9 +7,6 @@ include ':app', ':sample', | ||||
|         ':markwon-ext-tasklist', | ||||
|         ':markwon-html', | ||||
|         ':markwon-image', | ||||
|         ':markwon-image-gif', | ||||
|         ':markwon-image-okhttp', | ||||
|         ':markwon-image-svg', | ||||
|         ':markwon-recycler', | ||||
|         ':markwon-recycler-table', | ||||
|         ':markwon-syntax-highlight', | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov