From a5bce07af1917894d764c11c92b50f15a4561377 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Sun, 5 May 2024 03:36:49 -0700 Subject: [PATCH] Migrate ImageLoaderModule to Kotlin (#44413) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44413 ## Changelog: [Internal] - As in the title, the corresponding module is migrated from Java to Kotlin. Differential Revision: D56978931 --- .../ReactAndroid/api/ReactAndroid.api | 7 +- .../modules/image/ImageLoaderModule.java | 319 ------------------ .../react/modules/image/ImageLoaderModule.kt | 305 +++++++++++++++++ 3 files changed, 311 insertions(+), 320 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 98aa97e32fa213..d4e9ddf113cb41 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3343,7 +3343,9 @@ public class com/facebook/react/modules/i18nmanager/I18nUtil { public fun swapLeftAndRightInRTL (Landroid/content/Context;Z)V } -public class com/facebook/react/modules/image/ImageLoaderModule : com/facebook/fbreact/specs/NativeImageLoaderAndroidSpec, com/facebook/react/bridge/LifecycleEventListener { +public final class com/facebook/react/modules/image/ImageLoaderModule : com/facebook/fbreact/specs/NativeImageLoaderAndroidSpec, com/facebook/react/bridge/LifecycleEventListener { + public static final field Companion Lcom/facebook/react/modules/image/ImageLoaderModule$Companion; + public static final field NAME Ljava/lang/String; public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun (Lcom/facebook/react/bridge/ReactApplicationContext;Lcom/facebook/imagepipeline/core/ImagePipeline;Lcom/facebook/react/views/image/ReactCallerContextFactory;)V public fun (Lcom/facebook/react/bridge/ReactApplicationContext;Ljava/lang/Object;)V @@ -3357,6 +3359,9 @@ public class com/facebook/react/modules/image/ImageLoaderModule : com/facebook/f public fun queryCache (Lcom/facebook/react/bridge/ReadableArray;Lcom/facebook/react/bridge/Promise;)V } +public final class com/facebook/react/modules/image/ImageLoaderModule$Companion { +} + public class com/facebook/react/modules/intent/IntentModule : com/facebook/fbreact/specs/NativeIntentAndroidSpec { public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun canOpenURL (Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java deleted file mode 100644 index 362bf75d40f540..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.image; - -import android.net.Uri; -import android.text.TextUtils; -import android.util.SparseArray; -import androidx.annotation.Nullable; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -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.fbreact.specs.NativeImageLoaderAndroidSpec; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.GuardedAsyncTask; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.module.annotations.ReactModule; -import com.facebook.react.modules.fresco.ReactNetworkImageRequest; -import com.facebook.react.views.image.ReactCallerContextFactory; -import com.facebook.react.views.imagehelper.ImageSource; - -@ReactModule(name = NativeImageLoaderAndroidSpec.NAME) -public class ImageLoaderModule extends NativeImageLoaderAndroidSpec - implements LifecycleEventListener { - - private static final String ERROR_INVALID_URI = "E_INVALID_URI"; - private static final String ERROR_PREFETCH_FAILURE = "E_PREFETCH_FAILURE"; - private static final String ERROR_GET_SIZE_FAILURE = "E_GET_SIZE_FAILURE"; - - private @Nullable final Object mCallerContext; - private final Object mEnqueuedRequestMonitor = new Object(); - private final SparseArray> mEnqueuedRequests = new SparseArray<>(); - private @Nullable ImagePipeline mImagePipeline = null; - private @Nullable ReactCallerContextFactory mCallerContextFactory; - - public ImageLoaderModule(ReactApplicationContext reactContext) { - super(reactContext); - mCallerContext = this; - } - - public ImageLoaderModule(ReactApplicationContext reactContext, Object callerContext) { - super(reactContext); - mCallerContext = callerContext; - } - - public ImageLoaderModule( - ReactApplicationContext reactContext, - ImagePipeline imagePipeline, - ReactCallerContextFactory callerContextFactory) { - super(reactContext); - mCallerContextFactory = callerContextFactory; - mImagePipeline = imagePipeline; - mCallerContext = null; - } - - private @Nullable Object getCallerContext() { - return mCallerContextFactory != null - ? mCallerContextFactory.getOrCreateCallerContext("", "") - : mCallerContext; - } - - private ImagePipeline getImagePipeline() { - return mImagePipeline != null ? mImagePipeline : Fresco.getImagePipeline(); - } - - /** - * Fetch the width and height of the given image. - * - * @param uriString the URI of the remote image - * @param promise the promise that is fulfilled when operation successfully completed or rejected - * when there is an error - */ - @ReactMethod - public void getSize(final String uriString, final Promise promise) { - if (uriString == null || uriString.isEmpty()) { - promise.reject(ERROR_INVALID_URI, "Cannot get the size of an image for an empty URI"); - return; - } - - ImageSource source = new ImageSource(getReactApplicationContext(), uriString); - ImageRequest request = ImageRequestBuilder.newBuilderWithSource(source.getUri()).build(); - - DataSource> dataSource = - getImagePipeline().fetchDecodedImage(request, getCallerContext()); - - DataSubscriber> dataSubscriber = - new BaseDataSubscriber>() { - @Override - protected void onNewResultImpl( - DataSource> dataSource) { - if (!dataSource.isFinished()) { - return; - } - CloseableReference ref = dataSource.getResult(); - if (ref != null) { - try { - CloseableImage image = ref.get(); - - WritableMap sizes = Arguments.createMap(); - sizes.putInt("width", image.getWidth()); - sizes.putInt("height", image.getHeight()); - - promise.resolve(sizes); - } catch (Exception e) { - promise.reject(ERROR_GET_SIZE_FAILURE, e); - } finally { - CloseableReference.closeSafely(ref); - } - } else { - promise.reject(ERROR_GET_SIZE_FAILURE); - } - } - - @Override - protected void onFailureImpl(DataSource> dataSource) { - promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); - } - }; - dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()); - } - - /** - * Fetch the width and height of the given image with headers. - * - * @param uriString the URI of the remote image - * @param headers headers send with the request - * @param promise the promise that is fulfilled when operation successfully completed or rejected - * when there is an error - */ - @ReactMethod - public void getSizeWithHeaders( - final String uriString, final ReadableMap headers, final Promise promise) { - if (uriString == null || uriString.isEmpty()) { - promise.reject(ERROR_INVALID_URI, "Cannot get the size of an image for an empty URI"); - return; - } - - ImageSource source = new ImageSource(getReactApplicationContext(), uriString); - ImageRequestBuilder imageRequestBuilder = - ImageRequestBuilder.newBuilderWithSource(source.getUri()); - ImageRequest request = - ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers); - - DataSource> dataSource = - getImagePipeline().fetchDecodedImage(request, getCallerContext()); - - DataSubscriber> dataSubscriber = - new BaseDataSubscriber>() { - @Override - protected void onNewResultImpl( - DataSource> dataSource) { - if (!dataSource.isFinished()) { - return; - } - CloseableReference ref = dataSource.getResult(); - if (ref != null) { - try { - CloseableImage image = ref.get(); - - WritableMap sizes = Arguments.createMap(); - sizes.putInt("width", image.getWidth()); - sizes.putInt("height", image.getHeight()); - - promise.resolve(sizes); - } catch (Exception e) { - promise.reject(ERROR_GET_SIZE_FAILURE, e); - } finally { - CloseableReference.closeSafely(ref); - } - } else { - promise.reject(ERROR_GET_SIZE_FAILURE); - } - } - - @Override - protected void onFailureImpl(DataSource> dataSource) { - promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()); - } - }; - dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()); - } - - /** - * Prefetches the given image to the Fresco image disk cache. - * - * @param uriString the URI of the remote image to prefetch - * @param requestIdAsDouble the client-supplied request ID used to identify this request - * @param promise the promise that is fulfilled when the image is successfully prefetched or - * rejected when there is an error - */ - @Override - public void prefetchImage( - final String uriString, final double requestIdAsDouble, final Promise promise) { - final int requestId = (int) requestIdAsDouble; - - if (uriString == null || uriString.isEmpty()) { - promise.reject(ERROR_INVALID_URI, "Cannot prefetch an image for an empty URI"); - return; - } - - Uri uri = Uri.parse(uriString); - ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri).build(); - - DataSource prefetchSource = - getImagePipeline().prefetchToDiskCache(request, getCallerContext()); - DataSubscriber prefetchSubscriber = - new BaseDataSubscriber() { - @Override - protected void onNewResultImpl(DataSource dataSource) { - if (!dataSource.isFinished()) { - return; - } - try { - removeRequest(requestId); - promise.resolve(true); - } catch (Exception e) { - promise.reject(ERROR_PREFETCH_FAILURE, e); - } finally { - dataSource.close(); - } - } - - @Override - protected void onFailureImpl(DataSource dataSource) { - try { - removeRequest(requestId); - promise.reject(ERROR_PREFETCH_FAILURE, dataSource.getFailureCause()); - } finally { - dataSource.close(); - } - } - }; - registerRequest(requestId, prefetchSource); - prefetchSource.subscribe(prefetchSubscriber, CallerThreadExecutor.getInstance()); - } - - @Override - public void abortRequest(double requestId) { - DataSource request = removeRequest((int) requestId); - if (request != null) { - request.close(); - } - } - - @ReactMethod - public void queryCache(final ReadableArray uris, final Promise promise) { - // perform cache interrogation in async task as disk cache checks are expensive - new GuardedAsyncTask(getReactApplicationContext()) { - @Override - protected void doInBackgroundGuarded(Void... params) { - WritableMap result = Arguments.createMap(); - ImagePipeline imagePipeline = getImagePipeline(); - for (int i = 0; i < uris.size(); i++) { - String uriString = uris.getString(i); - if (!TextUtils.isEmpty(uriString)) { - final Uri uri = Uri.parse(uriString); - if (imagePipeline.isInBitmapMemoryCache(uri)) { - result.putString(uriString, "memory"); - } else if (imagePipeline.isInDiskCacheSync(uri)) { - result.putString(uriString, "disk"); - } - } - } - promise.resolve(result); - } - }.executeOnExecutor(GuardedAsyncTask.THREAD_POOL_EXECUTOR); - } - - private void registerRequest(int requestId, DataSource request) { - synchronized (mEnqueuedRequestMonitor) { - mEnqueuedRequests.put(requestId, request); - } - } - - private @Nullable DataSource removeRequest(int requestId) { - synchronized (mEnqueuedRequestMonitor) { - DataSource request = mEnqueuedRequests.get(requestId); - mEnqueuedRequests.remove(requestId); - return request; - } - } - - @Override - public void onHostResume() {} - - @Override - public void onHostPause() {} - - @Override - public void onHostDestroy() { - // cancel all requests - synchronized (mEnqueuedRequestMonitor) { - for (int i = 0, size = mEnqueuedRequests.size(); i < size; i++) { - @Nullable DataSource enqueuedRequest = mEnqueuedRequests.valueAt(i); - if (enqueuedRequest != null) { - enqueuedRequest.close(); - } - } - mEnqueuedRequests.clear(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt new file mode 100644 index 00000000000000..978a8f488f148f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt @@ -0,0 +1,305 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.image + +import android.net.Uri +import android.util.SparseArray +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.references.CloseableReference +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.fbreact.specs.NativeImageLoaderAndroidSpec +import com.facebook.imagepipeline.core.ImagePipeline +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequestBuilder +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.GuardedAsyncTask +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.modules.fresco.ReactNetworkImageRequest +import com.facebook.react.views.image.ReactCallerContextFactory +import com.facebook.react.views.imagehelper.ImageSource + +@ReactModule(name = NativeImageLoaderAndroidSpec.NAME) +public class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventListener { + private var _imagePipeline: ImagePipeline? = null + + private val enqueuedRequestMonitor = Any() + private val enqueuedRequests: SparseArray> = SparseArray>() + private var callerContextFactory: ReactCallerContextFactory? = null + + private val callerContext: Any? + get() = callerContextFactory?.getOrCreateCallerContext("", "") ?: field + + private var imagePipeline: ImagePipeline + get() = _imagePipeline ?: Fresco.getImagePipeline() + set(value) { + _imagePipeline = value + } + + public constructor(reactContext: ReactApplicationContext) : super(reactContext) { + this.callerContext = this + } + + public constructor( + reactContext: ReactApplicationContext, + callerContext: Any? + ) : super(reactContext) { + this.callerContext = callerContext + } + + public constructor( + reactContext: ReactApplicationContext, + imagePipeline: ImagePipeline, + callerContextFactory: ReactCallerContextFactory + ) : super(reactContext) { + this.callerContextFactory = callerContextFactory + this.imagePipeline = imagePipeline + this.callerContext = null + } + + /** + * Fetch the width and height of the given image. + * + * @param uriString the URI of the remote image + * @param promise the promise that is fulfilled when operation successfully completed or rejected + * when there is an error + */ + @ReactMethod + override public fun getSize(uriString: String?, promise: Promise) { + if (uriString == null || uriString.isEmpty()) { + promise.reject(ERROR_INVALID_URI, "Cannot get the size of an image for an empty URI") + return + } + val source = ImageSource(getReactApplicationContext(), uriString) + val request: ImageRequest = ImageRequestBuilder.newBuilderWithSource(source.uri).build() + val dataSource: DataSource> = + this.imagePipeline.fetchDecodedImage(request, this.callerContext) + val dataSubscriber: DataSubscriber> = + object : BaseDataSubscriber>() { + protected override fun onNewResultImpl( + dataSource: DataSource> + ) { + if (!dataSource.isFinished()) { + return + } + val ref = dataSource.getResult() + if (ref != null) { + try { + val image: CloseableImage = ref.get() + val sizes: WritableMap = Arguments.createMap() + sizes.putInt("width", image.getWidth()) + sizes.putInt("height", image.getHeight()) + promise.resolve(sizes) + } catch (e: Exception) { + promise.reject(ERROR_GET_SIZE_FAILURE, e) + } finally { + CloseableReference.closeSafely(ref) + } + } else { + promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") + } + } + + protected override fun onFailureImpl( + dataSource: DataSource> + ) { + promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()) + } + } + dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()) + } + + /** + * Fetch the width and height of the given image with headers. + * + * @param uriString the URI of the remote image + * @param headers headers send with the request + * @param promise the promise that is fulfilled when operation successfully completed or rejected + * when there is an error + */ + @ReactMethod + override public fun getSizeWithHeaders( + uriString: String?, + headers: ReadableMap?, + promise: Promise + ) { + if (uriString == null || uriString.isEmpty()) { + promise.reject(ERROR_INVALID_URI, "Cannot get the size of an image for an empty URI") + return + } + val source = ImageSource(getReactApplicationContext(), uriString) + val imageRequestBuilder: ImageRequestBuilder = + ImageRequestBuilder.newBuilderWithSource(source.uri) + val request: ImageRequest = + ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers) + val dataSource: DataSource> = + this.imagePipeline.fetchDecodedImage(request, this.callerContext) + val dataSubscriber: DataSubscriber> = + object : BaseDataSubscriber>() { + protected override fun onNewResultImpl( + dataSource: DataSource> + ) { + if (!dataSource.isFinished()) { + return + } + val ref = dataSource.getResult() + if (ref != null) { + try { + val image: CloseableImage = ref.get() + val sizes: WritableMap = Arguments.createMap() + sizes.putInt("width", image.getWidth()) + sizes.putInt("height", image.getHeight()) + promise.resolve(sizes) + } catch (e: Exception) { + promise.reject(ERROR_GET_SIZE_FAILURE, e) + } finally { + CloseableReference.closeSafely(ref) + } + } else { + promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") + } + } + + protected override fun onFailureImpl( + dataSource: DataSource> + ) { + promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.getFailureCause()) + } + } + dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()) + } + + /** + * Prefetches the given image to the Fresco image disk cache. + * + * @param uriString the URI of the remote image to prefetch + * @param requestIdAsDouble the client-supplied request ID used to identify this request + * @param promise the promise that is fulfilled when the image is successfully prefetched or + * rejected when there is an error + */ + override public fun prefetchImage( + uriString: String?, + requestIdAsDouble: Double, + promise: Promise + ) { + val requestId = requestIdAsDouble.toInt() + if (uriString == null || uriString.isEmpty()) { + promise.reject(ERROR_INVALID_URI, "Cannot prefetch an image for an empty URI") + return + } + val uri = Uri.parse(uriString) + val request: ImageRequest = ImageRequestBuilder.newBuilderWithSource(uri).build() + val prefetchSource: DataSource = + this.imagePipeline.prefetchToDiskCache(request, this.callerContext) + val prefetchSubscriber = + object : BaseDataSubscriber() { + protected override fun onNewResultImpl(dataSource: DataSource) { + if (!dataSource.isFinished) { + return + } + try { + removeRequest(requestId) + promise.resolve(true) + } catch (e: Exception) { + promise.reject(ERROR_PREFETCH_FAILURE, e) + } finally { + dataSource.close() + } + } + + protected override fun onFailureImpl(dataSource: DataSource) { + try { + removeRequest(requestId) + promise.reject(ERROR_PREFETCH_FAILURE, dataSource.failureCause) + } finally { + dataSource.close() + } + } + } + registerRequest(requestId, prefetchSource) + prefetchSource.subscribe(prefetchSubscriber, CallerThreadExecutor.getInstance()) + } + + override public fun abortRequest(requestId: Double) { + val request = removeRequest(requestId.toInt()) + request?.close() + } + + @ReactMethod + override public fun queryCache(uris: ReadableArray, promise: Promise) { + // perform cache interrogation in async task as disk cache checks are expensive + @Suppress("DEPRECATION") + object : GuardedAsyncTask(getReactApplicationContext()) { + protected override fun doInBackgroundGuarded(vararg params: Void) { + val result: WritableMap = Arguments.createMap() + val imagePipeline: ImagePipeline = this@ImageLoaderModule.imagePipeline + for (i in 0 until uris.size()) { + val uriString: String = uris.getString(i) + if (!uriString.isNullOrEmpty()) { + val uri = Uri.parse(uriString) + if (imagePipeline.isInBitmapMemoryCache(uri)) { + result.putString(uriString, "memory") + } else if (imagePipeline.isInDiskCacheSync(uri)) { + result.putString(uriString, "disk") + } + } + } + promise.resolve(result) + } + } + .executeOnExecutor(GuardedAsyncTask.THREAD_POOL_EXECUTOR) + } + + private fun registerRequest(requestId: Int, request: DataSource) { + synchronized(enqueuedRequestMonitor) { enqueuedRequests.put(requestId, request) } + } + + private fun removeRequest(requestId: Int): DataSource? { + synchronized(enqueuedRequestMonitor) { + val request: DataSource = enqueuedRequests.get(requestId) + enqueuedRequests.remove(requestId) + return request + } + } + + override fun onHostResume(): Unit = Unit + + override fun onHostPause(): Unit = Unit + + override fun onHostDestroy() { + // cancel all requests + synchronized(enqueuedRequestMonitor) { + var i = 0 + val size: Int = enqueuedRequests.size() + while (i < size) { + val enqueuedRequest: DataSource = enqueuedRequests.valueAt(i) + enqueuedRequest.close() + i++ + } + enqueuedRequests.clear() + } + } + + public companion object { + private const val ERROR_INVALID_URI = "E_INVALID_URI" + private const val ERROR_PREFETCH_FAILURE = "E_PREFETCH_FAILURE" + private const val ERROR_GET_SIZE_FAILURE = "E_GET_SIZE_FAILURE" + + public const val NAME: String = "ImageLoader" + } +}