Replace custom XmlFormat with Fresco built-in (#48533)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48533

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

Reviewed By: NickGerleman

Differential Revision: D66553842

fbshipit-source-id: 096e60140f893d461be1cdb48e250749e58bbb4c
This commit is contained in:
Peter Abbondanzo
2025-01-08 14:54:17 -08:00
committed by Facebook GitHub Bot
parent 819b5c2c8d
commit 6feb90bb29
3 changed files with 10 additions and 165 deletions
@@ -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;
@@ -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
}
}
}
@@ -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
}
}
}