Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(liveness): Add support for configuring the back camera for the no light challenge #195

Draft
wants to merge 1 commit into
base: feature/no-light-sku
Choose a base branch
from
Draft
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
Expand Up @@ -47,6 +47,8 @@ import com.amplifyframework.ui.liveness.model.FaceLivenessDetectionException
import com.amplifyframework.ui.liveness.model.LivenessCheckState
import com.amplifyframework.ui.liveness.state.AttemptCounter
import com.amplifyframework.ui.liveness.state.LivenessState
import com.amplifyframework.ui.liveness.ui.Camera
import com.amplifyframework.ui.liveness.ui.ChallengeOptions
import com.amplifyframework.ui.liveness.util.WebSocketCloseCode
import java.util.Date
import java.util.concurrent.Executors
Expand All @@ -67,11 +69,12 @@ internal typealias OnFreshnessColorDisplayed = (
@SuppressLint("UnsafeOptInUsageError")
internal class LivenessCoordinator(
val context: Context,
lifecycleOwner: LifecycleOwner,
private val lifecycleOwner: LifecycleOwner,
private val sessionId: String,
private val region: String,
private val credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
private val disableStartView: Boolean,
private val challengeOptions: ChallengeOptions,
private val onChallengeComplete: OnChallengeComplete,
val onChallengeFailed: Consumer<FaceLivenessDetectionException>
) {
Expand Down Expand Up @@ -141,24 +144,40 @@ internal class LivenessCoordinator(

init {
startLivenessSession()
if (challengeOptions.hasOneCameraConfigured()) {
launchCamera(challengeOptions.faceMovementAndLight.camera)
} else {
livenessState.loadingCameraPreview = true
}
}

private fun launchCamera(camera: Camera) {
MainScope().launch {
getCameraProvider(context).apply {
if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.DESTROYED) {
unbindAll()
if (this.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) {

val (chosenCamera, orientation) = when (camera) {
Camera.Front -> Pair(CameraSelector.DEFAULT_FRONT_CAMERA, "front")
Camera.Back -> Pair(CameraSelector.DEFAULT_BACK_CAMERA, "back")
}

if (this.hasCamera(chosenCamera)) {
bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_FRONT_CAMERA,
chosenCamera,
preview,
analysis
)
} else {
livenessState.loadingCameraPreview = false
val faceLivenessException = FaceLivenessDetectionException(
"A front facing camera is required but no front facing camera detected.",
"Enable a front facing camera."
"A $orientation facing camera is required but no $orientation facing camera detected.",
"Enable a $orientation facing camera."
)
processSessionError(faceLivenessException, true)
}
livenessState.loadingCameraPreview = false
}
}
}
Expand Down Expand Up @@ -189,7 +208,13 @@ internal class LivenessCoordinator(
faceLivenessSessionInformation,
faceLivenessSessionOptions,
BuildConfig.LIVENESS_VERSION_NAME,
{ livenessState.onLivenessSessionReady(it) },
{
livenessState.onLivenessSessionReady(it)
if (!challengeOptions.hasOneCameraConfigured()) {
val foundChallenge = challengeOptions.getOptions(it.challengeType)
launchCamera(foundChallenge.camera)
}
},
{
disconnectEventReceived = true
onChallengeComplete()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal data class LivenessState(
var initialLocalFaceFound by mutableStateOf(false)

var showingStartView by mutableStateOf(!disableStartView)
var loadingCameraPreview by mutableStateOf(false)

private var initialStreamFace: InitialStreamFace? = null
@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
Expand Down Expand Up @@ -72,6 +73,7 @@ import kotlinx.coroutines.launch
* @param region AWS region to stream the video to. Current supported regions are listed in [add link here]
* @param credentialsProvider to provide custom CredentialsProvider for authentication. Default uses initialized Amplify.Auth CredentialsProvider
* @param disableStartView to bypass warmup screen.
* @param challengeOptions is the list of ChallengeOptions that are to be overridden from the default configuration
* @param onComplete callback notifying a completed challenge
* @param onError callback containing exception for cause
*/
Expand All @@ -81,6 +83,7 @@ fun FaceLivenessDetector(
region: String,
credentialsProvider: AWSCredentialsProvider<AWSCredentials>? = null,
disableStartView: Boolean = false,
challengeOptions: ChallengeOptions = ChallengeOptions(),
onComplete: Action,
onError: Consumer<FaceLivenessDetectionException>
) {
Expand Down Expand Up @@ -124,6 +127,7 @@ fun FaceLivenessDetector(
region,
credentialsProvider = credentialsProvider,
disableStartView,
challengeOptions = challengeOptions,
onChallengeComplete = {
scope.launch {
// if we are already finished, we already provided a result in complete or failed
Expand Down Expand Up @@ -156,6 +160,7 @@ internal fun ChallengeView(
region: String,
credentialsProvider: AWSCredentialsProvider<AWSCredentials>?,
disableStartView: Boolean,
challengeOptions: ChallengeOptions,
onChallengeComplete: OnChallengeComplete,
onChallengeFailed: Consumer<FaceLivenessDetectionException>
) {
Expand All @@ -176,6 +181,7 @@ internal fun ChallengeView(
region,
credentialsProvider,
disableStartView,
challengeOptions,
onChallengeComplete = { currentOnChallengeComplete() },
onChallengeFailed = { currentOnChallengeFailed.accept(it) }
)
Expand Down Expand Up @@ -232,6 +238,15 @@ internal fun ChallengeView(

if (livenessState.showingStartView) {

if (livenessState.loadingCameraPreview) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.align(Alignment.Center),
strokeWidth = 2.dp,
)
}

FaceGuide(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -402,6 +417,40 @@ internal fun ChallengeView(
}
}

data class ChallengeOptions(
val faceMovementAndLight: LivenessChallenge.FaceMovementAndLight = LivenessChallenge.FaceMovementAndLight,
val faceMovement: LivenessChallenge.FaceMovement = LivenessChallenge.FaceMovement()
) {
fun getOptions(challengeType: FaceLivenessChallengeType): LivenessChallenge =
when(challengeType) {
FaceLivenessChallengeType.FaceMovementAndLightChallenge -> faceMovementAndLight
FaceLivenessChallengeType.FaceMovementChallenge -> faceMovement
}

/**
* @return true if all of the challenge options are configured to use the same camera configuration
*/
fun hasOneCameraConfigured(): Boolean =
listOf(
faceMovementAndLight,
faceMovement
).all { it.camera == faceMovementAndLight.camera }
}

sealed class LivenessChallenge(
val camera: Camera = Camera.Front
) {
class FaceMovement(camera: Camera = Camera.Front): LivenessChallenge(
camera = camera
)
object FaceMovementAndLight: LivenessChallenge()
}

sealed class Camera {
object Front: Camera()
object Back: Camera()
}

private fun FaceLivenessSession?.isFaceMovementAndLightChallenge(): Boolean =
this?.challengeType == FaceLivenessChallengeType.FaceMovementAndLightChallenge

Expand Down