Use Call.Factory instead of OkHttpClient in images module
This commit is contained in:
		
							parent
							
								
									6bf04e38ad
								
							
						
					
					
						commit
						f3476ca5cc
					
				| @ -29,6 +29,8 @@ class AsyncDrawableLoaderBuilder { | ||||
|         // we should not use file-scheme as it's a bit complicated to assume file usage (lack of permissions) | ||||
|         addSchemeHandler(DataUriSchemeHandler.create()); | ||||
|         addSchemeHandler(NetworkSchemeHandler.create()); | ||||
| 
 | ||||
|         defaultMediaDecoder = DefaultImageMediaDecoder.create(); | ||||
|     } | ||||
| 
 | ||||
|     void executorService(@NonNull ExecutorService executorService) { | ||||
| @ -78,11 +80,6 @@ class AsyncDrawableLoaderBuilder { | ||||
| 
 | ||||
|         isBuilt = true; | ||||
| 
 | ||||
|         // @since 4.0.0-SNAPSHOT | ||||
|         if (defaultMediaDecoder == null) { | ||||
|             defaultMediaDecoder = DefaultImageMediaDecoder.create(); | ||||
|         } | ||||
| 
 | ||||
|         if (executorService == null) { | ||||
|             executorService = Executors.newCachedThreadPool(); | ||||
|         } | ||||
|  | ||||
| @ -4,8 +4,10 @@ import android.graphics.drawable.Drawable; | ||||
| import android.net.Uri; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.os.SystemClock; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.support.annotation.VisibleForTesting; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import java.lang.ref.WeakReference; | ||||
| @ -24,126 +26,45 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|     private final ImagesPlugin.PlaceholderProvider placeholderProvider; | ||||
|     private final ImagesPlugin.ErrorHandler errorHandler; | ||||
| 
 | ||||
|     private final Handler mainThread; | ||||
|     private final Handler handler; | ||||
| 
 | ||||
|     // @since 3.1.0-SNAPSHOT use a hash-map with a weak AsyncDrawable as key for multiple requests | ||||
|     // @since 4.0.0-SNAPSHOT use a hash-map with a AsyncDrawable as key for multiple requests | ||||
|     //  for the same destination | ||||
|     private final Map<WeakReference<AsyncDrawable>, Future<?>> requests = new HashMap<>(2); | ||||
|     private final Map<AsyncDrawable, Future<?>> requests = new HashMap<>(2); | ||||
| 
 | ||||
|     AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder builder) { | ||||
|         this(builder, new Handler(Looper.getMainLooper())); | ||||
|     } | ||||
| 
 | ||||
|     // @since 4.0.0-SNAPSHOT | ||||
|     @VisibleForTesting | ||||
|     AsyncDrawableLoaderImpl(@NonNull AsyncDrawableLoaderBuilder builder, @NonNull Handler handler) { | ||||
|         this.executorService = builder.executorService; | ||||
|         this.schemeHandlers = builder.schemeHandlers; | ||||
|         this.mediaDecoders = builder.mediaDecoders; | ||||
|         this.defaultMediaDecoder = builder.defaultMediaDecoder; | ||||
|         this.placeholderProvider = builder.placeholderProvider; | ||||
|         this.errorHandler = builder.errorHandler; | ||||
|         this.mainThread = new Handler(Looper.getMainLooper()); | ||||
|         this.handler = handler; | ||||
|     } | ||||
| 
 | ||||
|     @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; | ||||
|         final Future<?> future = requests.get(drawable); | ||||
|         if (future == null) { | ||||
|             requests.put(drawable, execute(drawable)); | ||||
|         } | ||||
| 
 | ||||
|         // 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 Future<?> future = requests.remove(drawable); | ||||
|         if (future != null) { | ||||
|             future.cancel(true); | ||||
|         } | ||||
| 
 | ||||
|         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(); | ||||
|             } | ||||
|         } | ||||
|         handler.removeCallbacksAndMessages(drawable); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
| @ -155,7 +76,7 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     private Future<?> execute(@NonNull final String destination, @NonNull final WeakReference<AsyncDrawable> reference) { | ||||
|     private Future<?> execute(@NonNull final AsyncDrawable asyncDrawable) { | ||||
| 
 | ||||
|         // 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 | ||||
| @ -166,6 +87,8 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
|             @Override | ||||
|             public void run() { | ||||
| 
 | ||||
|                 final String destination = asyncDrawable.getDestination(); | ||||
| 
 | ||||
|                 final Uri uri = Uri.parse(destination); | ||||
| 
 | ||||
|                 Drawable drawable = null; | ||||
| @ -214,35 +137,22 @@ class AsyncDrawableLoaderImpl extends AsyncDrawableLoader { | ||||
| 
 | ||||
|                 final Drawable out = drawable; | ||||
| 
 | ||||
|                 mainThread.post(new Runnable() { | ||||
|                 handler.postAtTime(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
| 
 | ||||
|                         if (out != null) { | ||||
|                             // 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); | ||||
|                             } | ||||
|                         // validate that | ||||
|                         // * request was not cancelled | ||||
|                         // * out-result is present | ||||
|                         // * async-drawable is attached | ||||
|                         final Future<?> future = requests.remove(asyncDrawable); | ||||
|                         if (future != null | ||||
|                                 && out != null | ||||
|                                 && asyncDrawable.isAttached()) { | ||||
|                             asyncDrawable.setResult(out); | ||||
|                         } | ||||
| 
 | ||||
|                         requests.remove(reference); | ||||
|                         cleanUp(); | ||||
|                     } | ||||
|                 }); | ||||
|                 }, asyncDrawable, SystemClock.uptimeMillis()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean shouldCleanUp(@Nullable AsyncDrawable drawable) { | ||||
|         return drawable == null || !drawable.isAttached(); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("BooleanMethodIsAlwaysInverted") | ||||
|     private static boolean isMainThread() { | ||||
|         return Looper.myLooper() == Looper.getMainLooper(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -107,13 +107,13 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { | ||||
| 
 | ||||
|     /** | ||||
|      * Please note that if not specified a {@link DefaultImageMediaDecoder} will be used. So | ||||
|      * if you need to disable default-image-media-decoder specify here own no-op implementation. | ||||
|      * if you need to disable default-image-media-decoder specify here own no-op implementation or null. | ||||
|      * | ||||
|      * @see DefaultImageMediaDecoder | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public ImagesPlugin defaultMediaDecoder(@NonNull MediaDecoder mediaDecoder) { | ||||
|     public ImagesPlugin defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { | ||||
|         checkBuilderState(); | ||||
|         builder.defaultMediaDecoder(mediaDecoder); | ||||
|         return this; | ||||
|  | ||||
| @ -7,6 +7,7 @@ import java.io.InputStream; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| import okhttp3.Call; | ||||
| import okhttp3.OkHttpClient; | ||||
| import okhttp3.Request; | ||||
| import okhttp3.Response; | ||||
| @ -24,21 +25,31 @@ public class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static OkHttpNetworkSchemeHandler create() { | ||||
|         return new OkHttpNetworkSchemeHandler(new OkHttpClient()); | ||||
|         return create(new OkHttpClient()); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     public static OkHttpNetworkSchemeHandler create(@NonNull OkHttpClient client) { | ||||
|         return new OkHttpNetworkSchemeHandler(client); | ||||
|         // explicit cast, otherwise a recursive call | ||||
|         return create((Call.Factory) client); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @since 4.0.0-SNAPSHOT | ||||
|      */ | ||||
|     @NonNull | ||||
|     public static OkHttpNetworkSchemeHandler create(@NonNull Call.Factory factory) { | ||||
|         return new OkHttpNetworkSchemeHandler(factory); | ||||
|     } | ||||
| 
 | ||||
|     private static final String HEADER_CONTENT_TYPE = "Content-Type"; | ||||
| 
 | ||||
|     private final OkHttpClient client; | ||||
|     // @since 4.0.0-SNAPSHOT, previously just OkHttpClient | ||||
|     private final Call.Factory factory; | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     OkHttpNetworkSchemeHandler(@NonNull OkHttpClient client) { | ||||
|         this.client = client; | ||||
|     OkHttpNetworkSchemeHandler(@NonNull Call.Factory factory) { | ||||
|         this.factory = factory; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
| @ -52,7 +63,7 @@ public class OkHttpNetworkSchemeHandler extends SchemeHandler { | ||||
| 
 | ||||
|         final Response response; | ||||
|         try { | ||||
|             response = client.newCall(request).execute(); | ||||
|             response = factory.newCall(request).execute(); | ||||
|         } catch (Throwable t) { | ||||
|             throw new IllegalStateException("Exception obtaining network resource: " + raw, t); | ||||
|         } | ||||
|  | ||||
| @ -0,0 +1,99 @@ | ||||
| package ru.noties.markwon.image; | ||||
| 
 | ||||
| import android.os.Handler; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.junit.Before; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| import org.robolectric.RobolectricTestRunner; | ||||
| import org.robolectric.annotation.Config; | ||||
| 
 | ||||
| import java.util.concurrent.ExecutorService; | ||||
| 
 | ||||
| import static org.mockito.ArgumentMatchers.any; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| 
 | ||||
| @RunWith(RobolectricTestRunner.class) | ||||
| @Config(manifest = Config.NONE) | ||||
| public class AsyncDrawableLoaderImplTest { | ||||
| 
 | ||||
|     private BuilderImpl builder; | ||||
|     private AsyncDrawableLoaderImpl impl; | ||||
| 
 | ||||
|     @Before | ||||
|     public void before() { | ||||
|         builder = new BuilderImpl(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void placeholder() { | ||||
|         final ImagesPlugin.PlaceholderProvider placeholderProvider = | ||||
|                 mock(ImagesPlugin.PlaceholderProvider.class); | ||||
|         impl = builder.placeholderProvider(placeholderProvider) | ||||
|                 .build(); | ||||
|         impl.placeholder(mock(AsyncDrawable.class)); | ||||
|         verify(placeholderProvider, times(1)) | ||||
|                 .providePlaceholder(any(AsyncDrawable.class)); | ||||
|     } | ||||
| 
 | ||||
|     private static class BuilderImpl { | ||||
| 
 | ||||
|         AsyncDrawableLoaderBuilder builder; | ||||
|         Handler handler = mock(Handler.class); | ||||
| 
 | ||||
|         public BuilderImpl executorService(@NonNull ExecutorService executorService) { | ||||
|             builder.executorService(executorService); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl addSchemeHandler(@NonNull SchemeHandler schemeHandler) { | ||||
|             builder.addSchemeHandler(schemeHandler); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl addMediaDecoder(@NonNull MediaDecoder mediaDecoder) { | ||||
|             builder.addMediaDecoder(mediaDecoder); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl defaultMediaDecoder(@Nullable MediaDecoder mediaDecoder) { | ||||
|             builder.defaultMediaDecoder(mediaDecoder); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl removeSchemeHandler(@NonNull String scheme) { | ||||
|             builder.removeSchemeHandler(scheme); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl removeMediaDecoder(@NonNull String contentType) { | ||||
|             builder.removeMediaDecoder(contentType); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl placeholderProvider(@NonNull ImagesPlugin.PlaceholderProvider placeholderDrawableProvider) { | ||||
|             builder.placeholderProvider(placeholderDrawableProvider); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public BuilderImpl errorHandler(@NonNull ImagesPlugin.ErrorHandler errorHandler) { | ||||
|             builder.errorHandler(errorHandler); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         public BuilderImpl handler(Handler handler) { | ||||
|             this.handler = handler; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         @NonNull | ||||
|         AsyncDrawableLoaderImpl build() { | ||||
|             return new AsyncDrawableLoaderImpl(builder, handler); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov