Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,22 @@
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
import com.amplifyframework.predictions.aws.models.liveness.LivenessResponseStream
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
Expand Down Expand Up @@ -73,12 +77,16 @@
val credentialsProvider: CredentialsProvider,
val endpoint: String,
val region: String,
val sessionInformation: FaceLivenessSessionInformation,
val clientSessionInformation: FaceLivenessSessionInformation,
val livenessVersion: String?,
val onSessionInformationReceived: Consumer<SessionInformation>,
val onSessionResponseReceived: Consumer<SessionResponse>,
val onErrorReceived: Consumer<PredictionsException>,
val onComplete: Action
) {
internal data class SessionResponse(
val faceLivenessSession: SessionInformation,
val livenessChallengeType: FaceLivenessChallengeType
)

private val signer = AWSV4Signer()
private var credentials: Credentials? = null
Expand All @@ -94,6 +102,7 @@
@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
Expand Down Expand Up @@ -148,10 +157,33 @@
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

Check warning on line 170 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L170

Added line #L170 was not covered by tests
}

// 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) {
[email protected]?.close(
NORMAL_SOCKET_CLOSURE_STATUS_CODE,
Expand Down Expand Up @@ -362,16 +394,26 @@
// 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

Check warning on line 400 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L397-L400

Added lines #L397 - L400 were not covered by tests
)
faceDetectedStart = adjustedDate(videoStartTime)
val clientInfoEvent =
ClientSessionInformationEvent(
challenge = ClientChallenge(
faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge(

val resolvedChallengeType = challengeType

Check warning on line 404 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L404

Added line #L404 was not covered by tests
if (resolvedChallengeType == null) {
onErrorReceived.accept(
PredictionsException(
"Failed to send an initial face detected event",
AmplifyException.TODO_RECOVERY_SUGGESTION

Check warning on line 409 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L406-L409

Added lines #L406 - L409 were not covered by tests
)
)
} else {
val clientInfoEvent =
ClientSessionInformationEvent(
challenge = buildClientChallenge(
challengeType = resolvedChallengeType,

Check warning on line 416 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L413-L416

Added lines #L413 - L416 were not covered by tests
challengeId = challengeId,
initialFace = InitialFace(
boundingBox = initialDetectedFace!!,
Expand All @@ -380,14 +422,23 @@
videoStartTimestamp = videoStartTimestamp
)
)
)
sendClientInfoEvent(clientInfoEvent)
sendClientInfoEvent(clientInfoEvent)

Check warning on line 425 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L425

Added line #L425 was not covered by tests
}
}

fun sendFinalEvent(targetFaceRect: RectF, faceMatchedStart: Long, faceMatchedEnd: Long) {
val finalClientInfoEvent = ClientSessionInformationEvent(
challenge = ClientChallenge(
FaceMovementAndLightClientChallenge(
val resolvedChallengeType = challengeType

Check warning on line 430 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L430

Added line #L430 was not covered by tests
if (resolvedChallengeType == null) {
onErrorReceived.accept(
PredictionsException(
"Failed to send an initial face detected event",
AmplifyException.TODO_RECOVERY_SUGGESTION

Check warning on line 435 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L432-L435

Added lines #L432 - L435 were not covered by tests
)
)
} else {
val finalClientInfoEvent = ClientSessionInformationEvent(
challenge = buildClientChallenge(
challengeType = resolvedChallengeType,

Check warning on line 441 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L439-L441

Added lines #L439 - L441 were not covered by tests
challengeId = challengeId,
videoEndTimestamp = videoEndTimestamp,
initialFace = InitialFace(
Expand All @@ -398,16 +449,16 @@
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

Check warning on line 455 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L452-L455

Added lines #L452 - L455 were not covered by tests
)
)
)
)
)
sendClientInfoEvent(finalClientInfoEvent)
sendClientInfoEvent(finalClientInfoEvent)

Check warning on line 460 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L460

Added line #L460 was not covered by tests
}
}

fun sendColorDisplayedEvent(
Expand Down Expand Up @@ -525,8 +576,47 @@

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

Check warning on line 586 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L582-L586

Added lines #L582 - L586 were not covered by tests
): ClientChallenge = when (challengeType) {
FaceLivenessChallengeType.FaceMovementAndLightChallenge -> {
ClientChallenge(
faceMovementAndLightChallenge = FaceMovementAndLightClientChallenge(
challengeId = challengeId,
videoStartTimestamp = videoStartTimestamp,
videoEndTimestamp = videoEndTimestamp,
initialFace = initialFace,
targetFace = targetFace,
colorDisplayed = colorDisplayed

Check warning on line 596 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L589-L596

Added lines #L589 - L596 were not covered by tests
),
faceMovementChallenge = null

Check warning on line 598 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L598

Added line #L598 was not covered by tests
)
}
FaceLivenessChallengeType.FaceMovementChallenge -> {
ClientChallenge(
faceMovementAndLightChallenge = null,
faceMovementChallenge = FaceMovementClientChallenge(
challengeId = challengeId,
videoStartTimestamp = videoStartTimestamp,
videoEndTimestamp = videoEndTimestamp,
initialFace = initialFace,
targetFace = targetFace

Check warning on line 609 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt#L602-L609

Added lines #L602 - L609 were not covered by tests
)
)
}
}

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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
val targetIouThreshold: Float,
val targetIouWidthThreshold: Float,
val targetIouHeightThreshold: Float,
val targetHeightWidthRatio: Float,
val faceDetectionThreshold: Float,

Check warning on line 25 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/FaceTargetMatchingParameters.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/FaceTargetMatchingParameters.kt#L24-L25

Added lines #L24 - L25 were not covered by tests
val faceIouWidthThreshold: Float,
val faceIouHeightThreshold: Float,
val faceDistanceThreshold: Float,
val faceDistanceThresholdMin: Float,

Check warning on line 29 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/FaceTargetMatchingParameters.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/FaceTargetMatchingParameters.kt#L28-L29

Added lines #L28 - L29 were not covered by tests
val ovalFitTimeout: Int
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@

@Serializable
internal data class ClientChallenge(
@SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge: FaceMovementAndLightClientChallenge
@SerialName("FaceMovementAndLightChallenge") val faceMovementAndLightChallenge:

Check warning on line 22 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/ClientChallenge.kt#L22

Added line #L22 was not covered by tests
FaceMovementAndLightClientChallenge? = null,
@SerialName("FaceMovementChallenge") val faceMovementChallenge: FaceMovementClientChallenge? = null
)
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 20 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt#L20

Added line #L20 was not covered by tests
internal data class FaceMovementClientChallenge(
@SerialName("ChallengeId") val challengeId: String,

Check warning on line 22 in aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt

View check run for this annotation

Codecov / codecov/patch

aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/FaceMovementClientChallenge.kt#L22

Added line #L22 was not covered by tests
@SerialName("VideoStartTimestamp") val videoStartTimestamp: Long? = null,
@SerialName("VideoEndTimestamp") val videoEndTimestamp: Long? = null,
@SerialName("InitialFace") val initialFace: InitialFace? = null,
@SerialName("TargetFace") val targetFace: TargetFace? = null
)
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading