Skip to content

Commit b97af6f

Browse files
committed
feat(liveness): Add support for configuring the back camera for the no light challenge
1 parent 00283d4 commit b97af6f

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
@@ -67,11 +69,12 @@ internal typealias OnFreshnessColorDisplayed = (
6769
@SuppressLint("UnsafeOptInUsageError")
6870
internal class LivenessCoordinator(
6971
val context: Context,
70-
lifecycleOwner: LifecycleOwner,
72+
private val lifecycleOwner: LifecycleOwner,
7173
private val sessionId: String,
7274
private val region: String,
7375
private val credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
7476
private val disableStartView: Boolean,
77+
private val challengeOptions: ChallengeOptions,
7578
private val onChallengeComplete: OnChallengeComplete,
7679
val onChallengeFailed: Consumer<FaceLivenessDetectionException>
7780
) {
@@ -141,24 +144,40 @@ internal class LivenessCoordinator(
141144

142145
init {
143146
startLivenessSession()
147+
if (challengeOptions.hasOneCameraConfigured()) {
148+
launchCamera(challengeOptions.faceMovementAndLight.camera)
149+
} else {
150+
livenessState.loadingCameraPreview = true
151+
}
152+
}
153+
154+
private fun launchCamera(camera: Camera) {
144155
MainScope().launch {
145156
getCameraProvider(context).apply {
146157
if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.DESTROYED) {
147158
unbindAll()
148-
if (this.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) {
159+
160+
val (chosenCamera, orientation) = when (camera) {
161+
Camera.Front -> Pair(CameraSelector.DEFAULT_FRONT_CAMERA, "front")
162+
Camera.Back -> Pair(CameraSelector.DEFAULT_BACK_CAMERA, "back")
163+
}
164+
165+
if (this.hasCamera(chosenCamera)) {
149166
bindToLifecycle(
150167
lifecycleOwner,
151-
CameraSelector.DEFAULT_FRONT_CAMERA,
168+
chosenCamera,
152169
preview,
153170
analysis
154171
)
155172
} else {
173+
livenessState.loadingCameraPreview = false
156174
val faceLivenessException = FaceLivenessDetectionException(
157-
"A front facing camera is required but no front facing camera detected.",
158-
"Enable a front facing camera."
175+
"A $orientation facing camera is required but no $orientation facing camera detected.",
176+
"Enable a $orientation facing camera."
159177
)
160178
processSessionError(faceLivenessException, true)
161179
}
180+
livenessState.loadingCameraPreview = false
162181
}
163182
}
164183
}
@@ -189,7 +208,13 @@ internal class LivenessCoordinator(
189208
faceLivenessSessionInformation,
190209
faceLivenessSessionOptions,
191210
BuildConfig.LIVENESS_VERSION_NAME,
192-
{ livenessState.onLivenessSessionReady(it) },
211+
{
212+
livenessState.onLivenessSessionReady(it)
213+
if (!challengeOptions.hasOneCameraConfigured()) {
214+
val foundChallenge = challengeOptions.getOptions(it.challengeType)
215+
launchCamera(foundChallenge.camera)
216+
}
217+
},
193218
{
194219
disconnectEventReceived = true
195220
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)