Skip to content

Commit

Permalink
feature: Use cache for images from network
Browse files Browse the repository at this point in the history
  • Loading branch information
retyui committed Apr 12, 2024
1 parent 9813b2c commit 67a2708
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ import android.provider.MediaStore
import android.text.TextUtils
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import com.facebook.common.executors.CallerThreadExecutor
import com.facebook.common.logging.FLog
import com.facebook.common.memory.PooledByteBuffer
import com.facebook.common.memory.PooledByteBufferInputStream
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.BaseDataSubscriber
import com.facebook.datasource.DataSource
import com.facebook.datasource.DataSubscriber
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.facebook.infer.annotation.Assertions
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
Expand All @@ -31,6 +42,8 @@ import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.ReactConstants
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
import com.facebook.react.views.image.ReactCallerContextFactory
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
Expand All @@ -51,7 +64,19 @@ object MimeType {
const val WEBP = "image/webp"
}

class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
interface ImageLoadCallback {
fun onImageLoaded(inputStream: InputStream?)

fun onFailure(error: Throwable)
}

class ImageEditorModuleImpl(
private val reactContext: ReactApplicationContext,
private val callerContext: Any?,
private val callerContextFactory: ReactCallerContextFactory?,
private val imagePipeline: ImagePipeline?
) {

private val moduleCoroutineScope = CoroutineScope(Dispatchers.Default)

init {
Expand All @@ -65,6 +90,58 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
cleanTask()
}

private fun getCallerContext(): Any? {
return callerContextFactory?.getOrCreateCallerContext("", "") ?: callerContext
}

private fun getImagePipeline(): ImagePipeline {
return imagePipeline ?: Fresco.getImagePipeline()
}

private fun fetchAndCacheImage(
uri: String,
headers: ReadableMap?,
callback: ImageLoadCallback
) {
val uri = Uri.parse(uri)
val imageRequest: ImageRequest =
if (headers != null) {
val imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(uri)
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers)
} else ImageRequestBuilder.newBuilderWithSource(uri).build()

val dataSource: DataSource<CloseableReference<PooledByteBuffer>> =
getImagePipeline().fetchEncodedImage(imageRequest, getCallerContext())
val dataSubscriber: DataSubscriber<CloseableReference<PooledByteBuffer>> =
object : BaseDataSubscriber<CloseableReference<PooledByteBuffer>>() {
override fun onNewResultImpl(
dataSource: DataSource<CloseableReference<PooledByteBuffer>>
) {
if (!dataSource.isFinished()) {
return
}
// getImagePipeline().isInBitmapMemoryCache(uri) // true
// getImagePipeline().isInDiskCacheSync(uri) // true
val ref = dataSource.getResult()
val result = ref?.get()
if (result != null) {
val stream: InputStream = PooledByteBufferInputStream(result)
callback.onImageLoaded(stream)
} else {
callback.onImageLoaded(null)
}
}

override fun onFailureImpl(
dataSource: DataSource<CloseableReference<PooledByteBuffer>>
) {
dataSource.close()
callback.onFailure(dataSource.failureCause!!)
}
}
dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance())
}

/**
* Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped
* image files. This is run when the module is invalidated (i.e. app is shutting down) and when
Expand Down Expand Up @@ -148,7 +225,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
// memory
val hasTargetSize = targetWidth > 0 && targetHeight > 0
val cropped: Bitmap? =
if (hasTargetSize) {
if (hasTargetSize)
cropAndResizeTask(
outOptions,
uri,
Expand All @@ -160,9 +237,8 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
targetHeight,
headers
)
} else {
cropTask(outOptions, uri, x, y, width, height, headers)
}
else cropTask(outOptions, uri, x, y, width, height, headers)

if (cropped == null) {
throw IOException("Cannot decode bitmap: $uri")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@ import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule

@ReactModule(name = ImageEditorModule.NAME)
class ImageEditorModule(reactContext: ReactApplicationContext) :
NativeRNCImageEditorSpec(reactContext) {
class ImageEditorModule : NativeRNCImageEditorSpec {
private val moduleImpl: ImageEditorModuleImpl

init {
moduleImpl = ImageEditorModuleImpl(reactContext)
constructor(reactContext: ReactApplicationContext) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, this, null, null)
}

constructor(reactContext: ReactApplicationContext, callerContext: Any?) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, callerContext, null, null)
}

constructor(
reactContext: ReactApplicationContext,
imagePipeline: ImagePipeline?,
callerContextFactory: ReactCallerContextFactory?
) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, null, callerContextFactory, imagePipeline)
}

override fun getName(): String {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package com.reactnativecommunity.imageeditor

import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.views.image.ReactCallerContextFactory

@ReactModule(name = ImageEditorModule.NAME)
class ImageEditorModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
class ImageEditorModule : ReactContextBaseJavaModule {
private val moduleImpl: ImageEditorModuleImpl

init {
moduleImpl = ImageEditorModuleImpl(reactContext)
constructor(reactContext: ReactApplicationContext) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, this, null, null)
}

constructor(reactContext: ReactApplicationContext, callerContext: Any?) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, callerContext, null, null)
}

constructor(
reactContext: ReactApplicationContext,
imagePipeline: ImagePipeline?,
callerContextFactory: ReactCallerContextFactory?
) : super(reactContext) {
moduleImpl = ImageEditorModuleImpl(reactContext, null, callerContextFactory, imagePipeline)
}

override fun getName(): String {
Expand Down

0 comments on commit 67a2708

Please sign in to comment.