Skip to content

Commit 690ddf9

Browse files
authored
feat(liveness): Add support for configuring the back camera for the no light challenge (#242)
1 parent 3e56862 commit 690ddf9

File tree

4 files changed

+114
-8
lines changed

4 files changed

+114
-8
lines changed

liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
4747
import com.amplifyframework.ui.liveness.model.LivenessCheckState
4848
import com.amplifyframework.ui.liveness.state.AttemptCounter
4949
import com.amplifyframework.ui.liveness.state.LivenessState
50+
import com.amplifyframework.ui.liveness.ui.Camera
51+
import com.amplifyframework.ui.liveness.ui.ChallengeOptions
5052
import com.amplifyframework.ui.liveness.util.WebSocketCloseCode
5153
import java.util.Date
5254
import java.util.concurrent.Executors
@@ -68,11 +70,12 @@ internal typealias OnFreshnessColorDisplayed = (
6870
@SuppressLint("UnsafeOptInUsageError")
6971
internal class LivenessCoordinator(
7072
val context: Context,
71-
lifecycleOwner: LifecycleOwner,
73+
private val lifecycleOwner: LifecycleOwner,
7274
private val sessionId: String,
7375
private val region: String,
7476
private val credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
7577
private val disableStartView: Boolean,
78+
private val challengeOptions: ChallengeOptions,
7679
private val onChallengeComplete: OnChallengeComplete,
7780
val onChallengeFailed: Consumer<FaceLivenessDetectionException>
7881
) {
@@ -142,6 +145,14 @@ internal class LivenessCoordinator(
142145

143146
init {
144147
startLivenessSession()
148+
if (challengeOptions.hasOneCameraConfigured()) {
149+
launchCamera(challengeOptions.faceMovementAndLight.camera)
150+
} else {
151+
livenessState.loadingCameraPreview = true
152+
}
153+
}
154+
155+
private fun launchCamera(camera: Camera) {
145156
MainScope().launch {
146157
delay(5_000)
147158
if (!previewTextureView.hasReceivedUpdate) {
@@ -156,17 +167,24 @@ internal class LivenessCoordinator(
156167
getCameraProvider(context).apply {
157168
if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.DESTROYED) {
158169
unbindAll()
159-
if (this.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) {
170+
171+
val (chosenCamera, orientation) = when (camera) {
172+
Camera.Front -> Pair(CameraSelector.DEFAULT_FRONT_CAMERA, "front")
173+
Camera.Back -> Pair(CameraSelector.DEFAULT_BACK_CAMERA, "back")
174+
}
175+
176+
if (this.hasCamera(chosenCamera)) {
160177
bindToLifecycle(
161178
lifecycleOwner,
162-
CameraSelector.DEFAULT_FRONT_CAMERA,
179+
chosenCamera,
163180
preview,
164181
analysis
165182
)
166183
} else {
184+
livenessState.loadingCameraPreview = false
167185
val faceLivenessException = FaceLivenessDetectionException(
168-
"A front facing camera is required but no front facing camera detected.",
169-
"Enable a front facing camera."
186+
"A $orientation facing camera is required but no $orientation facing camera detected.",
187+
"Enable a $orientation facing camera."
170188
)
171189
processSessionError(faceLivenessException, true)
172190
}
@@ -200,7 +218,13 @@ internal class LivenessCoordinator(
200218
faceLivenessSessionInformation,
201219
faceLivenessSessionOptions,
202220
BuildConfig.LIVENESS_VERSION_NAME,
203-
{ livenessState.onLivenessSessionReady(it) },
221+
{
222+
livenessState.onLivenessSessionReady(it)
223+
if (!challengeOptions.hasOneCameraConfigured()) {
224+
val foundChallenge = challengeOptions.getLivenessChallenge(it.challengeType)
225+
launchCamera(foundChallenge.camera)
226+
}
227+
},
204228
{
205229
disconnectEventReceived = true
206230
onChallengeComplete()

liveness/src/main/java/com/amplifyframework/ui/liveness/ml/FaceDetector.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package com.amplifyframework.ui.liveness.ml
1818
import android.content.Context
1919
import android.graphics.RectF
2020
import androidx.annotation.VisibleForTesting
21-
import com.amplifyframework.predictions.aws.models.FaceTargetChallenge
2221
import com.amplifyframework.predictions.aws.models.FaceTargetMatchingParameters
2322
import com.amplifyframework.ui.liveness.R
2423
import com.amplifyframework.ui.liveness.camera.LivenessCoordinator.Companion.TARGET_HEIGHT

liveness/src/main/java/com/amplifyframework/ui/liveness/state/LivenessState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ internal data class LivenessState(
6060
var initialLocalFaceFound by mutableStateOf(false)
6161

6262
var showingStartView by mutableStateOf(!disableStartView)
63+
var loadingCameraPreview by mutableStateOf(false)
6364

6465
private var initialStreamFace: InitialStreamFace? = null
6566
@VisibleForTesting

liveness/src/main/java/com/amplifyframework/ui/liveness/ui/FaceLivenessDetector.kt

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding
2727
import androidx.compose.foundation.layout.size
2828
import androidx.compose.foundation.layout.width
2929
import androidx.compose.material3.Button
30+
import androidx.compose.material3.CircularProgressIndicator
3031
import androidx.compose.material3.LinearProgressIndicator
3132
import androidx.compose.material3.MaterialTheme
3233
import androidx.compose.material3.Surface
@@ -83,9 +84,37 @@ fun FaceLivenessDetector(
8384
disableStartView: Boolean = false,
8485
onComplete: Action,
8586
onError: Consumer<FaceLivenessDetectionException>
87+
) = FaceLivenessDetector(
88+
sessionId,
89+
region,
90+
credentialsProvider,
91+
disableStartView,
92+
onComplete,
93+
onError,
94+
ChallengeOptions()
95+
)
96+
97+
/**
98+
* @param sessionId of challenge
99+
* @param region AWS region to stream the video to. Current supported regions are listed in [add link here]
100+
* @param credentialsProvider to provide custom CredentialsProvider for authentication. Default uses initialized Amplify.Auth CredentialsProvider
101+
* @param disableStartView to bypass warmup screen.
102+
* @param challengeOptions is the list of ChallengeOptions that are to be overridden from the default configuration
103+
* @param onComplete callback notifying a completed challenge
104+
* @param onError callback containing exception for cause
105+
*/
106+
@Composable
107+
fun FaceLivenessDetector(
108+
sessionId: String,
109+
region: String,
110+
credentialsProvider: AWSCredentialsProvider<AWSCredentials>? = null,
111+
disableStartView: Boolean = false,
112+
onComplete: Action,
113+
onError: Consumer<FaceLivenessDetectionException>,
114+
challengeOptions: ChallengeOptions = ChallengeOptions(),
86115
) {
87116
val scope = rememberCoroutineScope()
88-
val key = Triple(sessionId, region, credentialsProvider)
117+
val key = DetectorStateKey(sessionId, region, credentialsProvider, challengeOptions)
89118
var isFinished by remember(key) { mutableStateOf(false) }
90119
val currentOnComplete by rememberUpdatedState(onComplete)
91120
val currentOnError by rememberUpdatedState(onError)
@@ -124,6 +153,7 @@ fun FaceLivenessDetector(
124153
region,
125154
credentialsProvider = credentialsProvider,
126155
disableStartView,
156+
challengeOptions = challengeOptions,
127157
onChallengeComplete = {
128158
scope.launch {
129159
// if we are already finished, we already provided a result in complete or failed
@@ -156,6 +186,7 @@ internal fun ChallengeView(
156186
region: String,
157187
credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
158188
disableStartView: Boolean,
189+
challengeOptions: ChallengeOptions,
159190
onChallengeComplete: OnChallengeComplete,
160191
onChallengeFailed: Consumer<FaceLivenessDetectionException>
161192
) {
@@ -176,6 +207,7 @@ internal fun ChallengeView(
176207
region,
177208
credentialsProvider,
178209
disableStartView,
210+
challengeOptions,
179211
onChallengeComplete = { currentOnChallengeComplete() },
180212
onChallengeFailed = { currentOnChallengeFailed.accept(it) }
181213
)
@@ -232,6 +264,15 @@ internal fun ChallengeView(
232264

233265
if (livenessState.showingStartView) {
234266

267+
if (livenessState.loadingCameraPreview) {
268+
CircularProgressIndicator(
269+
color = MaterialTheme.colorScheme.primary,
270+
modifier = Modifier
271+
.align(Alignment.Center),
272+
strokeWidth = 2.dp,
273+
)
274+
}
275+
235276
FaceGuide(
236277
modifier = Modifier
237278
.fillMaxSize()
@@ -402,6 +443,47 @@ internal fun ChallengeView(
402443
}
403444
}
404445

446+
internal data class DetectorStateKey(
447+
val sessionId: String,
448+
val region: String,
449+
val credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
450+
val challengeOptions: ChallengeOptions
451+
)
452+
453+
data class ChallengeOptions(
454+
val faceMovementAndLight: LivenessChallenge.FaceMovementAndLight = LivenessChallenge.FaceMovementAndLight,
455+
val faceMovement: LivenessChallenge.FaceMovement = LivenessChallenge.FaceMovement()
456+
) {
457+
internal fun getLivenessChallenge(challengeType: FaceLivenessChallengeType): LivenessChallenge =
458+
when (challengeType) {
459+
FaceLivenessChallengeType.FaceMovementAndLightChallenge -> faceMovementAndLight
460+
FaceLivenessChallengeType.FaceMovementChallenge -> faceMovement
461+
}
462+
463+
/**
464+
* @return true if all of the challenge options are configured to use the same camera configuration
465+
*/
466+
internal fun hasOneCameraConfigured(): Boolean =
467+
listOf(
468+
faceMovementAndLight,
469+
faceMovement
470+
).all { it.camera == faceMovementAndLight.camera }
471+
}
472+
473+
sealed class LivenessChallenge(
474+
open val camera: Camera = Camera.Front
475+
) {
476+
data class FaceMovement(override val camera: Camera = Camera.Front) : LivenessChallenge(
477+
camera = camera
478+
)
479+
data object FaceMovementAndLight : LivenessChallenge()
480+
}
481+
482+
sealed class Camera {
483+
data object Front : Camera()
484+
data object Back : Camera()
485+
}
486+
405487
private fun FaceLivenessSession?.isFaceMovementAndLightChallenge(): Boolean =
406488
this?.challengeType == FaceLivenessChallengeType.FaceMovementAndLightChallenge
407489

0 commit comments

Comments
 (0)