diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/exceptions/FaceLivenessUnsupportedChallengeTypeException.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/exceptions/FaceLivenessUnsupportedChallengeTypeException.kt new file mode 100644 index 0000000000..0ecd19a1fe --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/exceptions/FaceLivenessUnsupportedChallengeTypeException.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.exceptions + +import com.amplifyframework.annotations.InternalAmplifyApi +import com.amplifyframework.predictions.PredictionsException + +@InternalAmplifyApi +class FaceLivenessUnsupportedChallengeTypeException internal constructor( + message: String = "Received an unsupported ChallengeType from the backend.", + cause: Throwable? = null, + recoverySuggestion: String = "Verify that the Challenges configured in your backend are supported by the " + + "frontend code (e.g. Amplify UI)" +) : PredictionsException(message, cause, recoverySuggestion) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index dde12ec457..f59f057cf3 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -30,11 +30,13 @@ import com.amplifyframework.predictions.PredictionsException import com.amplifyframework.predictions.aws.BuildConfig import com.amplifyframework.predictions.aws.exceptions.AccessDeniedException import com.amplifyframework.predictions.aws.exceptions.FaceLivenessSessionNotFoundException +import com.amplifyframework.predictions.aws.exceptions.FaceLivenessUnsupportedChallengeTypeException import com.amplifyframework.predictions.aws.models.liveness.BoundingBox import com.amplifyframework.predictions.aws.models.liveness.ClientChallenge import com.amplifyframework.predictions.aws.models.liveness.ClientSessionInformationEvent import com.amplifyframework.predictions.aws.models.liveness.ColorDisplayed import com.amplifyframework.predictions.aws.models.liveness.FaceMovementAndLightClientChallenge +import com.amplifyframework.predictions.aws.models.liveness.FaceMovementClientChallenge import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor import com.amplifyframework.predictions.aws.models.liveness.InitialFace import com.amplifyframework.predictions.aws.models.liveness.InvalidSignatureException @@ -42,6 +44,8 @@ import com.amplifyframework.predictions.aws.models.liveness.LivenessResponseStre import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.aws.models.liveness.TargetFace import com.amplifyframework.predictions.aws.models.liveness.VideoEvent +import com.amplifyframework.predictions.models.Challenge +import com.amplifyframework.predictions.models.FaceLivenessChallengeType import com.amplifyframework.predictions.models.FaceLivenessSessionInformation import com.amplifyframework.util.UserAgent import java.net.URI @@ -73,12 +77,16 @@ internal class LivenessWebSocket( val credentialsProvider: CredentialsProvider, val endpoint: String, val region: String, - val sessionInformation: FaceLivenessSessionInformation, + val clientSessionInformation: FaceLivenessSessionInformation, val livenessVersion: String?, - val onSessionInformationReceived: Consumer, + val onSessionResponseReceived: Consumer, val onErrorReceived: Consumer, val onComplete: Action ) { + internal data class SessionResponse( + val faceLivenessSession: SessionInformation, + val livenessChallengeType: FaceLivenessChallengeType + ) private val signer = AWSV4Signer() private var credentials: Credentials? = null @@ -94,6 +102,7 @@ internal class LivenessWebSocket( @VisibleForTesting internal var webSocket: WebSocket? = null internal val challengeId = UUID.randomUUID().toString() + var challengeType: FaceLivenessChallengeType? = null private var initialDetectedFace: BoundingBox? = null private var faceDetectedStart = 0L private var videoStartTimestamp = 0L @@ -145,10 +154,34 @@ internal class LivenessWebSocket( try { when (val response = LivenessEventStream.decode(bytes, json)) { is LivenessResponseStream.Event -> { - if (response.serverSessionInformationEvent != null) { - onSessionInformationReceived.accept( - response.serverSessionInformationEvent.sessionInformation - ) + if (response.challengeEvent != null) { + challengeType = response.challengeEvent.challengeType + } else if (response.serverSessionInformationEvent != null) { + + val clientRequestedOldLightChallenge = clientSessionInformation.challengeVersions + .any { it == Challenge.FaceMovementAndLightChallenge("1.0.0") } + + if (challengeType == null && clientRequestedOldLightChallenge) { + // For the 1.0.0 version of FaceMovementAndLight challenge, backend doesn't send a + // ChallengeEvent so we need to manually check and set it if that specific challenge + // was requested. + challengeType = FaceLivenessChallengeType.FaceMovementAndLightChallenge + } + + // If challengeType hasn't been initialized by this point it's because server sent an + // unsupported challenge type so return an error to the client and close the web socket. + val resolvedChallengeType = challengeType + if (resolvedChallengeType == null) { + webSocketError = FaceLivenessUnsupportedChallengeTypeException() + destroy(UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE) + } else { + onSessionResponseReceived.accept( + SessionResponse( + response.serverSessionInformationEvent.sessionInformation, + resolvedChallengeType + ) + ) + } } else if (response.disconnectionEvent != null) { this@LivenessWebSocket.webSocket?.close( NORMAL_SOCKET_CLOSURE_STATUS_CODE, @@ -358,16 +391,26 @@ internal class LivenessWebSocket( // Send initial ClientSessionInformationEvent videoStartTimestamp = adjustedDate(videoStartTime) initialDetectedFace = BoundingBox( - left = initialFaceRect.left / sessionInformation.videoWidth, - top = initialFaceRect.top / sessionInformation.videoHeight, - height = initialFaceRect.height() / sessionInformation.videoHeight, - width = initialFaceRect.width() / sessionInformation.videoWidth + left = initialFaceRect.left / clientSessionInformation.videoWidth, + top = initialFaceRect.top / clientSessionInformation.videoHeight, + height = initialFaceRect.height() / clientSessionInformation.videoHeight, + width = initialFaceRect.width() / clientSessionInformation.videoWidth ) faceDetectedStart = adjustedDate(videoStartTime) - val clientInfoEvent = - ClientSessionInformationEvent( - challenge = ClientChallenge( - faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge( + + val resolvedChallengeType = challengeType + if (resolvedChallengeType == null) { + onErrorReceived.accept( + PredictionsException( + "Failed to send an initial face detected event", + AmplifyException.TODO_RECOVERY_SUGGESTION + ) + ) + } else { + val clientInfoEvent = + ClientSessionInformationEvent( + challenge = buildClientChallenge( + challengeType = resolvedChallengeType, challengeId = challengeId, initialFace = InitialFace( boundingBox = initialDetectedFace!!, @@ -376,14 +419,23 @@ internal class LivenessWebSocket( videoStartTimestamp = videoStartTimestamp ) ) - ) - sendClientInfoEvent(clientInfoEvent) + sendClientInfoEvent(clientInfoEvent) + } } fun sendFinalEvent(targetFaceRect: RectF, faceMatchedStart: Long, faceMatchedEnd: Long) { - val finalClientInfoEvent = ClientSessionInformationEvent( - challenge = ClientChallenge( - FaceMovementAndLightClientChallenge( + val resolvedChallengeType = challengeType + if (resolvedChallengeType == null) { + onErrorReceived.accept( + PredictionsException( + "Failed to send an initial face detected event", + AmplifyException.TODO_RECOVERY_SUGGESTION + ) + ) + } else { + val finalClientInfoEvent = ClientSessionInformationEvent( + challenge = buildClientChallenge( + challengeType = resolvedChallengeType, challengeId = challengeId, videoEndTimestamp = videoEndTimestamp, initialFace = InitialFace( @@ -394,16 +446,16 @@ internal class LivenessWebSocket( faceDetectedInTargetPositionStartTimestamp = adjustedDate(faceMatchedStart), faceDetectedInTargetPositionEndTimestamp = adjustedDate(faceMatchedEnd), boundingBox = BoundingBox( - left = targetFaceRect.left / sessionInformation.videoWidth, - top = targetFaceRect.top / sessionInformation.videoHeight, - height = targetFaceRect.height() / sessionInformation.videoHeight, - width = targetFaceRect.width() / sessionInformation.videoWidth + left = targetFaceRect.left / clientSessionInformation.videoWidth, + top = targetFaceRect.top / clientSessionInformation.videoHeight, + height = targetFaceRect.height() / clientSessionInformation.videoHeight, + width = targetFaceRect.width() / clientSessionInformation.videoWidth ) ) ) ) - ) - sendClientInfoEvent(finalClientInfoEvent) + sendClientInfoEvent(finalClientInfoEvent) + } } fun sendColorDisplayedEvent( @@ -523,8 +575,46 @@ internal class LivenessWebSocket( private fun isTimeDiffSafe(diffInMillis: Long) = kotlin.math.abs(diffInMillis) < FOUR_MINUTES + private fun buildClientChallenge( + challengeType: FaceLivenessChallengeType, + challengeId: String, + videoStartTimestamp: Long? = null, + videoEndTimestamp: Long? = null, + initialFace: InitialFace? = null, + targetFace: TargetFace? = null, + colorDisplayed: ColorDisplayed? = null + ): ClientChallenge = when (challengeType) { + FaceLivenessChallengeType.FaceMovementAndLightChallenge -> { + ClientChallenge( + faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge( + challengeId = challengeId, + videoStartTimestamp = videoStartTimestamp, + videoEndTimestamp = videoEndTimestamp, + initialFace = initialFace, + targetFace = targetFace, + colorDisplayed = colorDisplayed + ), + faceMovementChallenge = null + ) + } + FaceLivenessChallengeType.FaceMovementChallenge -> { + ClientChallenge( + faceMovementAndLightChallenge = null, + faceMovementChallenge = FaceMovementClientChallenge( + challengeId = challengeId, + videoStartTimestamp = videoStartTimestamp, + videoEndTimestamp = videoEndTimestamp, + initialFace = initialFace, + targetFace = targetFace + ) + ) + } + } + companion object { private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000 + // This is the same as the client-provided 'runtime error' status code + private const val UNSUPPORTED_CHALLENGE_CLOSURE_STATUS_CODE = 4005 private const val FOUR_MINUTES = 1000 * 60 * 4 @VisibleForTesting val datePattern = "EEE, d MMM yyyy HH:mm:ss z" private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ChallengeEvent.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ChallengeEvent.kt new file mode 100644 index 0000000000..f7c5c0afe0 --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ChallengeEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import com.amplifyframework.predictions.models.FaceLivenessChallengeType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChallengeEvent( + @SerialName("Type") val challengeType: FaceLivenessChallengeType, + @SerialName("Version") val version: String +) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt index a9710ef4e1..3e42d32a41 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt @@ -19,5 +19,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class ClientChallenge( - @SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge: FaceMovementAndLightClientChallenge + @SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge: + FaceMovementAndLightClientChallenge? = null, + @SerialName("FaceMovementChallenge") val faceMovementChallenge: FaceMovementClientChallenge? = null ) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt new file mode 100644 index 0000000000..4d1be9cfaa --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class FaceMovementClientChallenge( + @SerialName("ChallengeId") val challengeId: String, + @SerialName("VideoStartTimestamp") val videoStartTimestamp: Long? = null, + @SerialName("VideoEndTimestamp") val videoEndTimestamp: Long? = null, + @SerialName("InitialFace") val initialFace: InitialFace? = null, + @SerialName("TargetFace") val targetFace: TargetFace? = null, +) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementServerChallenge.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementServerChallenge.kt new file mode 100644 index 0000000000..d1d4716b50 --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementServerChallenge.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class FaceMovementServerChallenge( + @SerialName("OvalParameters") val ovalParameters: OvalParameters, + @SerialName("ChallengeConfig") val challengeConfig: ChallengeConfig, +) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt index 1960fa2728..73a09f930b 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt @@ -23,7 +23,8 @@ internal sealed class LivenessResponseStream { internal data class Event( @SerialName("ServerSessionInformationEvent") val serverSessionInformationEvent: ServerSessionInformationEvent? = null, - @SerialName("DisconnectionEvent") val disconnectionEvent: DisconnectionEvent? = null + @SerialName("DisconnectionEvent") val disconnectionEvent: DisconnectionEvent? = null, + @SerialName("ChallengeEvent") val challengeEvent: ChallengeEvent? = null ) : LivenessResponseStream() @Serializable diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ServerChallenge.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ServerChallenge.kt index e6e7cae88e..fc3f5a0528 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ServerChallenge.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ServerChallenge.kt @@ -19,5 +19,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class ServerChallenge( - @SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge: FaceMovementAndLightServerChallenge + @SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge: + FaceMovementAndLightServerChallenge? = null, + @SerialName("FaceMovementChallenge") val faceMovementChallenge: FaceMovementServerChallenge? = null ) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSession.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSession.kt index d6d1000fdd..f515248846 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSession.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSession.kt @@ -15,6 +15,8 @@ package com.amplifyframework.predictions.aws.service +import android.net.Uri +import androidx.annotation.VisibleForTesting import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import com.amplifyframework.core.Action import com.amplifyframework.core.Consumer @@ -29,7 +31,9 @@ import com.amplifyframework.predictions.aws.models.FaceTargetChallengeResponse import com.amplifyframework.predictions.aws.models.FaceTargetMatchingParameters import com.amplifyframework.predictions.aws.models.InitialFaceDetected import com.amplifyframework.predictions.aws.models.RgbColor +import com.amplifyframework.predictions.aws.models.liveness.ChallengeConfig import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor +import com.amplifyframework.predictions.aws.models.liveness.OvalParameters import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.models.ChallengeResponseEvent import com.amplifyframework.predictions.models.FaceLivenessSession @@ -38,8 +42,8 @@ import com.amplifyframework.predictions.models.FaceLivenessSessionInformation import com.amplifyframework.predictions.models.VideoEvent internal class RunFaceLivenessSession( - sessionId: String, - sessionInformation: FaceLivenessSessionInformation, + private val sessionId: String, + private val clientSessionInformation: FaceLivenessSessionInformation, val credentialsProvider: CredentialsProvider, livenessVersion: String?, onSessionStarted: Consumer, @@ -47,23 +51,22 @@ internal class RunFaceLivenessSession( onError: Consumer ) { - private val livenessEndpoint = "wss://streaming-rekognition.${sessionInformation.region}.amazonaws.com:443" - private val livenessWebSocket = LivenessWebSocket( credentialsProvider = credentialsProvider, - endpoint = "$livenessEndpoint/start-face-liveness-session-websocket?session-id=$sessionId" + - "&challenge-versions=${sessionInformation.challengeVersions}&video-width=" + - "${sessionInformation.videoWidth.toInt()}&video-height=${sessionInformation.videoHeight.toInt()}", - region = sessionInformation.region, - sessionInformation = sessionInformation, + endpoint = buildWebSocketEndpoint(), + region = clientSessionInformation.region, + clientSessionInformation = clientSessionInformation, livenessVersion = livenessVersion, - onSessionInformationReceived = { sessionInformation -> - val challenges = processSessionInformation(sessionInformation) + onSessionResponseReceived = { serverSessionResponse -> + val challenges = processSessionInformation(serverSessionResponse.faceLivenessSession) + val challengeType = serverSessionResponse.livenessChallengeType val faceLivenessSession = FaceLivenessSession( - challenges, - this@RunFaceLivenessSession::processVideoEvent, - this@RunFaceLivenessSession::processChallengeResponseEvent, - this@RunFaceLivenessSession::stopLivenessSession + challengeId = getChallengeId(), + challengeType = challengeType, + challenges = challenges, + onVideoEvent = this@RunFaceLivenessSession::processVideoEvent, + onChallengeResponseEvent = this@RunFaceLivenessSession::processChallengeResponseEvent, + stopLivenessSession = this@RunFaceLivenessSession::stopLivenessSession ) onSessionStarted.accept(faceLivenessSession) }, @@ -78,12 +81,56 @@ internal class RunFaceLivenessSession( } private fun processSessionInformation(sessionInformation: SessionInformation): List { - val challenge = sessionInformation.challenge.faceMovementAndLightChallenge val sessionChallenges = mutableListOf() - // Face target challenge - val ovalParameters = challenge.ovalParameters - val challengeConfig = challenge.challengeConfig + if (sessionInformation.challenge.faceMovementAndLightChallenge != null) { + val challenge = sessionInformation.challenge.faceMovementAndLightChallenge + + // Face target challenge + sessionChallenges.add(getFaceTargetChallenge(challenge.ovalParameters, challenge.challengeConfig)) + + // Freshness color challenge + val colorChallengeType = ColorChallengeType.SEQUENTIAL + val challengeColors = mutableListOf() + challenge.colorSequences.forEachIndexed { _, colorSequence -> + val currentColor = colorSequence.freshnessColor.rGB + val rgbColor = RgbColor(currentColor[0], currentColor[1], currentColor[2]) + var duration = colorSequence.flatDisplayDuration + var shouldScroll = false + if (colorSequence.flatDisplayDuration == 0f) { + duration = colorSequence.downscrollDuration + shouldScroll = true + } + challengeColors.add( + ColorDisplayInformation( + rgbColor, + duration, + shouldScroll + ) + ) + } + val colorChallenge = ColorChallenge( + livenessWebSocket.challengeId, + colorChallengeType, + challengeColors.toList() + ) + sessionChallenges.add(colorChallenge) + } else if (sessionInformation.challenge.faceMovementChallenge != null) { + val challenge = sessionInformation.challenge.faceMovementChallenge + + // Face target challenge + sessionChallenges.add(getFaceTargetChallenge(challenge.ovalParameters, challenge.challengeConfig)) + } + + return sessionChallenges.toList() + } + + private fun getChallengeId(): String = livenessWebSocket.challengeId + + private fun getFaceTargetChallenge( + ovalParameters: OvalParameters, + challengeConfig: ChallengeConfig + ): FaceTargetChallenge { val faceTargetMatching = FaceTargetMatchingParameters( challengeConfig.ovalIouThreshold, challengeConfig.ovalIouWidthThreshold, @@ -92,43 +139,13 @@ internal class RunFaceLivenessSession( challengeConfig.faceIouHeightThreshold, challengeConfig.ovalFitTimeout ) - val faceTargetChallenge = FaceTargetChallenge( + return FaceTargetChallenge( ovalParameters.width, ovalParameters.height, ovalParameters.centerX, ovalParameters.centerY, faceTargetMatching ) - sessionChallenges.add(faceTargetChallenge) - - // Freshness color challenge - val colorChallengeType = ColorChallengeType.SEQUENTIAL - val challengeColors = mutableListOf() - challenge.colorSequences.forEachIndexed { index, colorSequence -> - val currentColor = colorSequence.freshnessColor.rGB - val rgbColor = RgbColor(currentColor[0], currentColor[1], currentColor[2]) - var duration = colorSequence.flatDisplayDuration - var shouldScroll = false - if (colorSequence.flatDisplayDuration == 0f) { - duration = colorSequence.downscrollDuration - shouldScroll = true - } - challengeColors.add( - ColorDisplayInformation( - rgbColor, - duration, - shouldScroll - ) - ) - } - val colorChallenge = ColorChallenge( - livenessWebSocket.challengeId, - colorChallengeType, - challengeColors.toList() - ) - sessionChallenges.add(colorChallenge) - - return sessionChallenges.toList() } private fun processVideoEvent(videoEvent: VideoEvent) { @@ -183,4 +200,39 @@ internal class RunFaceLivenessSession( livenessWebSocket.clientStoppedSession = true reasonCode?.let { livenessWebSocket.destroy(it) } ?: livenessWebSocket.destroy() } + + @VisibleForTesting + fun buildWebSocketEndpoint(): String { + val challengeVersionString = clientSessionInformation.challengeVersions.joinToString(",") { + it.toQueryParamString() + } + + val uriBuilder = Uri.Builder() + .scheme("wss") + .encodedAuthority("streaming-rekognition.${clientSessionInformation.region}.amazonaws.com:443") + .appendPath("start-face-liveness-session-websocket") + .appendQueryParameter("session-id", sessionId) + .appendQueryParameter("video-width", clientSessionInformation.videoWidth.toInt().toString()) + .appendQueryParameter("video-height", clientSessionInformation.videoHeight.toInt().toString()) + .appendQueryParameter( + "challenge-versions", + challengeVersionString + ) + + if (clientSessionInformation.preCheckViewEnabled != null) { + uriBuilder.appendQueryParameter( + "precheck-view-enabled", + if (clientSessionInformation.preCheckViewEnabled!!) "1" else "0" + ) + } + + if (clientSessionInformation.attemptCount != null) { + uriBuilder.appendQueryParameter( + "attempt-count", + clientSessionInformation.attemptCount.toString() + ) + } + + return uriBuilder.build().toString() + } } diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt index a761a29b5a..33914a26f8 100644 --- a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/http/LivenessWebSocketTest.kt @@ -23,10 +23,13 @@ import com.amplifyframework.core.Action import com.amplifyframework.core.BuildConfig import com.amplifyframework.core.Consumer import com.amplifyframework.predictions.PredictionsException +import com.amplifyframework.predictions.aws.exceptions.FaceLivenessUnsupportedChallengeTypeException import com.amplifyframework.predictions.aws.models.liveness.ChallengeConfig +import com.amplifyframework.predictions.aws.models.liveness.ChallengeEvent import com.amplifyframework.predictions.aws.models.liveness.ColorSequence import com.amplifyframework.predictions.aws.models.liveness.DisconnectionEvent import com.amplifyframework.predictions.aws.models.liveness.FaceMovementAndLightServerChallenge +import com.amplifyframework.predictions.aws.models.liveness.FaceMovementServerChallenge import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor import com.amplifyframework.predictions.aws.models.liveness.InvalidSignatureException import com.amplifyframework.predictions.aws.models.liveness.LightChallengeType @@ -35,6 +38,8 @@ import com.amplifyframework.predictions.aws.models.liveness.ServerChallenge import com.amplifyframework.predictions.aws.models.liveness.ServerSessionInformationEvent import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.aws.models.liveness.ValidationException +import com.amplifyframework.predictions.models.Challenge +import com.amplifyframework.predictions.models.FaceLivenessChallengeType import com.amplifyframework.predictions.models.FaceLivenessSessionInformation import io.mockk.every import io.mockk.mockk @@ -81,7 +86,7 @@ internal class LivenessWebSocketTest { private lateinit var server: MockWebServer private val onComplete = mockk(relaxed = true) - private val onSessionInformationReceived = mockk>(relaxed = true) + private val onSessionResponseReceived = mockk>(relaxed = true) private val onErrorReceived = mockk>(relaxed = true) private val credentialsProvider = object : CredentialsProvider { override suspend fun resolve(attributes: Attributes): Credentials { @@ -94,7 +99,10 @@ internal class LivenessWebSocketTest { ) } } - private val sessionInformation = FaceLivenessSessionInformation(1f, 1f, "1", "3") + + private val defaultSessionInformation = createClientSessionInformation( + listOf(Challenge.FaceMovementChallenge("1.0.0")) + ) @Before fun setUp() { @@ -206,8 +214,162 @@ internal class LivenessWebSocketTest { } @Test - fun `server session event tracked`() { - val livenessWebSocket = createLivenessWebSocket() + fun `unsupported challengetype returns an exception`() { + val clientSessionInfo = createClientSessionInformation( + listOf(Challenge.FaceMovementAndLightChallenge("2.0.0")) + ) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = clientSessionInfo) + val unknownEvent = "{\"Type\":\"NewChallengeType\",\"Version\":\"1.0.0\"}" + + val challengeEventHeaders = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val encodedChallengeTypeByteString = + LivenessEventStream.encode(unknownEvent.toByteArray(), challengeEventHeaders).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedChallengeTypeByteString) + + assertEquals(null, livenessWebSocket.challengeType) + + val event = ServerSessionInformationEvent( + sessionInformation = SessionInformation( + challenge = ServerChallenge( + faceMovementAndLightChallenge = FaceMovementAndLightServerChallenge( + ovalParameters = OvalParameters(1.0f, 2.0f, .5f, .7f), + lightChallengeType = LightChallengeType.SEQUENTIAL, + challengeConfig = ChallengeConfig( + 1.0f, + 1.1f, + 1.2f, + 1.3f, + 1.4f, + 1.5f, + 1.6f, + 1.7f, + 1.8f, + 1.9f, + 10 + ), + colorSequences = listOf( + ColorSequence(FreshnessColor(listOf(0, 1, 2)), 4.0f, 5.0f) + ) + ) + ) + ) + ) + + val headers = mapOf( + ":event-type" to "ServerSessionInformationEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val data = json.encodeToString(event) + val encodedByteString = LivenessEventStream.encode(data.toByteArray(), headers).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) + assertEquals(FaceLivenessUnsupportedChallengeTypeException(), livenessWebSocket.webSocketError) + } + + @Test + fun `ensure challengetype is properly set when using the deprecated sessioninformation`() { + val sessionInfo = FaceLivenessSessionInformation( + 1f, + 1f, + "FaceMovementAndLightChallenge_1.0.0", + "3" + ) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = sessionInfo) + val event = ChallengeEvent( + challengeType = FaceLivenessChallengeType.FaceMovementAndLightChallenge, + version = "1.0.0" + ) + + val headers = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val data = json.encodeToString(event) + val encodedByteString = LivenessEventStream.encode(data.toByteArray(), headers).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) + + assertEquals(FaceLivenessChallengeType.FaceMovementAndLightChallenge, livenessWebSocket.challengeType) + } + + @Test + fun `server facemovementandlight challenge event tracked`() { + val sessionInfo = createClientSessionInformation(listOf(Challenge.FaceMovementAndLightChallenge("2.0.0"))) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = sessionInfo) + val event = ChallengeEvent( + challengeType = FaceLivenessChallengeType.FaceMovementAndLightChallenge, + version = "2.0.0" + ) + + val headers = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val data = json.encodeToString(event) + val encodedByteString = LivenessEventStream.encode(data.toByteArray(), headers).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) + + assertEquals(FaceLivenessChallengeType.FaceMovementAndLightChallenge, livenessWebSocket.challengeType) + } + + @Test + fun `server facemovement challenge event tracked`() { + val sessionInfo = createClientSessionInformation(listOf(Challenge.FaceMovementChallenge("1.0.0"))) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = sessionInfo) + + val event = ChallengeEvent( + challengeType = FaceLivenessChallengeType.FaceMovementChallenge, + version = "1.0.0" + ) + + val headers = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val data = json.encodeToString(event) + val encodedByteString = LivenessEventStream.encode(data.toByteArray(), headers).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) + + assertEquals(FaceLivenessChallengeType.FaceMovementChallenge, livenessWebSocket.challengeType) + } + + @Test + fun `server facemovementandlight session event tracked`() { + val sessionInfo = createClientSessionInformation(listOf(Challenge.FaceMovementAndLightChallenge("2.0.0"))) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = sessionInfo) + + val challengeEvent = ChallengeEvent( + challengeType = FaceLivenessChallengeType.FaceMovementAndLightChallenge, + version = "2.0.0" + ) + val challengeHeaders = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + val challengeData = json.encodeToString(challengeEvent) + val encodedChallengeHeadersByteString = + LivenessEventStream.encode(challengeData.toByteArray(), challengeHeaders).array().toByteString() + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedChallengeHeadersByteString) + + assertEquals(FaceLivenessChallengeType.FaceMovementAndLightChallenge, livenessWebSocket.challengeType) + val event = ServerSessionInformationEvent( sessionInformation = SessionInformation( challenge = ServerChallenge( @@ -234,6 +396,10 @@ internal class LivenessWebSocketTest { ) ) ) + val sessionResponse = LivenessWebSocket.SessionResponse( + event.sessionInformation, + FaceLivenessChallengeType.FaceMovementAndLightChallenge + ) val headers = mapOf( ":event-type" to "ServerSessionInformationEvent", ":content-type" to "application/json", @@ -245,7 +411,68 @@ internal class LivenessWebSocketTest { livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) - verify { onSessionInformationReceived.accept(event.sessionInformation) } + verify { onSessionResponseReceived.accept(sessionResponse) } + } + + @Test + fun `server facemovement session event tracked`() { + val sessionInfo = createClientSessionInformation(listOf(Challenge.FaceMovementChallenge("1.0.0"))) + val livenessWebSocket = createLivenessWebSocket(clientSessionInformation = sessionInfo) + + val challengeEvent = ChallengeEvent( + challengeType = FaceLivenessChallengeType.FaceMovementChallenge, + version = "1.0.0" + ) + val challengeHeaders = mapOf( + ":event-type" to "ChallengeEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + val challengeData = json.encodeToString(challengeEvent) + val encodedChallengeHeadersByteString = + LivenessEventStream.encode(challengeData.toByteArray(), challengeHeaders).array().toByteString() + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedChallengeHeadersByteString) + + assertEquals(FaceLivenessChallengeType.FaceMovementChallenge, livenessWebSocket.challengeType) + + val event = ServerSessionInformationEvent( + sessionInformation = SessionInformation( + challenge = ServerChallenge( + faceMovementChallenge = FaceMovementServerChallenge( + ovalParameters = OvalParameters(1.0f, 2.0f, .5f, .7f), + challengeConfig = ChallengeConfig( + 1.0f, + 1.1f, + 1.2f, + 1.3f, + 1.4f, + 1.5f, + 1.6f, + 1.7f, + 1.8f, + 1.9f, + 10 + ) + ) + ) + ) + ) + val sessionResponse = LivenessWebSocket.SessionResponse( + event.sessionInformation, + FaceLivenessChallengeType.FaceMovementChallenge + ) + val sessionHeaders = mapOf( + ":event-type" to "ServerSessionInformationEvent", + ":content-type" to "application/json", + ":message-type" to "event" + ) + + val data = json.encodeToString(event) + val encodedByteString = LivenessEventStream.encode(data.toByteArray(), sessionHeaders).array().toByteString() + + livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) + + verify { onSessionResponseReceived.accept(sessionResponse) } } @Test @@ -265,7 +492,7 @@ internal class LivenessWebSocketTest { livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) - verify(exactly = 0) { onSessionInformationReceived.accept(any()) } + verify(exactly = 0) { onSessionResponseReceived.accept(any()) } verify(exactly = 0) { onErrorReceived.accept(any()) } verify(exactly = 0) { webSocket.close(any(), any()) } } @@ -287,7 +514,7 @@ internal class LivenessWebSocketTest { livenessWebSocket.webSocketListener.onMessage(mockk(), encodedByteString) - verify(exactly = 0) { onSessionInformationReceived.accept(any()) } + verify(exactly = 0) { onSessionResponseReceived.accept(any()) } verify(exactly = 0) { onErrorReceived.accept(any()) } verify(exactly = 1) { webSocket.close(any(), any()) } } @@ -424,15 +651,25 @@ internal class LivenessWebSocketTest { assertEquals("AWS4-HMAC-SHA256", reconnectRequest.url.queryParameter("X-Amz-Algorithm")) } + private fun createClientSessionInformation(challengeVersions: List) = FaceLivenessSessionInformation( + videoWidth = 1f, + videoHeight = 1f, + region = "region", + preCheckViewEnabled = true, + attemptCount = 1, + challengeVersions = challengeVersions + ) + private fun createLivenessWebSocket( - livenessVersion: String? = null + livenessVersion: String? = null, + clientSessionInformation: FaceLivenessSessionInformation? = null ) = LivenessWebSocket( credentialsProvider, server.url("/").toString(), "", - sessionInformation, + clientSessionInformation ?: defaultSessionInformation, livenessVersion, - onSessionInformationReceived, + onSessionResponseReceived, onErrorReceived, onComplete ) diff --git a/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSessionTest.kt b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSessionTest.kt new file mode 100644 index 0000000000..ba840c8050 --- /dev/null +++ b/aws-predictions/src/test/java/com/amplifyframework/predictions/aws/service/RunFaceLivenessSessionTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.predictions.aws.service + +import com.amplifyframework.auth.CognitoCredentialsProvider +import com.amplifyframework.predictions.models.Challenge +import com.amplifyframework.predictions.models.FaceLivenessSessionInformation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class RunFaceLivenessSessionTest { + + private val region = "us-east-1" + private val sessionId = "123456" + private val videoWidth = 480f + private val videoHeight = 640f + + @Before + fun setUp() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @After + fun shutDown() { + Dispatchers.resetMain() + } + + @Test + fun `test websocket endpoint generation using the old liveness library`() { + val sessionInformation = FaceLivenessSessionInformation( + videoHeight = videoHeight, + videoWidth = videoWidth, + challenge = "FaceMovementAndLightChallenge_1.0.0", + region = region + ) + + val session = RunFaceLivenessSession( + sessionId = sessionId, + clientSessionInformation = sessionInformation, + credentialsProvider = CognitoCredentialsProvider(), + livenessVersion = "1.0.0", + onSessionStarted = {}, + onComplete = {}, + onError = {} + ) + + Assert.assertEquals( + generateEndpoint("FaceMovementAndLightChallenge_1.0.0"), + session.buildWebSocketEndpoint() + ) + } + + @Test + fun `test websocket endpoint generation supplying attempt count and enabled start view`() { + val attemptCount = 1 + val preCheckEnabled = true + val sessionInformation = FaceLivenessSessionInformation( + videoHeight = videoHeight, + videoWidth = videoWidth, + challengeVersions = listOf( + Challenge.FaceMovementAndLightChallenge("2.0.0") + ), + region = region, + preCheckViewEnabled = preCheckEnabled, + attemptCount = attemptCount + ) + + val session = RunFaceLivenessSession( + sessionId = sessionId, + clientSessionInformation = sessionInformation, + credentialsProvider = CognitoCredentialsProvider(), + livenessVersion = "1.0.0", + onSessionStarted = {}, + onComplete = {}, + onError = {} + ) + + Assert.assertEquals( + generateEndpoint("FaceMovementAndLightChallenge_2.0.0", preCheckEnabled, attemptCount), + session.buildWebSocketEndpoint() + ) + } + + @Test + fun `test websocket endpoint generation supplying different attempt count and enabled start view`() { + val attemptCount = 3 + val preCheckEnabled = true + val sessionInformation = FaceLivenessSessionInformation( + videoHeight = videoHeight, + videoWidth = videoWidth, + challengeVersions = listOf( + Challenge.FaceMovementAndLightChallenge("2.0.0") + ), + attemptCount = attemptCount, + region = region, + preCheckViewEnabled = preCheckEnabled + ) + + val session = RunFaceLivenessSession( + sessionId = sessionId, + clientSessionInformation = sessionInformation, + credentialsProvider = CognitoCredentialsProvider(), + livenessVersion = "1.0.0", + onSessionStarted = {}, + onComplete = {}, + onError = {} + ) + + Assert.assertEquals( + generateEndpoint("FaceMovementAndLightChallenge_2.0.0", preCheckEnabled, attemptCount), + session.buildWebSocketEndpoint() + ) + } + + @Test + fun `test websocket endpoint generation supplying attempt count and disabled start view`() { + val attemptCount = 3 + val preCheckEnabled = false + val sessionInformation = FaceLivenessSessionInformation( + videoHeight = videoHeight, + videoWidth = videoWidth, + challengeVersions = listOf( + Challenge.FaceMovementAndLightChallenge("2.0.0") + ), + attemptCount = attemptCount, + region = region, + preCheckViewEnabled = preCheckEnabled + ) + + val session = RunFaceLivenessSession( + sessionId = sessionId, + clientSessionInformation = sessionInformation, + credentialsProvider = CognitoCredentialsProvider(), + livenessVersion = "1.0.0", + onSessionStarted = {}, + onComplete = {}, + onError = {} + ) + + Assert.assertEquals( + generateEndpoint("FaceMovementAndLightChallenge_2.0.0", preCheckEnabled, attemptCount), + session.buildWebSocketEndpoint() + ) + } + + private fun generateEndpoint( + challenges: String, + preCheckEnabled: Boolean? = null, + attemptCount: Int? = null + ): String { + var endpoint = "wss://streaming-rekognition.$region.amazonaws.com:443/start-face-liveness-session-websocket" + + "?session-id=$sessionId&video-width=${videoWidth.toInt()}&video-height=${videoHeight.toInt()}" + + "&challenge-versions=$challenges" + + if (preCheckEnabled != null) { + val value = if (preCheckEnabled) "1" else "0" + endpoint = "$endpoint&precheck-view-enabled=$value" + } + + if (attemptCount != null) { + endpoint = "$endpoint&attempt-count=$attemptCount" + } + + return endpoint + } +} diff --git a/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessChallengeType.kt b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessChallengeType.kt new file mode 100644 index 0000000000..e2b48f0130 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessChallengeType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.models + +import com.amplifyframework.annotations.InternalAmplifyApi + +@InternalAmplifyApi +enum class FaceLivenessChallengeType { + FaceMovementChallenge, + FaceMovementAndLightChallenge +} diff --git a/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSession.kt b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSession.kt index bd5d5dd6f3..736413a84d 100644 --- a/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSession.kt +++ b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSession.kt @@ -19,6 +19,8 @@ import com.amplifyframework.annotations.InternalAmplifyApi @InternalAmplifyApi class FaceLivenessSession( + val challengeId: String, + val challengeType: FaceLivenessChallengeType, val challenges: List, private val onVideoEvent: (VideoEvent) -> Unit, private val onChallengeResponseEvent: (ChallengeResponseEvent) -> Unit, diff --git a/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSessionInformation.kt b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSessionInformation.kt index 57d272eafb..8bc28bc067 100644 --- a/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSessionInformation.kt +++ b/core/src/main/java/com/amplifyframework/predictions/models/FaceLivenessSessionInformation.kt @@ -17,9 +17,55 @@ package com.amplifyframework.predictions.models import com.amplifyframework.annotations.InternalAmplifyApi @InternalAmplifyApi -data class FaceLivenessSessionInformation( - val videoWidth: Float, - val videoHeight: Float, - val challengeVersions: String, +class FaceLivenessSessionInformation { + val videoWidth: Float + val videoHeight: Float + val challengeVersions: List val region: String -) + val preCheckViewEnabled: Boolean? + val attemptCount: Int? + + @Deprecated("Keeping compatibility for <= Amplify Liveness 1.2.6") + constructor( + videoWidth: Float, + videoHeight: Float, + challenge: String, + region: String + ) { + this.videoWidth = videoWidth + this.videoHeight = videoHeight + this.challengeVersions = listOf(Challenge.FaceMovementAndLightChallenge("1.0.0")) + this.region = region + this.preCheckViewEnabled = null + this.attemptCount = null + } + + constructor( + videoWidth: Float, + videoHeight: Float, + region: String, + challengeVersions: List, + preCheckViewEnabled: Boolean, + attemptCount: Int + ) { + this.videoWidth = videoWidth + this.videoHeight = videoHeight + this.region = region + this.challengeVersions = challengeVersions + this.preCheckViewEnabled = preCheckViewEnabled + this.attemptCount = attemptCount + } +} + +@InternalAmplifyApi +sealed class Challenge private constructor(val name: String, open val version: String) { + + @InternalAmplifyApi + data class FaceMovementChallenge(override val version: String) : Challenge("FaceMovementChallenge", version) + + @InternalAmplifyApi + data class FaceMovementAndLightChallenge(override val version: String) : + Challenge("FaceMovementAndLightChallenge", version) + + fun toQueryParamString(): String = "${name}_$version" +}