From a5236baeec76131836f71f7b4d78e422b36c0af5 Mon Sep 17 00:00:00 2001 From: abbo <> Date: Mon, 30 Sep 2024 13:09:40 -0700 Subject: [PATCH] Port XML support to image requests Differential Revision: D63476283 --- .../ReactAndroid/api/ReactAndroid.api | 22 ++++ .../react/modules/fresco/FrescoModule.kt | 23 +++- .../react/modules/fresco/XmlFormat.kt | 113 ++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create 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 08145b0c6cc773..016247ccde6260 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3442,6 +3442,28 @@ 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 getFORMAT ()Lcom/facebook/imageformat/ImageFormat; +} + +public final class com/facebook/react/modules/fresco/XmlFormat$XmlDrawableFactory : com/facebook/imagepipeline/drawable/DrawableFactory { + public fun ()V + public fun createDrawable (Lcom/facebook/imagepipeline/image/CloseableImage;)Landroid/graphics/drawable/Drawable; + public fun supportsImageType (Lcom/facebook/imagepipeline/image/CloseableImage;)Z +} + +public final class com/facebook/react/modules/fresco/XmlFormat$XmlFormatChecker : com/facebook/imageformat/ImageFormat$FormatChecker { + public fun ()V + public fun determineFormat ([BI)Lcom/facebook/imageformat/ImageFormat; + public fun getHeaderSize ()I +} + +public final class com/facebook/react/modules/fresco/XmlFormat$XmlFormatDecoder : com/facebook/imagepipeline/decoder/ImageDecoder { + public fun (Landroid/content/Context;)V + public fun decode (Lcom/facebook/imagepipeline/image/EncodedImage;ILcom/facebook/imagepipeline/image/QualityInfo;Lcom/facebook/imagepipeline/common/ImageDecodeOptions;)Lcom/facebook/imagepipeline/image/CloseableImage; +} + public final class com/facebook/react/modules/i18nmanager/I18nManagerModule : com/facebook/fbreact/specs/NativeI18nManagerSpec { public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun allowRTL (Z)V 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 43d08b45231a9a..c34c956d926727 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 @@ -8,17 +8,20 @@ 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.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 import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.common.ReactConstants +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.loadVectorDrawablesOnImages import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.common.ModuleDataCleaner import com.facebook.react.modules.network.ForwardingCookieHandler @@ -75,10 +78,16 @@ constructor( val reactContext = reactApplicationContext reactContext.addLifecycleEventListener(this) if (!hasBeenInitialized()) { - if (config == null) { - config = getDefaultConfig(reactContext) + val pipelineConfig = config ?: getDefaultConfig(reactContext) + val draweeConfigBuilder = DraweeConfig.newBuilder() + if (loadVectorDrawablesOnImages()) { + draweeConfigBuilder.addCustomDrawableFactory(XmlFormat.XmlDrawableFactory()) } - Fresco.initialize(reactContext.applicationContext, config) + Fresco.initialize( + reactContext.applicationContext, + pipelineConfig, + draweeConfigBuilder.build(), + ) hasBeenInitialized = true } else if (config != null) { FLog.w( @@ -149,6 +158,13 @@ constructor( requestListeners.add(SystraceRequestListener()) val client = OkHttpClientProvider.createClient() + // Add support for XML drawable images + val decoderConfigBuilder = ImageDecoderConfig.Builder() + if (loadVectorDrawablesOnImages()) { + decoderConfigBuilder.addDecodingCapability( + XmlFormat.FORMAT, XmlFormat.XmlFormatChecker(), XmlFormat.XmlFormatDecoder(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) @@ -156,6 +172,7 @@ constructor( container.setCookieJar(JavaNetCookieJar(handler)) return newBuilder(context.applicationContext, client) .setNetworkFetcher(ReactOkHttpNetworkFetcher(client)) + .setImageDecoderConfig(decoderConfigBuilder.build()) .setDownsampleMode(DownsampleMode.AUTO) .setRequestListeners(requestListeners) } 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 new file mode 100644 index 00000000000000..3673d50efcbd7a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/fresco/XmlFormat.kt @@ -0,0 +1,113 @@ +/* + * 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.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 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(), + ) + + public 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 + } + } + + public 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") + val xmlResource = Uri.parse(xmlResourceName) + // Android resource names are of the format [res|resources]:/[?package]/[res id] + val xmlResourceId = + xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id") + // 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 + } + } + } + + public class XmlDrawableFactory : DrawableFactory { + override fun supportsImageType(image: CloseableImage): Boolean { + return image is CloseableXmlImage + } + + override fun createDrawable(image: CloseableImage): Drawable? { + return (image as CloseableXmlImage).drawable + } + } +}