@@ -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
@@ -145,10 +154,34 @@ internal class LivenessWebSocket(
145
154
try {
146
155
when (val response = LivenessEventStream .decode(bytes, json)) {
147
156
is LivenessResponseStream .Event -> {
148
- if (response.serverSessionInformationEvent != null ) {
149
- onSessionInformationReceived.accept(
150
- response.serverSessionInformationEvent.sessionInformation
151
- )
157
+ if (response.challengeEvent != null ) {
158
+ challengeType = response.challengeEvent.challengeType
159
+ } else if (response.serverSessionInformationEvent != null ) {
160
+
161
+ val clientRequestedOldLightChallenge = clientSessionInformation.challengeVersions
162
+ .any { it == Challenge .FaceMovementAndLightChallenge (" 1.0.0" ) }
163
+
164
+ if (challengeType == null && clientRequestedOldLightChallenge) {
165
+ // For the 1.0.0 version of FaceMovementAndLight challenge, backend doesn't send a
166
+ // ChallengeEvent so we need to manually check and set it if that specific challenge
167
+ // was requested.
168
+ challengeType = FaceLivenessChallengeType .FaceMovementAndLightChallenge
169
+ }
170
+
171
+ // If challengeType hasn't been initialized by this point it's because server sent an
172
+ // unsupported challenge type so return an error to the client and close the web socket.
173
+ val resolvedChallengeType = challengeType
174
+ if (resolvedChallengeType == null ) {
175
+ webSocketError = FaceLivenessUnsupportedChallengeTypeException ()
176
+ destroy(UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE )
177
+ } else {
178
+ onSessionResponseReceived.accept(
179
+ SessionResponse (
180
+ response.serverSessionInformationEvent.sessionInformation,
181
+ resolvedChallengeType
182
+ )
183
+ )
184
+ }
152
185
} else if (response.disconnectionEvent != null ) {
153
186
this @LivenessWebSocket.webSocket?.close(
154
187
NORMAL_SOCKET_CLOSURE_STATUS_CODE ,
@@ -358,16 +391,26 @@ internal class LivenessWebSocket(
358
391
// Send initial ClientSessionInformationEvent
359
392
videoStartTimestamp = adjustedDate(videoStartTime)
360
393
initialDetectedFace = BoundingBox (
361
- left = initialFaceRect.left / sessionInformation .videoWidth,
362
- top = initialFaceRect.top / sessionInformation .videoHeight,
363
- height = initialFaceRect.height() / sessionInformation .videoHeight,
364
- width = initialFaceRect.width() / sessionInformation .videoWidth
394
+ left = initialFaceRect.left / clientSessionInformation .videoWidth,
395
+ top = initialFaceRect.top / clientSessionInformation .videoHeight,
396
+ height = initialFaceRect.height() / clientSessionInformation .videoHeight,
397
+ width = initialFaceRect.width() / clientSessionInformation .videoWidth
365
398
)
366
399
faceDetectedStart = adjustedDate(videoStartTime)
367
- val clientInfoEvent =
368
- ClientSessionInformationEvent (
369
- challenge = ClientChallenge (
370
- faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge (
400
+
401
+ val resolvedChallengeType = challengeType
402
+ if (resolvedChallengeType == null ) {
403
+ onErrorReceived.accept(
404
+ PredictionsException (
405
+ " Failed to send an initial face detected event" ,
406
+ AmplifyException .TODO_RECOVERY_SUGGESTION
407
+ )
408
+ )
409
+ } else {
410
+ val clientInfoEvent =
411
+ ClientSessionInformationEvent (
412
+ challenge = buildClientChallenge(
413
+ challengeType = resolvedChallengeType,
371
414
challengeId = challengeId,
372
415
initialFace = InitialFace (
373
416
boundingBox = initialDetectedFace!! ,
@@ -376,14 +419,23 @@ internal class LivenessWebSocket(
376
419
videoStartTimestamp = videoStartTimestamp
377
420
)
378
421
)
379
- )
380
- sendClientInfoEvent(clientInfoEvent)
422
+ sendClientInfoEvent(clientInfoEvent )
423
+ }
381
424
}
382
425
383
426
fun sendFinalEvent (targetFaceRect : RectF , faceMatchedStart : Long , faceMatchedEnd : Long ) {
384
- val finalClientInfoEvent = ClientSessionInformationEvent (
385
- challenge = ClientChallenge (
386
- FaceMovementAndLightClientChallenge (
427
+ val resolvedChallengeType = challengeType
428
+ if (resolvedChallengeType == null ) {
429
+ onErrorReceived.accept(
430
+ PredictionsException (
431
+ " Failed to send an initial face detected event" ,
432
+ AmplifyException .TODO_RECOVERY_SUGGESTION
433
+ )
434
+ )
435
+ } else {
436
+ val finalClientInfoEvent = ClientSessionInformationEvent (
437
+ challenge = buildClientChallenge(
438
+ challengeType = resolvedChallengeType,
387
439
challengeId = challengeId,
388
440
videoEndTimestamp = videoEndTimestamp,
389
441
initialFace = InitialFace (
@@ -394,16 +446,16 @@ internal class LivenessWebSocket(
394
446
faceDetectedInTargetPositionStartTimestamp = adjustedDate(faceMatchedStart),
395
447
faceDetectedInTargetPositionEndTimestamp = adjustedDate(faceMatchedEnd),
396
448
boundingBox = BoundingBox (
397
- left = targetFaceRect.left / sessionInformation .videoWidth,
398
- top = targetFaceRect.top / sessionInformation .videoHeight,
399
- height = targetFaceRect.height() / sessionInformation .videoHeight,
400
- width = targetFaceRect.width() / sessionInformation .videoWidth
449
+ left = targetFaceRect.left / clientSessionInformation .videoWidth,
450
+ top = targetFaceRect.top / clientSessionInformation .videoHeight,
451
+ height = targetFaceRect.height() / clientSessionInformation .videoHeight,
452
+ width = targetFaceRect.width() / clientSessionInformation .videoWidth
401
453
)
402
454
)
403
455
)
404
456
)
405
- )
406
- sendClientInfoEvent(finalClientInfoEvent)
457
+ sendClientInfoEvent(finalClientInfoEvent )
458
+ }
407
459
}
408
460
409
461
fun sendColorDisplayedEvent (
@@ -523,8 +575,46 @@ internal class LivenessWebSocket(
523
575
524
576
private fun isTimeDiffSafe (diffInMillis : Long ) = kotlin.math.abs(diffInMillis) < FOUR_MINUTES
525
577
578
+ private fun buildClientChallenge (
579
+ challengeType : FaceLivenessChallengeType ,
580
+ challengeId : String ,
581
+ videoStartTimestamp : Long? = null,
582
+ videoEndTimestamp : Long? = null,
583
+ initialFace : InitialFace ? = null,
584
+ targetFace : TargetFace ? = null,
585
+ colorDisplayed : ColorDisplayed ? = null
586
+ ): ClientChallenge = when (challengeType) {
587
+ FaceLivenessChallengeType .FaceMovementAndLightChallenge -> {
588
+ ClientChallenge (
589
+ faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge (
590
+ challengeId = challengeId,
591
+ videoStartTimestamp = videoStartTimestamp,
592
+ videoEndTimestamp = videoEndTimestamp,
593
+ initialFace = initialFace,
594
+ targetFace = targetFace,
595
+ colorDisplayed = colorDisplayed
596
+ ),
597
+ faceMovementChallenge = null
598
+ )
599
+ }
600
+ FaceLivenessChallengeType .FaceMovementChallenge -> {
601
+ ClientChallenge (
602
+ faceMovementAndLightChallenge = null ,
603
+ faceMovementChallenge = FaceMovementClientChallenge (
604
+ challengeId = challengeId,
605
+ videoStartTimestamp = videoStartTimestamp,
606
+ videoEndTimestamp = videoEndTimestamp,
607
+ initialFace = initialFace,
608
+ targetFace = targetFace
609
+ )
610
+ )
611
+ }
612
+ }
613
+
526
614
companion object {
527
615
private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000
616
+ // This is the same as the client-provided 'runtime error' status code
617
+ private const val UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE = 4005
528
618
private const val FOUR_MINUTES = 1000 * 60 * 4
529
619
@VisibleForTesting val datePattern = " EEE, d MMM yyyy HH:mm:ss z"
530
620
private val LOG = Amplify .Logging .logger(CategoryType .PREDICTIONS , " amplify:aws-predictions" )
0 commit comments