From de8839ae792990751258f389c29b2d27789cca04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Thu, 31 Oct 2024 23:27:20 +0100 Subject: [PATCH 1/4] feat(image): adding `only-if-cached` cache control option --- .../Libraries/Image/ImageSource.d.ts | 2 +- .../Libraries/Image/ImageSource.js | 2 +- .../react/modules/fresco/ImageCacheControl.kt | 7 ++++ .../fresco/ReactOkHttpNetworkFetcher.kt | 3 ++ .../react/views/image/ReactImageView.kt | 37 +++++++++++++++++++ .../js/examples/Image/ImageExample.js | 20 ++++++++-- 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/react-native/Libraries/Image/ImageSource.d.ts b/packages/react-native/Libraries/Image/ImageSource.d.ts index 1b7d51729b5d42..2d47b6ba194b09 100644 --- a/packages/react-native/Libraries/Image/ImageSource.d.ts +++ b/packages/react-native/Libraries/Image/ImageSource.d.ts @@ -51,7 +51,7 @@ export interface ImageURISource { * to a URL load request, no attempt is made to load the data from the originating source, * and the load is considered to have failed. * - * @platform ios (for `force-cache` and `only-if-cached`) + * @platform ios (for `force-cache`) */ cache?: 'default' | 'reload' | 'force-cache' | 'only-if-cached' | undefined; /** diff --git a/packages/react-native/Libraries/Image/ImageSource.js b/packages/react-native/Libraries/Image/ImageSource.js index a489de2ff07409..1698c945b0b8e1 100644 --- a/packages/react-native/Libraries/Image/ImageSource.js +++ b/packages/react-native/Libraries/Image/ImageSource.js @@ -66,7 +66,7 @@ export interface ImageURISource { * to a URL load request, no attempt is made to load the data from the originating source, * and the load is considered to have failed. * - * @platform ios (for `force-cache` and `only-if-cached`) + * @platform ios (for `force-cache`) */ +cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ImageCacheControl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ImageCacheControl.kt index 689edb1d6f013d..f6f9d4e85d1f64 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ImageCacheControl.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ImageCacheControl.kt @@ -15,4 +15,11 @@ public enum class ImageCacheControl { * be used to satisfy a URL load request. */ RELOAD, + /** + * The existing cache data will be used to satisfy a request, regardless of + * its age or expiration date. If there is no existing data in the cache corresponding + * to a URL load request, no attempt is made to load the data from the originating source, + * and the load is considered to have failed. + */ + ONLY_IF_CACHED, } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ReactOkHttpNetworkFetcher.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ReactOkHttpNetworkFetcher.kt index 49717cfa51ab92..502f220a090695 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ReactOkHttpNetworkFetcher.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/ReactOkHttpNetworkFetcher.kt @@ -43,6 +43,9 @@ internal class ReactOkHttpNetworkFetcher(private val okHttpClient: OkHttpClient) ImageCacheControl.RELOAD -> { cacheControlBuilder.noCache() } + ImageCacheControl.ONLY_IF_CACHED -> { + cacheControlBuilder.onlyIfCached() + } ImageCacheControl.DEFAULT -> { // No-op } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index b384b5e2f2e651..67e1e7446328e6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -22,8 +22,12 @@ import android.graphics.Shader.TileMode import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.net.Uri +import com.facebook.common.executors.CallerThreadExecutor import com.facebook.common.references.CloseableReference import com.facebook.common.util.UriUtil +import com.facebook.datasource.DataSource +import com.facebook.datasource.BaseDataSubscriber +import com.facebook.datasource.DataSubscriber import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder import com.facebook.drawee.controller.ControllerListener @@ -311,6 +315,7 @@ public class ReactImageView( null, "default" -> ImageCacheControl.DEFAULT "reload" -> ImageCacheControl.RELOAD + "only-if-cached" -> ImageCacheControl.ONLY_IF_CACHED else -> ImageCacheControl.DEFAULT } } @@ -439,6 +444,38 @@ public class ReactImageView( imagePipeline.evictFromCache(uri) } + if (cacheControl == ImageCacheControl.ONLY_IF_CACHED) { + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.isInDiskCache(uri) + + dataSource.subscribe(object : BaseDataSubscriber() { + override fun onNewResultImpl(dataSource: DataSource) { + if (!dataSource.isFinished()) return + + val isInCache: Boolean = dataSource.getResult() ?: false + + if (isInCache) { + setupImageRequest(uri, cacheControl, postprocessor, resizeOptions) + } + + dataSource.close() + } + + override fun onFailureImpl(dataSource: DataSource) {} + }, CallerThreadExecutor.getInstance()) + + return + } + + setupImageRequest(uri, cacheControl, postprocessor, resizeOptions) + } + + private fun setupImageRequest( + uri: Uri, + cacheControl: ImageCacheControl, + postprocessor: Postprocessor?, + resizeOptions: ResizeOptions? + ) { val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(uri) .setPostprocessor(postprocessor) diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 814c7312ea775d..c47c9d43a43e65 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -659,6 +659,19 @@ function CacheControlAndroidExample(): React.Node { key={reload} /> + + + Only-if-cached + + + @@ -1091,9 +1104,10 @@ exports.examples = [ }, { title: 'Cache Policy', - description: ('First image will be loaded and will be cached. ' + - 'Second image is the same but will be reloaded if re-rendered ' + - 'as the cache policy is set to reload.': string), + description: `- First image will be loaded and cached. +- Second image is the same but will be reloaded if re-rendered as the cache policy is set to reload. +- Third image will never be loaded as the cache policy is set to only-if-cached and the image has not been loaded before. + `, render: function (): React.Node { return ; }, From d01ccb83a3e20cb80241fd80840c0541f60d087c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Fri, 1 Nov 2024 17:23:03 +0100 Subject: [PATCH 2/4] fix(image): [android] adding error handling when there is no cache --- .../react/views/image/ReactImageView.kt | 20 ++++++++++++++++++- .../js/examples/Image/ImageExample.js | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 67e1e7446328e6..ef6a32b6400891 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -456,12 +456,30 @@ public class ReactImageView( if (isInCache) { setupImageRequest(uri, cacheControl, postprocessor, resizeOptions) + } else { + val eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) + eventDispatcher?.dispatchEvent( + createErrorEvent( + UIManagerHelper.getSurfaceId(this@ReactImageView), + id, + Exception("Image not found in cache for uri $uri") + ) + ) } dataSource.close() } - override fun onFailureImpl(dataSource: DataSource) {} + override fun onFailureImpl(dataSource: DataSource) { + val failureCause = dataSource.failureCause ?: Throwable("Unknown error occurred while checking if image is in cache") + + val eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) + eventDispatcher?.dispatchEvent( + createErrorEvent( + UIManagerHelper.getSurfaceId(this@ReactImageView), id, failureCause)) + } }, CallerThreadExecutor.getInstance()) return diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index c47c9d43a43e65..81e42ad8adb72e 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -670,6 +670,7 @@ function CacheControlAndroidExample(): React.Node { }} style={styles.base} key={reload} + onError={e => console.log(e.nativeEvent.error)} /> From 0e36839d89f4f5f3fa8184ad3bb1d44a066250ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Fri, 1 Nov 2024 17:57:32 +0100 Subject: [PATCH 3/4] fix(image): [android] checking for bitmap and encoded memory caches next to disk check --- .../react/views/image/ReactImageView.kt | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index ef6a32b6400891..b9f5e114763443 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -25,8 +25,8 @@ import android.net.Uri import com.facebook.common.executors.CallerThreadExecutor import com.facebook.common.references.CloseableReference import com.facebook.common.util.UriUtil -import com.facebook.datasource.DataSource import com.facebook.datasource.BaseDataSubscriber +import com.facebook.datasource.DataSource import com.facebook.datasource.DataSubscriber import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder @@ -439,6 +439,13 @@ public class ReactImageView( val resizeOptions = if (doResize) resizeOptions else null + val imageRequestBuilder = + ImageRequestBuilder.newBuilderWithSource(uri) + .setPostprocessor(postprocessor) + .setResizeOptions(resizeOptions) + .setAutoRotateEnabled(true) + .setProgressiveRenderingEnabled(progressiveRenderingEnabled) + if (cacheControl == ImageCacheControl.RELOAD) { val imagePipeline = Fresco.getImagePipeline() imagePipeline.evictFromCache(uri) @@ -446,16 +453,20 @@ public class ReactImageView( if (cacheControl == ImageCacheControl.ONLY_IF_CACHED) { val imagePipeline = Fresco.getImagePipeline() - val dataSource = imagePipeline.isInDiskCache(uri) + val imageRequest = imageRequestBuilder.build() + val dataSource = imagePipeline.isInDiskCache(imageRequest) + val inBitmapCache = imagePipeline.isInBitmapMemoryCache(imageRequest) + val inEncodedMemoryCache = imagePipeline.isInEncodedMemoryCache(imageRequest) dataSource.subscribe(object : BaseDataSubscriber() { override fun onNewResultImpl(dataSource: DataSource) { if (!dataSource.isFinished()) return - val isInCache: Boolean = dataSource.getResult() ?: false + val isInDiskCache: Boolean = dataSource.getResult() ?: false + val isInCache: Boolean = inBitmapCache || inEncodedMemoryCache || isInDiskCache if (isInCache) { - setupImageRequest(uri, cacheControl, postprocessor, resizeOptions) + setupImageRequest(imageRequestBuilder, uri, cacheControl, postprocessor, resizeOptions) } else { val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) @@ -472,35 +483,32 @@ public class ReactImageView( } override fun onFailureImpl(dataSource: DataSource) { - val failureCause = dataSource.failureCause ?: Throwable("Unknown error occurred while checking if image is in cache") + val failureCause = dataSource.failureCause + ?: Throwable("Unknown error occurred while checking if image is in cache for uri $uri") val eventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) + UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) eventDispatcher?.dispatchEvent( - createErrorEvent( - UIManagerHelper.getSurfaceId(this@ReactImageView), id, failureCause)) + createErrorEvent( + UIManagerHelper.getSurfaceId(this@ReactImageView), id, failureCause + ) + ) } }, CallerThreadExecutor.getInstance()) return } - setupImageRequest(uri, cacheControl, postprocessor, resizeOptions) + setupImageRequest(imageRequestBuilder, uri, cacheControl, postprocessor, resizeOptions) } private fun setupImageRequest( + imageRequestBuilder: ImageRequestBuilder, uri: Uri, cacheControl: ImageCacheControl, postprocessor: Postprocessor?, resizeOptions: ResizeOptions? ) { - val imageRequestBuilder = - ImageRequestBuilder.newBuilderWithSource(uri) - .setPostprocessor(postprocessor) - .setResizeOptions(resizeOptions) - .setAutoRotateEnabled(true) - .setProgressiveRenderingEnabled(progressiveRenderingEnabled) - if (resizeMethod == ImageResizeMethod.NONE) { imageRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER) } From acad3d24897880535b55d3ed610d9a570bb7707a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Sat, 2 Nov 2024 17:21:16 +0100 Subject: [PATCH 4/4] fix(image): [android] optimising request using lowest permitted request level --- .../react/views/image/ReactImageView.kt | 84 ++++--------------- 1 file changed, 16 insertions(+), 68 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index b9f5e114763443..5365d689f5f01b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -22,12 +22,8 @@ import android.graphics.Shader.TileMode import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.net.Uri -import com.facebook.common.executors.CallerThreadExecutor import com.facebook.common.references.CloseableReference import com.facebook.common.util.UriUtil -import com.facebook.datasource.BaseDataSubscriber -import com.facebook.datasource.DataSource -import com.facebook.datasource.DataSubscriber import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder import com.facebook.drawee.controller.ControllerListener @@ -45,6 +41,7 @@ import com.facebook.imagepipeline.image.ImageInfo import com.facebook.imagepipeline.postprocessors.IterativeBoxBlurPostProcessor import com.facebook.imagepipeline.request.BasePostprocessor import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequest.RequestLevel import com.facebook.imagepipeline.request.ImageRequestBuilder import com.facebook.imagepipeline.request.Postprocessor import com.facebook.react.bridge.ReactContext @@ -320,6 +317,13 @@ public class ReactImageView( } } + private fun computeRequestLevel(cacheControl: ImageCacheControl): RequestLevel { + return when (cacheControl) { + ImageCacheControl.ONLY_IF_CACHED -> RequestLevel.DISK_CACHE + else -> RequestLevel.FULL_FETCH + } + } + public fun setDefaultSource(name: String?) { val newDefaultDrawable = instance.getResourceDrawable(context, name) if (defaultImageDrawable != newDefaultDrawable) { @@ -431,6 +435,7 @@ public class ReactImageView( val imageSource = this.imageSource ?: return val uri = imageSource.uri val cacheControl = imageSource.cacheControl + val requestLevel = computeRequestLevel(cacheControl) val postprocessorList = mutableListOf() iterativeBoxBlurPostProcessor?.let { postprocessorList.add(it) } @@ -439,76 +444,19 @@ public class ReactImageView( val resizeOptions = if (doResize) resizeOptions else null - val imageRequestBuilder = - ImageRequestBuilder.newBuilderWithSource(uri) - .setPostprocessor(postprocessor) - .setResizeOptions(resizeOptions) - .setAutoRotateEnabled(true) - .setProgressiveRenderingEnabled(progressiveRenderingEnabled) - if (cacheControl == ImageCacheControl.RELOAD) { val imagePipeline = Fresco.getImagePipeline() imagePipeline.evictFromCache(uri) } - if (cacheControl == ImageCacheControl.ONLY_IF_CACHED) { - val imagePipeline = Fresco.getImagePipeline() - val imageRequest = imageRequestBuilder.build() - val dataSource = imagePipeline.isInDiskCache(imageRequest) - val inBitmapCache = imagePipeline.isInBitmapMemoryCache(imageRequest) - val inEncodedMemoryCache = imagePipeline.isInEncodedMemoryCache(imageRequest) - - dataSource.subscribe(object : BaseDataSubscriber() { - override fun onNewResultImpl(dataSource: DataSource) { - if (!dataSource.isFinished()) return - - val isInDiskCache: Boolean = dataSource.getResult() ?: false - val isInCache: Boolean = inBitmapCache || inEncodedMemoryCache || isInDiskCache - - if (isInCache) { - setupImageRequest(imageRequestBuilder, uri, cacheControl, postprocessor, resizeOptions) - } else { - val eventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) - eventDispatcher?.dispatchEvent( - createErrorEvent( - UIManagerHelper.getSurfaceId(this@ReactImageView), - id, - Exception("Image not found in cache for uri $uri") - ) - ) - } - - dataSource.close() - } - - override fun onFailureImpl(dataSource: DataSource) { - val failureCause = dataSource.failureCause - ?: Throwable("Unknown error occurred while checking if image is in cache for uri $uri") - - val eventDispatcher = - UIManagerHelper.getEventDispatcherForReactTag(context as ReactContext, id) - eventDispatcher?.dispatchEvent( - createErrorEvent( - UIManagerHelper.getSurfaceId(this@ReactImageView), id, failureCause - ) - ) - } - }, CallerThreadExecutor.getInstance()) - - return - } - - setupImageRequest(imageRequestBuilder, uri, cacheControl, postprocessor, resizeOptions) - } + val imageRequestBuilder = + ImageRequestBuilder.newBuilderWithSource(uri) + .setPostprocessor(postprocessor) + .setResizeOptions(resizeOptions) + .setAutoRotateEnabled(true) + .setProgressiveRenderingEnabled(progressiveRenderingEnabled) + .setLowestPermittedRequestLevel(requestLevel) - private fun setupImageRequest( - imageRequestBuilder: ImageRequestBuilder, - uri: Uri, - cacheControl: ImageCacheControl, - postprocessor: Postprocessor?, - resizeOptions: ResizeOptions? - ) { if (resizeMethod == ImageResizeMethod.NONE) { imageRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER) }