Skip to content
Open
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 @@ -3,6 +3,7 @@
import android.app.Activity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.simprints.core.DeviceID
Expand Down Expand Up @@ -55,9 +56,14 @@
private val saveLicenseCheckEvent: SaveLicenseCheckEventUseCase,
private val shouldShowInstructions: ShouldShowInstructionsScreenUseCase,
@param:DeviceID private val deviceID: String,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
// Updated in live feedback screen
var attemptNumber: Int = 0
// Number of times the user has (re)started a capture attempt for the current capture step.
var attemptNumber: Int
get() = savedStateHandle[KEY_ATTEMPT_NUMBER] ?: 0
private set(value) {
savedStateHandle[KEY_ATTEMPT_NUMBER] = value
}
var samplesToCapture = 1
var initialised = false
lateinit var bioSDK: ModalitySdkType
Expand Down Expand Up @@ -88,10 +94,14 @@
this.samplesToCapture = samplesToCapture
}

fun initFaceBioSdk(

Check failure on line 97 in face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Simprints_Android-Simprints-ID&issues=AaAP8LuDF6kTw8p1wvAC&open=AaAP8LuDF6kTw8p1wvAC&pullRequest=1791
activity: Activity,
sdk: ModalitySdkType,
) = viewModelScope.launch {
if (::bioSDK.isInitialized && bioSDK != sdk) {
resetForNewCaptureStep()
}

if (initialised) {
Simber.i("Face bio SDK already initialised", tag = FACE_CAPTURE)
return@launch
Expand Down Expand Up @@ -207,10 +217,21 @@

fun recapture() {
Simber.i("Starting face recapture flow", tag = FACE_CAPTURE)
attemptNumber++
faceDetections = listOf()
_recaptureEvent.send()
}

/**
** Resets the state that is scoped to a single capture step
**/
private fun resetForNewCaptureStep() {
Simber.i("Resetting face capture state for a new capture step", tag = FACE_CAPTURE)
attemptNumber = 0
initialised = false
faceDetections = emptyList()
}
Comment thread
Copilot marked this conversation as resolved.

private fun saveFaceDetections() {
Simber.i("Saving captures to disk", tag = FACE_CAPTURE)
faceDetections.forEach { saveImage(it, it.id) }
Expand Down Expand Up @@ -241,4 +262,8 @@
) {
eventReporter.addCaptureConfirmationEvent(startTime, isContinue)
}

private companion object {
private const val KEY_ATTEMPT_NUMBER = "FaceCaptureViewModel.attemptNumber"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ internal class LiveFeedbackViewModel @Inject constructor(
private val getSpoofCheckConfiguration: GetSpoofCheckConfigurationUseCase,
@param:DispatcherBG private val bgDispatcher: CoroutineDispatcher,
) : ViewModel() {
private var attemptNumber: Int = 1
private var attemptNumber: Int = 0
private var samplesToCapture: Int = 1
private var qualityThreshold: Float = 0f

Expand Down Expand Up @@ -368,6 +368,7 @@ internal class LiveFeedbackViewModel @Inject constructor(
val duration = measureTimedValue {
// Still track the capture attempt events for analytics and troubleshooting
sendCaptureEvents(attemptNumber)
attemptNumber++

userCaptures.forEach {
it.original.recycle()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.simprints.face.capture.screens

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.SavedStateHandle
import com.google.common.truth.Truth.*
import com.simprints.core.tools.time.Timestamp
import com.simprints.face.capture.models.FaceDetection
Expand Down Expand Up @@ -67,6 +68,7 @@ class FaceCaptureViewModelTest {
private lateinit var shouldShowInstructionsScreen: ShouldShowInstructionsScreenUseCase

private lateinit var viewModel: FaceCaptureViewModel
private lateinit var savedStateHandle: SavedStateHandle

private val faceDetections = listOf<FaceDetection>(
mockk(relaxed = true) {
Expand All @@ -81,6 +83,7 @@ class FaceCaptureViewModelTest {
coEvery { faceImageUseCase.invoke(any(), any()) } returns null
every { bitmapToByteArrayUseCase.invoke(any()) } returns byteArrayOf()
every { authStore.signedInProjectId } returns "projectId"
savedStateHandle = SavedStateHandle()

viewModel = FaceCaptureViewModel(
authStore,
Expand All @@ -95,6 +98,7 @@ class FaceCaptureViewModelTest {
saveLicenseCheckEvent,
shouldShowInstructionsScreen,
"deviceId",
savedStateHandle,
)
}

Expand Down Expand Up @@ -157,6 +161,63 @@ class FaceCaptureViewModelTest {
assertThat(viewModel.getSampleDetection()).isNull()
}

@Test
fun `Recapture increments attempt number on each call`() {
assertThat(viewModel.attemptNumber).isEqualTo(0)

viewModel.recapture()
assertThat(viewModel.attemptNumber).isEqualTo(1)

viewModel.recapture()
assertThat(viewModel.attemptNumber).isEqualTo(2)
}

@Test
fun `Attempt number survives being restored from a SavedStateHandle`() {
viewModel.recapture()
viewModel.recapture()
assertThat(viewModel.attemptNumber).isEqualTo(2)

val restoredViewModel = FaceCaptureViewModel(
authStore,
configRepository,
faceImageUseCase,
eventReporter,
bitmapToByteArrayUseCase,
licenseRepository,
mockk {
coEvery { this@mockk(any()).initializer } returns faceBioSdkInitializer
},
saveLicenseCheckEvent,
shouldShowInstructionsScreen,
"deviceId",
savedStateHandle,
)

assertThat(restoredViewModel.attemptNumber).isEqualTo(2)
}

@Test
fun `Attempt number and init flag reset when a capture step starts for a different SDK`() {
val license = "license"
coEvery {
licenseRepository.getCachedLicense(Vendor.RankOne)
} returns License("2133-12-30T17:32:28Z", license, LicenseVersion("1.0"))
every { faceBioSdkInitializer.tryInitWithLicense(any(), license) } returns true
coJustRun { saveLicenseCheckEvent(any(), any()) }

viewModel.initFaceBioSdk(mockk(), ModalitySdkType.RANK_ONE)
viewModel.recapture()
viewModel.recapture()
assertThat(viewModel.attemptNumber).isEqualTo(2)
assertThat(viewModel.initialised).isTrue()

// A new capture step is started for a different SDK within the same (Activity-scoped) ViewModel
viewModel.initFaceBioSdk(mockk(), ModalitySdkType.SIM_FACE)

assertThat(viewModel.attemptNumber).isEqualTo(0)
}

@Test
fun `Requests exit form on back press`() {
viewModel.handleBackButton()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,34 @@ internal class LiveFeedbackViewModelTest {
assertThat(viewModel.state.value.phase).isEqualTo(LiveFeedbackState.Phase.FINISHED)
}

@Test
fun `spoof ENFORCED failing retry reports an incrementing attempt number`() = runTest {
every { getSpoofCheckConfiguration.invoke(any(), any()) } returns spoofConfig(FaceConfiguration.SpoofCheckMode.ENFORCED)
every { faceDetector.analyze(frame) } returns getFace()
coEvery { faceDetector.spoofCheck(any(), any()) } returns SpoofCheckResult(score = 0.9f)
val attemptNumbers = mutableListOf<Int>()
coEvery {
eventReporter.addCaptureEvents(any(), capture(attemptNumbers), any(), any(), any())
} just runs

viewModel.initAutoCapture()
viewModel.initCapture(ModalitySdkType.SIM_FACE, 1, 0)

// Attempt 0 fails spoof check
viewModel.process(frame, frame)
viewModel.startCapture()
viewModel.process(frame, frame)
advanceUntilIdle()

// Attempt 1 (retry) reaches maxAttempts and finishes
viewModel.process(frame, frame)
viewModel.startCapture()
viewModel.process(frame, frame)
advanceUntilIdle()

assertThat(attemptNumbers).containsAtLeast(0, 1)
}

@Test
fun `frames are skipped while validating and progress uses the validation tint`() = runTest {
every { getSpoofCheckConfiguration.invoke(any(), any()) } returns spoofConfig()
Expand Down