Skip to content

Commit

Permalink
Port XML support to image requests
Browse files Browse the repository at this point in the history
Differential Revision: D63476283
  • Loading branch information
abbo authored and facebook-github-bot committed Sep 30, 2024
1 parent 86cac68 commit a5236ba
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 3 deletions.
22 changes: 22 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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 <init> ()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 <init> (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 <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
public fun allowRTL (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -149,13 +158,21 @@ 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)
val handler = ForwardingCookieHandler(context)
container.setCookieJar(JavaNetCookieJar(handler))
return newBuilder(context.applicationContext, client)
.setNetworkFetcher(ReactOkHttpNetworkFetcher(client))
.setImageDecoderConfig(decoderConfigBuilder.build())
.setDownsampleMode(DownsampleMode.AUTO)
.setRequestListeners(requestListeners)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}

0 comments on commit a5236ba

Please sign in to comment.