diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java deleted file mode 100644 index bbb3b73a8..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java +++ /dev/null @@ -1,236 +0,0 @@ -package fr.free.nrw.commons.media; - -import android.net.Uri; -import android.os.Looper; -import android.os.SystemClock; -import androidx.annotation.Nullable; -import com.facebook.imagepipeline.common.BytesRange; -import com.facebook.imagepipeline.image.EncodedImage; -import com.facebook.imagepipeline.producers.BaseNetworkFetcher; -import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks; -import com.facebook.imagepipeline.producers.Consumer; -import com.facebook.imagepipeline.producers.FetchState; -import com.facebook.imagepipeline.producers.NetworkFetcher; -import com.facebook.imagepipeline.producers.ProducerContext; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executor; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import okhttp3.CacheControl; -import okhttp3.Call; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import timber.log.Timber; - -// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled -// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java -@Singleton -public class CustomOkHttpNetworkFetcher - extends BaseNetworkFetcher { - - private static final String QUEUE_TIME = "queue_time"; - private static final String FETCH_TIME = "fetch_time"; - private static final String TOTAL_TIME = "total_time"; - private static final String IMAGE_SIZE = "image_size"; - private final Call.Factory mCallFactory; - private final @Nullable - CacheControl mCacheControl; - private final Executor mCancellationExecutor; - private final JsonKvStore defaultKvStore; - - /** - * @param okHttpClient client to use - */ - @Inject - public CustomOkHttpNetworkFetcher(final OkHttpClient okHttpClient, - @Named("default_preferences") final JsonKvStore defaultKvStore) { - this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore); - } - - /** - * @param callFactory custom {@link Call.Factory} for fetching image from the network - * @param cancellationExecutor executor on which fetching cancellation is performed if - * cancellation is requested from the UI Thread - */ - public CustomOkHttpNetworkFetcher(final Call.Factory callFactory, - final Executor cancellationExecutor, - final JsonKvStore defaultKvStore) { - this(callFactory, cancellationExecutor, defaultKvStore, true); - } - - /** - * @param callFactory custom {@link Call.Factory} for fetching image from the network - * @param cancellationExecutor executor on which fetching cancellation is performed if - * cancellation is requested from the UI Thread - * @param disableOkHttpCache true if network requests should not be cached by OkHttp - */ - public CustomOkHttpNetworkFetcher( - final Call.Factory callFactory, final Executor cancellationExecutor, - final JsonKvStore defaultKvStore, - final boolean disableOkHttpCache) { - this.defaultKvStore = defaultKvStore; - mCallFactory = callFactory; - mCancellationExecutor = cancellationExecutor; - mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null; - } - - @Override - public OkHttpNetworkFetchState createFetchState( - final Consumer consumer, final ProducerContext context) { - return new OkHttpNetworkFetchState(consumer, context); - } - - @Override - public void fetch( - final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) { - fetchState.submitTime = SystemClock.elapsedRealtime(); - final Uri uri = fetchState.getUri(); - - try { - if (defaultKvStore - .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) { - Timber.d("Skipping loading of image as limited connection mode is enabled"); - callback.onFailure( - new Exception("Failing image request as limited connection mode is enabled")); - return; - } - final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get(); - - if (mCacheControl != null) { - requestBuilder.cacheControl(mCacheControl); - } - - final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange(); - if (bytesRange != null) { - requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue()); - } - - fetchWithRequest(fetchState, callback, requestBuilder.build()); - } catch (final Exception e) { - // handle error while creating the request - callback.onFailure(e); - } - } - - @Override - public void onFetchCompletion(final OkHttpNetworkFetchState fetchState, final int byteSize) { - fetchState.fetchCompleteTime = SystemClock.elapsedRealtime(); - } - - @Override - public Map getExtraMap(final OkHttpNetworkFetchState fetchState, - final int byteSize) { - final Map extraMap = new HashMap<>(4); - extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime)); - extraMap - .put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime)); - extraMap - .put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime)); - extraMap.put(IMAGE_SIZE, Integer.toString(byteSize)); - return extraMap; - } - - protected void fetchWithRequest( - final OkHttpNetworkFetchState fetchState, - final NetworkFetcher.Callback callback, - final Request request) { - final Call call = mCallFactory.newCall(request); - - fetchState - .getContext() - .addCallbacks( - new BaseProducerContextCallbacks() { - @Override - public void onCancellationRequested() { - onFetchCancellationRequested(call); - } - }); - - call.enqueue( - new okhttp3.Callback() { - @Override - public void onResponse(final Call call, final Response response) { - onFetchResponse(fetchState, call, response, callback); - } - - @Override - public void onFailure(final Call call, final IOException e) { - handleException(call, e, callback); - } - }); - } - - private void onFetchCancellationRequested(final Call call) { - if (Looper.myLooper() != Looper.getMainLooper()) { - call.cancel(); - } else { - mCancellationExecutor.execute(call::cancel); - } - } - - private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call, - final Response response, - final NetworkFetcher.Callback callback) { - fetchState.responseTime = SystemClock.elapsedRealtime(); - try (final ResponseBody body = response.body()) { - if (!response.isSuccessful()) { - handleException( - call, new IOException("Unexpected HTTP code " + response), - callback); - return; - } - - final BytesRange responseRange = - BytesRange.fromContentRangeHeader(response.header("Content-Range")); - if (responseRange != null - && !(responseRange.from == 0 - && responseRange.to == BytesRange.TO_END_OF_CONTENT)) { - // Only treat as a partial image if the range is not all of the content - fetchState.setResponseBytesRange(responseRange); - fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT); - } - - long contentLength = body.contentLength(); - if (contentLength < 0) { - contentLength = 0; - } - callback.onResponse(body.byteStream(), (int) contentLength); - } catch (final Exception e) { - handleException(call, e, callback); - } - } - - /** - * Handles exceptions. - * - *

