Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ jobs:
face:infra:bio-sdk-resolver
face:infra:roc-v1
face:infra:roc-v3
face:infra:simface
reportsId: face

fingerprint-unit-tests:
Expand Down
2 changes: 1 addition & 1 deletion build-logic/build_properties.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extra.apply {
* Dev version >= 2024.2.1 is required for receiving biometric sdk age restrictions
* Dev version >= 2024.2.2 is required for float quality thresholds
* Dev version >= 2024.3.0 is required to receive configuration ID
* Dev version >= 2025.2.0 is required to support enrolment record updates
* Dev version >= 2025.2.0 is required to support enrolment record updates and SimFace configuration
*/
set("VERSION_NAME", "2025.2.0")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package com.simprints.face.capture

import android.os.Bundle
import com.simprints.face.capture.screens.controller.FaceCaptureControllerFragmentArgs
import com.simprints.infra.config.store.models.FaceConfiguration

object FaceCaptureContract {
val DESTINATION = R.id.faceCaptureControllerFragment

fun getArgs(samplesToCapture: Int): Bundle = FaceCaptureControllerFragmentArgs(samplesToCapture).toBundle()
fun getArgs(
samplesToCapture: Int,
faceSDK: FaceConfiguration.BioSdk,
): Bundle = FaceCaptureControllerFragmentArgs(FaceCaptureParams(samplesToCapture, faceSDK)).toBundle()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.simprints.face.capture

import android.os.Parcelable
import androidx.annotation.Keep
import com.simprints.infra.config.store.models.FaceConfiguration
import kotlinx.parcelize.Parcelize

@Keep
@Parcelize
data class FaceCaptureParams(
val samplesToCapture: Int,
val faceSDK: FaceConfiguration.BioSdk,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ import com.simprints.core.tools.time.Timestamp
import com.simprints.face.capture.FaceCaptureResult
import com.simprints.face.capture.models.FaceDetection
import com.simprints.face.capture.usecases.BitmapToByteArrayUseCase
import com.simprints.face.capture.usecases.ShouldShowInstructionsScreenUseCase
import com.simprints.face.capture.usecases.IsUsingAutoCaptureUseCase
import com.simprints.face.capture.usecases.SaveFaceImageUseCase
import com.simprints.face.capture.usecases.ShouldShowInstructionsScreenUseCase
import com.simprints.face.capture.usecases.SimpleCaptureEventReporter
import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.license.LicenseRepository
import com.simprints.infra.license.LicenseStatus
import com.simprints.infra.license.SaveLicenseCheckEventUseCase
import com.simprints.infra.license.determineLicenseStatus
import com.simprints.infra.license.models.License
import com.simprints.infra.license.models.License.Companion.NO_LICENSE
import com.simprints.infra.license.models.LicenseState
import com.simprints.infra.license.models.LicenseVersion
import com.simprints.infra.license.models.Vendor
Expand Down Expand Up @@ -59,6 +61,7 @@ internal class FaceCaptureViewModel @Inject constructor(
var attemptNumber: Int = 0
var samplesToCapture = 1
var initialised = false
lateinit var bioSDK: FaceConfiguration.BioSdk

var shouldCheckCameraPermissions = AtomicBoolean(true)

Expand Down Expand Up @@ -92,58 +95,64 @@ internal class FaceCaptureViewModel @Inject constructor(
this.samplesToCapture = samplesToCapture
}

fun initFaceBioSdk(activity: Activity) = viewModelScope.launch {
fun initFaceBioSdk(
activity: Activity,
sdk: FaceConfiguration.BioSdk,
) = viewModelScope.launch {
if (initialised) {
Simber.i("Face bio SDK already initialised", tag = FACE_CAPTURE)
return@launch
}
this@FaceCaptureViewModel.bioSDK = sdk

Simber.i("Starting face capture flow", tag = FACE_CAPTURE)
if (sdk == FaceConfiguration.BioSdk.RANK_ONE) {
val licenseVendor = Vendor.RankOne
val license = licenseRepository.getCachedLicense(licenseVendor)
var licenseStatus = license.determineLicenseStatus()
if (licenseStatus == LicenseStatus.VALID) {
licenseStatus = initialize(activity, license!!)
}

val licenseVendor = Vendor.RankOne
val license = licenseRepository.getCachedLicense(licenseVendor)
var licenseStatus = license.determineLicenseStatus()
if (licenseStatus == LicenseStatus.VALID) {
licenseStatus = initialize(activity, license!!)
}

// In some cases license is invalidated on initialisation attempt
if (licenseStatus != LicenseStatus.VALID) {
Simber.i("Face license is $licenseStatus - attempting download", tag = LICENSE)
licenseStatus = refreshLicenceAndRetry(
activity,
licenseVendor,
LicenseVersion(
configManager
.getProjectConfiguration()
.face
?.rankOne
?.version
.orEmpty(),
),
)
}
// Still invalid after attempted refresh
if (licenseStatus != LicenseStatus.VALID) {
Simber.i("Face license is $licenseStatus", tag = LICENSE)
licenseRepository.deleteCachedLicense(Vendor.RankOne)
_invalidLicense.send()
// In some cases license is invalidated on initialisation attempt
if (licenseStatus != LicenseStatus.VALID) {
Simber.i("Face license is $licenseStatus - attempting download", tag = LICENSE)
licenseStatus = refreshLicenceAndRetry(
activity,
licenseVendor,
LicenseVersion(
configManager
.getProjectConfiguration()
.face
?.rankOne
?.version
.orEmpty(),
),
)
}
// Still invalid after attempted refresh
if (licenseStatus != LicenseStatus.VALID) {
Simber.i("Face license is $licenseStatus", tag = LICENSE)
licenseRepository.deleteCachedLicense(Vendor.RankOne)
_invalidLicense.send()
}
saveLicenseCheckEvent(licenseVendor, licenseStatus)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future we can abstract all this logic in the license repo, so that it takes the SDK as param and returns whatever license is appropriate and the call here is the same regardless of the SDK.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote for removing the license module altogether. :D

Made a ticket for it: https://simprints.atlassian.net/browse/MS-1011

} else {
initialize(activity, NO_LICENSE)
}
saveLicenseCheckEvent(licenseVendor, licenseStatus)
}

fun setupAutoCapture() = viewModelScope.launch {
_isAutoCaptureEnabled.postValue(isUsingAutoCapture())
}

fun shouldShowInstructionsScreen(): Boolean =
shouldShowInstructions()
fun shouldShowInstructionsScreen(): Boolean = shouldShowInstructions()

private suspend fun initialize(
activity: Activity,
license: License,
): LicenseStatus {
val initializer = resolveFaceBioSdk().initializer
val initializer = resolveFaceBioSdk(bioSDK).initializer
if (!initializer.tryInitWithLicense(activity, license.data)) {
// License is valid but the SDK failed to initialize
// This is should reported as an error
Expand Down Expand Up @@ -173,8 +182,8 @@ internal class FaceCaptureViewModel @Inject constructor(
fun flowFinished() {
Simber.i("Finishing capture flow", tag = FACE_CAPTURE)
viewModelScope.launch {
val projectConfiguration = configManager.getProjectConfiguration()
if (projectConfiguration.face?.imageSavingStrategy?.shouldSaveImage() == true) {
val faceConfiguration = configManager.getProjectConfiguration().face?.getSdkConfiguration(bioSDK)
if (faceConfiguration?.imageSavingStrategy?.shouldSaveImage() == true) {
saveFaceDetections()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c
findNavController().finishWithResult(this, result)
}

viewModel.setupCapture(args.samplesToCapture)
viewModel.setupCapture(args.params.samplesToCapture)
initFaceBioSdk()
viewModel.recaptureEvent.observe(
viewLifecycleOwner,
Expand Down Expand Up @@ -131,7 +131,7 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c
R.id.facePreparationFragment
} else {
R.id.faceLiveFeedbackFragment
}
},
)
graph?.let {
internalNavController?.setGraph(graph, null)
Expand All @@ -147,6 +147,6 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c
InvalidFaceLicenseAlert.toAlertArgs(),
)
}
viewModel.initFaceBioSdk(requireActivity())
viewModel.initFaceBioSdk(requireActivity(), args.params.faceSDK)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import com.simprints.face.capture.screens.FaceCaptureViewModel
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FACE_CAPTURE
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ORCHESTRATION
import com.simprints.infra.logging.Simber
import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.navigation.navigateSafely
import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.view.setCheckedWithLeftDrawable
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -93,7 +93,7 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
// Wait till the views gets its final size then init frame processor and setup the camera
binding.faceCaptureCamera.post {
if (view != null) {
vm.initCapture(mainVm.samplesToCapture, mainVm.attemptNumber)
vm.initCapture(mainVm.bioSDK, mainVm.samplesToCapture, mainVm.attemptNumber)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.simprints.face.capture.usecases.SimpleCaptureEventReporter
import com.simprints.face.infra.basebiosdk.detection.Face
import com.simprints.face.infra.basebiosdk.detection.FaceDetector
import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.experimental
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FACE_CAPTURE
Expand Down Expand Up @@ -82,6 +83,7 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
}

fun initCapture(
bioSdk: FaceConfiguration.BioSdk,
samplesToCapture: Int,
attemptNumber: Int,
) {
Expand All @@ -90,10 +92,10 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
this.samplesToCapture = samplesToCapture
this.attemptNumber = attemptNumber
viewModelScope.launch {
faceDetector = resolveFaceBioSdk().detector
faceDetector = resolveFaceBioSdk(bioSdk).detector

val config = configManager.getProjectConfiguration()
qualityThreshold = config.face?.qualityThreshold ?: 0f
qualityThreshold = config.face?.getSdkConfiguration(bioSdk)?.qualityThreshold ?: 0f
singleQualityFallbackCaptureRequired = config.experimental().singleQualityFallbackRequired
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import com.simprints.face.capture.models.FaceDetection
import com.simprints.face.capture.screens.FaceCaptureViewModel
import com.simprints.face.capture.screens.livefeedback.CropToTargetOverlayAnalyzer
import com.simprints.infra.logging.Simber
import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.navigation.navigateSafely
import com.simprints.infra.uibase.view.applySystemBarInsets
import com.simprints.infra.uibase.view.setCheckedWithLeftDrawable
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -93,7 +93,7 @@ internal class LiveFeedbackAutoCaptureFragment : Fragment(R.layout.fragment_live
// Wait till the views gets its final size then init frame processor and setup the camera
binding.faceCaptureCamera.post {
if (view != null) {
vm.initCapture(mainVm.samplesToCapture, mainVm.attemptNumber)
vm.initCapture(mainVm.bioSDK, mainVm.samplesToCapture, mainVm.attemptNumber)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.simprints.face.infra.basebiosdk.detection.Face
import com.simprints.face.infra.basebiosdk.detection.FaceDetector
import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase
import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.experimental
import com.simprints.infra.config.sync.ConfigManager
import dagger.hilt.android.lifecycle.HiltViewModel
Expand Down Expand Up @@ -64,7 +65,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor(
capturingState.value = CapturingState.NOT_STARTED // reset view
isAutoCaptureHeldOff = true
}

fun startCapture() {
isAutoCaptureHeldOff = false
}
Expand All @@ -84,8 +85,9 @@ internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor(

if (!isAutoCaptureHeldOff) {
currentDetection.postValue(faceDetection)
if (faceDetection.status == FaceDetection.Status.VALID
&& capturingState.value == CapturingState.NOT_STARTED) {
if (faceDetection.status == FaceDetection.Status.VALID &&
capturingState.value == CapturingState.NOT_STARTED
) {
capturingState.postValue(CapturingState.CAPTURING)
captureImagingStartTime = captureStartTime.ms
autoCaptureImagingTimeoutJob = viewModelScope.launch {
Expand All @@ -109,16 +111,17 @@ internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor(
}

fun initCapture(
bioSdk: FaceConfiguration.BioSdk,
samplesToKeep: Int,
attemptNumber: Int,
) {
this.samplesToKeep = samplesToKeep
this.attemptNumber = attemptNumber
viewModelScope.launch {
faceDetector = resolveFaceBioSdk().detector
faceDetector = resolveFaceBioSdk(bioSdk).detector

val config = configManager.getProjectConfiguration()
qualityThreshold = config.face?.qualityThreshold ?: 0f
qualityThreshold = config.face?.getSdkConfiguration(bioSdk)?.qualityThreshold ?: 0f
singleQualityFallbackCaptureRequired = config.experimental().singleQualityFallbackRequired
autoCaptureImagingDurationMillis = config.experimental().faceAutoCaptureImagingDurationMillis
}
Expand All @@ -142,11 +145,13 @@ internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor(

private fun updateUserCapturesWith(faceDetection: FaceDetection) {
if (userCaptures.count() == samplesToKeep) {
userCaptures.indices.minByOrNull { index ->
userCaptures[index].face?.quality ?: -1f
}?.takeIf { it >= 0 }?.let { worseQualityCaptureIndex ->
userCaptures[worseQualityCaptureIndex] = faceDetection
}
userCaptures.indices
.minByOrNull { index ->
userCaptures[index].face?.quality ?: -1f
}?.takeIf { it >= 0 }
?.let { worseQualityCaptureIndex ->
userCaptures[worseQualityCaptureIndex] = faceDetection
}
} else {
userCaptures.add(faceDetection)
}
Expand Down
4 changes: 2 additions & 2 deletions face/capture/src/main/res/navigation/graph_face_capture.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
android:name="com.simprints.face.capture.screens.controller.FaceCaptureControllerFragment"
android:label="FaceCaptureController">
<argument
android:name="samplesToCapture"
app:argType="integer" />
android:name="params"
app:argType="com.simprints.face.capture.FaceCaptureParams" />
</fragment>

<include app:graph="@navigation/graph_alert" />
Expand Down
Loading