diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index a022edc27f..834f703b55 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -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:
diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts
index 56e6ad7b42..78d9ccc235 100644
--- a/build-logic/build_properties.gradle.kts
+++ b/build-logic/build_properties.gradle.kts
@@ -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")
diff --git a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureContract.kt b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureContract.kt
index 61082da57c..127f18418b 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureContract.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureContract.kt
@@ -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()
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureParams.kt b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureParams.kt
new file mode 100644
index 0000000000..8425499a1b
--- /dev/null
+++ b/face/capture/src/main/java/com/simprints/face/capture/FaceCaptureParams.kt
@@ -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
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt
index 610574bd57..e8c309b1e3 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt
@@ -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
@@ -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)
@@ -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)
+ } 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
@@ -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()
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt
index 011f403c81..654ccd8289 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt
@@ -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,
@@ -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)
@@ -147,6 +147,6 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c
InvalidFaceLicenseAlert.toAlertArgs(),
)
}
- viewModel.initFaceBioSdk(requireActivity())
+ viewModel.initFaceBioSdk(requireActivity(), args.params.faceSDK)
}
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt
index 3b75d0684f..51f7040d3e 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt
@@ -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
@@ -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)
}
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt
index 8e95a04d85..25db13cb36 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModel.kt
@@ -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
@@ -82,6 +83,7 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
}
fun initCapture(
+ bioSdk: FaceConfiguration.BioSdk,
samplesToCapture: Int,
attemptNumber: Int,
) {
@@ -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
}
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt
index 4ed667846a..53b5c96f22 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragment.kt
@@ -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
@@ -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)
}
}
diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt
index c8103f3fd6..0017653a91 100644
--- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt
+++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModel.kt
@@ -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
@@ -64,7 +65,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModel @Inject constructor(
capturingState.value = CapturingState.NOT_STARTED // reset view
isAutoCaptureHeldOff = true
}
-
+
fun startCapture() {
isAutoCaptureHeldOff = false
}
@@ -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 {
@@ -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
}
@@ -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)
}
diff --git a/face/capture/src/main/res/navigation/graph_face_capture.xml b/face/capture/src/main/res/navigation/graph_face_capture.xml
index 36ebd6cab2..fffa462fcd 100644
--- a/face/capture/src/main/res/navigation/graph_face_capture.xml
+++ b/face/capture/src/main/res/navigation/graph_face_capture.xml
@@ -9,8 +9,8 @@
android:name="com.simprints.face.capture.screens.controller.FaceCaptureControllerFragment"
android:label="FaceCaptureController">
+ android:name="params"
+ app:argType="com.simprints.face.capture.FaceCaptureParams" />
diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt
index 8e6c15ab60..54032ebb32 100644
--- a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt
+++ b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt
@@ -5,12 +5,13 @@ import com.google.common.truth.Truth.assertThat
import com.simprints.core.tools.time.Timestamp
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.basebiosdk.initialization.FaceBioSdkInitializer
import com.simprints.infra.authstore.AuthStore
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FaceConfiguration.ImageSavingStrategy
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.license.LicenseRepository
@@ -100,7 +101,7 @@ class FaceCaptureViewModelTest {
bitmapToByteArrayUseCase,
licenseRepository,
mockk {
- coEvery { this@mockk().initializer } returns faceBioSdkInitializer
+ coEvery { this@mockk(any()).initializer } returns faceBioSdkInitializer
},
saveLicenseCheckEvent,
isUsingAutoCapture,
@@ -111,7 +112,13 @@ class FaceCaptureViewModelTest {
@Test
fun `Save face detections should not be called when image saving strategy set to NEVER`() {
- coEvery { configManager.getProjectConfiguration().face?.imageSavingStrategy } returns ImageSavingStrategy.NEVER
+ coEvery {
+ configManager
+ .getProjectConfiguration()
+ .face
+ ?.getSdkConfiguration(any())
+ ?.imageSavingStrategy
+ } returns ImageSavingStrategy.NEVER
viewModel.captureFinished(faceDetections)
viewModel.flowFinished()
@@ -120,8 +127,15 @@ class FaceCaptureViewModelTest {
@Test
fun `Save face detections should be called when image saving strategy set to ONLY_GOOD_SCAN`() {
- coEvery { configManager.getProjectConfiguration().face?.imageSavingStrategy } returns ImageSavingStrategy.ONLY_GOOD_SCAN
-
+ coEvery {
+ configManager
+ .getProjectConfiguration()
+ .face
+ ?.getSdkConfiguration(any())
+ ?.imageSavingStrategy
+ } returns ImageSavingStrategy.ONLY_GOOD_SCAN
+
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.SIM_FACE)
viewModel.captureFinished(faceDetections)
viewModel.flowFinished()
coVerify(atLeast = 1) { faceImageUseCase.invoke(any(), any()) }
@@ -129,6 +143,7 @@ class FaceCaptureViewModelTest {
@Test
fun `Save biometric reference creation when flow finishes`() {
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.SIM_FACE)
viewModel.captureFinished(faceDetections)
viewModel.flowFinished()
coVerify(atLeast = 1) {
@@ -173,6 +188,22 @@ class FaceCaptureViewModelTest {
verify { eventReporter.addCaptureConfirmationEvent(any(), any()) }
}
+ @Test
+ fun `test initFaceBioSdk should not check licence for SIM_FACE`() {
+ // Given
+ every { faceBioSdkInitializer.tryInitWithLicense(any(), any()) } returns true
+
+ // When
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.SIM_FACE)
+
+ // Then
+ coVerify(exactly = 1) { faceBioSdkInitializer.tryInitWithLicense(any(), eq("")) }
+ coVerify(exactly = 0) {
+ licenseRepository.getCachedLicense(any())
+ saveLicenseCheckEvent(any(), any())
+ }
+ }
+
@Test
fun `test initFaceBioSdk should initialize faceBioSdk only once`() {
// Given
@@ -185,9 +216,9 @@ class FaceCaptureViewModelTest {
coJustRun { saveLicenseCheckEvent(Vendor.RankOne, capture(licenseStatusSlot)) }
// When
- viewModel.initFaceBioSdk(mockk())
- viewModel.initFaceBioSdk(mockk())
- viewModel.initFaceBioSdk(mockk())
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
// Then
coVerify(exactly = 1) { faceBioSdkInitializer.tryInitWithLicense(any(), license) }
@@ -214,7 +245,7 @@ class FaceCaptureViewModelTest {
coEvery { faceBioSdkInitializer.tryInitWithLicense(any(), license) } returns false
// When
- viewModel.initFaceBioSdk(mockk())
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
// Then
viewModel.invalidLicense.assertEventReceived()
coVerify { licenseRepository.redownloadLicence(any(), any(), any(), any()) }
@@ -243,7 +274,7 @@ class FaceCaptureViewModelTest {
coJustRun { saveLicenseCheckEvent(Vendor.RankOne, capture(licenseStatusSlot)) }
// When
- viewModel.initFaceBioSdk(mockk())
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
// Then
viewModel.invalidLicense.assertEventNotReceived()
@@ -274,7 +305,7 @@ class FaceCaptureViewModelTest {
coJustRun { saveLicenseCheckEvent(Vendor.RankOne, capture(licenseStatusSlot)) }
// When
- viewModel.initFaceBioSdk(mockk())
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
// Then
viewModel.invalidLicense.assertEventNotReceived()
@@ -320,7 +351,7 @@ class FaceCaptureViewModelTest {
}
@Test
- fun `test initFaceBioSdk should return error when re-download fails`() {
+ fun `test initFaceBioSdk should return error when licednce re-download fails`() {
// Given
val license = "license"
coEvery {
@@ -338,7 +369,7 @@ class FaceCaptureViewModelTest {
coJustRun { saveLicenseCheckEvent(Vendor.RankOne, capture(licenseStatusSlot)) }
// When
- viewModel.initFaceBioSdk(mockk())
+ viewModel.initFaceBioSdk(mockk(), FaceConfiguration.BioSdk.RANK_ONE)
// Then
viewModel.invalidLicense.assertEventReceived()
diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt
index 18a70d257c..0ff289eabe 100644
--- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt
+++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt
@@ -12,6 +12,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.testtools.common.coroutines.TestCoroutineRule
@@ -62,12 +63,18 @@ internal class LiveFeedbackFragmentViewModelTest {
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
- coEvery { configManager.getProjectConfiguration().face?.qualityThreshold } returns QUALITY_THRESHOLD
+ coEvery {
+ configManager
+ .getProjectConfiguration()
+ .face
+ ?.getSdkConfiguration(any())
+ ?.qualityThreshold
+ } returns QUALITY_THRESHOLD
coEvery { configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns false
every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) }
justRun { previewFrame.recycle() }
val resolveFaceBioSdkUseCase = mockk {
- coEvery { this@mockk.invoke() } returns mockk {
+ coEvery { this@mockk.invoke(any()) } returns mockk {
every { detector } returns faceDetector
}
}
@@ -84,7 +91,7 @@ internal class LiveFeedbackFragmentViewModelTest {
fun `Process fallback image when valid face correctly but not started capture`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.process(frame)
val currentDetection = viewModel.currentDetection.testObserver()
@@ -97,7 +104,7 @@ internal class LiveFeedbackFragmentViewModelTest {
fun `Process valid face correctly`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.process(frame)
viewModel.startCapture()
viewModel.process(frame)
@@ -127,7 +134,7 @@ internal class LiveFeedbackFragmentViewModelTest {
)
val detections = viewModel.currentDetection.testObserver()
- viewModel.initCapture(2, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 2, 0)
viewModel.process(frame)
viewModel.process(frame)
@@ -162,7 +169,7 @@ internal class LiveFeedbackFragmentViewModelTest {
)
val detections = viewModel.currentDetection.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.process(frame)
viewModel.process(frame)
viewModel.process(frame)
@@ -182,7 +189,7 @@ internal class LiveFeedbackFragmentViewModelTest {
val currentDetectionObserver = viewModel.currentDetection.testObserver()
val capturingStateObserver = viewModel.capturingState.testObserver()
- viewModel.initCapture(2, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 2, 0)
viewModel.process(frame)
viewModel.startCapture()
viewModel.process(frame)
diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt
index cd81d3dd98..0bc5310d6f 100644
--- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt
+++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedbackautocapture/LiveFeedbackAutoCaptureFragmentViewModelTest.kt
@@ -12,6 +12,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.testtools.common.coroutines.TestCoroutineRule
@@ -65,12 +66,18 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
- coEvery { configManager.getProjectConfiguration().face?.qualityThreshold } returns QUALITY_THRESHOLD
+ coEvery {
+ configManager
+ .getProjectConfiguration()
+ .face
+ ?.getSdkConfiguration(any())
+ ?.qualityThreshold
+ } returns QUALITY_THRESHOLD
coEvery { configManager.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns false
every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) }
justRun { previewFrame.recycle() }
val resolveFaceBioSdkUseCase = mockk {
- coEvery { this@mockk.invoke() } returns mockk {
+ coEvery { this@mockk.invoke(any()) } returns mockk {
every { detector } returns faceDetector
}
}
@@ -83,14 +90,13 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
)
}
-
@Test
fun `Do not start capture if valid quality face detected but before start capture clicked`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.process(frame)
assertThat(currentDetection.observedValues)
@@ -105,7 +111,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.holdOffCapture()
viewModel.process(frame)
@@ -122,7 +128,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
@@ -138,7 +144,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
@@ -153,7 +159,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
viewModel.holdOffCapture()
@@ -173,7 +179,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetection = viewModel.currentDetection.testObserver()
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
viewModel.process(frame)
@@ -191,7 +197,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
fun `Process fallback image when valid face correctly but not started capture`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.process(frame) // a fallback image frame before the preparation delay elapses
viewModel.startCapture()
viewModel.process(frame)
@@ -206,7 +212,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
fun `Process valid face correctly`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
advanceTimeBy(AUTO_CAPTURE_IMAGING_DURATION_MS + 1)
@@ -236,7 +242,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
)
val detections = viewModel.currentDetection.testObserver()
- viewModel.initCapture(2, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 2, 0)
viewModel.startCapture()
viewModel.process(frame)
@@ -273,7 +279,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
)
val detections = viewModel.currentDetection.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
// fallback image frames before the preparation delay elapses
viewModel.process(frame)
viewModel.process(frame)
@@ -291,10 +297,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
@Test
fun `Use default imaging duration when not configured`() = runTest {
coEvery { faceDetector.analyze(frame) } returns getFace()
- coEvery { configManager.getProjectConfiguration().experimental().faceAutoCaptureImagingDurationMillis } returns AUTO_CAPTURE_IMAGING_DURATION_MS
+ coEvery {
+ configManager.getProjectConfiguration().experimental().faceAutoCaptureImagingDurationMillis
+ } returns AUTO_CAPTURE_IMAGING_DURATION_MS
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
advanceTimeBy(AUTO_CAPTURE_IMAGING_DURATION_MS)
@@ -313,7 +321,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
coEvery { configManager.getProjectConfiguration().custom } returns mapOf("faceAutoCaptureImagingDurationMillis" to configDuration)
val capturingState = viewModel.capturingState.testObserver()
- viewModel.initCapture(1, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, 1, 0)
viewModel.startCapture()
viewModel.process(frame)
advanceTimeBy(configDuration.toLong())
@@ -333,7 +341,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
val currentDetectionObserver = viewModel.currentDetection.testObserver()
val capturingStateObserver = viewModel.capturingState.testObserver()
val samplesToKeep = 2
- viewModel.initCapture(samplesToKeep, 0)
+ viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, samplesToKeep, 0)
viewModel.process(frame) // won't be observed during the preparation phase
viewModel.startCapture()
(1..100).forEach {
diff --git a/face/infra/bio-sdk-resolver/build.gradle.kts b/face/infra/bio-sdk-resolver/build.gradle.kts
index 56744fba16..55f0b9b37f 100644
--- a/face/infra/bio-sdk-resolver/build.gradle.kts
+++ b/face/infra/bio-sdk-resolver/build.gradle.kts
@@ -10,4 +10,5 @@ dependencies {
implementation(project(":face:infra:base-bio-sdk"))
implementation(project(":face:infra:roc-v1"))
api(project(":face:infra:roc-v3"))
+ implementation(project(":face:infra:simface"))
}
diff --git a/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCase.kt b/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCase.kt
index e733d0612c..407a305a8a 100644
--- a/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCase.kt
+++ b/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCase.kt
@@ -1,6 +1,7 @@
package com.simprints.face.infra.biosdkresolver
import com.simprints.infra.config.store.ConfigRepository
+import com.simprints.infra.config.store.models.FaceConfiguration
import javax.inject.Inject
import javax.inject.Singleton
@@ -9,15 +10,19 @@ class ResolveFaceBioSdkUseCase @Inject constructor(
private val configRepository: ConfigRepository,
private val rocV1BioSdk: RocV1BioSdk,
private val rocV3BioSdk: RocV3BioSdk,
+ private val simFaceBioSdk: SimFaceBioSdk,
) {
- suspend operator fun invoke(): FaceBioSDK {
- val version = configRepository
- .getProjectConfiguration()
- .face
- ?.rankOne
- ?.version
- ?.takeIf { it.isNotBlank() } // Ensures version is not null or empty
- requireNotNull(version) { "FaceBioSDK version is null or empty" }
- return if (version == rocV3BioSdk.version) rocV3BioSdk else rocV1BioSdk
+ suspend operator fun invoke(bioSdk: FaceConfiguration.BioSdk): FaceBioSDK = when (bioSdk) {
+ FaceConfiguration.BioSdk.SIM_FACE -> simFaceBioSdk
+ FaceConfiguration.BioSdk.RANK_ONE -> {
+ val version = configRepository
+ .getProjectConfiguration()
+ .face
+ ?.rankOne
+ ?.version
+ ?.takeIf { it.isNotBlank() } // Ensures version is not null or empty
+ requireNotNull(version) { "FaceBioSDK version is null or empty" }
+ if (version == rocV3BioSdk.version) rocV3BioSdk else rocV1BioSdk
+ }
}
}
diff --git a/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/SimFaceBioSdk.kt b/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/SimFaceBioSdk.kt
new file mode 100644
index 0000000000..2e9abe0fe0
--- /dev/null
+++ b/face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/SimFaceBioSdk.kt
@@ -0,0 +1,23 @@
+package com.simprints.face.infra.biosdkresolver
+
+import com.simprints.face.infra.basebiosdk.matching.FaceMatcher
+import com.simprints.face.infra.basebiosdk.matching.FaceSample
+import com.simprints.face.infra.simface.detection.SimFaceDetector
+import com.simprints.face.infra.simface.initialization.SimFaceInitializer
+import com.simprints.face.infra.simface.matching.SimFaceMatcher
+import com.simprints.simface.core.SimFace
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SimFaceBioSdk @Inject constructor(
+ override val initializer: SimFaceInitializer,
+ override val detector: SimFaceDetector,
+ private val simFace: SimFace,
+) : FaceBioSDK {
+ override val version: String = "1"
+ override val templateFormat: String = simFace.getTemplateVersion()
+ override val matcherName: String = "SIM_FACE"
+
+ override fun createMatcher(probeSamples: List): FaceMatcher = SimFaceMatcher(simFace, probeSamples)
+}
diff --git a/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCaseTest.kt b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCaseTest.kt
index 98e84280a6..21f8ec64d8 100644
--- a/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCaseTest.kt
+++ b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/ResolveFaceBioSdkUseCaseTest.kt
@@ -2,6 +2,7 @@ package com.simprints.face.infra.biosdkresolver
import com.google.common.truth.Truth.*
import com.simprints.infra.config.store.ConfigRepository
+import com.simprints.infra.config.store.models.FaceConfiguration
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -12,17 +13,28 @@ class ResolveFaceBioSdkUseCaseTest {
private val configRepository: ConfigRepository = mockk()
private lateinit var rocV1BioSdk: RocV1BioSdk
private lateinit var rocV3BioSdk: RocV3BioSdk
+ private lateinit var simFaceBioSdk: SimFaceBioSdk
@Before
fun setUp() {
rocV1BioSdk = RocV1BioSdk(mockk(), mockk())
rocV3BioSdk = RocV3BioSdk(mockk(), mockk())
- resolveFaceBioSdkUseCase =
- ResolveFaceBioSdkUseCase(configRepository, rocV1BioSdk, rocV3BioSdk)
+ simFaceBioSdk = SimFaceBioSdk(mockk(), mockk(), mockk(relaxed = true))
+
+ resolveFaceBioSdkUseCase = ResolveFaceBioSdkUseCase(configRepository, rocV1BioSdk, rocV3BioSdk, simFaceBioSdk)
+ }
+
+ @Test
+ fun `return SimFace SDK when requested`() = runTest {
+ // When
+ val result = resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.SIM_FACE)
+
+ // Then
+ assertThat(result).isEqualTo(simFaceBioSdk)
}
@Test(expected = IllegalArgumentException::class)
- fun `throw exception when version is null`() = runTest {
+ fun `throw exception when RankOne version is null`() = runTest {
// Given
coEvery {
configRepository
@@ -33,13 +45,13 @@ class ResolveFaceBioSdkUseCaseTest {
} returns null
// When
- resolveFaceBioSdkUseCase.invoke()
+ resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE)
// Then: Expect IllegalArgumentException to be thrown
}
@Test(expected = IllegalArgumentException::class)
- fun `throw exception when version is empty`() = runTest {
+ fun `throw exception when RankOne version is empty`() = runTest {
// Given
coEvery {
configRepository
@@ -50,7 +62,7 @@ class ResolveFaceBioSdkUseCaseTest {
} returns ""
// When
- resolveFaceBioSdkUseCase.invoke()
+ resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE)
// Then: Expect IllegalArgumentException to be thrown
}
@@ -69,7 +81,7 @@ class ResolveFaceBioSdkUseCaseTest {
} returns rocV3BioSdk.version
// When
- val result = resolveFaceBioSdkUseCase.invoke()
+ val result = resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE)
// Then
assertThat(result).isEqualTo(rocV3BioSdk)
@@ -88,7 +100,7 @@ class ResolveFaceBioSdkUseCaseTest {
} returns rocV1BioSdk.version
// When
- val result = resolveFaceBioSdkUseCase.invoke()
+ val result = resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE)
// Then
assertThat(result).isEqualTo(rocV1BioSdk)
diff --git a/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/RocV3BioSdkTest.kt b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/RocV3BioSdkTest.kt
index 080431f071..43fdc8d41c 100644
--- a/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/RocV3BioSdkTest.kt
+++ b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/RocV3BioSdkTest.kt
@@ -1,12 +1,10 @@
package com.simprints.face.infra.biosdkresolver
-
import com.google.common.truth.Truth.*
import io.mockk.*
import org.junit.Test
class RocV3BioSdkTest {
-
private lateinit var rocV3BioSdk: RocV3BioSdk
@Test
@@ -17,5 +15,4 @@ class RocV3BioSdkTest {
assertThat(matcher).isNotNull()
}
-
}
diff --git a/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/SimFaceSdkTest.kt b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/SimFaceSdkTest.kt
new file mode 100644
index 0000000000..63c7f9d422
--- /dev/null
+++ b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/SimFaceSdkTest.kt
@@ -0,0 +1,18 @@
+package com.simprints.face.infra.biosdkresolver
+
+import com.google.common.truth.*
+import io.mockk.*
+import org.junit.Test
+
+class SimFaceSdkTest {
+ private lateinit var bioSdk: SimFaceBioSdk
+
+ @Test
+ fun createMatcher() {
+ bioSdk = SimFaceBioSdk(mockk(), mockk(), mockk(relaxed = true))
+
+ val matcher = bioSdk.createMatcher(emptyList())
+
+ Truth.assertThat(matcher).isNotNull()
+ }
+}
diff --git a/face/infra/simface/.gitignore b/face/infra/simface/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/face/infra/simface/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/face/infra/simface/build.gradle.kts b/face/infra/simface/build.gradle.kts
new file mode 100644
index 0000000000..0f351d9c64
--- /dev/null
+++ b/face/infra/simface/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("simprints.infra")
+}
+
+android {
+ namespace = "com.simprints.face.infra.simface"
+}
+
+dependencies {
+ implementation(project(":face:infra:base-bio-sdk"))
+ api(libs.simface)
+}
diff --git a/face/infra/simface/src/main/AndroidManifest.xml b/face/infra/simface/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..8072ee00db
--- /dev/null
+++ b/face/infra/simface/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/face/infra/simface/src/main/java/com/simprints/face/infra/simface/SimFaceProviderModule.kt b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/SimFaceProviderModule.kt
new file mode 100644
index 0000000000..c7e4a1f2eb
--- /dev/null
+++ b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/SimFaceProviderModule.kt
@@ -0,0 +1,31 @@
+package com.simprints.face.infra.simface
+
+import com.simprints.face.infra.basebiosdk.detection.FaceDetector
+import com.simprints.face.infra.basebiosdk.initialization.FaceBioSdkInitializer
+import com.simprints.face.infra.simface.detection.SimFaceDetector
+import com.simprints.face.infra.simface.initialization.SimFaceInitializer
+import com.simprints.simface.core.SimFace
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object SimFaceProviderModule {
+ @Provides
+ @Singleton
+ fun provideSimFace(): SimFace = SimFace()
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class SimFaceModule {
+ @Binds
+ abstract fun provideSimFaceSdkInitializer(impl: SimFaceInitializer): FaceBioSdkInitializer
+
+ @Binds
+ abstract fun provideSimFaceDetector(impl: SimFaceDetector): FaceDetector
+}
diff --git a/face/infra/simface/src/main/java/com/simprints/face/infra/simface/detection/SimFaceDetector.kt b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/detection/SimFaceDetector.kt
new file mode 100644
index 0000000000..1ecd381ddd
--- /dev/null
+++ b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/detection/SimFaceDetector.kt
@@ -0,0 +1,38 @@
+package com.simprints.face.infra.simface.detection
+
+import android.graphics.Bitmap
+import com.simprints.face.infra.basebiosdk.detection.Face
+import com.simprints.face.infra.basebiosdk.detection.FaceDetector
+import com.simprints.simface.core.SimFace
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+class SimFaceDetector @Inject constructor(
+ private val simFace: SimFace,
+) : FaceDetector {
+ override fun analyze(bitmap: Bitmap): Face? = runBlocking {
+ // Load a bitmap image for processing
+ val faces = simFace.detectFaceBlocking(bitmap)
+ val face = faces.getOrNull(0) ?: return@runBlocking null
+ // Skip the obviously bad images, but leave the rest to be determined by the caller
+ if (face.quality < BAD_FACE_THRESHOLD) return@runBlocking null
+
+ val alignedBitmap = face.alignedFaceImage(bitmap)
+ val template = simFace.getEmbedding(alignedBitmap)
+
+ Face(
+ sourceWidth = bitmap.width,
+ sourceHeight = bitmap.height,
+ absoluteBoundingBox = face.absoluteBoundingBox,
+ yaw = face.yaw,
+ roll = face.roll,
+ quality = face.quality,
+ template = template,
+ format = simFace.getTemplateVersion(),
+ )
+ }
+
+ companion object {
+ private const val BAD_FACE_THRESHOLD = 0.1
+ }
+}
diff --git a/face/infra/simface/src/main/java/com/simprints/face/infra/simface/initialization/SimFaceInitializer.kt b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/initialization/SimFaceInitializer.kt
new file mode 100644
index 0000000000..36f3c52921
--- /dev/null
+++ b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/initialization/SimFaceInitializer.kt
@@ -0,0 +1,22 @@
+package com.simprints.face.infra.simface.initialization
+
+import android.app.Activity
+import android.content.Context
+import com.simprints.face.infra.basebiosdk.initialization.FaceBioSdkInitializer
+import com.simprints.simface.core.SimFace
+import com.simprints.simface.core.SimFaceConfig
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+
+class SimFaceInitializer @Inject constructor(
+ @ApplicationContext private val applicationContext: Context,
+ private val simFace: SimFace,
+) : FaceBioSdkInitializer {
+ override fun tryInitWithLicense(
+ activity: Activity,
+ license: String,
+ ): Boolean {
+ simFace.initialize(SimFaceConfig(applicationContext))
+ return true
+ }
+}
diff --git a/face/infra/simface/src/main/java/com/simprints/face/infra/simface/matching/SimFaceMatcher.kt b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/matching/SimFaceMatcher.kt
new file mode 100644
index 0000000000..4606754760
--- /dev/null
+++ b/face/infra/simface/src/main/java/com/simprints/face/infra/simface/matching/SimFaceMatcher.kt
@@ -0,0 +1,26 @@
+package com.simprints.face.infra.simface.matching
+
+import com.simprints.face.infra.basebiosdk.matching.FaceIdentity
+import com.simprints.face.infra.basebiosdk.matching.FaceMatcher
+import com.simprints.face.infra.basebiosdk.matching.FaceSample
+import com.simprints.simface.core.SimFace
+
+class SimFaceMatcher(
+ private val simFace: SimFace,
+ override val probeSamples: List,
+) : FaceMatcher(probeSamples) {
+ private val probeTemplates = probeSamples.map { it.template }
+
+ override suspend fun getHighestComparisonScoreForCandidate(candidate: FaceIdentity): Float = probeTemplates
+ .flatMap { probeTemplate ->
+ candidate.faces.map { face ->
+ val baseScore = simFace.verificationScore(probeTemplate, face.template)
+ // TODO: remove the adjustment after we find out why the returned range is biased towards [0.5;1]
+ (baseScore - 0.5).coerceAtLeast(0.0).toFloat() * 200f
+ }
+ }.max()
+
+ override fun close() {
+ // No-op
+ }
+}
diff --git a/face/infra/simface/src/test/java/com/simprints/face/infra/simface/detection/SimFaceDetectorTest.kt b/face/infra/simface/src/test/java/com/simprints/face/infra/simface/detection/SimFaceDetectorTest.kt
new file mode 100644
index 0000000000..16edf6a2b1
--- /dev/null
+++ b/face/infra/simface/src/test/java/com/simprints/face/infra/simface/detection/SimFaceDetectorTest.kt
@@ -0,0 +1,59 @@
+package com.simprints.face.infra.simface.detection
+
+import android.graphics.Bitmap
+import com.google.common.truth.Truth.*
+import com.simprints.simface.core.SimFace
+import com.simprints.simface.data.FaceDetection
+import io.mockk.*
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+
+class SimFaceDetectorTest {
+ @MockK
+ lateinit var simFace: SimFace
+
+ @MockK
+ lateinit var image: Bitmap
+
+ @MockK
+ lateinit var faceDetection: FaceDetection
+
+ lateinit var detector: SimFaceDetector
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this, relaxed = true)
+
+ detector = SimFaceDetector(simFace)
+ }
+
+ @Test
+ fun `returns null if no faces detected`() = runTest {
+ coEvery { simFace.detectFaceBlocking(any()) } returns emptyList()
+ assertThat(detector.analyze(image)).isNull()
+ }
+
+ @Test
+ fun `returns null if low quality face`() = runTest {
+ every { faceDetection.quality } returns 0.0f
+ coEvery { simFace.detectFaceBlocking(any()) } returns listOf(faceDetection)
+ assertThat(detector.analyze(image)).isNull()
+ }
+
+ @Test
+ fun `returns face embedding for good quality face`() = runTest {
+ every { faceDetection.quality } returns 0.8f
+ every { faceDetection.alignedFaceImage(any()) } returns image
+ every { simFace.getEmbedding(any()) } returns byteArrayOf(1, 2, 3, 4)
+ coEvery { simFace.detectFaceBlocking(any()) } returns listOf(faceDetection)
+
+ val face = detector.analyze(image)
+ assertThat(face).isNotNull()
+ assertThat(face?.quality).isEqualTo(0.8f)
+ assertThat(face?.template).isEqualTo(byteArrayOf(1, 2, 3, 4))
+
+ verify { simFace.getEmbedding(any()) }
+ }
+}
diff --git a/face/infra/simface/src/test/java/com/simprints/face/infra/simface/matching/SimFaceMatcherTest.kt b/face/infra/simface/src/test/java/com/simprints/face/infra/simface/matching/SimFaceMatcherTest.kt
new file mode 100644
index 0000000000..59d79adf18
--- /dev/null
+++ b/face/infra/simface/src/test/java/com/simprints/face/infra/simface/matching/SimFaceMatcherTest.kt
@@ -0,0 +1,75 @@
+package com.simprints.face.infra.simface.matching
+
+import com.google.common.truth.Truth.*
+import com.simprints.face.infra.basebiosdk.matching.FaceIdentity
+import com.simprints.face.infra.basebiosdk.matching.FaceSample
+import com.simprints.simface.core.SimFace
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+// Dummy test to generate jacoco reports.
+class SimFaceMatcherTest {
+ @Test
+ fun getMatcherName() {
+ assertThat(SimFaceMatcher(mockk(relaxed = true), emptyList())).isNotNull()
+ }
+
+ @Test
+ fun `comparison score in correct range`() = runTest {
+ val expectedResults = mapOf(
+ 0.0 to 0.0f,
+ 0.1 to 0.0f,
+ 0.2 to 0.0f,
+ 0.3 to 0.0f,
+ 0.4 to 0.0f,
+ 0.5 to 0.0f,
+ 0.55 to 10f,
+ 0.60 to 20f,
+ 0.65 to 30f,
+ 0.70 to 40f,
+ 0.75 to 50f,
+ 0.80 to 60f,
+ 0.85 to 70f,
+ 0.90 to 80f,
+ 0.95 to 90f,
+ 1.00 to 100f,
+ )
+
+ expectedResults.forEach { (verificationScore, expectedScore) ->
+ val simFace = mockk {
+ every { verificationScore(any(), any()) } returns verificationScore
+ }
+ val matcher = SimFaceMatcher(
+ simFace = simFace,
+ probeSamples = listOf(FaceSample(faceId = "id", template = byteArrayOf(1))),
+ )
+ val result = matcher.getHighestComparisonScoreForCandidate(
+ candidate = FaceIdentity(
+ subjectId = "id",
+ faces = listOf(FaceSample(faceId = "id", template = byteArrayOf(1))),
+ ),
+ )
+ assertThat(result - expectedScore).isLessThan(0.0001f)
+ }
+ }
+
+ @Test
+ fun `returns only the highest score`() = runTest {
+ val simFace = mockk {
+ every { verificationScore(any(), any()) } returnsMany listOf(0.0, 0.9, 0.6)
+ }
+ val matcher = SimFaceMatcher(
+ simFace = simFace,
+ probeSamples = listOf(FaceSample(faceId = "id", template = byteArrayOf(1))),
+ )
+ val result = matcher.getHighestComparisonScoreForCandidate(
+ candidate = FaceIdentity(
+ subjectId = "id",
+ faces = listOf(FaceSample(faceId = "id", template = byteArrayOf(1))),
+ ),
+ )
+ assertThat(result - 80f).isLessThan(0.0001f)
+ }
+}
diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt
index 12df3a0b14..13960ec7de 100644
--- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt
+++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/EnrolLastBiometricParams.kt
@@ -3,6 +3,7 @@ package com.simprints.feature.enrollast
import android.os.Parcelable
import androidx.annotation.Keep
import com.simprints.core.domain.tokenization.TokenizableString
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.Finger
import com.simprints.infra.config.store.models.FingerprintConfiguration
import kotlinx.parcelize.Parcelize
@@ -34,6 +35,7 @@ sealed class EnrolLastBiometricStepResult : Parcelable {
@Parcelize
data class FaceMatchResult(
val results: List,
+ val sdk: FaceConfiguration.BioSdk,
) : EnrolLastBiometricStepResult()
@Keep
diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt
index fc14b0b1f4..0529524d61 100644
--- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt
+++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/CheckForDuplicateEnrolmentsUseCase.kt
@@ -22,7 +22,7 @@ internal class CheckForDuplicateEnrolmentsUseCase @Inject constructor() {
Simber.e(
"No match response. Must be either fingerprint, face or both",
MissingMatchResultException(),
- tag = ENROLMENT
+ tag = ENROLMENT,
)
EnrolLastState.ErrorType.NO_MATCH_RESULTS
}
@@ -57,15 +57,17 @@ internal class CheckForDuplicateEnrolmentsUseCase @Inject constructor() {
?.toFloat()
} ?: Float.MAX_VALUE
- val faceThreshold = configuration.face
- ?.decisionPolicy
- ?.high
- ?.toFloat()
- ?: Float.MAX_VALUE
+ val faceThreshold = faceResponse?.let {
+ configuration.face
+ ?.getSdkConfiguration(faceResponse.sdk)
+ ?.decisionPolicy
+ ?.high
+ ?.toFloat()
+ } ?: Float.MAX_VALUE
return fingerprintResponse?.results?.any { it.confidenceScore >= fingerprintThreshold } == true ||
faceResponse?.results?.any { it.confidenceScore >= faceThreshold } == true
}
- private class MissingMatchResultException() : IllegalStateException("No match response in duplicate check.")
+ private class MissingMatchResultException : IllegalStateException("No match response in duplicate check.")
}
diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt
index 1f71cf4a47..dd4e167ecd 100644
--- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt
+++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/BuildSubjectUseCaseTest.kt
@@ -53,7 +53,7 @@ class BuildSubjectUseCaseTest {
listOf(
EnrolLastBiometricStepResult.EnrolLastBiometricsResult(null),
EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), mockk()),
- EnrolLastBiometricStepResult.FaceMatchResult(emptyList()),
+ EnrolLastBiometricStepResult.FaceMatchResult(emptyList(), mockk()),
),
),
)
@@ -117,7 +117,7 @@ class BuildSubjectUseCaseTest {
val result = useCase(
createParams(
listOf(
- EnrolLastBiometricStepResult.FaceMatchResult(emptyList()),
+ EnrolLastBiometricStepResult.FaceMatchResult(emptyList(), mockk()),
EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("first")),
EnrolLastBiometricStepResult.FaceCaptureResult(REFERENCE_ID, mockFaceResultsList("second")),
),
diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt
index b27cf0b8b4..e442d02b81 100644
--- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt
+++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/CheckDuplicateEnrolmentsErrorsUseCaseTest.kt
@@ -49,9 +49,8 @@ class CheckDuplicateEnrolmentsErrorsUseCaseTest {
projectConfig = mockProjectConfig(),
steps = listOf(
EnrolLastBiometricStepResult.FaceMatchResult(
- listOf(
- matchResult(LOW_CONFIDENCE),
- ),
+ listOf(matchResult(LOW_CONFIDENCE)),
+ mockk(),
),
),
)
@@ -80,9 +79,8 @@ class CheckDuplicateEnrolmentsErrorsUseCaseTest {
projectConfig = mockProjectConfig(highConfidence = null),
steps = listOf(
EnrolLastBiometricStepResult.FaceMatchResult(
- listOf(
- matchResult(HIGH_CONFIDENCE),
- ),
+ listOf(matchResult(HIGH_CONFIDENCE)),
+ mockk(),
),
),
)
@@ -121,9 +119,8 @@ class CheckDuplicateEnrolmentsErrorsUseCaseTest {
projectConfig = mockProjectConfig(),
steps = listOf(
EnrolLastBiometricStepResult.FaceMatchResult(
- listOf(
- matchResult(HIGH_CONFIDENCE),
- ),
+ listOf(matchResult(HIGH_CONFIDENCE)),
+ mockk(),
),
),
)
@@ -140,7 +137,7 @@ class CheckDuplicateEnrolmentsErrorsUseCaseTest {
every { fingerprint?.getSdkConfiguration(any())?.decisionPolicy } returns highConfidence?.let {
DecisionPolicy(0, 0, it)
}
- every { face?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) }
+ every { face?.getSdkConfiguration(any())?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) }
}
private fun matchResult(confidence: Float) = MatchResult("subjectId", confidence)
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt
index b63b8ad98e..9a50a4e5f4 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt
@@ -1,6 +1,7 @@
package com.simprints.matcher
import com.simprints.core.domain.common.FlowType
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource
import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery
@@ -14,6 +15,7 @@ object MatchContract {
fingerprintSamples: List = emptyList(),
faceSamples: List = emptyList(),
fingerprintSDK: FingerprintConfiguration.BioSdk? = null,
+ faceSDK: FaceConfiguration.BioSdk? = null,
flowType: FlowType,
subjectQuery: SubjectQuery,
biometricDataSource: BiometricDataSource,
@@ -21,6 +23,7 @@ object MatchContract {
MatchParams(
referenceId,
faceSamples,
+ faceSDK,
fingerprintSamples,
fingerprintSDK,
flowType,
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt
index 110754051a..52e0f1fe5e 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt
@@ -4,6 +4,7 @@ import android.os.Parcelable
import androidx.annotation.Keep
import com.simprints.core.domain.common.FlowType
import com.simprints.core.domain.fingerprint.IFingerIdentifier
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource
import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery
@@ -15,6 +16,7 @@ import kotlinx.parcelize.Parcelize
data class MatchParams(
val probeReferenceId: String,
val probeFaceSamples: List = emptyList(),
+ val faceSDK: FaceConfiguration.BioSdk? = null,
val probeFingerprintSamples: List = emptyList(),
val fingerprintSDK: FingerprintConfiguration.BioSdk? = null,
val flowType: FlowType,
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchResult.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchResult.kt
index ccc138d704..db10dd97aa 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/MatchResult.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchResult.kt
@@ -1,6 +1,7 @@
package com.simprints.matcher
import androidx.annotation.Keep
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import java.io.Serializable
@@ -21,6 +22,7 @@ interface MatchResultItem : Serializable {
@Keep
data class FaceMatchResult(
override val results: List,
+ val sdk: FaceConfiguration.BioSdk,
) : MatchResult {
@Keep
data class Item(
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt
index 6d440d371e..2bc7923e96 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt
@@ -89,7 +89,7 @@ internal class MatchViewModel @Inject constructor(
_matchResponse.send(
when {
- isFaceMatch -> FaceMatchResult(matcherState.matchResultItems)
+ isFaceMatch -> FaceMatchResult(matcherState.matchResultItems, params.faceSDK!!)
else -> FingerprintMatchResult(matcherState.matchResultItems, params.fingerprintSDK!!)
},
)
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt
index 5077c81e0e..0f7bfe3654 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt
@@ -41,7 +41,12 @@ internal class FaceMatcherUseCase @Inject constructor(
project: Project,
): Flow = channelFlow {
Simber.i("Initialising matcher", tag = crashReportTag)
- val bioSdk = resolveFaceBioSdk()
+ if (matchParams.faceSDK == null) {
+ Simber.w("Face SDK was not provided", tag = crashReportTag)
+ send(MatcherState.Success(emptyList(), 0, ""))
+ return@channelFlow
+ }
+ val bioSdk = resolveFaceBioSdk(matchParams.faceSDK)
if (matchParams.probeFaceSamples.isEmpty()) {
send(MatcherState.Success(emptyList(), 0, bioSdk.matcherName))
diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt
index 5d6317487e..1bf876f375 100644
--- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt
+++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt
@@ -47,7 +47,12 @@ internal class FingerprintMatcherUseCase @Inject constructor(
project: Project,
): Flow = channelFlow {
Simber.i("Initialising matcher", tag = crashReportTag)
- val bioSdkWrapper = resolveBioSdkWrapper(matchParams.fingerprintSDK!!)
+ if (matchParams.fingerprintSDK == null) {
+ Simber.w("Fingerprint SDK was not provided", tag = crashReportTag)
+ send(MatcherState.Success(emptyList(), 0, ""))
+ return@channelFlow
+ }
+ val bioSdkWrapper = resolveBioSdkWrapper(matchParams.fingerprintSDK)
if (matchParams.probeFingerprintSamples.isEmpty()) {
send(MatcherState.Success(emptyList(), 0, bioSdkWrapper.matcherName))
diff --git a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt
index 8f3d164bd8..a79254f9ea 100644
--- a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt
+++ b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt
@@ -1,13 +1,14 @@
package com.simprints.matcher.screen
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.*
import com.jraska.livedata.test
import com.simprints.core.domain.common.FlowType
import com.simprints.core.domain.fingerprint.IFingerIdentifier
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.infra.authstore.AuthStore
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource
@@ -20,15 +21,8 @@ import com.simprints.matcher.usecases.MatcherUseCase
import com.simprints.matcher.usecases.SaveMatchEventUseCase
import com.simprints.testtools.common.coroutines.TestCoroutineRule
import com.simprints.testtools.common.livedata.getOrAwaitValue
-import io.mockk.CapturingSlot
-import io.mockk.MockKAnnotations
-import io.mockk.coEvery
-import io.mockk.coJustRun
-import io.mockk.every
+import io.mockk.*
import io.mockk.impl.annotations.MockK
-import io.mockk.mockk
-import io.mockk.slot
-import io.mockk.verify
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
@@ -99,7 +93,7 @@ internal class MatchViewModelTest {
matchResultItems = responseItems,
totalCandidates = responseItems.size,
matcherName = "MatcherName",
- )
+ ),
)
}
coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) }
@@ -111,6 +105,7 @@ internal class MatchViewModelTest {
MatchParams(
probeReferenceId = "referenceId",
probeFaceSamples = listOf(getFaceSample()),
+ faceSDK = FaceConfiguration.BioSdk.RANK_ONE,
flowType = FlowType.ENROL,
queryForCandidates = mockk {},
biometricDataSource = BiometricDataSource.Simprints,
@@ -146,7 +141,7 @@ internal class MatchViewModelTest {
matchResultItems = responseItems,
totalCandidates = responseItems.size,
matcherName = MATCHER_NAME,
- )
+ ),
)
}
coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) }
@@ -156,6 +151,7 @@ internal class MatchViewModelTest {
MatchParams(
probeReferenceId = "referenceId",
probeFaceSamples = listOf(getFaceSample()),
+ faceSDK = FaceConfiguration.BioSdk.RANK_ONE,
flowType = FlowType.ENROL,
queryForCandidates = mockk {},
biometricDataSource = BiometricDataSource.Simprints,
@@ -173,7 +169,7 @@ internal class MatchViewModelTest {
),
)
assertThat(viewModel.matchResponse.getOrAwaitValue().peekContent()).isEqualTo(
- FaceMatchResult(responseItems),
+ FaceMatchResult(responseItems, FaceConfiguration.BioSdk.RANK_ONE),
)
verify { saveMatchEvent.invoke(any(), any(), any(), eq(7), eq(MATCHER_NAME), any()) }
@@ -199,7 +195,7 @@ internal class MatchViewModelTest {
matchResultItems = responseItems,
totalCandidates = responseItems.size,
matcherName = MATCHER_NAME,
- )
+ ),
)
}
diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt
index d2f06f5fcd..e48281fede 100644
--- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt
+++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt
@@ -6,6 +6,7 @@ import com.simprints.core.domain.common.FlowType
import com.simprints.core.domain.face.FaceSample
import com.simprints.face.infra.basebiosdk.matching.FaceMatcher
import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.Project
import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository
import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource
@@ -48,7 +49,7 @@ internal class FaceMatcherUseCaseTest {
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
- coEvery { resolveFaceBioSdk().createMatcher(any()) } returns faceMatcher
+ coEvery { resolveFaceBioSdk(any()).createMatcher(any()) } returns faceMatcher
useCase = FaceMatcherUseCase(
enrolmentRecordRepository,
resolveFaceBioSdk,
@@ -94,6 +95,7 @@ internal class FaceMatcherUseCaseTest {
probeFaceSamples = listOf(
MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)),
),
+ faceSDK = FaceConfiguration.BioSdk.RANK_ONE,
flowType = FlowType.VERIFY,
queryForCandidates = SubjectQuery(),
biometricDataSource = BiometricDataSource.Simprints,
@@ -140,6 +142,7 @@ internal class FaceMatcherUseCaseTest {
probeFaceSamples = listOf(
MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)),
),
+ faceSDK = FaceConfiguration.BioSdk.RANK_ONE,
flowType = FlowType.VERIFY,
queryForCandidates = SubjectQuery(),
biometricDataSource = BiometricDataSource.Simprints,
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt
index 188518d65b..6b535a7b37 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt
@@ -3,6 +3,7 @@ package com.simprints.feature.orchestrator.steps
import android.os.Parcelable
import androidx.core.os.bundleOf
import com.simprints.core.domain.common.FlowType
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.enrolment.records.repository.domain.models.BiometricDataSource
import com.simprints.infra.enrolment.records.repository.domain.models.SubjectQuery
@@ -22,6 +23,7 @@ internal data class MatchStepStubPayload(
val subjectQuery: SubjectQuery,
val biometricDataSource: BiometricDataSource,
val fingerprintSDK: FingerprintConfiguration.BioSdk?,
+ val faceSDK: FaceConfiguration.BioSdk?,
) : Parcelable {
fun toFaceStepArgs(
referenceId: String,
@@ -29,6 +31,7 @@ internal data class MatchStepStubPayload(
) = MatchContract.getArgs(
referenceId = referenceId,
faceSamples = samples,
+ faceSDK = faceSDK,
flowType = flowType,
subjectQuery = subjectQuery,
biometricDataSource = biometricDataSource,
@@ -54,6 +57,7 @@ internal data class MatchStepStubPayload(
subjectQuery: SubjectQuery,
biometricDataSource: BiometricDataSource,
fingerprintSDK: FingerprintConfiguration.BioSdk? = null,
- ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery, biometricDataSource, fingerprintSDK))
+ faceSDK: FaceConfiguration.BioSdk? = null,
+ ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery, biometricDataSource, fingerprintSDK, faceSDK))
}
}
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt
index 4996599c5b..21baca8249 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCase.kt
@@ -45,6 +45,7 @@ internal class MapStepsForLastBiometricEnrolUseCase @Inject constructor() {
is FaceMatchResult -> EnrolLastBiometricStepResult.FaceMatchResult(
result.results.map { MatchResult(it.subjectId, it.confidence) },
+ result.sdk,
)
else -> null
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
index e0c967ceaf..5754c93eaf 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCase.kt
@@ -61,17 +61,20 @@ internal class CreateIdentifyResponseUseCase @Inject constructor(
results: List,
projectConfiguration: ProjectConfiguration,
) = results.filterIsInstance().lastOrNull()?.let { faceMatchResult ->
- projectConfiguration.face?.decisionPolicy?.let { faceDecisionPolicy ->
- val matches = faceMatchResult.results
- val goodResults = matches
- .filter { it.confidence >= faceDecisionPolicy.low }
- .sortedByDescending { it.confidence }
- // Attempt to include only high confidence matches
- goodResults
- .filter { it.confidence >= faceDecisionPolicy.high }
- .ifEmpty { goodResults }
- .take(projectConfiguration.identification.maxNbOfReturnedCandidates)
- .map { AppMatchResult(it.subjectId, it.confidence, faceDecisionPolicy) }
- }
+ projectConfiguration.face
+ ?.getSdkConfiguration(faceMatchResult.sdk)
+ ?.decisionPolicy
+ ?.let { faceDecisionPolicy ->
+ val matches = faceMatchResult.results
+ val goodResults = matches
+ .filter { it.confidence >= faceDecisionPolicy.low }
+ .sortedByDescending { it.confidence }
+ // Attempt to include only high confidence matches
+ goodResults
+ .filter { it.confidence >= faceDecisionPolicy.high }
+ .ifEmpty { goodResults }
+ .take(projectConfiguration.identification.maxNbOfReturnedCandidates)
+ .map { AppMatchResult(it.subjectId, it.confidence, faceDecisionPolicy) }
+ }
} ?: emptyList()
}
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt
index e21ada73db..200f79f82d 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCase.kt
@@ -57,17 +57,19 @@ internal class CreateVerifyResponseUseCase @Inject constructor() {
.filterIsInstance()
.lastOrNull()
?.let { faceMatchResult ->
- projectConfiguration.face?.let { faceConfiguration ->
- faceMatchResult.results
- .maxByOrNull { it.confidence }
- ?.let {
- AppMatchResult(
- guid = it.subjectId,
- confidenceScore = it.confidence,
- decisionPolicy = faceConfiguration.decisionPolicy,
- verificationMatchThreshold = faceConfiguration.verificationMatchThreshold,
- )
- }
- }
+ projectConfiguration.face
+ ?.getSdkConfiguration(faceMatchResult.sdk)
+ ?.let { faceConfiguration ->
+ faceMatchResult.results
+ .maxByOrNull { it.confidence }
+ ?.let {
+ AppMatchResult(
+ guid = it.subjectId,
+ confidenceScore = it.confidence,
+ decisionPolicy = faceConfiguration.decisionPolicy,
+ verificationMatchThreshold = faceConfiguration.verificationMatchThreshold,
+ )
+ }
+ }
}
}
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt
index f83b775016..9316714d30 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCase.kt
@@ -5,6 +5,7 @@ import com.simprints.matcher.FaceMatchResult
import com.simprints.matcher.FingerprintMatchResult
import java.io.Serializable
import javax.inject.Inject
+import kotlin.text.compareTo
internal class IsNewEnrolmentUseCase @Inject constructor() {
/**
@@ -40,16 +41,18 @@ internal class IsNewEnrolmentUseCase @Inject constructor() {
?.medium
?.toFloat()
?.let { threshold -> fingerprintResult.results.all { it.confidence < threshold } }
- } ?: true
+ } != false
// Missing results and configuration are ignored as "valid" to allow creating new records.
private fun isNewEnrolmentFaceResult(
projectConfiguration: ProjectConfiguration,
faceResult: FaceMatchResult?,
- ): Boolean = projectConfiguration.face
- ?.decisionPolicy
- ?.medium
- ?.toFloat()
- ?.let { threshold -> faceResult?.results?.all { it.confidence < threshold } }
- ?: true
+ ): Boolean = faceResult?.let {
+ projectConfiguration.face
+ ?.getSdkConfiguration(faceResult.sdk)
+ ?.decisionPolicy
+ ?.medium
+ ?.toFloat()
+ ?.let { threshold -> faceResult.results.all { it.confidence < threshold } }
+ } != false
}
diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt
index d0336a6617..e3d021d25f 100644
--- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt
+++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCase.kt
@@ -319,9 +319,10 @@ internal class BuildStepsUseCase @Inject constructor(
return capturingModalitiesForFlowType(projectConfiguration, flowType)
.flatMap { modality ->
buildCaptureStepsForModality(modality, projectConfiguration, ageGroup, flowType)
- }.takeIf { it.isNotEmpty() } ?: projectConfiguration.general.modalities.flatMap { modality ->
- buildCaptureStepsForModality(modality, projectConfiguration, ageGroup, flowType)
- }
+ }.takeIf { it.isNotEmpty() }
+ ?: projectConfiguration.general.modalities.flatMap { modality ->
+ buildCaptureStepsForModality(modality, projectConfiguration, ageGroup, flowType)
+ }
}
/**
@@ -364,15 +365,16 @@ internal class BuildStepsUseCase @Inject constructor(
}
Modality.FACE -> {
- determineFaceSDKs(projectConfiguration, ageGroup).map {
- // Face bio SDK is currently ignored until we add a second one
+ determineFaceSDKs(projectConfiguration, ageGroup).map { bioSDK ->
+ val sdkConfiguration = projectConfiguration.face?.getSdkConfiguration(bioSDK)
+
// TODO: samplesToCapture can be read directly from FaceCapture
- val samplesToCapture = projectConfiguration.face?.nbOfImagesToCapture ?: 0
+ val samplesToCapture = sdkConfiguration?.nbOfImagesToCapture ?: 0
Step(
id = StepId.FACE_CAPTURE,
navigationActionId = R.id.action_orchestratorFragment_to_faceCapture,
destinationId = FaceCaptureContract.DESTINATION,
- payload = FaceCaptureContract.getArgs(samplesToCapture),
+ payload = FaceCaptureContract.getArgs(samplesToCapture, bioSDK),
)
}
}
@@ -412,26 +414,27 @@ internal class BuildStepsUseCase @Inject constructor(
navigationActionId = R.id.action_orchestratorFragment_to_matcher,
destinationId = MatchContract.DESTINATION,
payload = MatchStepStubPayload.asBundle(
- flowType,
- subjectQuery,
- biometricDataSource,
- bioSDK,
+ flowType = flowType,
+ subjectQuery = subjectQuery,
+ biometricDataSource = biometricDataSource,
+ fingerprintSDK = bioSDK,
),
)
}
}
Modality.FACE -> {
- determineFaceSDKs(projectConfiguration, ageGroup).map {
+ determineFaceSDKs(projectConfiguration, ageGroup).map { bioSDK ->
// Face bio SDK is currently ignored until we add a second one
Step(
id = StepId.FACE_MATCHER,
navigationActionId = R.id.action_orchestratorFragment_to_matcher,
destinationId = MatchContract.DESTINATION,
payload = MatchStepStubPayload.asBundle(
- flowType,
- subjectQuery,
- biometricDataSource,
+ flowType = flowType,
+ subjectQuery = subjectQuery,
+ biometricDataSource = biometricDataSource,
+ faceSDK = bioSDK,
),
)
}
@@ -530,6 +533,13 @@ internal class BuildStepsUseCase @Inject constructor(
) {
sdks.add(FaceConfiguration.BioSdk.RANK_ONE)
}
+ if (projectConfiguration.face
+ ?.simFace
+ ?.allowedAgeRange
+ ?.contains(ageGroup) == true
+ ) {
+ sdks.add(FaceConfiguration.BioSdk.SIM_FACE)
+ }
}
}
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt
index eb4f9cdbc4..24ce04a8c3 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapStepsForLastBiometricEnrolUseCaseTest.kt
@@ -8,6 +8,7 @@ import com.simprints.feature.enrollast.EnrolLastBiometricStepResult
import com.simprints.feature.enrollast.FaceTemplateCaptureResult
import com.simprints.feature.enrollast.FingerTemplateCaptureResult
import com.simprints.fingerprint.capture.FingerprintCaptureResult
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.Finger
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.events.sampledata.SampleDefaults.GUID1
@@ -39,11 +40,11 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest {
fun `maps FaceMatchResult correctly`() {
val result = useCase(
listOf(
- FaceMatchResult(emptyList()),
+ FaceMatchResult(emptyList(), FaceConfiguration.BioSdk.RANK_ONE),
),
)
- assertThat(result.first()).isEqualTo(EnrolLastBiometricStepResult.FaceMatchResult(emptyList()))
+ assertThat(result.first()).isEqualTo(EnrolLastBiometricStepResult.FaceMatchResult(emptyList(), FaceConfiguration.BioSdk.RANK_ONE))
}
@Test
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
index 637a86e56e..5e34c5b0ea 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateIdentifyResponseUseCaseTest.kt
@@ -2,6 +2,7 @@ package com.simprints.feature.orchestrator.usecases.response
import com.google.common.truth.Truth.assertThat
import com.simprints.infra.config.store.models.DecisionPolicy
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.FingerprintConfiguration
import com.simprints.infra.events.session.SessionEventRepository
import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse
@@ -36,7 +37,7 @@ class CreateIdentifyResponseUseCaseTest {
fun `Returns no identifications if no decision policy`() = runTest {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
@@ -50,7 +51,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(10f, 20f, 30f)),
@@ -65,7 +66,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(20f, 25f, 30f, 40f)),
@@ -80,7 +81,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
},
results = listOf(createFaceMatchResult(15f, 30f, 100f)),
@@ -95,7 +96,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
20,
50,
@@ -114,7 +115,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
20,
50,
@@ -133,7 +134,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
20,
50,
@@ -152,7 +153,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
20,
50,
@@ -174,7 +175,7 @@ class CreateIdentifyResponseUseCaseTest {
val result = useCase(
mockk {
every { identification.maxNbOfReturnedCandidates } returns 2
- every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(20, 50, 100)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
20,
50,
@@ -193,6 +194,7 @@ class CreateIdentifyResponseUseCaseTest {
private fun createFaceMatchResult(vararg confidences: Float): Serializable = FaceMatchResult(
confidences.map { FaceMatchResult.Item(subjectId = "1", confidence = it) },
+ FaceConfiguration.BioSdk.RANK_ONE,
)
private fun createFingerprintMatchResult(vararg confidences: Float): Serializable = FingerprintMatchResult(
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt
index d4a725c7f6..ecfa5bf937 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/CreateVerifyResponseUseCaseTest.kt
@@ -1,13 +1,12 @@
package com.simprints.feature.orchestrator.usecases.response
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.*
import com.simprints.infra.config.store.models.DecisionPolicy
import com.simprints.infra.orchestration.data.responses.AppErrorResponse
import com.simprints.infra.orchestration.data.responses.AppVerifyResponse
import com.simprints.matcher.FaceMatchResult
import com.simprints.matcher.FingerprintMatchResult
-import io.mockk.every
-import io.mockk.mockk
+import io.mockk.*
import org.junit.Before
import org.junit.Test
import java.io.Serializable
@@ -37,9 +36,9 @@ class CreateVerifyResponseUseCaseTest {
fun `Returns face matches with highest score`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null
- every { face?.verificationMatchThreshold } returns null
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null
every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null
},
results = listOf(createFaceMatchResult(10f, 50f, 100f)),
@@ -52,7 +51,7 @@ class CreateVerifyResponseUseCaseTest {
fun `Returns fingerprint matches with highest score`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns null
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
10,
20,
@@ -70,13 +69,13 @@ class CreateVerifyResponseUseCaseTest {
fun `Returns matches with highest face match score`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
10,
20,
30,
)
- every { face?.verificationMatchThreshold } returns 50f
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
},
results = listOf(
@@ -92,13 +91,13 @@ class CreateVerifyResponseUseCaseTest {
fun `Returns matches with highest fingerprint match score`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(
10,
20,
30,
)
- every { face?.verificationMatchThreshold } returns 50f
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
every { fingerprint?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
},
results = listOf(
@@ -114,8 +113,8 @@ class CreateVerifyResponseUseCaseTest {
fun `When face verificationMatchThreshold is null - verificationSuccess is null`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
- every { face?.verificationMatchThreshold } returns null
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns null
},
results = listOf(
createFaceMatchResult(10f, 50f, 100f),
@@ -148,8 +147,8 @@ class CreateVerifyResponseUseCaseTest {
fun `When face match score is above verificationMatchThreshold - verificationSuccess is true`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
- every { face?.verificationMatchThreshold } returns 50f
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
},
results = listOf(
createFaceMatchResult(51f),
@@ -182,8 +181,8 @@ class CreateVerifyResponseUseCaseTest {
fun `When face match score is equal to verificationMatchThreshold - verificationSuccess is true`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
- every { face?.verificationMatchThreshold } returns 50f
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
},
results = listOf(
createFaceMatchResult(50f),
@@ -216,8 +215,8 @@ class CreateVerifyResponseUseCaseTest {
fun `When face match score is below verificationMatchThreshold - verificationSuccess is false`() {
val result = useCase(
mockk {
- every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
- every { face?.verificationMatchThreshold } returns 50f
+ every { face?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy(10, 20, 30)
+ every { face?.getSdkConfiguration((any()))?.verificationMatchThreshold } returns 50f
},
results = listOf(
createFaceMatchResult(49f),
@@ -253,5 +252,6 @@ class CreateVerifyResponseUseCaseTest {
private fun createFaceMatchResult(vararg confidences: Float): Serializable = FaceMatchResult(
confidences.map { FaceMatchResult.Item(subjectId = "1", confidence = it) },
+ mockk(),
)
}
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt
index 49e9045344..49eed8295b 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/response/IsNewEnrolmentUseCaseTest.kt
@@ -2,6 +2,7 @@ package com.simprints.feature.orchestrator.usecases.response
import com.google.common.truth.Truth.assertThat
import com.simprints.infra.config.store.models.DecisionPolicy
+import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.matcher.FaceMatchResult
import com.simprints.matcher.FingerprintMatchResult
@@ -22,7 +23,7 @@ internal class IsNewEnrolmentUseCaseTest {
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
- every { projectConfiguration.face?.decisionPolicy } returns faceConfidenceDecisionPolicy
+ every { projectConfiguration.face?.getSdkConfiguration((any()))?.decisionPolicy } returns faceConfidenceDecisionPolicy
every { projectConfiguration.fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns fingerprintConfidenceDecisionPolicy
useCase = IsNewEnrolmentUseCase()
@@ -49,12 +50,7 @@ internal class IsNewEnrolmentUseCaseTest {
assertThat(
useCase(
projectConfiguration,
- listOf(
- FingerprintMatchResult(
- listOf(FingerprintMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)),
- mockk(),
- ),
- ),
+ listOf(FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)), mockk())),
),
).isTrue()
}
@@ -66,12 +62,7 @@ internal class IsNewEnrolmentUseCaseTest {
assertThat(
useCase(
projectConfiguration,
- listOf(
- FingerprintMatchResult(
- listOf(FingerprintMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE)),
- mockk(),
- ),
- ),
+ listOf(FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE)), mockk())),
),
).isFalse()
}
@@ -83,9 +74,7 @@ internal class IsNewEnrolmentUseCaseTest {
assertThat(
useCase(
projectConfiguration,
- listOf(
- FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE))),
- ),
+ listOf(FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)), FaceConfiguration.BioSdk.RANK_ONE)),
),
).isTrue()
}
@@ -97,9 +86,7 @@ internal class IsNewEnrolmentUseCaseTest {
assertThat(
useCase(
projectConfiguration,
- listOf(
- FaceMatchResult(listOf(FaceMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE))),
- ),
+ listOf(FaceMatchResult(listOf(FaceMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE)), FaceConfiguration.BioSdk.RANK_ONE)),
),
).isFalse()
}
@@ -116,7 +103,7 @@ internal class IsNewEnrolmentUseCaseTest {
listOf(FingerprintMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)),
mockk(),
),
- FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE))),
+ FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)), FaceConfiguration.BioSdk.RANK_ONE),
),
),
).isTrue()
@@ -134,7 +121,7 @@ internal class IsNewEnrolmentUseCaseTest {
listOf(FingerprintMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)),
mockk(),
),
- FaceMatchResult(listOf(FaceMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE))),
+ FaceMatchResult(listOf(FaceMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE)), FaceConfiguration.BioSdk.RANK_ONE),
),
),
).isFalse()
@@ -152,7 +139,7 @@ internal class IsNewEnrolmentUseCaseTest {
listOf(FingerprintMatchResult.Item("", HIGHER_THAN_MEDIUM_SCORE)),
mockk(),
),
- FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE))),
+ FaceMatchResult(listOf(FaceMatchResult.Item("", LOWER_THAN_MEDIUM_SCORE)), FaceConfiguration.BioSdk.RANK_ONE),
),
),
).isFalse()
diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt
index b8af2ae3ea..fc4f3c1669 100644
--- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt
+++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt
@@ -75,7 +75,7 @@ class BuildStepsUseCaseTest {
every { projectConfiguration.fingerprint?.getSdkConfiguration(NEC) } returns nec
every { projectConfiguration.face?.allowedSDKs } returns listOf(FaceConfiguration.BioSdk.RANK_ONE)
- every { projectConfiguration.face?.nbOfImagesToCapture } returns 3
+ every { projectConfiguration.face?.rankOne?.nbOfImagesToCapture } returns 3
every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns null
return projectConfiguration
@@ -698,6 +698,32 @@ class BuildStepsUseCaseTest {
)
}
+ @Test
+ fun `build capture and match steps with all SDKs - verify action - returns expected steps`() {
+ val projectConfiguration = mockCommonProjectConfiguration()
+ val ageGroup = AgeGroup(18, 60)
+ every { secugenSimMatcher.allowedAgeRange } returns ageGroup
+ every { nec.allowedAgeRange } returns ageGroup
+ every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroup
+ every { projectConfiguration.face?.simFace?.allowedAgeRange } returns ageGroup
+
+ val action = mockk(relaxed = true)
+
+ val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup)
+
+ assertStepOrder(
+ steps,
+ StepId.FINGERPRINT_CAPTURE,
+ StepId.FINGERPRINT_CAPTURE,
+ StepId.FACE_CAPTURE,
+ StepId.FACE_CAPTURE,
+ StepId.FINGERPRINT_MATCHER,
+ StepId.FINGERPRINT_MATCHER,
+ StepId.FACE_MATCHER,
+ StepId.FACE_MATCHER,
+ )
+ }
+
@Test
fun `build capture and match steps - confirm identity action - returns expected steps`() {
val projectConfiguration = mockCommonProjectConfiguration()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3e8208c40a..1a8ebbb350 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -69,6 +69,7 @@ roc_wrapper_version = "1.23.0"
roc_wrapper-v3_version = "3.1.0"
nec_version = "1.5.0"
secugen_version = "1.1.0"
+simface = "2025.2.0"
junit_version = "4.13.2"
junit_ext_version = "1.2.1"
@@ -204,6 +205,7 @@ nec-wrapper = { module = " com.simprints:necwrapper", version.ref = "nec_version
nec-lib = { module = "com.nec:lib", version.ref = "nec_version" }
# secugen sdk hosted in https://github.com/Simprints/secugen-wrapper
secugen = { module = "com.simprints:secugenwrapper", version.ref = "secugen_version" }
+simface = { module = "com.simprints.biometrics:simface", version.ref = "simface" }
#hilt
hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt_version" }
diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt
index 7a76901c84..805b67f9cf 100644
--- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt
+++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt
@@ -114,6 +114,7 @@ internal data class OldProjectConfig(
?: DecisionPolicy(0, 0, 0),
version = DEFAULT_FACE_SDK_VERSION,
),
+ simFace = null,
)
}
diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt
index 8e9654d8ec..4a1f779f18 100644
--- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt
+++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FaceConfiguration.kt
@@ -7,12 +7,13 @@ import com.simprints.infra.config.store.models.FaceConfiguration
internal fun FaceConfiguration.toProto(): ProtoFaceConfiguration = ProtoFaceConfiguration
.newBuilder()
.addAllAllowedSdks(allowedSDKs.map { it.toProto() })
- .also {
- if (rankOne != null) it.rankOne = rankOne.toProto()
- }.build()
+ .also { if (rankOne != null) it.rankOne = rankOne.toProto() }
+ .also { if (simFace != null) it.simFace = simFace.toProto() }
+ .build()
internal fun FaceConfiguration.BioSdk.toProto() = when (this) {
FaceConfiguration.BioSdk.RANK_ONE -> ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE
+ FaceConfiguration.BioSdk.SIM_FACE -> ProtoFaceConfiguration.ProtoBioSdk.SIM_FACE
}
internal fun FaceConfiguration.FaceSdkConfiguration.toProto() = ProtoFaceConfiguration.ProtoFaceSdkConfiguration
@@ -35,12 +36,14 @@ internal fun FaceConfiguration.ImageSavingStrategy.toProto(): ProtoFaceConfigura
internal fun ProtoFaceConfiguration.toDomain(): FaceConfiguration = FaceConfiguration(
allowedSDKs = allowedSdksList.map { it.toDomain() },
- if (hasRankOne()) rankOne.toDomain() else null,
+ rankOne = if (hasRankOne()) rankOne.toDomain() else null,
+ simFace = if (hasSimFace()) simFace.toDomain() else null,
)
@Suppress("SameReturnValue")
internal fun ProtoFaceConfiguration.ProtoBioSdk.toDomain() = when (this) {
ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE -> FaceConfiguration.BioSdk.RANK_ONE
+ ProtoFaceConfiguration.ProtoBioSdk.SIM_FACE -> FaceConfiguration.BioSdk.SIM_FACE
ProtoFaceConfiguration.ProtoBioSdk.UNRECOGNIZED -> FaceConfiguration.BioSdk.RANK_ONE
}
diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt
index 5293958589..f6061ea06c 100644
--- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt
+++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt
@@ -3,22 +3,8 @@ package com.simprints.infra.config.store.models
data class FaceConfiguration(
val allowedSDKs: List,
val rankOne: FaceSdkConfiguration?,
+ val simFace: FaceSdkConfiguration?,
) {
- val nbOfImagesToCapture: Int
- get() = rankOne?.nbOfImagesToCapture!!
-
- val qualityThreshold: Float
- get() = rankOne?.qualityThreshold!!
-
- val imageSavingStrategy: ImageSavingStrategy
- get() = rankOne?.imageSavingStrategy!!
-
- val decisionPolicy: DecisionPolicy
- get() = rankOne?.decisionPolicy!!
-
- val verificationMatchThreshold: Float?
- get() = rankOne?.verificationMatchThreshold
-
data class FaceSdkConfiguration(
val nbOfImagesToCapture: Int,
val qualityThreshold: Float,
@@ -29,8 +15,14 @@ data class FaceConfiguration(
val verificationMatchThreshold: Float? = null,
)
+ fun getSdkConfiguration(sdk: BioSdk): FaceSdkConfiguration? = when (sdk) {
+ BioSdk.RANK_ONE -> rankOne
+ BioSdk.SIM_FACE -> simFace
+ }
+
enum class BioSdk {
RANK_ONE,
+ SIM_FACE,
}
enum class ImageSavingStrategy {
diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt
index 2bb05f82c4..bc2b6cfe84 100644
--- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt
+++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFaceConfiguration.kt
@@ -7,11 +7,13 @@ import com.simprints.infra.config.store.models.FaceConfiguration
@Keep
internal data class ApiFaceConfiguration(
val allowedSDKs: List,
- val rankOne: ApiFaceSdkConfiguration,
+ val rankOne: ApiFaceSdkConfiguration? = null,
+ val simFace: ApiFaceSdkConfiguration? = null,
) {
fun toDomain(): FaceConfiguration = FaceConfiguration(
allowedSDKs = allowedSDKs.map { it.toDomain() },
- rankOne = rankOne.toDomain(),
+ rankOne = rankOne?.toDomain(),
+ simFace = simFace?.toDomain(),
)
@Keep
@@ -38,10 +40,12 @@ internal data class ApiFaceConfiguration(
@Keep
enum class BioSdk {
RANK_ONE,
+ SIM_FACE,
;
fun toDomain() = when (this) {
RANK_ONE -> FaceConfiguration.BioSdk.RANK_ONE
+ SIM_FACE -> FaceConfiguration.BioSdk.SIM_FACE
}
}
diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto
index 09a38238e5..b8227fef36 100644
--- a/infra/config-store/src/main/proto/project_config.proto
+++ b/infra/config-store/src/main/proto/project_config.proto
@@ -39,9 +39,11 @@ message ProtoFaceConfiguration {
repeated ProtoBioSdk allowed_sdks = 5;
optional ProtoFaceSdkConfiguration rank_one = 6;
+ optional ProtoFaceSdkConfiguration sim_face = 7;
enum ProtoBioSdk {
RANK_ONE = 0;
+ SIM_FACE = 1;
}
enum ImageSavingStrategy {
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt
index 3c749b910e..4d655ad51a 100644
--- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FaceConfigurationTest.kt
@@ -18,6 +18,7 @@ class FaceConfigurationTest {
val protoFaceConfigurationWithoutAgeRange = protoFaceConfiguration
.toBuilder()
.setRankOne(protoFaceConfiguration.rankOne.toBuilder().clearAllowedAgeRange())
+ .setSimFace(protoFaceConfiguration.simFace.toBuilder().clearAllowedAgeRange())
.build()
assertThat(protoFaceConfigurationWithoutAgeRange.toDomain()).isEqualTo(faceConfiguration)
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FaceConfigurationTest.kt
new file mode 100644
index 0000000000..0aa85e72d1
--- /dev/null
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FaceConfigurationTest.kt
@@ -0,0 +1,40 @@
+package com.simprints.infra.config.store.models
+
+import com.google.common.truth.Truth
+import org.junit.Test
+
+class FaceConfigurationTest {
+ @Test
+ fun `should retrieve Rank One configuration when RANK_ONE is requested `() {
+ val faceConfiguration = createConfiguration()
+ Truth
+ .assertThat(faceConfiguration.getSdkConfiguration(FaceConfiguration.BioSdk.RANK_ONE))
+ .isEqualTo(faceConfiguration.rankOne)
+ }
+
+ @Test
+ fun `should retrieve SimFace configuration when SIM_FACE is requested `() {
+ val faceConfiguration = createConfiguration()
+ Truth
+ .assertThat(faceConfiguration.getSdkConfiguration(FaceConfiguration.BioSdk.SIM_FACE))
+ .isEqualTo(faceConfiguration.simFace)
+ }
+
+ private fun createConfiguration(): FaceConfiguration = FaceConfiguration(
+ allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE),
+ rankOne = FaceConfiguration.FaceSdkConfiguration(
+ nbOfImagesToCapture = 2,
+ qualityThreshold = 0.5f,
+ imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER,
+ decisionPolicy = DecisionPolicy(20, 50, 100),
+ version = "1",
+ ),
+ simFace = FaceConfiguration.FaceSdkConfiguration(
+ nbOfImagesToCapture = 3,
+ qualityThreshold = 0.1f,
+ imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.ONLY_GOOD_SCAN,
+ decisionPolicy = DecisionPolicy(20, 50, 100),
+ version = "14",
+ ),
+ )
+}
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FingerprintConfigurationTest.kt
index ed029f6dba..46e3e3e5bc 100644
--- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FingerprintConfigurationTest.kt
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/FingerprintConfigurationTest.kt
@@ -1,6 +1,6 @@
package com.simprints.infra.config.store.models
-import com.google.common.truth.Truth
+import com.google.common.truth.*
import org.junit.Test
class FingerprintConfigurationTest {
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt
index 42f964b079..b5d1fddc5d 100644
--- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt
@@ -11,9 +11,9 @@ import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.Up
import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS
import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS
import com.simprints.infra.config.store.testtools.faceConfiguration
+import com.simprints.infra.config.store.testtools.faceSdkConfiguration
import com.simprints.infra.config.store.testtools.fingerprintConfiguration
import com.simprints.infra.config.store.testtools.projectConfiguration
-import com.simprints.infra.config.store.testtools.rankOneConfiguration
import com.simprints.infra.config.store.testtools.simprintsUpSyncConfigurationConfiguration
import com.simprints.infra.config.store.testtools.synchronizationConfiguration
import org.junit.Test
@@ -271,7 +271,7 @@ class ProjectConfigurationTest {
fun `isAgeRestricted should return false when all are empty`() {
// Arrange
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = AgeGroup(0, null))),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = AgeGroup(0, null))),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = AgeGroup(0, null)),
nec = null,
@@ -291,7 +291,7 @@ class ProjectConfigurationTest {
val emptyAgeRange = AgeGroup(0, 0)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = emptyAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = emptyAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = emptyAgeRange),
nec = null,
@@ -312,7 +312,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(20, 30)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = null,
@@ -348,7 +348,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(20, 30)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = null,
@@ -372,7 +372,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(15, 30)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = null,
@@ -398,7 +398,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(20, 30)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = fingerprintConfiguration.nec?.copy(allowedAgeRange = duplicateAgeRange),
@@ -422,7 +422,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(20, 30)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = null,
@@ -445,7 +445,7 @@ class ProjectConfigurationTest {
val secugenSimMatcherAgeRange = AgeGroup(20, null)
val projectConfiguration = projectConfiguration.copy(
- face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)),
+ face = faceConfiguration.copy(rankOne = faceSdkConfiguration.copy(allowedAgeRange = faceAgeRange)),
fingerprint = fingerprintConfiguration.copy(
secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange),
nec = null,
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt
index 3cc38c03f3..593ffadfe7 100644
--- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFaceConfigurationTest.kt
@@ -16,7 +16,10 @@ class ApiFaceConfigurationTest {
@Test
fun `should map correctly the model with allowedAgeRange present`() {
val apiFaceConfigurationWithAgeRange = apiFaceConfiguration.copy(
- rankOne = apiFaceConfiguration.rankOne.copy(
+ rankOne = apiFaceConfiguration.rankOne?.copy(
+ allowedAgeRange = ApiAllowedAgeRange(10, 20),
+ ),
+ simFace = apiFaceConfiguration.simFace?.copy(
allowedAgeRange = ApiAllowedAgeRange(10, 20),
),
)
@@ -24,6 +27,9 @@ class ApiFaceConfigurationTest {
rankOne = faceConfiguration.rankOne!!.copy(
allowedAgeRange = AgeGroup(10, 20),
),
+ simFace = faceConfiguration.simFace!!.copy(
+ allowedAgeRange = AgeGroup(10, 20),
+ ),
)
assertThat(apiFaceConfigurationWithAgeRange.toDomain()).isEqualTo(faceConfigurationWithAgeRange)
}
diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt
index 3dc0306870..8289ad55e6 100644
--- a/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt
+++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt
@@ -146,7 +146,7 @@ internal val protoDecisionPolicy =
.setMedium(30)
.setHigh(40)
.build()
-internal val rankOneConfiguration = FaceSdkConfiguration(
+internal val faceSdkConfiguration = FaceSdkConfiguration(
nbOfImagesToCapture = 2,
qualityThreshold = -1f,
imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER,
@@ -167,10 +167,20 @@ internal val apiFaceConfiguration = ApiFaceConfiguration(
verificationMatchThreshold = null,
version = "1.0",
),
+ simFace = ApiFaceSdkConfiguration(
+ nbOfImagesToCapture = 2,
+ qualityThreshold = -1f,
+ decisionPolicy = apiDecisionPolicy,
+ imageSavingStrategy = ApiFaceConfiguration.ImageSavingStrategy.NEVER,
+ allowedAgeRange = null,
+ verificationMatchThreshold = null,
+ version = "1.0",
+ ),
)
internal val faceConfiguration = FaceConfiguration(
allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE),
- rankOne = rankOneConfiguration,
+ rankOne = faceSdkConfiguration,
+ simFace = faceSdkConfiguration,
)
internal val protoFaceConfiguration = ProtoFaceConfiguration
.newBuilder()
@@ -185,6 +195,16 @@ internal val protoFaceConfiguration = ProtoFaceConfiguration
.setVersion("1.0")
.setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build())
.build(),
+ ).setSimFace(
+ ProtoFaceConfiguration.ProtoFaceSdkConfiguration
+ .newBuilder()
+ .setNbOfImagesToCapture(2)
+ .setQualityThresholdPrecise(-1f)
+ .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER)
+ .setDecisionPolicy(protoDecisionPolicy)
+ .setVersion("1.0")
+ .setAllowedAgeRange(ProtoAllowedAgeRange.newBuilder().build())
+ .build(),
).build()
internal val apiVero2Configuration = ApiVero2Configuration(
diff --git a/infra/license/src/main/java/com/simprints/infra/license/models/License.kt b/infra/license/src/main/java/com/simprints/infra/license/models/License.kt
index 3033162c15..3cd1dff017 100644
--- a/infra/license/src/main/java/com/simprints/infra/license/models/License.kt
+++ b/infra/license/src/main/java/com/simprints/infra/license/models/License.kt
@@ -7,4 +7,8 @@ data class License(
val expiration: String?,
val data: String,
val version: LicenseVersion,
-)
+) {
+ companion object {
+ val NO_LICENSE = License(null, "", LicenseVersion.UNLIMITED)
+ }
+}
diff --git a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt
index f38cde00ba..6ec2357a1a 100644
--- a/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt
+++ b/infra/sync/src/test/java/com/simprints/infra/sync/config/testtools/Models.kt
@@ -49,6 +49,13 @@ internal val faceConfiguration =
decisionPolicy = decisionPolicy,
version = "1.0",
),
+ simFace = FaceConfiguration.FaceSdkConfiguration(
+ nbOfImagesToCapture = 2,
+ qualityThreshold = -1f,
+ imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER,
+ decisionPolicy = decisionPolicy,
+ version = "1.0",
+ ),
)
internal val fingerprintConfiguration = FingerprintConfiguration(
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d2ceae1541..f193228fe7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -64,6 +64,15 @@ dependencyResolutionManagement {
password = properties.getProperty("GITHUB_TOKEN", System.getenv("GITHUB_TOKEN"))
}
}
+
+ maven {
+ url = uri("https://maven.pkg.github.com/Simprints/Biometrics-SimFace")
+ credentials {
+ username =
+ properties.getProperty("GITHUB_USERNAME", System.getenv("GITHUB_USERNAME"))
+ password = properties.getProperty("GITHUB_TOKEN", System.getenv("GITHUB_TOKEN"))
+ }
+ }
}
}
@@ -93,6 +102,7 @@ include(
":face:infra:bio-sdk-resolver",
":face:infra:roc-v1",
":face:infra:roc-v3",
+ ":face:infra:simface",
)
// Feature modules