Skip to content

Commit 9a074b9

Browse files
committed
feat(liveness): Add support for configuring the back camera for the no light challenge
1 parent 2d9049f commit 9a074b9

File tree

3 files changed

+81
-6
lines changed

3 files changed

+81
-6
lines changed

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

+31-6
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,20 +167,28 @@ 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
}
191+
livenessState.loadingCameraPreview = false
173192
}
174193
}
175194
}
@@ -200,7 +219,13 @@ internal class LivenessCoordinator(
200219
faceLivenessSessionInformation,
201220
faceLivenessSessionOptions,
202221
BuildConfig.LIVENESS_VERSION_NAME,
203-
{ livenessState.onLivenessSessionReady(it) },
222+
{
223+
livenessState.onLivenessSessionReady(it)
224+
if (!challengeOptions.hasOneCameraConfigured()) {
225+
val foundChallenge = challengeOptions.getOptions(it.challengeType)
226+
launchCamera(foundChallenge.camera)
227+
}
228+
},
204229
{
205230
disconnectEventReceived = true
206231
onChallengeComplete()

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

+1
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

+49
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
@@ -72,6 +73,7 @@ import kotlinx.coroutines.launch
7273
* @param region AWS region to stream the video to. Current supported regions are listed in [add link here]
7374
* @param credentialsProvider to provide custom CredentialsProvider for authentication. Default uses initialized Amplify.Auth CredentialsProvider
7475
* @param disableStartView to bypass warmup screen.
76+
* @param challengeOptions is the list of ChallengeOptions that are to be overridden from the default configuration
7577
* @param onComplete callback notifying a completed challenge
7678
* @param onError callback containing exception for cause
7779
*/
@@ -81,6 +83,7 @@ fun FaceLivenessDetector(
8183
region: String,
8284
credentialsProvider: AWSCredentialsProvider<AWSCredentials>? = null,
8385
disableStartView: Boolean = false,
86+
challengeOptions: ChallengeOptions = ChallengeOptions(),
8487
onComplete: Action,
8588
onError: Consumer<FaceLivenessDetectionException>
8689
) {
@@ -124,6 +127,7 @@ fun FaceLivenessDetector(
124127
region,
125128
credentialsProvider = credentialsProvider,
126129
disableStartView,
130+
challengeOptions = challengeOptions,
127131
onChallengeComplete = {
128132
scope.launch {
129133
// if we are already finished, we already provided a result in complete or failed
@@ -156,6 +160,7 @@ internal fun ChallengeView(
156160
region: String,
157161
credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
158162
disableStartView: Boolean,
163+
challengeOptions: ChallengeOptions,
159164
onChallengeComplete: OnChallengeComplete,
160165
onChallengeFailed: Consumer<FaceLivenessDetectionException>
161166
) {
@@ -176,6 +181,7 @@ internal fun ChallengeView(
176181
region,
177182
credentialsProvider,
178183
disableStartView,
184+
challengeOptions,
179185
onChallengeComplete = { currentOnChallengeComplete() },
180186
onChallengeFailed = { currentOnChallengeFailed.accept(it) }
181187
)
@@ -232,6 +238,15 @@ internal fun ChallengeView(
232238

233239
if (livenessState.showingStartView) {
234240

241+
if (livenessState.loadingCameraPreview) {
242+
CircularProgressIndicator(
243+
color = MaterialTheme.colorScheme.primary,
244+
modifier = Modifier
245+
.align(Alignment.Center),
246+
strokeWidth = 2.dp,
247+
)
248+
}
249+
235250
FaceGuide(
236251
modifier = Modifier
237252
.fillMaxSize()
@@ -402,6 +417,40 @@ internal fun ChallengeView(
402417
}
403418
}
404419

420+
data class ChallengeOptions(
421+
val faceMovementAndLight: LivenessChallenge.FaceMovementAndLight = LivenessChallenge.FaceMovementAndLight,
422+
val faceMovement: LivenessChallenge.FaceMovement = LivenessChallenge.FaceMovement()
423+
) {
424+
fun getOptions(challengeType: FaceLivenessChallengeType): LivenessChallenge =
425+
when(challengeType) {
426+
FaceLivenessChallengeType.FaceMovementAndLightChallenge -> faceMovementAndLight
427+
FaceLivenessChallengeType.FaceMovementChallenge -> faceMovement
428+
}
429+
430+
/**
431+
* @return true if all of the challenge options are configured to use the same camera configuration
432+
*/
433+
fun hasOneCameraConfigured(): Boolean =
434+
listOf(
435+
faceMovementAndLight,
436+
faceMovement
437+
).all { it.camera == faceMovementAndLight.camera }
438+
}
439+
440+
sealed class LivenessChallenge(
441+
val camera: Camera = Camera.Front
442+
) {
443+
class FaceMovement(camera: Camera = Camera.Front): LivenessChallenge(
444+
camera = camera
445+
)
446+
object FaceMovementAndLight: LivenessChallenge()
447+
}
448+
449+
sealed class Camera {
450+
object Front: Camera()
451+
object Back: Camera()
452+
}
453+
405454
private fun FaceLivenessSession?.isFaceMovementAndLightChallenge(): Boolean =
406455
this?.challengeType == FaceLivenessChallengeType.FaceMovementAndLightChallenge
407456

0 commit comments

Comments
 (0)