@@ -30,18 +30,22 @@ import com.amplifyframework.predictions.PredictionsException
30
30
import com.amplifyframework.predictions.aws.BuildConfig
31
31
import com.amplifyframework.predictions.aws.exceptions.AccessDeniedException
32
32
import com.amplifyframework.predictions.aws.exceptions.FaceLivenessSessionNotFoundException
33
+ import com.amplifyframework.predictions.aws.exceptions.FaceLivenessUnsupportedChallengeTypeException
33
34
import com.amplifyframework.predictions.aws.models.liveness.BoundingBox
34
35
import com.amplifyframework.predictions.aws.models.liveness.ClientChallenge
35
36
import com.amplifyframework.predictions.aws.models.liveness.ClientSessionInformationEvent
36
37
import com.amplifyframework.predictions.aws.models.liveness.ColorDisplayed
37
38
import com.amplifyframework.predictions.aws.models.liveness.FaceMovementAndLightClientChallenge
39
+ import com.amplifyframework.predictions.aws.models.liveness.FaceMovementClientChallenge
38
40
import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor
39
41
import com.amplifyframework.predictions.aws.models.liveness.InitialFace
40
42
import com.amplifyframework.predictions.aws.models.liveness.InvalidSignatureException
41
43
import com.amplifyframework.predictions.aws.models.liveness.LivenessResponseStream
42
44
import com.amplifyframework.predictions.aws.models.liveness.SessionInformation
43
45
import com.amplifyframework.predictions.aws.models.liveness.TargetFace
44
46
import com.amplifyframework.predictions.aws.models.liveness.VideoEvent
47
+ import com.amplifyframework.predictions.models.Challenge
48
+ import com.amplifyframework.predictions.models.FaceLivenessChallengeType
45
49
import com.amplifyframework.predictions.models.FaceLivenessSessionInformation
46
50
import com.amplifyframework.util.UserAgent
47
51
import java.net.URI
@@ -73,12 +77,16 @@ internal class LivenessWebSocket(
73
77
val credentialsProvider : CredentialsProvider ,
74
78
val endpoint : String ,
75
79
val region : String ,
76
- val sessionInformation : FaceLivenessSessionInformation ,
80
+ val clientSessionInformation : FaceLivenessSessionInformation ,
77
81
val livenessVersion : String? ,
78
- val onSessionInformationReceived : Consumer <SessionInformation >,
82
+ val onSessionResponseReceived : Consumer <SessionResponse >,
79
83
val onErrorReceived : Consumer <PredictionsException >,
80
84
val onComplete : Action
81
85
) {
86
+ internal data class SessionResponse (
87
+ val faceLivenessSession : SessionInformation ,
88
+ val livenessChallengeType : FaceLivenessChallengeType
89
+ )
82
90
83
91
private val signer = AWSV4Signer ()
84
92
private var credentials: Credentials ? = null
@@ -94,6 +102,7 @@ internal class LivenessWebSocket(
94
102
@VisibleForTesting
95
103
internal var webSocket: WebSocket ? = null
96
104
internal val challengeId = UUID .randomUUID().toString()
105
+ var challengeType: FaceLivenessChallengeType ? = null
97
106
private var initialDetectedFace: BoundingBox ? = null
98
107
private var faceDetectedStart = 0L
99
108
private var videoStartTimestamp = 0L
@@ -148,10 +157,33 @@ internal class LivenessWebSocket(
148
157
try {
149
158
when (val response = LivenessEventStream .decode(bytes, json)) {
150
159
is LivenessResponseStream .Event -> {
151
- if (response.serverSessionInformationEvent != null ) {
152
- onSessionInformationReceived.accept(
153
- response.serverSessionInformationEvent.sessionInformation
154
- )
160
+ if (response.challengeEvent != null ) {
161
+ challengeType = response.challengeEvent.challengeType
162
+ } else if (response.serverSessionInformationEvent != null ) {
163
+ val clientRequestedOldLightChallenge = clientSessionInformation.challengeVersions
164
+ .any { it == Challenge .FaceMovementAndLightChallenge (" 1.0.0" ) }
165
+
166
+ if (challengeType == null && clientRequestedOldLightChallenge) {
167
+ // For the 1.0.0 version of FaceMovementAndLight challenge, backend doesn't send a
168
+ // ChallengeEvent so we need to manually check and set it if that specific challenge
169
+ // was requested.
170
+ challengeType = FaceLivenessChallengeType .FaceMovementAndLightChallenge
171
+ }
172
+
173
+ // If challengeType hasn't been initialized by this point it's because server sent an
174
+ // unsupported challenge type so return an error to the client and close the web socket.
175
+ val resolvedChallengeType = challengeType
176
+ if (resolvedChallengeType == null ) {
177
+ webSocketError = FaceLivenessUnsupportedChallengeTypeException ()
178
+ destroy(UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE )
179
+ } else {
180
+ onSessionResponseReceived.accept(
181
+ SessionResponse (
182
+ response.serverSessionInformationEvent.sessionInformation,
183
+ resolvedChallengeType
184
+ )
185
+ )
186
+ }
155
187
} else if (response.disconnectionEvent != null ) {
156
188
this @LivenessWebSocket.webSocket?.close(
157
189
NORMAL_SOCKET_CLOSURE_STATUS_CODE ,
@@ -362,16 +394,26 @@ internal class LivenessWebSocket(
362
394
// Send initial ClientSessionInformationEvent
363
395
videoStartTimestamp = adjustedDate(videoStartTime)
364
396
initialDetectedFace = BoundingBox (
365
- left = initialFaceRect.left / sessionInformation .videoWidth,
366
- top = initialFaceRect.top / sessionInformation .videoHeight,
367
- height = initialFaceRect.height() / sessionInformation .videoHeight,
368
- width = initialFaceRect.width() / sessionInformation .videoWidth
397
+ left = initialFaceRect.left / clientSessionInformation .videoWidth,
398
+ top = initialFaceRect.top / clientSessionInformation .videoHeight,
399
+ height = initialFaceRect.height() / clientSessionInformation .videoHeight,
400
+ width = initialFaceRect.width() / clientSessionInformation .videoWidth
369
401
)
370
402
faceDetectedStart = adjustedDate(videoStartTime)
371
- val clientInfoEvent =
372
- ClientSessionInformationEvent (
373
- challenge = ClientChallenge (
374
- faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge (
403
+
404
+ val resolvedChallengeType = challengeType
405
+ if (resolvedChallengeType == null ) {
406
+ onErrorReceived.accept(
407
+ PredictionsException (
408
+ " Failed to send an initial face detected event" ,
409
+ AmplifyException .TODO_RECOVERY_SUGGESTION
410
+ )
411
+ )
412
+ } else {
413
+ val clientInfoEvent =
414
+ ClientSessionInformationEvent (
415
+ challenge = buildClientChallenge(
416
+ challengeType = resolvedChallengeType,
375
417
challengeId = challengeId,
376
418
initialFace = InitialFace (
377
419
boundingBox = initialDetectedFace!! ,
@@ -380,14 +422,23 @@ internal class LivenessWebSocket(
380
422
videoStartTimestamp = videoStartTimestamp
381
423
)
382
424
)
383
- )
384
- sendClientInfoEvent(clientInfoEvent)
425
+ sendClientInfoEvent(clientInfoEvent )
426
+ }
385
427
}
386
428
387
429
fun sendFinalEvent (targetFaceRect : RectF , faceMatchedStart : Long , faceMatchedEnd : Long ) {
388
- val finalClientInfoEvent = ClientSessionInformationEvent (
389
- challenge = ClientChallenge (
390
- FaceMovementAndLightClientChallenge (
430
+ val resolvedChallengeType = challengeType
431
+ if (resolvedChallengeType == null ) {
432
+ onErrorReceived.accept(
433
+ PredictionsException (
434
+ " Failed to send an initial face detected event" ,
435
+ AmplifyException .TODO_RECOVERY_SUGGESTION
436
+ )
437
+ )
438
+ } else {
439
+ val finalClientInfoEvent = ClientSessionInformationEvent (
440
+ challenge = buildClientChallenge(
441
+ challengeType = resolvedChallengeType,
391
442
challengeId = challengeId,
392
443
videoEndTimestamp = videoEndTimestamp,
393
444
initialFace = InitialFace (
@@ -398,16 +449,16 @@ internal class LivenessWebSocket(
398
449
faceDetectedInTargetPositionStartTimestamp = adjustedDate(faceMatchedStart),
399
450
faceDetectedInTargetPositionEndTimestamp = adjustedDate(faceMatchedEnd),
400
451
boundingBox = BoundingBox (
401
- left = targetFaceRect.left / sessionInformation .videoWidth,
402
- top = targetFaceRect.top / sessionInformation .videoHeight,
403
- height = targetFaceRect.height() / sessionInformation .videoHeight,
404
- width = targetFaceRect.width() / sessionInformation .videoWidth
452
+ left = targetFaceRect.left / clientSessionInformation .videoWidth,
453
+ top = targetFaceRect.top / clientSessionInformation .videoHeight,
454
+ height = targetFaceRect.height() / clientSessionInformation .videoHeight,
455
+ width = targetFaceRect.width() / clientSessionInformation .videoWidth
405
456
)
406
457
)
407
458
)
408
459
)
409
- )
410
- sendClientInfoEvent(finalClientInfoEvent)
460
+ sendClientInfoEvent(finalClientInfoEvent )
461
+ }
411
462
}
412
463
413
464
fun sendColorDisplayedEvent (
@@ -525,8 +576,47 @@ internal class LivenessWebSocket(
525
576
526
577
private fun isTimeDiffSafe (diffInMillis : Long ) = kotlin.math.abs(diffInMillis) < FOUR_MINUTES
527
578
579
+ private fun buildClientChallenge (
580
+ challengeType : FaceLivenessChallengeType ,
581
+ challengeId : String ,
582
+ videoStartTimestamp : Long? = null,
583
+ videoEndTimestamp : Long? = null,
584
+ initialFace : InitialFace ? = null,
585
+ targetFace : TargetFace ? = null,
586
+ colorDisplayed : ColorDisplayed ? = null
587
+ ): ClientChallenge = when (challengeType) {
588
+ FaceLivenessChallengeType .FaceMovementAndLightChallenge -> {
589
+ ClientChallenge (
590
+ faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge (
591
+ challengeId = challengeId,
592
+ videoStartTimestamp = videoStartTimestamp,
593
+ videoEndTimestamp = videoEndTimestamp,
594
+ initialFace = initialFace,
595
+ targetFace = targetFace,
596
+ colorDisplayed = colorDisplayed
597
+ ),
598
+ faceMovementChallenge = null
599
+ )
600
+ }
601
+ FaceLivenessChallengeType .FaceMovementChallenge -> {
602
+ ClientChallenge (
603
+ faceMovementAndLightChallenge = null ,
604
+ faceMovementChallenge = FaceMovementClientChallenge (
605
+ challengeId = challengeId,
606
+ videoStartTimestamp = videoStartTimestamp,
607
+ videoEndTimestamp = videoEndTimestamp,
608
+ initialFace = initialFace,
609
+ targetFace = targetFace
610
+ )
611
+ )
612
+ }
613
+ }
614
+
528
615
companion object {
529
616
private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000
617
+
618
+ // This is the same as the client-provided 'runtime error' status code
619
+ private const val UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE = 4005
530
620
private const val FOUR_MINUTES = 1000 * 60 * 4
531
621
532
622
@VisibleForTesting val datePattern = " EEE, d MMM yyyy HH:mm:ss z"
0 commit comments