From 5147247069a62f6f0a0069b16ffb38bbc5cd050f Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Fri, 10 Oct 2025 13:46:44 -0300 Subject: [PATCH 1/3] LivenessCoordinator uses a proper coroutine scope that cancels on destroy --- .../ui/liveness/camera/LivenessCoordinator.kt | 27 ++++++++++--------- samples/liveness/app/.gitignore | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt index 1febd66d..342364bd 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt @@ -55,6 +55,7 @@ import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -82,6 +83,7 @@ internal class LivenessCoordinator( private val attemptCounter = AttemptCounter() private val analysisExecutor = Executors.newSingleThreadExecutor() + private val coordinatorScope = MainScope() val livenessState = LivenessState( sessionId = sessionId, @@ -153,7 +155,7 @@ internal class LivenessCoordinator( } private fun launchCamera(camera: Camera) { - MainScope().launch { + coordinatorScope.launch { delay(5_000) if (!previewTextureView.hasReceivedUpdate) { val faceLivenessException = FaceLivenessDetectionException( @@ -163,7 +165,7 @@ internal class LivenessCoordinator( processSessionError(faceLivenessException, true) } } - MainScope().launch { + coordinatorScope.launch { getCameraProvider(context).apply { if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.DESTROYED) { unbindAll() @@ -241,7 +243,8 @@ internal class LivenessCoordinator( FaceLivenessDetectionException.UnsupportedChallengeTypeException(throwable = error) to true else -> FaceLivenessDetectionException( error.message ?: "Unknown error.", - error.recoverySuggestion, error + error.recoverySuggestion, + error ) to false } processSessionError(faceLivenessException, shouldStopLivenessSession) @@ -250,7 +253,7 @@ internal class LivenessCoordinator( } private fun unbindCamera(context: Context) { - MainScope().launch { + coordinatorScope.launch { getCameraProvider(context).apply { unbindAll() } @@ -303,7 +306,7 @@ internal class LivenessCoordinator( private fun stopEncoder(onComplete: () -> Unit) { encoder.stop { - MainScope().launch { + coordinatorScope.launch { onComplete() } } @@ -322,16 +325,16 @@ internal class LivenessCoordinator( livenessState.onDestroy(true, webSocketCloseCode) unbindCamera(context) analysisExecutor.shutdown() + coordinatorScope.cancel() } - private suspend fun getCameraProvider(context: Context): ProcessCameraProvider = - suspendCoroutine { continuation -> - ProcessCameraProvider.getInstance(context).also { cameraProvider -> - cameraProvider.addListener({ - continuation.resume(cameraProvider.get()) - }, ContextCompat.getMainExecutor(context)) - } + private suspend fun getCameraProvider(context: Context): ProcessCameraProvider = suspendCoroutine { continuation -> + ProcessCameraProvider.getInstance(context).also { cameraProvider -> + cameraProvider.addListener({ + continuation.resume(cameraProvider.get()) + }, ContextCompat.getMainExecutor(context)) } + } companion object { const val TARGET_FPS_MIN = 24 diff --git a/samples/liveness/app/.gitignore b/samples/liveness/app/.gitignore index 7915f57c..f103701d 100644 --- a/samples/liveness/app/.gitignore +++ b/samples/liveness/app/.gitignore @@ -1,4 +1,4 @@ /build /buildNative **/awsconfiguration.json -**/amplifyconfiguration.json \ No newline at end of file +**/amplifyconfiguration**.json \ No newline at end of file From c657c6840b617520fdc28ce1295dc9984d0c42f0 Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Fri, 10 Oct 2025 13:55:42 -0300 Subject: [PATCH 2/3] Add a coroutine name --- .../ui/liveness/camera/LivenessCoordinator.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt index 342364bd..d3884e17 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt @@ -54,10 +54,12 @@ import java.util.Date import java.util.concurrent.Executors import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.plus internal typealias OnMuxedSegment = (bytes: ByteArray, timestamp: Long) -> Unit internal typealias OnChallengeComplete = () -> Unit @@ -83,7 +85,7 @@ internal class LivenessCoordinator( private val attemptCounter = AttemptCounter() private val analysisExecutor = Executors.newSingleThreadExecutor() - private val coordinatorScope = MainScope() + private val coordinatorScope = MainScope() + CoroutineName("LivenessCoordinator") val livenessState = LivenessState( sessionId = sessionId, From 5c8695a3ebecc1a9f2b5c6e1fe7f85890b47191e Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Fri, 10 Oct 2025 14:22:54 -0300 Subject: [PATCH 3/3] Use noncancellable coroutines for releasing resources --- .../ui/liveness/camera/LivenessCoordinator.kt | 23 ++++++++----------- .../liveness/camera/LivenessVideoEncoder.kt | 10 +++++--- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt index d3884e17..3a91eb28 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt @@ -56,6 +56,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.MainScope +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -255,10 +256,8 @@ internal class LivenessCoordinator( } private fun unbindCamera(context: Context) { - coordinatorScope.launch { - getCameraProvider(context).apply { - unbindAll() - } + coordinatorScope.launch(NonCancellable) { + getCameraProvider(context).unbindAll() } } @@ -299,28 +298,24 @@ internal class LivenessCoordinator( fun processLivenessCheckComplete() { livenessState.onLivenessChallengeComplete() - stopEncoder { livenessState.onFullChallengeComplete() } + coordinatorScope.launch { + encoder.stop() + livenessState.onFullChallengeComplete() + } } private fun processFinalEventsSent() { unbindCamera(context) } - private fun stopEncoder(onComplete: () -> Unit) { - encoder.stop { - coordinatorScope.launch { - onComplete() - } - } - } - /** * This is only called when onDispose is triggered from FaceLivenessDetector view. * If we begin calling destroy in other places, we should ensure we are still tracking the proper error code. */ fun destroy(context: Context) { // Destroy all resources so a new coordinator can safely be created - encoder.stop { + coordinatorScope.launch(NonCancellable) { + encoder.stop() encoder.destroy() } val webSocketCloseCode = if (!disconnectEventReceived) WebSocketCloseCode.DISPOSED else null diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessVideoEncoder.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessVideoEncoder.kt index 138af1d8..000321ce 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessVideoEncoder.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessVideoEncoder.kt @@ -27,6 +27,8 @@ import androidx.annotation.WorkerThread import com.amplifyframework.core.Amplify import com.amplifyframework.ui.liveness.util.isKeyFrame import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine internal class LivenessVideoEncoder private constructor( width: Int, @@ -212,7 +214,7 @@ internal class LivenessVideoEncoder private constructor( } } - fun stop(onComplete: () -> Unit) { + suspend fun stop() = suspendCoroutine { continuation -> encoderHandler.post { encoding = false livenessMuxer?.stop() @@ -220,11 +222,11 @@ internal class LivenessVideoEncoder private constructor( if (LOGGING_ENABLED) { Log.i(TAG, "Stopping encoder") } - onComplete() + continuation.resume(Unit) } } - fun destroy() { + suspend fun destroy() = suspendCoroutine { continuation -> encoderHandler.post { if (LOGGING_ENABLED) { Log.i(TAG, "Destroying encoder") @@ -241,6 +243,8 @@ internal class LivenessVideoEncoder private constructor( // may already be stopped } encoder.release() + + continuation.resume(Unit) } } }