@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding
27
27
import androidx.compose.foundation.layout.size
28
28
import androidx.compose.foundation.layout.width
29
29
import androidx.compose.material3.Button
30
+ import androidx.compose.material3.CircularProgressIndicator
30
31
import androidx.compose.material3.LinearProgressIndicator
31
32
import androidx.compose.material3.MaterialTheme
32
33
import androidx.compose.material3.Surface
@@ -83,9 +84,37 @@ fun FaceLivenessDetector(
83
84
disableStartView : Boolean = false,
84
85
onComplete : Action ,
85
86
onError : Consumer <FaceLivenessDetectionException >
87
+ ) = FaceLivenessDetector (
88
+ sessionId,
89
+ region,
90
+ credentialsProvider,
91
+ disableStartView,
92
+ onComplete,
93
+ onError,
94
+ ChallengeOptions ()
95
+ )
96
+
97
+ /* *
98
+ * @param sessionId of challenge
99
+ * @param region AWS region to stream the video to. Current supported regions are listed in [add link here]
100
+ * @param credentialsProvider to provide custom CredentialsProvider for authentication. Default uses initialized Amplify.Auth CredentialsProvider
101
+ * @param disableStartView to bypass warmup screen.
102
+ * @param challengeOptions is the list of ChallengeOptions that are to be overridden from the default configuration
103
+ * @param onComplete callback notifying a completed challenge
104
+ * @param onError callback containing exception for cause
105
+ */
106
+ @Composable
107
+ fun FaceLivenessDetector (
108
+ sessionId : String ,
109
+ region : String ,
110
+ credentialsProvider : AWSCredentialsProvider <AWSCredentials >? = null,
111
+ disableStartView : Boolean = false,
112
+ onComplete : Action ,
113
+ onError : Consumer <FaceLivenessDetectionException >,
114
+ challengeOptions : ChallengeOptions = ChallengeOptions (),
86
115
) {
87
116
val scope = rememberCoroutineScope()
88
- val key = Triple (sessionId, region, credentialsProvider)
117
+ val key = DetectorStateKey (sessionId, region, credentialsProvider, challengeOptions )
89
118
var isFinished by remember(key) { mutableStateOf(false ) }
90
119
val currentOnComplete by rememberUpdatedState(onComplete)
91
120
val currentOnError by rememberUpdatedState(onError)
@@ -124,6 +153,7 @@ fun FaceLivenessDetector(
124
153
region,
125
154
credentialsProvider = credentialsProvider,
126
155
disableStartView,
156
+ challengeOptions = challengeOptions,
127
157
onChallengeComplete = {
128
158
scope.launch {
129
159
// if we are already finished, we already provided a result in complete or failed
@@ -156,6 +186,7 @@ internal fun ChallengeView(
156
186
region : String ,
157
187
credentialsProvider : AWSCredentialsProvider <AWSCredentials >? ,
158
188
disableStartView : Boolean ,
189
+ challengeOptions : ChallengeOptions ,
159
190
onChallengeComplete : OnChallengeComplete ,
160
191
onChallengeFailed : Consumer <FaceLivenessDetectionException >
161
192
) {
@@ -176,6 +207,7 @@ internal fun ChallengeView(
176
207
region,
177
208
credentialsProvider,
178
209
disableStartView,
210
+ challengeOptions,
179
211
onChallengeComplete = { currentOnChallengeComplete() },
180
212
onChallengeFailed = { currentOnChallengeFailed.accept(it) }
181
213
)
@@ -232,6 +264,15 @@ internal fun ChallengeView(
232
264
233
265
if (livenessState.showingStartView) {
234
266
267
+ if (livenessState.loadingCameraPreview) {
268
+ CircularProgressIndicator (
269
+ color = MaterialTheme .colorScheme.primary,
270
+ modifier = Modifier
271
+ .align(Alignment .Center ),
272
+ strokeWidth = 2 .dp,
273
+ )
274
+ }
275
+
235
276
FaceGuide (
236
277
modifier = Modifier
237
278
.fillMaxSize()
@@ -402,6 +443,47 @@ internal fun ChallengeView(
402
443
}
403
444
}
404
445
446
+ internal data class DetectorStateKey (
447
+ val sessionId : String ,
448
+ val region : String ,
449
+ val credentialsProvider : AWSCredentialsProvider <AWSCredentials >? ,
450
+ val challengeOptions : ChallengeOptions
451
+ )
452
+
453
+ data class ChallengeOptions (
454
+ val faceMovementAndLight : LivenessChallenge .FaceMovementAndLight = LivenessChallenge .FaceMovementAndLight ,
455
+ val faceMovement : LivenessChallenge .FaceMovement = LivenessChallenge .FaceMovement ()
456
+ ) {
457
+ internal fun getLivenessChallenge (challengeType : FaceLivenessChallengeType ): LivenessChallenge =
458
+ when (challengeType) {
459
+ FaceLivenessChallengeType .FaceMovementAndLightChallenge -> faceMovementAndLight
460
+ FaceLivenessChallengeType .FaceMovementChallenge -> faceMovement
461
+ }
462
+
463
+ /* *
464
+ * @return true if all of the challenge options are configured to use the same camera configuration
465
+ */
466
+ internal fun hasOneCameraConfigured (): Boolean =
467
+ listOf (
468
+ faceMovementAndLight,
469
+ faceMovement
470
+ ).all { it.camera == faceMovementAndLight.camera }
471
+ }
472
+
473
+ sealed class LivenessChallenge (
474
+ open val camera : Camera = Camera .Front
475
+ ) {
476
+ data class FaceMovement (override val camera : Camera = Camera .Front ) : LivenessChallenge(
477
+ camera = camera
478
+ )
479
+ data object FaceMovementAndLight : LivenessChallenge ()
480
+ }
481
+
482
+ sealed class Camera {
483
+ data object Front : Camera ()
484
+ data object Back : Camera ()
485
+ }
486
+
405
487
private fun FaceLivenessSession?.isFaceMovementAndLightChallenge (): Boolean =
406
488
this ?.challengeType == FaceLivenessChallengeType .FaceMovementAndLightChallenge
407
489
0 commit comments