From 511ca107fe6a94dbb5a4f3cf7c74715c0e34b46e Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 8 Jan 2025 08:41:54 -0800 Subject: [PATCH] Replace custom XmlFormat with Fresco built-in Summary: Replaces the custom `XmlFormat` introduced in https://github.com/facebook/react-native/pull/46711 with the built-in support from Fresco. Fresco utilizes a very similar approach to load binary XML files and offers the XML format as part of its built-in `DefaultImageFormats` Changelog: [Android][Changed] - Replaced custom XML decoder with Fresco's built-in decoder Differential Revision: D66553842 --- .../ReactAndroid/api/ReactAndroid.api | 6 - .../react/modules/fresco/FrescoModule.kt | 26 ++-- .../react/modules/fresco/XmlFormat.kt | 143 ------------------ 3 files changed, 10 insertions(+), 165 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/XmlFormat.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 9516cca7452858..e4a154c092c9d8 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3311,12 +3311,6 @@ public final class com/facebook/react/modules/fresco/SystraceRequestListener : c public fun requiresExtraMap (Ljava/lang/String;)Z } -public final class com/facebook/react/modules/fresco/XmlFormat { - public static final field INSTANCE Lcom/facebook/react/modules/fresco/XmlFormat; - public final fun addDecodingCapability (Lcom/facebook/imagepipeline/decoder/ImageDecoderConfig$Builder;Landroid/content/Context;)Lcom/facebook/imagepipeline/decoder/ImageDecoderConfig$Builder; - public final fun getDrawableFactory ()Lcom/facebook/imagepipeline/drawable/DrawableFactory; -} - public final class com/facebook/react/modules/i18nmanager/I18nManagerModule : com/facebook/fbreact/specs/NativeI18nManagerSpec { public static final field Companion Lcom/facebook/react/modules/i18nmanager/I18nManagerModule$Companion; public static final field NAME Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.kt index 126e80b5511426..560afc139757da 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/FrescoModule.kt @@ -10,11 +10,10 @@ package com.facebook.react.modules.fresco import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory.newBuilder +import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.core.DownsampleMode import com.facebook.imagepipeline.core.ImagePipeline import com.facebook.imagepipeline.core.ImagePipelineConfig -import com.facebook.imagepipeline.decoder.ImageDecoderConfig import com.facebook.imagepipeline.listener.RequestListener import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactApplicationContext @@ -80,9 +79,6 @@ constructor( if (!hasBeenInitialized()) { val pipelineConfig = config ?: getDefaultConfig(reactContext) val draweeConfigBuilder = DraweeConfig.newBuilder() - if (ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) { - draweeConfigBuilder.addCustomDrawableFactory(XmlFormat.getDrawableFactory()) - } Fresco.initialize( reactContext.applicationContext, pipelineConfig, @@ -158,22 +154,20 @@ constructor( requestListeners.add(SystraceRequestListener()) val client = OkHttpClientProvider.createClient() - // Add support for XML drawable images - val decoderConfigBuilder = ImageDecoderConfig.Builder() - if (ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) { - XmlFormat.addDecodingCapability(decoderConfigBuilder, context) - } - // make sure to forward cookies for any requests via the okHttpClient // so that image requests to endpoints that use cookies still work val container = OkHttpCompat.getCookieJarContainer(client) val handler = ForwardingCookieHandler() container.setCookieJar(JavaNetCookieJar(handler)) - return newBuilder(context.applicationContext, client) - .setNetworkFetcher(ReactOkHttpNetworkFetcher(client)) - .setImageDecoderConfig(decoderConfigBuilder.build()) - .setDownsampleMode(DownsampleMode.AUTO) - .setRequestListeners(requestListeners) + val builder = + OkHttpImagePipelineConfigFactory.newBuilder(context.applicationContext, client) + .setNetworkFetcher(ReactOkHttpNetworkFetcher(client)) + .setDownsampleMode(DownsampleMode.AUTO) + .setRequestListeners(requestListeners) + builder + .experiment() + .setBinaryXmlEnabled(ReactNativeFeatureFlags.loadVectorDrawablesOnImages()) + return builder } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/XmlFormat.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/XmlFormat.kt deleted file mode 100644 index 7b58dfa07abf50..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/XmlFormat.kt +++ /dev/null @@ -1,143 +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.fresco - -import android.content.Context -import android.graphics.drawable.Drawable -import android.net.Uri -import com.facebook.common.logging.FLog -import com.facebook.imageformat.ImageFormat -import com.facebook.imageformat.ImageFormat.FormatChecker -import com.facebook.imageformat.ImageFormatCheckerUtils -import com.facebook.imagepipeline.common.ImageDecodeOptions -import com.facebook.imagepipeline.decoder.ImageDecoder -import com.facebook.imagepipeline.decoder.ImageDecoderConfig -import com.facebook.imagepipeline.drawable.DrawableFactory -import com.facebook.imagepipeline.image.CloseableImage -import com.facebook.imagepipeline.image.DefaultCloseableImage -import com.facebook.imagepipeline.image.EncodedImage -import com.facebook.imagepipeline.image.QualityInfo - -public object XmlFormat { - - public fun addDecodingCapability( - builder: ImageDecoderConfig.Builder, - context: Context, - ): ImageDecoderConfig.Builder { - return builder.addDecodingCapability( - XmlFormat.FORMAT, - XmlFormat.XmlFormatChecker(), - XmlFormat.XmlFormatDecoder(context), - ) - } - - public fun getDrawableFactory(): DrawableFactory { - return XmlDrawableFactory() - } - - private val FORMAT: ImageFormat = ImageFormat("XML", "xml") - private const val TAG: String = "XmlFormat" - /** - * These are the first 4 bytes of a binary XML file. We can only support binary XML files and not - * raw XML files because Android explicitly disallows raw XML files when inflating drawables. - * Binary XML files are created at build time by Android's AAPT. - * - * @see - * https://developer.android.com/reference/android/view/LayoutInflater#inflate(org.xmlpull.v1.XmlPullParser,%20android.view.ViewGroup) - */ - private val BINARY_XML_HEADER: ByteArray = - byteArrayOf( - 3.toByte(), - 0.toByte(), - 8.toByte(), - 0.toByte(), - ) - - private class XmlFormatChecker : FormatChecker { - override val headerSize: Int = BINARY_XML_HEADER.size - - override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat { - return when { - headerSize < BINARY_XML_HEADER.size -> ImageFormat.UNKNOWN - ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER) -> FORMAT - else -> ImageFormat.UNKNOWN - } - } - } - - private class CloseableXmlImage(val name: String, val drawable: Drawable) : - DefaultCloseableImage() { - private var closed = false - - override fun getSizeInBytes(): Int { - return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel - } - - override fun close() { - closed = true - } - - override fun isClosed(): Boolean { - return closed - } - - override fun getWidth(): Int { - return drawable.intrinsicWidth.takeIf { it >= 0 } ?: 0 - } - - override fun getHeight(): Int { - return drawable.intrinsicHeight.takeIf { it >= 0 } ?: 0 - } - } - - private class XmlFormatDecoder(private val context: Context) : ImageDecoder { - override fun decode( - encodedImage: EncodedImage, - length: Int, - qualityInfo: QualityInfo, - options: ImageDecodeOptions - ): CloseableImage? { - return try { - val xmlResourceName = encodedImage.source ?: error("No source in encoded image") - // Use insecure URI parser since we do not care about the validity of the URI - val xmlResource = Uri.parse(xmlResourceName) - // Only support binary XML files from resources, not assets or raw files - val xmlResourceId = parseImageSourceResourceId(xmlResource) - // Use application context to avoid leaking the activity - val drawable = context.applicationContext.resources.getDrawable(xmlResourceId, null) - CloseableXmlImage(xmlResourceName, drawable) - } catch (error: Throwable) { - FLog.e(TAG, "Cannot decode xml ${error}", error) - null - } - } - - /** - * This parsing implementation is only designed to work with URI's that have been generated by - * the ResourceDrawableIdHelper that ImageSource uses. It will ignore package names and schemes - * in its quest to extract a basic integer resource ID. - * - * ResourceDrawableIdHelper generates URIs in the format of res:/[resourceId] - * - * @throws IllegalStateException if the resource ID cannot be parsed from the provided uri - */ - private fun parseImageSourceResourceId(xmlResource: Uri): Int { - return xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id") - } - } - - private class XmlDrawableFactory : DrawableFactory { - override fun supportsImageType(image: CloseableImage): Boolean { - return image is CloseableXmlImage - } - - override fun createDrawable(image: CloseableImage): Drawable? { - return (image as CloseableXmlImage).drawable - } - } -}