Closed theskyblockman closed 4 months ago
This is indeed turned off by use, so this may be a false positive of StrictMode
This seems quite strange, I have never seen a false positive from StrictMode before.
I use my own ContentProvider so I could listen for when a file is opened and closed, when I initialized the ZoomImage
I see a giant wall of events (the fd attached to the URI is opened more than 75 times in less than 300ms!) which could be (and probably is) the cause of StrictMode's error, I believe this should be treated a little bit better, here I use ProxyFileDescriptor
so the OS is under a bit more strain than with a normal content URI but this seems abusive to not keep a ParcelFileDescriptor
around as it does not store any state anyways (I do, but only for optimization)
For convenience I also add the Composable I use with ZoomImage
:
@Composable
fun InteractiveImage(
modifier: Modifier = Modifier,
bitmap: ImageBitmap,
uri: Uri?,
subsample: Boolean = true,
fileName: String,
isFullscreen: Boolean,
contentScale: ContentScale = ContentScale.Fit,
onClick: () -> Unit,
) {
val context = LocalContext.current
val zoomState: ZoomState = rememberZoomState()
LaunchedEffect(context, zoomState, uri, subsample) {
if (subsample) {
zoomState.subsampling.setImageSource(ImageSource.fromContent(context, uri!!))
}
}
val painter = remember {
BitmapPainter(bitmap)
}
Box(
contentAlignment = Alignment.Center,
modifier = if (isFullscreen) Modifier.background(Color.Black) else Modifier
) {
ZoomImage(
painter = painter,
contentDescription = stringResource(R.string.image_alt_text, fileName),
modifier = modifier
.fillMaxSize(),
contentScale = contentScale,
onTap = {
onClick()
},
zoomState = zoomState
)
}
}
I have reproduced the problem of opening files multiple times in a short period of time. This is caused by the failure of concurrency control. I am working hard to fix this problem.
I have not reproduced the LeakedClosableViolation problem reported by StrictMode on the simulator of API 31. Can you give me more precise development environment information or give me a sample code that can be reproduced on the simulator?
I use my own content URIs which depending on the context of my app enables me to read a file I encrypted earlier. In my class who's implementing ContentProvider
I essentially have this method (some values need to be tweaked to make it work in another environment) :
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (mode != "r") {
throw UnsupportedOperationException("Only reading is supported")
}
val file = getFile(uri) ?: return null // Set a File object here to test
val storageManager = context?.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
?: throw SecurityException("No storage manager")
val handlerThread = HandlerThread("BackgroundFileReader")
handlerThread.start()
val randomAccessFile =
RandomAccessFile(file, "r")
return storageManager.openProxyFileDescriptor(
ParcelFileDescriptor.MODE_READ_ONLY, EncryptedProxyFileDescriptorCallback(
file,
randomAccessFile,
handlerThread
), Handler(handlerThread.looper)
)
}
class EncryptedProxyFileDescriptorCallback(
private val file: File,
private val randomAccessFile: RandomAccessFile,
private val handlerThread: HandlerThread
) : ProxyFileDescriptorCallback() {
override fun onGetSize(): Long {
return file.length()
}
override fun onRead(offset: Long, size: Int, data: ByteArray?): Int {
if (data == null) return if (offset + size > file.size) (file.size - offset).toInt() else size
randomAccessFile.seek(offset)
val result = randomAccessFile.read(data, 0, size)
return result
}
init {
Log.d("EncryptedContentProvider", "Initializing file")
}
override fun onRelease() {
Log.d("EncryptedContentProvider", "Releasing file")
handlerThread.quitSafely()
randomAccessFile.close()
}
}
This is the ContentProvider
for the URI I have as an argument in https://github.com/panpf/zoomimage/issues/29#issuecomment-2228105630
I heavily edited the code to accept clear files, normally I run decryption on the relevant part of the file which adds more latency/processing time. I use quite large files (around 5000x8000) to do my tests.
This piece of code is part of a relatively large app running all the latest versions of Kotlin, Jetpack Compose and AGP, also it does not have any networking involved.
Version 1.1.0-alpha03 attempts to optimize this issue, please retest
I have retested with 1.1.0-alpha03, the file descriptor is now only created 4 times when the image is opened and the subsampling is initialized, which is a totally acceptable behavior. Now, the StrictMode error is gone. Bug fixed.
This is good news
Describe the bug
.close
isn't called (or.use
isn't used) when the file descriptor (here from a content URI) is not needed anymore when using supersampling.Affected platforms
Select of the platforms below:
Versions
Running Devices
Code sample
Reproduction step
Error/stacktrace
Details
StrictMode policy violation: android.os.strictmode.LeakedClosableViolation: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks. at android.os.StrictMode$AndroidCloseGuardReporter.report(StrictMode.java:1987) at dalvik.system.CloseGuard.warnIfOpen(CloseGuard.java:336) at android.os.ParcelFileDescriptor.finalize(ParcelFileDescriptor.java:1069) at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:339) at java.lang.Daemons$FinalizerDaemon.processReference(Daemons.java:324) at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:300) at java.lang.Daemons$Daemon.run(Daemons.java:145) at java.lang.Thread.run(Thread.java:1012) Caused by: java.lang.Throwable: Explicit termination method 'close' not called at dalvik.system.CloseGuard.openWithCallSite(CloseGuard.java:288) at dalvik.system.CloseGuard.open(CloseGuard.java:257) at android.os.ParcelFileDescriptor.(ParcelFileDescriptor.java:206)
at android.os.ParcelFileDescriptor$2.createFromParcel(ParcelFileDescriptor.java:1129)
at android.os.ParcelFileDescriptor$2.createFromParcel(ParcelFileDescriptor.java:1120)
at android.os.storage.IStorageManager$Stub$Proxy.openProxyFileDescriptor(IStorageManager.java:3577)
at android.os.storage.StorageManager.openProxyFileDescriptor(StorageManager.java:2141)
at android.os.storage.StorageManager.openProxyFileDescriptor(StorageManager.java:2202)
at fr.theskyblockman.lifechest.vault.EncryptedContentProvider.openFile(EncryptedContentProvider.kt:159)
at android.content.ContentProvider.openAssetFile(ContentProvider.java:2138)
at android.content.ContentProvider.openTypedAssetFile(ContentProvider.java:2314)
at android.content.ContentProvider.openTypedAssetFile(ContentProvider.java:2381)
at android.content.ContentProvider$Transport.openTypedAssetFile(ContentProvider.java:562)
at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:2034)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1849)
at android.content.ContentResolver.openInputStream(ContentResolver.java:1525)
at com.github.panpf.zoomimage.subsampling.ContentImageSource$openSource$2.invokeSuspend(AndroidImageSource.kt:101)
at com.github.panpf.zoomimage.subsampling.ContentImageSource$openSource$2.invoke(Unknown Source:8)
at com.github.panpf.zoomimage.subsampling.ContentImageSource$openSource$2.invoke(Unknown Source:4)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
at com.github.panpf.zoomimage.subsampling.ContentImageSource.openSource-IoAF18A(AndroidImageSource.kt:99)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$getOrCreateDecoder$2.invokeSuspend(BitmapFactoryDecodeHelper.kt:80)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$getOrCreateDecoder$2.invoke(Unknown Source:8)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$getOrCreateDecoder$2.invoke(Unknown Source:4)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper.getOrCreateDecoder(BitmapFactoryDecodeHelper.kt:79)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper.access$getOrCreateDecoder(BitmapFactoryDecodeHelper.kt:25)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$decodeRegion$2.invokeSuspend(BitmapFactoryDecodeHelper.kt:47)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$decodeRegion$2.invoke(Unknown Source:8)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper$decodeRegion$2.invoke(Unknown Source:4)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
at com.github.panpf.zoomimage.subsampling.internal.BitmapFactoryDecodeHelper.decodeRegion(BitmapFactoryDecodeHelper.kt:39)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$decode$tileBitmap$1.invokeSuspend(TileDecoder.kt:57)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$decode$tileBitmap$1.invoke(Unknown Source:8)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$decode$tileBitmap$1.invoke(Unknown Source:4)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$useDecoder$2.invokeSuspend(TileDecoder.kt:75)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$decode$tileBitmap$1.invoke(Unknown Source:8)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$decode$tileBitmap$1.invoke(Unknown Source:4)
at com.github.panpf.zoomimage.subsampling.internal.TileDecoder$useDecoder$2.invokeSuspend(TileDecoder.kt:75)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)