OkHttp notifies callers of cancellations via an IOException. If IOException is caught - * after request cancellation, then the exception is interpreted as successful cancellation and - * onCancellation is called. Otherwise onFailure is called. - */ - private void handleException(final Call call, final Exception e, final Callback callback) { - if (call.isCanceled()) { - callback.onCancellation(); - } else { - callback.onFailure(e); - } - } - - public static class OkHttpNetworkFetchState extends FetchState { - - public long submitTime; - public long responseTime; - public long fetchCompleteTime; - - public OkHttpNetworkFetchState( - final Consumer consumer, final ProducerContext producerContext) { - super(consumer, producerContext); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt new file mode 100644 index 000000000..c8de4022b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt @@ -0,0 +1,199 @@ +package fr.free.nrw.commons.media + +import android.os.Looper +import android.os.SystemClock +import com.facebook.imagepipeline.common.BytesRange +import com.facebook.imagepipeline.image.EncodedImage +import com.facebook.imagepipeline.producers.BaseNetworkFetcher +import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks +import com.facebook.imagepipeline.producers.Consumer +import com.facebook.imagepipeline.producers.FetchState +import com.facebook.imagepipeline.producers.NetworkFetcher +import com.facebook.imagepipeline.producers.ProducerContext +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.kvstore.JsonKvStore +import okhttp3.CacheControl +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import timber.log.Timber +import java.io.IOException +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled +// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java +@Singleton +class CustomOkHttpNetworkFetcher +@JvmOverloads constructor( + private val mCallFactory: Call.Factory, + private val mCancellationExecutor: Executor, + private val defaultKvStore: JsonKvStore, + disableOkHttpCache: Boolean = true +) : BaseNetworkFetcher() { + + private val mCacheControl = + if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null + private val isLimitedConnectionMode: Boolean + get() = defaultKvStore.getBoolean( + CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, + false + ) + + /** + * @param okHttpClient client to use + */ + @Inject + constructor( + okHttpClient: OkHttpClient, + @Named("default_preferences") defaultKvStore: JsonKvStore + ) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore) + + /** + * @param mCallFactory custom [Call.Factory] for fetching image from the network + * @param mCancellationExecutor executor on which fetching cancellation is performed if + * cancellation is requested from the UI Thread + * @param disableOkHttpCache true if network requests should not be cached by OkHttp + */ + override fun createFetchState(consumer: Consumer, context: ProducerContext) = + OkHttpNetworkFetchState(consumer, context) + + override fun fetch( + fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback + ) { + fetchState.submitTime = SystemClock.elapsedRealtime() + + try { + if (isLimitedConnectionMode) { + Timber.d("Skipping loading of image as limited connection mode is enabled") + callback.onFailure(Exception("Failing image request as limited connection mode is enabled")) + return + } + + val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get() + + if (mCacheControl != null) { + requestBuilder.cacheControl(mCacheControl) + } + + val bytesRange = fetchState.context.imageRequest.bytesRange + if (bytesRange != null) { + requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue()) + } + + fetchWithRequest(fetchState, callback, requestBuilder.build()) + } catch (e: Exception) { + // handle error while creating the request + callback.onFailure(e) + } + } + + override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) { + fetchState.fetchCompleteTime = SystemClock.elapsedRealtime() + } + + override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) = + fetchState.toExtraMap(byteSize) + + private fun fetchWithRequest( + fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request + ) { + val call = mCallFactory.newCall(request) + + fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() { + override fun onCancellationRequested() { + onFetchCancellationRequested(call) + } + }) + + call.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) = + onFetchResponse(fetchState, call, response, callback) + + override fun onFailure(call: Call, e: IOException) = + handleException(call, e, callback) + }) + } + + private fun onFetchCancellationRequested(call: Call) { + if (Looper.myLooper() != Looper.getMainLooper()) { + call.cancel() + } else { + mCancellationExecutor.execute { call.cancel() } + } + } + + private fun onFetchResponse( + fetchState: OkHttpNetworkFetchState, + call: Call, + response: Response, + callback: NetworkFetcher.Callback + ) { + fetchState.responseTime = SystemClock.elapsedRealtime() + try { + response.body.use { body -> + if (!response.isSuccessful) { + handleException(call, IOException("Unexpected HTTP code $response"), callback) + return + } + val responseRange = + BytesRange.fromContentRangeHeader(response.header("Content-Range")) + if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) { + // Only treat as a partial image if the range is not all of the content + fetchState.responseBytesRange = responseRange + fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT + } + + var contentLength = body!!.contentLength() + if (contentLength < 0) { + contentLength = 0 + } + callback.onResponse(body.byteStream(), contentLength.toInt()) + } + } catch (e: Exception) { + handleException(call, e, callback) + } + } + + /** + * Handles exceptions. + * + * OkHttp notifies callers of cancellations via an IOException. If IOException is caught + * after request cancellation, then the exception is interpreted as successful cancellation and + * onCancellation is called. Otherwise onFailure is called. + */ + private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) { + if (call.isCanceled()) { + callback.onCancellation() + } else { + callback.onFailure(e) + } + } +} + +class OkHttpNetworkFetchState( + consumer: Consumer?, producerContext: ProducerContext? +) : FetchState(consumer, producerContext) { + var submitTime: Long = 0 + var responseTime: Long = 0 + var fetchCompleteTime: Long = 0 + + fun toExtraMap(byteSize: Int) = buildMap { + put(QUEUE_TIME, (responseTime - submitTime).toString()) + put(FETCH_TIME, (fetchCompleteTime - responseTime).toString()) + put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString()) + put(IMAGE_SIZE, byteSize.toString()) + } + + companion object { + private const val QUEUE_TIME = "queue_time" + private const val FETCH_TIME = "fetch_time" + private const val TOTAL_TIME = "total_time" + private const val IMAGE_SIZE = "image_size" + } +} + diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt index 16a35a67b..225b4bd80 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt @@ -32,7 +32,7 @@ import java.util.concurrent.Executor class CustomOkHttpNetworkFetcherUnitTest { private lateinit var fetcher: CustomOkHttpNetworkFetcher private lateinit var okHttpClient: OkHttpClient - private lateinit var state: CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState + private lateinit var state: OkHttpNetworkFetchState @Mock private lateinit var callback: NetworkFetcher.Callback @@ -162,7 +162,7 @@ class CustomOkHttpNetworkFetcherUnitTest { val method: Method = CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( "onFetchResponse", - CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, + OkHttpNetworkFetchState::class.java, Call::class.java, Response::class.java, NetworkFetcher.Callback::class.java, @@ -196,7 +196,7 @@ class CustomOkHttpNetworkFetcherUnitTest { val method: Method = CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( "onFetchResponse", - CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, + OkHttpNetworkFetchState::class.java, Call::class.java, Response::class.java, NetworkFetcher.Callback::class.java, @@ -230,7 +230,7 @@ class CustomOkHttpNetworkFetcherUnitTest { val method: Method = CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( "onFetchResponse", - CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, + OkHttpNetworkFetchState::class.java, Call::class.java, Response::class.java, NetworkFetcher.Callback::class.java,