package com.swmansion.rnscreens.gamma.helpers import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log import androidx.core.graphics.drawable.toDrawable import com.facebook.common.executors.CallerThreadExecutor import com.facebook.common.references.CloseableReference import com.facebook.datasource.BaseDataSubscriber import com.facebook.datasource.DataSource import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableStaticBitmap import com.facebook.imagepipeline.request.ImageRequestBuilder import java.util.Locale private const val TAG = "ImageLoader" /** * Resolves an icon from a system drawable resource name or an image uri, whichever is provided * ([systemDrawableResourceName] takes precedence). [onComplete] is always invoked exactly once — * synchronously for drawable resources and empty sources, asynchronously on the main thread for * image uris — with the resolved drawable, or `null` when the source resolves to no icon or fails * to load. */ internal fun resolveImage( context: Context, systemDrawableResourceName: String?, imageUri: String?, onComplete: (Drawable?) -> Unit, ) { when { systemDrawableResourceName != null -> onComplete(getSystemDrawableResource(context, systemDrawableResourceName)) imageUri != null -> loadImage(context, imageUri, onComplete) else -> onComplete(null) } } /** * Loads an image from [uri]. [onComplete] is always invoked exactly once on the main thread — with * the loaded drawable on success, or `null` on failure, an unresolvable uri, or an unsupported * (non-static-bitmap) image. Callers that must always learn the outcome (e.g. the toolbar menu * update queue) rely on this "always completes" contract. */ internal fun loadImage( context: Context, uri: String, onComplete: (Drawable?) -> Unit, ) { val resolvedUri = ImageSource(context, uri).getUri(context) if (resolvedUri == null) { Handler(Looper.getMainLooper()).post { onComplete(null) } return } // Since image loading might happen on a background thread // ref. https://frescolib.org/docs/intro-image-pipeline.html // We should schedule delivering the result on the UI thread. loadImageInternal(context, resolvedUri) { drawable -> Handler(Looper.getMainLooper()).post { onComplete(drawable) } } } private fun loadImageInternal( context: Context, uri: Uri, onComplete: (Drawable?) -> Unit, ) { val imageRequest = ImageRequestBuilder .newBuilderWithSource(uri) .build() val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context) dataSource.subscribe( object : BaseDataSubscriber>() { override fun onNewResultImpl(dataSource: DataSource?>) { if (!dataSource.isFinished) return val imageReference = dataSource.result if (imageReference == null) { onComplete(null) return } // Copy the bitmap so the drawable outlives the Fresco reference, then release it. val drawable = imageReference.use { imageReference -> val closeableImage = imageReference.get() if (closeableImage is CloseableStaticBitmap) { val bitmap = closeableImage.underlyingBitmap bitmap ?.copy(bitmap.config ?: Bitmap.Config.ARGB_8888, false) ?.toDrawable(context.resources) } else { null } } onComplete(drawable) } override fun onFailureImpl(dataSource: DataSource?>) { Log.e(TAG, "[RNScreens] Error loading image: $uri", dataSource.failureCause) onComplete(null) } override fun onCancellation(dataSource: DataSource?>) { onComplete(null) } }, CallerThreadExecutor.getInstance(), ) } // Adapted from https://github.com/callstackincubator/react-native-bottom-tabs/blob/main/packages/react-native-bottom-tabs/android/src/main/java/com/rcttabview/ImageSource.kt private class ImageSource( private val context: Context, private val uriString: String?, ) { private fun isLocalResourceUri(uri: Uri?): Boolean = uri?.scheme?.startsWith("res") ?: false fun getUri(context: Context): Uri? { val uri = computeUri(context) if (isLocalResourceUri(uri)) { return Uri.parse( uri!!.toString().replace("res:/", "android.resource://${context.packageName}/"), ) } return uri } private fun computeUri(context: Context): Uri? { val stringUri = uriString ?: return null return try { val uri = Uri.parse(stringUri) // Verify scheme is set, so that relative uri (used by static resources) are not handled. if (uri.scheme == null) { computeLocalUri(stringUri, context) } else { uri } } catch (_: Exception) { computeLocalUri(stringUri, context) } } private fun computeLocalUri( name: String, context: Context, ): Uri? = ResourceIdHelper.getResourceUri(context, name) } // Adapted from https://github.com/expo/expo/blob/sdk-52/packages/expo-image/android/src/main/java/expo/modules/image/ResourceIdHelper.kt private object ResourceIdHelper { private val idMap = mutableMapOf() @SuppressLint("DiscouragedApi") private fun getIdForResourceType( context: Context, name: String, type: String, ): Int { if (name.isEmpty()) return -1 val normalizedName = name.lowercase(Locale.ROOT).replace("-", "_") val key = "$type/$normalizedName" synchronized(this) { idMap[key]?.let { return it } val id = context.resources.getIdentifier(normalizedName, type, context.packageName) idMap[key] = id return id } } fun getResourceUri( context: Context, name: String, ): Uri? { val normalizedName = name.lowercase(Locale.ROOT).replace("-", "_") val drawableResId = getIdForResourceType(context, name, "drawable") if (drawableResId != 0) { return Uri.parse("res:/$drawableResId") } val rawResId = getIdForResourceType(context, name, "raw") if (rawResId != 0) { return Uri.parse("res:/$rawResId") } return if (name.startsWith("asset:/")) { Uri.parse("file:///android_asset/" + name.removePrefix("asset:/")) } else { Uri.parse("file:///android_asset/$name") } } }