From c839ca42648ce10e0d8ebfe63ce409cbaa79b53a Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 11:06:15 +0300 Subject: [PATCH 1/6] MS-988 Add SimFace as a module --- face/infra/bio-sdk-resolver/build.gradle.kts | 1 + .../infra/biosdkresolver/SimFaceBioSdk.kt | 23 ++++++ .../infra/biosdkresolver/RocV3BioSdkTest.kt | 3 - .../infra/biosdkresolver/SimFaceSdkTest.kt | 18 +++++ face/infra/simface/.gitignore | 1 + face/infra/simface/build.gradle.kts | 12 +++ .../simface/src/main/AndroidManifest.xml | 2 + .../infra/simface/SimFaceProviderModule.kt | 31 ++++++++ .../simface/detection/SimFaceDetector.kt | 38 ++++++++++ .../initialization/SimFaceInitializer.kt | 22 ++++++ .../infra/simface/matching/SimFaceMatcher.kt | 26 +++++++ .../simface/detection/SimFaceDetectorTest.kt | 59 +++++++++++++++ .../simface/matching/SimFaceMatcherTest.kt | 75 +++++++++++++++++++ gradle/libs.versions.toml | 2 + .../infra/sync/config/testtools/Models.kt | 7 ++ settings.gradle.kts | 10 +++ 16 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 face/infra/bio-sdk-resolver/src/main/java/com/simprints/face/infra/biosdkresolver/SimFaceBioSdk.kt create mode 100644 face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/SimFaceSdkTest.kt create mode 100644 face/infra/simface/.gitignore create mode 100644 face/infra/simface/build.gradle.kts create mode 100644 face/infra/simface/src/main/AndroidManifest.xml create mode 100644 face/infra/simface/src/main/java/com/simprints/face/infra/simface/SimFaceProviderModule.kt create mode 100644 face/infra/simface/src/main/java/com/simprints/face/infra/simface/detection/SimFaceDetector.kt create mode 100644 face/infra/simface/src/main/java/com/simprints/face/infra/simface/initialization/SimFaceInitializer.kt create mode 100644 face/infra/simface/src/main/java/com/simprints/face/infra/simface/matching/SimFaceMatcher.kt create mode 100644 face/infra/simface/src/test/java/com/simprints/face/infra/simface/detection/SimFaceDetectorTest.kt create mode 100644 face/infra/simface/src/test/java/com/simprints/face/infra/simface/matching/SimFaceMatcherTest.kt 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/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/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..3923944bb5 --- /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()) + + 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/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/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 From 3be32e23ec56af941705271624e665bae8c38611 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 14:12:44 +0300 Subject: [PATCH 2/6] MS-988 Add SimFace to configuration store --- .../migrations/models/OldProjectConfig.kt | 1 + .../store/local/models/FaceConfiguration.kt | 11 +++-- .../config/store/models/FaceConfiguration.kt | 7 ++++ .../remote/models/ApiFaceConfiguration.kt | 8 +++- .../src/main/proto/project_config.proto | 2 + .../local/models/FaceConfigurationTest.kt | 1 + .../store/models/FaceConfigurationTest.kt | 40 +++++++++++++++++++ .../models/FingerprintConfigurationTest.kt | 2 +- .../store/models/ProjectConfigurationTest.kt | 18 ++++----- .../remote/models/ApiFaceConfigurationTest.kt | 8 +++- .../infra/config/store/testtools/Models.kt | 24 ++++++++++- 11 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 infra/config-store/src/test/java/com/simprints/infra/config/store/models/FaceConfigurationTest.kt 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..0e69f2c624 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,6 +3,7 @@ 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!! @@ -29,8 +30,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( From cd7c7a0c934c37e797163524279b48330ccea318 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 14:44:23 +0300 Subject: [PATCH 3/6] MS-988 Use the configuration depending on the Face SDK configured --- .../face/capture/FaceCaptureContract.kt | 6 +- .../face/capture/FaceCaptureParams.kt | 13 +++ .../capture/screens/FaceCaptureViewModel.kt | 81 ++++++++++--------- .../FaceCaptureControllerFragment.kt | 6 +- .../livefeedback/LiveFeedbackFragment.kt | 4 +- .../LiveFeedbackFragmentViewModel.kt | 6 +- .../LiveFeedbackAutoCaptureFragment.kt | 4 +- ...iveFeedbackAutoCaptureFragmentViewModel.kt | 25 +++--- .../res/navigation/graph_face_capture.xml | 4 +- .../screens/FaceCaptureViewModelTest.kt | 57 ++++++++++--- .../LiveFeedbackFragmentViewModelTest.kt | 21 +++-- ...eedbackAutoCaptureFragmentViewModelTest.kt | 42 ++++++---- .../ResolveFaceBioSdkUseCase.kt | 5 +- .../ResolveFaceBioSdkUseCaseTest.kt | 9 ++- .../infra/biosdkresolver/SimFaceSdkTest.kt | 2 +- .../enrollast/EnrolLastBiometricParams.kt | 2 + .../CheckForDuplicateEnrolmentsUseCase.kt | 16 ++-- .../screen/usecase/BuildSubjectUseCaseTest.kt | 4 +- ...eckDuplicateEnrolmentsErrorsUseCaseTest.kt | 17 ++-- .../com/simprints/matcher/MatchContract.kt | 3 + .../java/com/simprints/matcher/MatchParams.kt | 2 + .../java/com/simprints/matcher/MatchResult.kt | 2 + .../matcher/screen/MatchViewModel.kt | 2 +- .../matcher/usecases/FaceMatcherUseCase.kt | 7 +- .../usecases/FingerprintMatcherUseCase.kt | 7 +- .../matcher/screen/MatchViewModelTest.kt | 22 +++-- .../usecases/FaceMatcherUseCaseTest.kt | 5 +- .../steps/MatchStepStubPayload.kt | 6 +- .../MapStepsForLastBiometricEnrolUseCase.kt | 1 + .../response/CreateIdentifyResponseUseCase.kt | 27 ++++--- .../response/CreateVerifyResponseUseCase.kt | 26 +++--- .../response/IsNewEnrolmentUseCase.kt | 17 ++-- .../usecases/steps/BuildStepsUseCase.kt | 40 +++++---- ...apStepsForLastBiometricEnrolUseCaseTest.kt | 5 +- .../CreateIdentifyResponseUseCaseTest.kt | 20 ++--- .../CreateVerifyResponseUseCaseTest.kt | 36 ++++----- .../response/IsNewEnrolmentUseCaseTest.kt | 31 +++---- .../usecases/steps/BuildStepsUseCaseTest.kt | 28 ++++++- .../config/store/models/FaceConfiguration.kt | 15 ---- .../simprints/infra/license/models/License.kt | 6 +- 40 files changed, 380 insertions(+), 252 deletions(-) create mode 100644 face/capture/src/main/java/com/simprints/face/capture/FaceCaptureParams.kt 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/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..43f27de2b4 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 @@ -10,7 +11,9 @@ class ResolveFaceBioSdkUseCase @Inject constructor( private val rocV1BioSdk: RocV1BioSdk, private val rocV3BioSdk: RocV3BioSdk, ) { - suspend operator fun invoke(): FaceBioSDK { + suspend operator fun invoke(bioSdk: FaceConfiguration.BioSdk): FaceBioSDK { + // TODO consider SimFace in the resolution + val version = configRepository .getProjectConfiguration() .face 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..e37c4d7743 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 @@ -33,7 +34,7 @@ class ResolveFaceBioSdkUseCaseTest { } returns null // When - resolveFaceBioSdkUseCase.invoke() + resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE) // Then: Expect IllegalArgumentException to be thrown } @@ -50,7 +51,7 @@ class ResolveFaceBioSdkUseCaseTest { } returns "" // When - resolveFaceBioSdkUseCase.invoke() + resolveFaceBioSdkUseCase.invoke(FaceConfiguration.BioSdk.RANK_ONE) // Then: Expect IllegalArgumentException to be thrown } @@ -69,7 +70,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 +89,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/SimFaceSdkTest.kt b/face/infra/bio-sdk-resolver/src/test/java/com/simprints/face/infra/biosdkresolver/SimFaceSdkTest.kt index 3923944bb5..63c7f9d422 100644 --- 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 @@ -9,7 +9,7 @@ class SimFaceSdkTest { @Test fun createMatcher() { - bioSdk = SimFaceBioSdk(mockk(), mockk(), mockk()) + bioSdk = SimFaceBioSdk(mockk(), mockk(), mockk(relaxed = true)) val matcher = bioSdk.createMatcher(emptyList()) 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/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 0e69f2c624..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 @@ -5,21 +5,6 @@ data class FaceConfiguration( 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, 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) + } +} From 9baa81a690b82e54d4b394e2a7dcbdf9fe13d509 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 14:50:49 +0300 Subject: [PATCH 4/6] MS-988 Add SimFace to the face bio sdk resolver --- .../ResolveFaceBioSdkUseCase.kt | 24 ++++++++++--------- .../ResolveFaceBioSdkUseCaseTest.kt | 19 +++++++++++---- 2 files changed, 28 insertions(+), 15 deletions(-) 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 43f27de2b4..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 @@ -10,17 +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(bioSdk: FaceConfiguration.BioSdk): FaceBioSDK { - // TODO consider SimFace in the resolution - - 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/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 e37c4d7743..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 @@ -13,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 @@ -40,7 +51,7 @@ class ResolveFaceBioSdkUseCaseTest { } @Test(expected = IllegalArgumentException::class) - fun `throw exception when version is empty`() = runTest { + fun `throw exception when RankOne version is empty`() = runTest { // Given coEvery { configRepository From 1bddb1228db45c4aaf06252d391846070348cf96 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 14:51:43 +0300 Subject: [PATCH 5/6] MS-988 Update dev build version name --- build-logic/build_properties.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") From be324d35902528396a0d7d0a16b94e67013c2eb3 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 28 May 2025 16:29:17 +0300 Subject: [PATCH 6/6] MS-988 Add simface modules to pr checks --- .github/workflows/pr-checks.yml | 1 + 1 file changed, 1 insertion(+) 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: