Retain camera recording and cache ownership until writer completion
This commit is contained in:
1 parent
3ae8d12434
commit
09386ee392
2 files changed
+125
No files matched your search
@@ -0,0 +1,124 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.camera.core.UseCase
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/** Main: файлы захвата живут до OnImageSaved/onError или VideoRecordEvent.Finalize.
|
||||
* discard/close запрещают передачу sender, но не удаляют файл под работающим CameraX writer.
|
||||
* Владение Recording сохраняется до успешного close либо Finalize; binding снимает только свои use cases.
|
||||
* Неудачная очистка остаётся в process registry и повторяется без UI/native core. */
|
||||
internal class CameraCaptureResources : AutoCloseable {
|
||||
companion object { private val retained = ConcurrentHashMap.newKeySet<CameraCaptureResources>() }
|
||||
private class Capture(val file: File) {
|
||||
var pending = true
|
||||
var discarded = false
|
||||
var recording: AutoCloseable? = null
|
||||
}
|
||||
private data class Binding(val provider: ProcessCameraProvider, val cases: Array<out UseCase>)
|
||||
private val files = mutableMapOf<File, Capture>()
|
||||
private var binding: Binding? = null
|
||||
private var closed = false
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var scheduled = false
|
||||
private var delayMs = 250L
|
||||
private val retry = Runnable { scheduled = false; cleanup() }
|
||||
|
||||
fun own(file: File) {
|
||||
check(!closed && file !in files) { "Capture owner closed or file already owned" }
|
||||
files[file] = Capture(file)
|
||||
LogManager.addLog("DEBUG", "Capture", "Writer owns cache path=$file")
|
||||
}
|
||||
|
||||
fun recording(file: File, recording: AutoCloseable) {
|
||||
val capture = checkNotNull(files[file]) { "Recording has no file owner" }
|
||||
if (!capture.pending) return // Синхронный Finalize уже подтвердил закрытый Recording.
|
||||
capture.recording = recording
|
||||
if (capture.discarded || closed) cleanup()
|
||||
}
|
||||
|
||||
/** Подтверждение writer; false означает discard/close и запрещает показывать либо отправлять результат. */
|
||||
fun completed(file: File): Boolean {
|
||||
val capture = files[file] ?: return false
|
||||
capture.pending = false; capture.recording = null
|
||||
val usable = !closed && !capture.discarded
|
||||
LogManager.addLog("INFO", "Capture", "Writer finished path=$file usable=$usable")
|
||||
cleanup()
|
||||
return usable
|
||||
}
|
||||
|
||||
fun discard(file: File?) {
|
||||
val capture = files[file] ?: return
|
||||
capture.discarded = true
|
||||
LogManager.addLog("INFO", "Capture", "Discard requested path=$file writerPending=${capture.pending}")
|
||||
cleanup()
|
||||
}
|
||||
|
||||
/** Вызывать только после успешного приёма sender: после этого владелец больше не удаляет файл. */
|
||||
fun transferred(file: File) {
|
||||
val capture = checkNotNull(files[file]) { "Transferred file has no owner" }
|
||||
check(!capture.pending && !capture.discarded && !closed) { "Capture cannot be transferred" }
|
||||
files.remove(file)
|
||||
LogManager.addLog("INFO", "Capture", "Cache transferred to sender path=$file")
|
||||
}
|
||||
|
||||
/** Сохранить use cases до bindToLifecycle: исключение после эффекта получает независимый rollback. */
|
||||
fun binding(provider: ProcessCameraProvider, vararg cases: UseCase) {
|
||||
check(!closed && binding == null) { "Previous camera binding still owned" }
|
||||
binding = Binding(provider, cases)
|
||||
}
|
||||
|
||||
fun unbind(): Boolean {
|
||||
val current = binding ?: return true
|
||||
try {
|
||||
current.provider.unbind(*current.cases); binding = null
|
||||
LogManager.addLog("INFO", "Capture", "Own camera use cases unbound count=${current.cases.size}")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "Capture", "Camera unbind failed; owner retained: $e")
|
||||
schedule()
|
||||
}
|
||||
return binding == null
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closed = true
|
||||
files.values.forEach { it.discarded = true }
|
||||
cleanup()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
if (closed) unbind()
|
||||
for (capture in files.values.toList()) {
|
||||
if (!capture.discarded) continue
|
||||
capture.recording?.let { record ->
|
||||
try { record.close(); capture.recording = null }
|
||||
catch (e: Exception) { LogManager.addLog("ERROR", "Capture", "Recording close failed path=${capture.file}; retained: $e") }
|
||||
}
|
||||
if (!capture.pending) try {
|
||||
check(capture.file.delete() || !capture.file.exists()) { "File deletion rejected" }
|
||||
files.remove(capture.file)
|
||||
LogManager.addLog("DEBUG", "Capture", "Discarded cache removed path=${capture.file}")
|
||||
} catch (e: Exception) { LogManager.addLog("ERROR", "Capture", "Cache cleanup failed path=${capture.file}; retained: $e") }
|
||||
}
|
||||
schedule()
|
||||
}
|
||||
|
||||
private fun schedule() {
|
||||
val pending = (closed && binding != null) || files.values.any { it.discarded }
|
||||
if (!pending) {
|
||||
main.removeCallbacks(retry); scheduled = false; delayMs = 250; retained.remove(this)
|
||||
return
|
||||
}
|
||||
retained.add(this)
|
||||
// Ожидание callback не опрашиваем: owner удерживается до подтверждения writer.
|
||||
val retryable = binding != null || files.values.any { it.discarded && (!it.pending || it.recording != null) }
|
||||
if (!scheduled && retryable) {
|
||||
scheduled = main.postDelayed(retry, delayMs)
|
||||
if (scheduled) delayMs = (delayMs * 2).coerceAtMost(30_000)
|
||||
else LogManager.addLog("ERROR", "Capture", "Cleanup timer rejected; owner retained")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ kotlin {
|
||||
"com/utun/chat/data/TelecomCallManager.kt", "com/utun/chat/data/UtunConnection.kt", "com/utun/chat/data/UtunConnectionService.kt",
|
||||
"com/utun/chat/data/QrScannerSession.kt", "com/utun/chat/data/VideoFilePlayer.kt",
|
||||
"com/utun/chat/data/VideoFileMetadata.kt", "com/google/**")
|
||||
kotlin.include("com/utun/chat/data/CameraCaptureResources.kt")
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
|
||||
Reference in new issue
Block a user