From 34c645612144f1d8df8bb608b5cc88cb92ed017c Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Thu, 17 Oct 2024 13:39:36 -0700 Subject: [PATCH] Add setup for supporting binary XML drawables Reviewed By: oprisnik Differential Revision: D64332678 fbshipit-source-id: 5238183624f0311e8c8497b3713d1b2a1a52a04d --- .../imagepipeline/xml/CloseableXmlImage.kt | 40 +++++++++++ .../imagepipeline/xml/XmlDrawableFactory.kt | 22 ++++++ .../imagepipeline/xml/XmlFormatDecoder.kt | 70 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 imagepipeline/src/main/java/com/facebook/imagepipeline/xml/CloseableXmlImage.kt create mode 100644 imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlDrawableFactory.kt create mode 100644 imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlFormatDecoder.kt diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/CloseableXmlImage.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/CloseableXmlImage.kt new file mode 100644 index 0000000000..b1ed0c5c55 --- /dev/null +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/CloseableXmlImage.kt @@ -0,0 +1,40 @@ +/* + * 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.imagepipeline.xml + +import android.graphics.drawable.Drawable +import com.facebook.imagepipeline.image.DefaultCloseableImage + +internal class CloseableXmlImage(private var drawable: Drawable?) : DefaultCloseableImage() { + private var closed = false + + override fun getSizeInBytes(): Int { + return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel + } + + override fun close() { + drawable = null + 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 + } + + fun buildCopy(): Drawable? { + return drawable?.constantState?.newDrawable() + } +} diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlDrawableFactory.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlDrawableFactory.kt new file mode 100644 index 0000000000..fc44b0eb08 --- /dev/null +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlDrawableFactory.kt @@ -0,0 +1,22 @@ +/* + * 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.imagepipeline.xml + +import android.graphics.drawable.Drawable +import com.facebook.imagepipeline.drawable.DrawableFactory +import com.facebook.imagepipeline.image.CloseableImage + +internal class XmlDrawableFactory : DrawableFactory { + override fun supportsImageType(image: CloseableImage): Boolean { + return image is CloseableXmlImage + } + + override fun createDrawable(image: CloseableImage): Drawable? { + return (image as? CloseableXmlImage)?.buildCopy() + } +} diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlFormatDecoder.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlFormatDecoder.kt new file mode 100644 index 0000000000..4df69dd93b --- /dev/null +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/xml/XmlFormatDecoder.kt @@ -0,0 +1,70 @@ +/* + * 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.imagepipeline.xml + +import android.content.res.Resources +import android.net.Uri +import androidx.core.content.res.ResourcesCompat +import com.facebook.common.logging.FLog +import com.facebook.common.util.UriUtil +import com.facebook.imagepipeline.common.ImageDecodeOptions +import com.facebook.imagepipeline.decoder.ImageDecoder +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.image.EncodedImage +import com.facebook.imagepipeline.image.QualityInfo +import java.util.concurrent.ConcurrentHashMap + +internal class XmlFormatDecoder(private val resources: Resources) : ImageDecoder { + + private val resourceIdCache: MutableMap = ConcurrentHashMap() + + 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 xmlResourceId = getXmlResourceId(xmlResourceName) + val drawable = ResourcesCompat.getDrawable(resources, xmlResourceId, null) + drawable?.let { CloseableXmlImage(it) } + } catch (error: Throwable) { + FLog.e(TAG, "Cannot decode xml", error) + null + } + } + + private fun getXmlResourceId(xmlResourceName: String): Int { + return resourceIdCache.getOrPut(xmlResourceName) { + parseImageSourceResourceId(Uri.parse(xmlResourceName)) + } + } + + /** + * This parsing implementation is only designed to work with URIs that have been generated by + * UriUtil, which inserts the resource ID into the path of the URI. + * - Local resource URI format: res://[resourceId] + * - Qualified resource URI format: res://[package]/[resourceId] + * + * @throws IllegalStateException if the resource ID cannot be parsed from the provided uri + */ + private fun parseImageSourceResourceId(xmlResource: Uri): Int { + return if (UriUtil.isLocalResourceUri(xmlResource) || + UriUtil.isQualifiedResourceUri(xmlResource)) { + xmlResource.pathSegments.lastOrNull()?.toIntOrNull() + ?: error("Unable to read resource ID from ${xmlResource.path}") + } else { + error("Unsupported uri ${xmlResource}") + } + } + + companion object { + private const val TAG: String = "XmlFormatDecoder" + } +}