diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt index 95e594d595..acc66e2bf1 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/IntentToActionMapper.kt @@ -50,10 +50,7 @@ internal class IntentToActionMapper @Inject constructor( return when (actionIdentifier.packageName) { OdkConstants.PACKAGE_NAME -> mapOdkAction(actionIdentifier, extras, project) CommCareConstants.PACKAGE_NAME -> mapCommCareAction(actionIdentifier, extras, project) - LibSimprintsConstants.PACKAGE_NAME -> { - mapLibSimprintsAction(actionIdentifier, extras, project) - } - + LibSimprintsConstants.PACKAGE_NAME -> mapLibSimprintsAction(actionIdentifier, extras, project) else -> throw InvalidRequestException( "Unsupported package name", ClientApiError.INVALID_STATE_FOR_INTENT_ACTION ) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt index 5a75c99d0f..3e628a6223 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt @@ -40,10 +40,11 @@ internal class EnrolRequestBuilder( actionIdentifier = actionIdentifier, projectId = extractor.getProjectId(), userId = extractor.getUserId().asTokenizableRaw(), + moduleId = extractor.getModuleId().asTokenizableRaw(), biometricDataSource = extractor.getBiometricDataSource(), + subjectAge = extractor.getSubjectAge(), callerPackageName = extractor.getCallerPackageName(), metadata = extractor.getMetadata(), - moduleId = extractor.getModuleId().asTokenizableRaw(), unknownExtras = extractor.getUnknownExtras() ) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt index bbd9564d27..2aca5cc1f4 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt @@ -44,6 +44,7 @@ internal class IdentifyRequestBuilder( userId = extractor.getUserId().asTokenizableRaw(), moduleId = extractor.getModuleId().asTokenizableRaw(), biometricDataSource = extractor.getBiometricDataSource(), + subjectAge = extractor.getSubjectAge(), callerPackageName = extractor.getCallerPackageName(), metadata = extractor.getMetadata(), unknownExtras = extractor.getUnknownExtras() diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt index 9466329bf0..dc2265ea37 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt @@ -43,6 +43,7 @@ internal class VerifyRequestBuilder( userId = extractor.getUserId().asTokenizableRaw(), moduleId = extractor.getModuleId().asTokenizableRaw(), biometricDataSource = extractor.getBiometricDataSource(), + subjectAge = extractor.getSubjectAge(), callerPackageName = extractor.getCallerPackageName(), metadata = extractor.getMetadata(), verifyGuid = extractor.getVerifyGuid(), diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt index ead54db419..80445ed6cd 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt @@ -1,6 +1,7 @@ package com.simprints.feature.clientapi.mappers.request.extractors import android.content.Intent +import com.simprints.core.tools.json.JsonHelper import com.simprints.feature.clientapi.extensions.extractString import com.simprints.feature.clientapi.models.ClientApiConstants import com.simprints.libsimprints.Constants @@ -14,7 +15,7 @@ internal abstract class ActionRequestExtractor(private val extras: Map>(getMetadata()) + parsedMetadata[Constants.SIMPRINTS_SUBJECT_AGE] as? Int + } catch (e: Exception) { + null + } + } + protected open fun Intent.extractString(key: String): String = this.getStringExtra(key) ?: "" open fun getUnknownExtras(): Map = extras.filter { it.key.isNotBlank() && !expectedKeys.contains(it.key) } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt index 9f83ca5745..2ae23600dd 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapper.kt @@ -82,6 +82,7 @@ internal class LibSimprintsResponseMapper @Inject constructor() { AppErrorReason.BACKEND_MAINTENANCE_ERROR -> Constants.SIMPRINTS_BACKEND_MAINTENANCE_ERROR AppErrorReason.PROJECT_PAUSED -> Constants.SIMPRINTS_PROJECT_PAUSED AppErrorReason.PROJECT_ENDING -> Constants.SIMPRINTS_PROJECT_ENDING + AppErrorReason.AGE_GROUP_NOT_SUPPORTED -> Constants.SIMPRINTS_AGE_GROUP_NOT_SUPPORTED /* TODO incorporate these error codes into the client api diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt index 494dd7ca75..3fd214a9ed 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt @@ -18,6 +18,7 @@ internal class IsFlowCompletedWithErrorUseCase @Inject constructor() { AppErrorReason.GUID_NOT_FOUND_OFFLINE, AppErrorReason.PROJECT_PAUSED, AppErrorReason.PROJECT_ENDING, + AppErrorReason.AGE_GROUP_NOT_SUPPORTED, -> true AppErrorReason.ROOTED_DEVICE, diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt index 54546a9836..b6e60e3d00 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt @@ -37,6 +37,7 @@ internal abstract class RequestActionFactory { every { mockExtractor.getUserId() } returns MOCK_USER_ID every { mockExtractor.getModuleId() } returns MOCK_MODULE_ID every { mockExtractor.getMetadata() } returns MOCK_METADATA + every { mockExtractor.getSubjectAge() } returns null every { mockExtractor.getBiometricDataSource() } returns MOCK_BIOMETRIC_DATA_SOURCE every { mockExtractor.getCallerPackageName() } returns MOCK_CALLER_PACKAGE_NAME every { mockExtractor.getUnknownExtras() } returns emptyMap() diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt index 41a5182dbc..a57d0c8cf9 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt @@ -169,6 +169,7 @@ class LibSimprintsResponseMapperTest { AppErrorReason.BACKEND_MAINTENANCE_ERROR to Constants.SIMPRINTS_BACKEND_MAINTENANCE_ERROR, AppErrorReason.PROJECT_PAUSED to Constants.SIMPRINTS_PROJECT_PAUSED, AppErrorReason.PROJECT_ENDING to Constants.SIMPRINTS_PROJECT_ENDING, + AppErrorReason.AGE_GROUP_NOT_SUPPORTED to Constants.SIMPRINTS_AGE_GROUP_NOT_SUPPORTED, ).forEach { (reason, expectedCode) -> val extras = mapper( ActionResponse.ErrorActionResponse( diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionItemAdapter.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionItemAdapter.kt index 295b0f9d45..95c0e12782 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionItemAdapter.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionItemAdapter.kt @@ -5,45 +5,82 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.simprints.feature.dashboard.databinding.ItemFingerSelectionBinding +import com.simprints.feature.dashboard.databinding.HeaderSdkNameBinding import com.simprints.infra.config.store.models.Finger import com.simprints.infra.resources.R as IDR internal class FingerSelectionItemAdapter( - private val getItems: () -> List, -) : - RecyclerView.Adapter() { + private val getItems: () -> List, +) : RecyclerView.Adapter() { - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): FingerSelectionItemViewHolder { + companion object { + private const val TYPE_HEADER = 0 + private const val TYPE_ITEM = 1 + } + + override fun getItemCount(): Int = getItems().sumOf { it.items.size + 1 } + + override fun getItemViewType(position: Int): Int { + var pos = 0 + for (section in getItems()) { + if (position == pos) { + return TYPE_HEADER + } + pos += section.items.size + 1 + if (position < pos) { + return TYPE_ITEM + } + } + throw IllegalArgumentException("Invalid position") + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) - val binding = ItemFingerSelectionBinding.inflate(inflater, parent, false) - return FingerSelectionItemViewHolder( - parent.context, - getItems, - binding - ) + return if (viewType == TYPE_HEADER) { + val binding = HeaderSdkNameBinding.inflate(inflater, parent, false) + HeaderViewHolder(parent.context, binding) + } else { + val binding = ItemFingerSelectionBinding.inflate(inflater, parent, false) + return FingerSelectionItemViewHolder(parent.context, binding) + } } - override fun getItemCount(): Int = getItems().size + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + var totalItems = 0 + for (section in getItems()) { + if (position == totalItems && holder is HeaderViewHolder) { + holder.bind(section.sdkName) + return + } else if (position < totalItems + section.items.size + 1 && holder is FingerSelectionItemViewHolder) { + holder.bind(section.items[position - totalItems - 1]) + return + } + totalItems += section.items.size + 1 + } + } + + class HeaderViewHolder( + val context: Context, + binding: HeaderSdkNameBinding + ) : RecyclerView.ViewHolder(binding.root) { + private val textView = binding.headerText - override fun onBindViewHolder(viewHolder: FingerSelectionItemViewHolder, position: Int) { - viewHolder.bind() + fun bind(sdkName: String) { + textView.text = sdkName + } } class FingerSelectionItemViewHolder( val context: Context, - private val getItems: () -> List, binding: ItemFingerSelectionBinding ) : RecyclerView.ViewHolder(binding.root) { private val fingerNameTextView = binding.fingerNameTextView private val fingerQuantityTextView = binding.fingerQuantityTextView - fun bind() { - fingerNameTextView.text = getItems()[adapterPosition].finger.toString(context) - fingerQuantityTextView.text = getItems()[adapterPosition].quantity.toString() + fun bind(item: FingerSelectionItem) { + fingerNameTextView.text = item.finger.toString(context) + fingerQuantityTextView.text = item.quantity.toString() } } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModel.kt index 0a1753b9f6..5ff9011779 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModel.kt @@ -15,16 +15,20 @@ internal class FingerSelectionViewModel @Inject constructor( private val configManager: ConfigManager, ) : ViewModel() { - val fingerSelections: LiveData> + val fingerSelections: LiveData> get() = _fingerSelections - private val _fingerSelections = MutableLiveData>() + private val _fingerSelections = MutableLiveData>() fun start() { viewModelScope.launch { - _fingerSelections.postValue( - configManager.getProjectConfiguration().fingerprint!!.bioSdkConfiguration.fingersToCapture - .toFingerSelectionItems() - ) + val fingerSelections = mutableListOf() + configManager.getProjectConfiguration().fingerprint?.secugenSimMatcher?.fingersToCapture?.let { + fingerSelections.add(FingerSelectionSection("SimMatcher", it.toFingerSelectionItems())) + } + configManager.getProjectConfiguration().fingerprint?.nec?.fingersToCapture?.let { + fingerSelections.add(FingerSelectionSection("NEC", it.toFingerSelectionItems())) + } + _fingerSelections.postValue(fingerSelections) } } @@ -45,6 +49,11 @@ internal class FingerSelectionViewModel @Inject constructor( } } +data class FingerSelectionSection( + val sdkName: String, + val items: List +) + data class FingerSelectionItem( var finger: Finger, var quantity: Int diff --git a/feature/dashboard/src/main/res/layout/header_sdk_name.xml b/feature/dashboard/src/main/res/layout/header_sdk_name.xml new file mode 100644 index 0000000000..f77387dd97 --- /dev/null +++ b/feature/dashboard/src/main/res/layout/header_sdk_name.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionFragmentTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionFragmentTest.kt index 0e321f944f..0a7a7d3bc7 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionFragmentTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionFragmentTest.kt @@ -23,6 +23,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config +private const val SIM_MATCHER_NAME = "SimMatcher" + @RunWith(AndroidJUnit4::class) @HiltAndroidTest @Config(application = HiltTestApplication::class) @@ -33,56 +35,47 @@ class FingerSelectionFragmentTest { @BindValue @JvmField - internal val viewModel = mockk(relaxed = true) { - every { fingerSelections } returns mockk { - every { value } returns listOf( - FingerSelectionItem(Finger.LEFT_THUMB, 1), - FingerSelectionItem(Finger.RIGHT_THUMB, 1), - FingerSelectionItem(Finger.LEFT_INDEX_FINGER, 2) - ) - every { observe(any(), any()) } answers { - secondArg>>().onChanged( - listOf( - FingerSelectionItem(Finger.LEFT_THUMB, 1), - FingerSelectionItem(Finger.RIGHT_THUMB, 1), - FingerSelectionItem(Finger.LEFT_INDEX_FINGER, 2) - ) - ) - } - } - } + internal val viewModel = mockk(relaxed = true) @Test fun `should display the fingers correctly`() { mockFingerSelections( listOf( - FingerSelectionItem(Finger.LEFT_THUMB, 1), - FingerSelectionItem(Finger.RIGHT_THUMB, 3), - FingerSelectionItem(Finger.LEFT_INDEX_FINGER, 2) + FingerSelectionSection( + sdkName = SIM_MATCHER_NAME, + items = listOf( + FingerSelectionItem(Finger.LEFT_THUMB, 1), + FingerSelectionItem(Finger.RIGHT_THUMB, 2), + FingerSelectionItem(Finger.LEFT_INDEX_FINGER, 3) + ) + ), ) ) launchFragmentInHiltContainer() - onView(withId(R.id.fingerSelectionRecyclerView)).check(matches(hasChildCount(3))) + onView(withId(R.id.fingerSelectionRecyclerView)).check(matches(hasChildCount(4))) onView(nThFingerSelection(0)) + .check(matches(hasDescendant(withText(SIM_MATCHER_NAME)))) + + onView(nThFingerSelection(1)) .check(matches(hasDescendant(withText(IDR.string.fingerprint_capture_finger_l_1)))) .check(matches(hasDescendant(withText("1")))) - onView(nThFingerSelection(1)) + onView(nThFingerSelection(2)) .check(matches(hasDescendant(withText(IDR.string.fingerprint_capture_finger_r_1)))) - .check(matches(hasDescendant(withText("3")))) + .check(matches(hasDescendant(withText("2")))) - onView(nThFingerSelection(2)) + onView(nThFingerSelection(3)) .check(matches(hasDescendant(withText(IDR.string.fingerprint_capture_finger_l_2)))) - .check(matches(hasDescendant(withText("2")))) + .check(matches(hasDescendant(withText("3")))) } - private fun mockFingerSelections(fingers: List) { + private fun mockFingerSelections(fingers: List) { every { viewModel.fingerSelections } returns mockk { every { value } returns fingers every { observe(any(), any()) } answers { - secondArg>>().onChanged(fingers) + secondArg>>().onChanged(fingers) } } } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModelTest.kt index 38a54de321..60099648c6 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/fingerselection/FingerSelectionViewModelTest.kt @@ -29,28 +29,27 @@ class FingerSelectionViewModelTest { @get:Rule val testCoroutineRule = TestCoroutineRule() - private val bioSdkConfigurationMock = - mockk() - private val fingerprintConfiguration = mockk { - every { bioSdkConfiguration } returns bioSdkConfigurationMock - } + private val fingerprintConfiguration = mockk() private val configManager = mockk(relaxed = true) { coEvery { getProjectConfiguration().fingerprint } returns fingerprintConfiguration } - private val viewModel = FingerSelectionViewModel( - configManager, - ) + private val viewModel = FingerSelectionViewModel(configManager) @Test - fun start_loadsFingerStateCorrectly() { - every { bioSdkConfigurationMock.fingersToCapture } returns listOf( + fun start_loadsSingleSdkFingerStatesCorrectly() { + every { fingerprintConfiguration.secugenSimMatcher?.fingersToCapture } returns listOf( LEFT_THUMB, LEFT_THUMB, RIGHT_THUMB, RIGHT_THUMB ) + every { fingerprintConfiguration.nec } returns null viewModel.start() - assertThat(viewModel.fingerSelections.value).containsExactlyElementsIn( + val fingerSelections: List? = viewModel.fingerSelections.value + assertThat(fingerSelections).hasSize(1) + assertThat(fingerSelections?.first()?.sdkName).isEqualTo("SimMatcher") + assertThat(fingerSelections?.first()?.items).hasSize(2) + assertThat(fingerSelections?.first()?.items).containsExactlyElementsIn( listOf( FingerSelectionItem(LEFT_THUMB, 2), FingerSelectionItem(RIGHT_THUMB, 2) @@ -58,9 +57,42 @@ class FingerSelectionViewModelTest { ).inOrder() } + @Test + fun start_loadsTwoSdksFingerStatesCorrectly() { + every { fingerprintConfiguration.secugenSimMatcher?.fingersToCapture } returns listOf( + LEFT_THUMB, + RIGHT_THUMB, RIGHT_THUMB + ) + every { fingerprintConfiguration.nec?.fingersToCapture } returns listOf( + LEFT_INDEX_FINGER, LEFT_INDEX_FINGER, LEFT_INDEX_FINGER, + RIGHT_INDEX_FINGER, RIGHT_INDEX_FINGER, RIGHT_INDEX_FINGER, RIGHT_INDEX_FINGER + ) + + viewModel.start() + + val fingerSelections: List? = viewModel.fingerSelections.value + assertThat(fingerSelections).hasSize(2) + assertThat(fingerSelections?.first()?.sdkName).isEqualTo("SimMatcher") + assertThat(fingerSelections?.first()?.items).hasSize(2) + assertThat(fingerSelections?.first()?.items).containsExactlyElementsIn( + listOf( + FingerSelectionItem(LEFT_THUMB, 1), + FingerSelectionItem(RIGHT_THUMB, 2) + ) + ).inOrder() + assertThat(fingerSelections?.get(1)?.sdkName).isEqualTo("NEC") + assertThat(fingerSelections?.get(1)?.items).hasSize(2) + assertThat(fingerSelections?.get(1)?.items).containsExactlyElementsIn( + listOf( + FingerSelectionItem(LEFT_INDEX_FINGER, 3), + FingerSelectionItem(RIGHT_INDEX_FINGER, 4) + ) + ).inOrder() + } + @Test fun scatteredFingers_areAggregated() { - every { bioSdkConfigurationMock.fingersToCapture } returns listOf( + every { fingerprintConfiguration.secugenSimMatcher?.fingersToCapture } returns listOf( LEFT_THUMB, RIGHT_THUMB, RIGHT_INDEX_FINGER, @@ -92,10 +124,15 @@ class FingerSelectionViewModelTest { LEFT_INDEX_FINGER, RIGHT_3RD_FINGER, ) + every { fingerprintConfiguration.nec } returns null viewModel.start() - assertThat(viewModel.fingerSelections.value).containsExactlyElementsIn( + val fingerSelections: List? = viewModel.fingerSelections.value + assertThat(fingerSelections).hasSize(1) + assertThat(fingerSelections?.first()?.sdkName).isEqualTo("SimMatcher") + assertThat(fingerSelections?.first()?.items).hasSize(10) + assertThat(fingerSelections?.first()?.items).containsExactlyElementsIn( listOf( FingerSelectionItem(LEFT_THUMB, 1), FingerSelectionItem(RIGHT_THUMB, 1), 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 ec858cd518..ff128f778a 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 @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.annotation.Keep import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.config.store.models.Finger +import com.simprints.infra.config.store.models.FingerprintConfiguration import kotlinx.parcelize.Parcelize @Keep @@ -23,7 +24,10 @@ sealed class EnrolLastBiometricStepResult : Parcelable { @Keep @Parcelize - data class FingerprintMatchResult(val results: List) : EnrolLastBiometricStepResult() + data class FingerprintMatchResult( + val results: List, + val sdk: FingerprintConfiguration.BioSdk, + ) : EnrolLastBiometricStepResult() @Keep @Parcelize diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCase.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCase.kt index 5cde6f9899..29359eb578 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCase.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCase.kt @@ -1,7 +1,6 @@ package com.simprints.feature.enrollast.screen.usecase import com.simprints.feature.enrollast.EnrolLastBiometricStepResult -import com.simprints.feature.enrollast.MatchResult import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ENROLMENT import com.simprints.infra.logging.Simber @@ -37,28 +36,30 @@ internal class HasDuplicateEnrolmentsUseCase @Inject constructor() { private fun getFingerprintMatchResult(steps: List) = steps.filterIsInstance() - .lastOrNull()?.results + .lastOrNull() private fun getFaceMatchResult(steps: List) = - steps.filterIsInstance().lastOrNull()?.results + steps.filterIsInstance() + .lastOrNull() private fun isAnyResponseWithHighConfidence( configuration: ProjectConfiguration, - fingerprintResponse: List?, - faceResponse: List?, + fingerprintResponse: EnrolLastBiometricStepResult.FingerprintMatchResult?, + faceResponse: EnrolLastBiometricStepResult.FaceMatchResult?, ): Boolean { - val fingerprintThreshold = configuration.fingerprint - ?.bioSdkConfiguration - ?.decisionPolicy - ?.high?.toFloat() - ?: Float.MAX_VALUE + val fingerprintThreshold = fingerprintResponse?.let { + configuration.fingerprint + ?.getSdkConfiguration(fingerprintResponse.sdk) + ?.decisionPolicy + ?.high?.toFloat() + } ?: Float.MAX_VALUE + val faceThreshold = configuration.face ?.decisionPolicy ?.high?.toFloat() ?: Float.MAX_VALUE - return fingerprintResponse?.any { it.confidenceScore >= fingerprintThreshold } == true - || faceResponse?.any { it.confidenceScore >= faceThreshold } == true + return fingerprintResponse?.results?.any { it.confidenceScore >= fingerprintThreshold } == true + || faceResponse?.results?.any { it.confidenceScore >= faceThreshold } == true } - } 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 0a268d8d75..58e04dff3e 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 @@ -15,6 +15,7 @@ import com.simprints.testtools.unit.EncodingUtilsImplForTests import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import org.junit.Before import org.junit.Test @@ -50,7 +51,7 @@ class BuildSubjectUseCaseTest { fun `has no samples if no valid steps provided`() { val result = useCase(createParams(listOf( EnrolLastBiometricStepResult.EnrolLastBiometricsResult(null), - EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList()), + EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), mockk()), EnrolLastBiometricStepResult.FaceMatchResult(emptyList()), ))) @@ -61,7 +62,7 @@ class BuildSubjectUseCaseTest { @Test fun `maps first available fingerprint capture step results`() { val result = useCase(createParams(listOf( - EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList()), + EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), mockk()), EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.RIGHT_THUMB))), EnrolLastBiometricStepResult.FingerprintCaptureResult(listOf(mockFingerprintResults(Finger.LEFT_THUMB))), ))) diff --git a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCaseTest.kt b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCaseTest.kt index 757eea92b3..e4ef03ae91 100644 --- a/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCaseTest.kt +++ b/feature/enrol-last-biometric/src/test/java/com/simprints/feature/enrollast/screen/usecase/HasDuplicateEnrolmentsUseCaseTest.kt @@ -33,9 +33,10 @@ class HasDuplicateEnrolmentsUseCaseTest { fun `returns false if no fingerprint matches with high score`() { val result = useCase( projectConfig = mockProjectConfig(), - steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult(listOf( - matchResult(LOW_CONFIDENCE) - ))) + steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult( + listOf(matchResult(LOW_CONFIDENCE)), + mockk() + )) ) assertThat(result).isFalse() @@ -57,9 +58,10 @@ class HasDuplicateEnrolmentsUseCaseTest { fun `returns false if no fingerprint high score threshold`() { val result = useCase( projectConfig = mockProjectConfig(highConfidence = null), - steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult(listOf( - matchResult(HIGH_CONFIDENCE) - ))) + steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult( + listOf(matchResult(HIGH_CONFIDENCE)), + mockk(), + )) ) assertThat(result).isFalse() @@ -91,9 +93,10 @@ class HasDuplicateEnrolmentsUseCaseTest { fun `returns true if there are fingerprint matches with high score`() { val result = useCase( projectConfig = mockProjectConfig(), - steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult(listOf( - matchResult(HIGH_CONFIDENCE) - ))) + steps = listOf(EnrolLastBiometricStepResult.FingerprintMatchResult( + listOf(matchResult(HIGH_CONFIDENCE)), + mockk(), + )) ) assertThat(result).isTrue() @@ -117,7 +120,7 @@ class HasDuplicateEnrolmentsUseCaseTest { ): ProjectConfiguration = mockk(relaxed = true) { every { general.duplicateBiometricEnrolmentCheck } returns checkEnabled // cannot mock Int? directly due to Java inter-op issues, so mocking decision policy instead - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns highConfidence?.let { + every { fingerprint?.getSdkConfiguration(any())?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) } every { face?.decisionPolicy } returns highConfidence?.let { DecisionPolicy(0, 0, it) } 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 fd2a7f223d..5fc1990c57 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.FingerprintConfiguration import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.matcher.screen.MatchFragmentArgs @@ -12,12 +13,14 @@ object MatchContract { fun getArgs( fingerprintSamples: List = emptyList(), faceSamples: List = emptyList(), + fingerprintSDK: FingerprintConfiguration.BioSdk? = null, flowType: FlowType, subjectQuery: SubjectQuery, biometricDataSource: BiometricDataSource, ) = MatchFragmentArgs(MatchParams( faceSamples, fingerprintSamples, + fingerprintSDK, flowType, subjectQuery, biometricDataSource, 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 6327eb6005..9c89dd3f2b 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.FingerprintConfiguration import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports @@ -14,6 +15,7 @@ import kotlinx.parcelize.Parcelize data class MatchParams( val probeFaceSamples: List = emptyList(), val probeFingerprintSamples: List = emptyList(), + val fingerprintSDK: FingerprintConfiguration.BioSdk? = null, val flowType: FlowType, val queryForCandidates: SubjectQuery, val biometricDataSource: BiometricDataSource, 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 ca009b3558..ad62396133 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.FingerprintConfiguration import java.io.Serializable @Keep @@ -32,6 +33,7 @@ data class FaceMatchResult( @Keep data class FingerprintMatchResult( override val results: List, + val sdk: FingerprintConfiguration.BioSdk, ) : MatchResult { @Keep 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 446b3b014a..557ad64efe 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 @@ -53,7 +53,7 @@ internal class MatchViewModel @Inject constructor( else -> fingerprintMatcher } - val (sortedResults, totalCandidates) = matcherUseCase( + val matcherResult = matcherUseCase( params, onLoadingCandidates = { tag -> Simber.tag(tag).i("Loading candidates") @@ -67,20 +67,20 @@ internal class MatchViewModel @Inject constructor( startTime, endTime, params, - totalCandidates, - matcherUseCase.matcherName(), - sortedResults + matcherResult.totalCandidates, + matcherResult.matcherName, + matcherResult.matchResultItems ) - setMatchState(totalCandidates, sortedResults) + setMatchState(matcherResult.totalCandidates, matcherResult.matchResultItems) // wait a bit for the user to see the results delay(matchingEndWaitTimeInMillis) _matchResponse.send( when { - isFaceMatch -> FaceMatchResult(sortedResults) - else -> FingerprintMatchResult(sortedResults) + isFaceMatch -> FaceMatchResult(matcherResult.matchResultItems) + else -> FingerprintMatchResult(matcherResult.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 089a6e0b02..aa37325eb9 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 @@ -10,7 +10,7 @@ import com.simprints.infra.facebiosdk.matching.FaceSample import com.simprints.infra.logging.LoggingConstants import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.MatchParams -import com.simprints.matcher.MatchResultItem +import com.simprints.matcher.usecases.MatcherUseCase.MatcherResult import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -25,23 +25,22 @@ internal class FaceMatcherUseCase @Inject constructor( ) : MatcherUseCase { override val crashReportTag = LoggingConstants.CrashReportTag.FACE_MATCHING.name - override suspend fun matcherName ()= faceMatcher.matcherName override suspend operator fun invoke( matchParams: MatchParams, onLoadingCandidates: (tag: String) -> Unit, - ): Pair, Int> = coroutineScope { + ): MatcherResult = coroutineScope { if (matchParams.probeFaceSamples.isEmpty()) { - return@coroutineScope Pair(emptyList(), 0) + return@coroutineScope MatcherResult(emptyList(), 0, faceMatcher.matcherName) } val samples = mapSamples(matchParams.probeFaceSamples) val totalCandidates = enrolmentRecordRepository.count(matchParams.queryForCandidates, dataSource = matchParams.biometricDataSource) if (totalCandidates == 0) { - return@coroutineScope Pair(emptyList(), 0) + return@coroutineScope MatcherResult(emptyList(), 0, faceMatcher.matcherName) } onLoadingCandidates(crashReportTag) - createRanges(totalCandidates) + val resultItems = createRanges(totalCandidates) .map { range -> async(dispatcher) { val batchCandidates = getCandidates(matchParams.queryForCandidates, range, dataSource = matchParams.biometricDataSource) @@ -50,7 +49,8 @@ internal class FaceMatcherUseCase @Inject constructor( } .awaitAll() .reduce { acc, subSet -> acc.addAll(subSet) } - .toList() to totalCandidates + .toList() + MatcherResult(resultItems, totalCandidates, faceMatcher.matcherName) } private fun mapSamples(probes: List) = probes 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 8472313776..000651b930 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 @@ -6,7 +6,9 @@ import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerIdentifier import com.simprints.fingerprint.infra.basebiosdk.matching.domain.Fingerprint import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerprintIdentity +import com.simprints.fingerprint.infra.biosdk.BioSdkWrapper import com.simprints.fingerprint.infra.biosdk.ResolveBioSdkWrapperUseCase +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration.FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository @@ -15,7 +17,7 @@ import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.logging.LoggingConstants import com.simprints.matcher.FingerprintMatchResult import com.simprints.matcher.MatchParams -import com.simprints.matcher.MatchResultItem +import com.simprints.matcher.usecases.MatcherUseCase.MatcherResult import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -31,32 +33,33 @@ internal class FingerprintMatcherUseCase @Inject constructor( ) : MatcherUseCase { override val crashReportTag = LoggingConstants.CrashReportTag.MATCHING.name - override suspend fun matcherName() = resolveBioSdkWrapper().matcherName override suspend operator fun invoke( matchParams: MatchParams, onLoadingCandidates: (tag: String) -> Unit, - ): Pair, Int> = coroutineScope { + ): MatcherResult = coroutineScope { + val bioSdkWrapper = resolveBioSdkWrapper(matchParams.fingerprintSDK!!) + if (matchParams.probeFingerprintSamples.isEmpty()) { - return@coroutineScope Pair(emptyList(), 0) + return@coroutineScope MatcherResult(emptyList(), 0, bioSdkWrapper.matcherName) } val samples = mapSamples(matchParams.probeFingerprintSamples) // Only candidates with supported template format are considered val queryWithSupportedFormat = matchParams.queryForCandidates.copy( - fingerprintSampleFormat = resolveBioSdkWrapper().supportedTemplateFormat + fingerprintSampleFormat = bioSdkWrapper.supportedTemplateFormat ) val totalCandidates = enrolmentRecordRepository.count(queryWithSupportedFormat, dataSource = matchParams.biometricDataSource) if (totalCandidates == 0) { - return@coroutineScope Pair(emptyList(), 0) + return@coroutineScope MatcherResult(emptyList(), 0, bioSdkWrapper.matcherName) } onLoadingCandidates(crashReportTag) - createRanges(totalCandidates) + val resultItems = createRanges(totalCandidates) .map { range -> async(dispatcher) { val batchCandidates = getCandidates(queryWithSupportedFormat, range, matchParams.biometricDataSource) - match(samples, batchCandidates, matchParams.flowType) + match(samples, batchCandidates, matchParams.flowType, bioSdkWrapper, bioSdk = matchParams.fingerprintSDK) .fold(MatchResultSet()) { acc, item -> acc.add(FingerprintMatchResult.Item(item.id, item.score)) } @@ -64,7 +67,8 @@ internal class FingerprintMatcherUseCase @Inject constructor( } .awaitAll() .reduce { acc, subSet -> acc.addAll(subSet) } - .toList() to totalCandidates + .toList() + MatcherResult(resultItems, totalCandidates, bioSdkWrapper.matcherName) } private fun mapSamples(probes: List) = probes @@ -93,17 +97,22 @@ internal class FingerprintMatcherUseCase @Inject constructor( probes: List, candidates: List, flowType: FlowType, - ) = resolveBioSdkWrapper().match( + bioSdkWrapper: BioSdkWrapper, + bioSdk: FingerprintConfiguration.BioSdk, + ) = bioSdkWrapper.match( FingerprintIdentity("", probes), candidates, - isCrossFingerMatchingEnabled(flowType), + isCrossFingerMatchingEnabled(flowType, bioSdk), ) - private suspend fun isCrossFingerMatchingEnabled(flowType: FlowType): Boolean = configManager + private suspend fun isCrossFingerMatchingEnabled( + flowType: FlowType, + bioSdk: FingerprintConfiguration.BioSdk, + ): Boolean = configManager .takeIf { flowType == FlowType.VERIFY } ?.getProjectConfiguration() ?.fingerprint - ?.bioSdkConfiguration + ?.getSdkConfiguration(bioSdk) ?.comparisonStrategyForVerification == CROSS_FINGER_USING_MEAN_OF_MAX private fun IFingerIdentifier.toMatcherDomain() = when (this) { diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt index 3a69e548d8..3ac20ab2ac 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt @@ -6,15 +6,19 @@ import com.simprints.matcher.MatchResultItem internal interface MatcherUseCase { val crashReportTag: String - suspend fun matcherName(): String /** - * Returns a list of [MatchResultItem]s sorted by confidence score in descending order - * and the total number of candidates that were considered. + * Returns a MatcherResult which contains a list of [MatchResultItem]s sorted by confidence score in descending order, + * the total number of candidates that were considered and the name of the matcher that was used */ suspend operator fun invoke( matchParams: MatchParams, onLoadingCandidates: (tag: String) -> Unit = {}, - ): Pair, Int> + ): MatcherResult + data class MatcherResult( + val matchResultItems: List, + val totalCandidates: Int, + val matcherName: String, + ) } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt index 2dbbd2e739..ed005fea02 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/SaveMatchEventUseCase.kt @@ -3,6 +3,7 @@ package com.simprints.matcher.usecases import com.simprints.core.ExternalScope import com.simprints.core.domain.common.FlowType import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.events.SessionEventRepository @@ -39,7 +40,7 @@ internal class SaveMatchEventUseCase @Inject constructor( matcherName, matchParams.queryForCandidates, matchEntries.firstOrNull(), - if (matchParams.isFaceMatch()) null else getFingerprintComparisonStrategy(), + if (matchParams.isFaceMatch()) null else getFingerprintComparisonStrategy(matchParams.fingerprintSDK!!), ) } else { getOneToManyEvent( @@ -54,9 +55,10 @@ internal class SaveMatchEventUseCase @Inject constructor( } } - private suspend fun getFingerprintComparisonStrategy() = configManager.getProjectConfiguration() + private suspend fun getFingerprintComparisonStrategy(bioSdk: FingerprintConfiguration.BioSdk) = + configManager.getProjectConfiguration() .fingerprint - ?.bioSdkConfiguration + ?.getSdkConfiguration(bioSdk) ?.comparisonStrategyForVerification ?.let { when (it) { 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 3d3fc7f483..1ebc5899e6 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 @@ -7,12 +7,14 @@ 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.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.FingerprintMatchResult import com.simprints.matcher.MatchParams import com.simprints.matcher.usecases.FaceMatcherUseCase import com.simprints.matcher.usecases.FingerprintMatcherUseCase +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 @@ -64,8 +66,6 @@ internal class MatchViewModelTest { cb1 = slot() every { timeHelper.now() } returns Timestamp(0L) - coEvery { faceMatcherUseCase.matcherName() } returns MATCHER_NAME - coEvery { fingerprintMatcherUseCase.matcherName() } returns MATCHER_NAME viewModel = MatchViewModel( faceMatcherUseCase, @@ -83,7 +83,11 @@ internal class MatchViewModelTest { coEvery { faceMatcherUseCase.invoke(any(), capture(cb1)) } answers { cb1.captured.invoke("tag1") - responseItems to responseItems.size + MatcherUseCase.MatcherResult( + matchResultItems = responseItems, + totalCandidates = responseItems.size, + matcherName = "MatcherName" + ) } coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) } @@ -121,7 +125,11 @@ internal class MatchViewModelTest { coEvery { faceMatcherUseCase.invoke(any(), capture(cb1)) } answers { cb1.captured.invoke("tag1") - responseItems to responseItems.size + MatcherUseCase.MatcherResult( + matchResultItems = responseItems, + totalCandidates = responseItems.size, + matcherName = MATCHER_NAME + ) } coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) } @@ -165,7 +173,11 @@ internal class MatchViewModelTest { coEvery { fingerprintMatcherUseCase.invoke(any(), capture(cb1)) } answers { cb1.captured.invoke("tag1") - responseItems to responseItems.size + MatcherUseCase.MatcherResult( + matchResultItems = responseItems, + totalCandidates = responseItems.size, + matcherName = MATCHER_NAME + ) } coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) } @@ -174,6 +186,7 @@ internal class MatchViewModelTest { viewModel.setupMatch( MatchParams( probeFingerprintSamples = listOf(getFingerprintSample()), + fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.ENROL, queryForCandidates = mockk {}, biometricDataSource = BiometricDataSource.Simprints, @@ -190,7 +203,7 @@ internal class MatchViewModelTest { ) ) assertThat(viewModel.matchResponse.getOrAwaitValue().peekContent()).isEqualTo( - FingerprintMatchResult(responseItems) + FingerprintMatchResult(responseItems, SECUGEN_SIM_MATCHER) ) verify { saveMatchEvent.invoke(any(), any(), any(), eq(7), eq(MATCHER_NAME), any()) } @@ -207,7 +220,6 @@ internal class MatchViewModelTest { ) companion object { - const val MATCHER_NAME = "any matcher" } } 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 bc122fc68f..c3505ed09c 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 @@ -99,7 +99,7 @@ internal class FaceMatcherUseCaseTest { var onLoadingCalled = false - val results = useCase.invoke( + val result = useCase.invoke( matchParams = MatchParams( probeFaceSamples = listOf( MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)) @@ -115,7 +115,7 @@ internal class FaceMatcherUseCaseTest { assertThat(onLoadingCalled).isTrue() - assertThat(results.first.first().subjectId).isEqualTo("subjectId") - assertThat(results.first.first().confidence).isEqualTo(42f) + assertThat(result.matchResultItems.first().subjectId).isEqualTo("subjectId") + assertThat(result.matchResultItems.first().confidence).isEqualTo(42f) } } diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt index 88d93b9612..648515af8d 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt @@ -7,7 +7,7 @@ import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.fingerprint.infra.biosdk.BioSdkWrapper import com.simprints.fingerprint.infra.biosdk.ResolveBioSdkWrapperUseCase -import com.simprints.infra.config.store.models.FingerprintConfiguration +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.store.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource @@ -52,10 +52,10 @@ internal class FingerprintMatcherUseCaseTest { @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - coEvery { resolveBioSdkWrapperUseCase() } returns bioSdkWrapper + coEvery { resolveBioSdkWrapperUseCase(any()) } returns bioSdkWrapper coEvery { configManager.getProjectConfiguration().fingerprint?.allowedSDKs - } returns listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) + } returns listOf(SECUGEN_SIM_MATCHER) useCase = FingerprintMatcherUseCase( enrolmentRecordRepository, @@ -66,19 +66,12 @@ internal class FingerprintMatcherUseCaseTest { ) } - @Test - fun `Correctly get the matcher name`() = runTest { - coEvery { bioSdkWrapper.matcherName } returns "SIM_AFIS" - coEvery { configManager.getProjectConfiguration().fingerprint?.allowedSDKs } returns listOf( - FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER - ) - assertThat(useCase.matcherName()).isEqualTo("SIM_AFIS") - } - @Test fun `Skips matching if there are no probes`() = runTest { useCase.invoke( MatchParams( + probeFingerprintSamples = emptyList(), + fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, @@ -103,6 +96,7 @@ internal class FingerprintMatcherUseCaseTest { byteArrayOf(1, 2, 3) ), ), + fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, @@ -152,6 +146,7 @@ internal class FingerprintMatcherUseCaseTest { byteArrayOf(1, 2, 3) ), ), + fingerprintSDK = SECUGEN_SIM_MATCHER, flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt index d31437d45e..985e558b63 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.FingerprintConfiguration.FingerComparisonStrategy import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource @@ -46,7 +47,7 @@ class SaveMatchEventUseCaseTest { coEvery { eventRepository.addOrUpdateEvent(any()) } just Runs coEvery { - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.comparisonStrategyForVerification + configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(SECUGEN_SIM_MATCHER)?.comparisonStrategyForVerification } returns FingerComparisonStrategy.SAME_FINGER useCase = SaveMatchEventUseCase( @@ -104,6 +105,7 @@ class SaveMatchEventUseCaseTest { byteArrayOf(1, 2, 3) ) ), + fingerprintSDK = SECUGEN_SIM_MATCHER, biometricDataSource = BiometricDataSource.Simprints, ), 2, @@ -131,18 +133,19 @@ class SaveMatchEventUseCaseTest { @Test fun `Correctly saves one to many match event`() = runTest { useCase.invoke( - Timestamp(1L), - Timestamp(2L), - MatchParams( - emptyList(), - emptyList(), - FlowType.IDENTIFY, - SubjectQuery(), + startTime = Timestamp(1L), + endTime = Timestamp(2L), + matchParams = MatchParams( + probeFaceSamples = emptyList(), + probeFingerprintSamples = emptyList(), + fingerprintSDK = null, + flowType = FlowType.IDENTIFY, + queryForCandidates = SubjectQuery(), biometricDataSource = BiometricDataSource.Simprints, ), - 2, - "faceMatcherName", - listOf( + candidatesCount = 2, + matcherName = "faceMatcherName", + results = listOf( FaceMatchResult.Item("guid1", 0.5f), FaceMatchResult.Item("guid2", 0.1f) ), diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt index cf46b194ce..e1e0f1b2c2 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.module.SimpleModule +import com.simprints.core.domain.response.AppErrorReason import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameDeserializer import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameSerializer @@ -14,6 +15,7 @@ import com.simprints.core.livedata.send import com.simprints.core.tools.json.JsonHelper import com.simprints.face.capture.FaceCaptureResult import com.simprints.feature.orchestrator.cache.OrchestratorCache +import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.model.OrchestratorResult import com.simprints.feature.orchestrator.steps.MatchStepStubPayload import com.simprints.feature.orchestrator.steps.Step @@ -26,12 +28,16 @@ import com.simprints.feature.orchestrator.usecases.ShouldCreatePersonUseCase import com.simprints.feature.orchestrator.usecases.UpdateDailyActivityUseCase import com.simprints.feature.orchestrator.usecases.response.AppResponseBuilderUseCase import com.simprints.feature.orchestrator.usecases.steps.BuildStepsUseCase +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.LocationStore +import com.simprints.fingerprint.capture.FingerprintCaptureParams import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.Simber import com.simprints.infra.orchestration.data.ActionRequest +import com.simprints.infra.orchestration.data.responses.AppErrorResponse +import com.simprints.infra.orchestration.data.responses.AppResponse import com.simprints.matcher.MatchParams import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -69,38 +75,57 @@ internal class OrchestratorViewModel @Inject constructor( val projectConfiguration = configManager.getProjectConfiguration() modalities = projectConfiguration.general.modalities.toSet() - steps = stepsBuilder.build(action, projectConfiguration) - actionRequest = action + try { + steps = stepsBuilder.build(action, projectConfiguration) + } catch (e: SubjectAgeNotSupportedException) { + sendErrorResponse(AppErrorResponse(AppErrorReason.AGE_GROUP_NOT_SUPPORTED)) + return@launch + } + doNextStep() } fun handleResult(result: Serializable) = viewModelScope.launch { Simber.d(result.toString()) - val errorResponse = mapRefusalOrErrorResult(result) + val projectConfiguration = configManager.getProjectConfiguration() + val errorResponse = mapRefusalOrErrorResult(result, projectConfiguration) if (errorResponse != null) { // Shortcut the flow execution if any refusal or error result is found - addCallbackEvent(errorResponse) - _appResponse.send(OrchestratorResult(actionRequest, errorResponse)) + sendErrorResponse(errorResponse) return@launch } - steps.firstOrNull { it.status == StepStatus.IN_PROGRESS }?.let { - it.status = StepStatus.COMPLETED - it.result = result + steps.firstOrNull { it.status == StepStatus.IN_PROGRESS }?.let { step -> + step.status = StepStatus.COMPLETED + step.result = result - updateMatcherStepPayload(it, result) + updateMatcherStepPayload(step, result) } if (shouldCreatePerson(actionRequest, modalities, steps)) { createPersonEvent(steps.mapNotNull { it.result }) } + if (result is SelectSubjectAgeGroupResult) { + val captureAndMatchSteps = stepsBuilder.buildCaptureAndMatchStepsForAgeGroup( + actionRequest!!, + projectConfiguration, + result.ageGroup + ) + steps = steps + captureAndMatchSteps + } + doNextStep() } + private fun sendErrorResponse(errorResponse: AppResponse) { + addCallbackEvent(errorResponse) + _appResponse.send(OrchestratorResult(actionRequest, errorResponse)) + } + fun restoreStepsIfNeeded() { if (steps.isEmpty()) { // Restore the steps from cache @@ -139,16 +164,15 @@ internal class OrchestratorViewModel @Inject constructor( private fun buildAppResponse() = viewModelScope.launch { val projectConfiguration = configManager.getProjectConfiguration() - val cachedActionRequest = actionRequest val appResponse = appResponseBuilder( projectConfiguration, - cachedActionRequest, + actionRequest, steps.mapNotNull { it.result }, ) updateDailyActivity(appResponse) addCallbackEvent(appResponse) - _appResponse.send(OrchestratorResult(cachedActionRequest, appResponse)) + _appResponse.send(OrchestratorResult(actionRequest, appResponse)) } private fun updateMatcherStepPayload(currentStep: Step, result: Serializable) { @@ -168,7 +192,17 @@ internal class OrchestratorViewModel @Inject constructor( } } if (currentStep.id == StepId.FINGERPRINT_CAPTURE && result is FingerprintCaptureResult) { - val matchingStep = steps.firstOrNull { it.id == StepId.FINGERPRINT_MATCHER } + val captureParams = currentStep.payload.getParcelable("params") + // Find the matching step for the same fingerprint SDK as there may be multiple match steps + val matchingStep = steps.firstOrNull { step -> + if (step.id != StepId.FINGERPRINT_MATCHER) { + false + } + else { + val stepSdk = step.payload.getParcelable(MatchStepStubPayload.STUB_KEY)?.fingerprintSDK + stepSdk == captureParams?.fingerprintSDK + } + } if (matchingStep != null) { val fingerprintSamples = result.results.mapNotNull { it.sample } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/exceptions/SubjectAgeNotSupportedException.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/exceptions/SubjectAgeNotSupportedException.kt new file mode 100644 index 0000000000..8c32d6ed53 --- /dev/null +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/exceptions/SubjectAgeNotSupportedException.kt @@ -0,0 +1,3 @@ +package com.simprints.feature.orchestrator.exceptions + +internal class SubjectAgeNotSupportedException : IllegalArgumentException("Subject age is not supported") 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 6d3e942c11..1054dae6e1 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.FingerprintConfiguration import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.matcher.MatchContract @@ -20,6 +21,7 @@ internal data class MatchStepStubPayload( val flowType: FlowType, val subjectQuery: SubjectQuery, val biometricDataSource: BiometricDataSource, + val fingerprintSDK: FingerprintConfiguration.BioSdk?, ) : Parcelable { fun toFaceStepArgs(samples: List) = MatchContract.getArgs( @@ -31,6 +33,7 @@ internal data class MatchStepStubPayload( fun toFingerprintStepArgs(samples: List) = MatchContract.getArgs( fingerprintSamples = samples, + fingerprintSDK = fingerprintSDK, flowType = flowType, subjectQuery = subjectQuery, biometricDataSource = biometricDataSource, @@ -43,6 +46,7 @@ internal data class MatchStepStubPayload( flowType: FlowType, subjectQuery: SubjectQuery, biometricDataSource: BiometricDataSource, - ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery, biometricDataSource)) + fingerprintSDK: FingerprintConfiguration.BioSdk? = null, + ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery, biometricDataSource, fingerprintSDK)) } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt index ededf2d62b..468d94746f 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt @@ -4,9 +4,12 @@ import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.AlertResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.fetchsubject.FetchSubjectResult +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolResult import com.simprints.fingerprint.connect.FingerprintConnectResult +import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.allowedAgeRanges import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse @@ -19,8 +22,12 @@ internal class MapRefusalOrErrorResultUseCase @Inject constructor( private val eventRepository: SessionEventRepository, ) { - suspend operator fun invoke(result: Serializable): AppResponse? = when (result) { + suspend operator fun invoke( + result: Serializable, + projectConfiguration: ProjectConfiguration + ): AppResponse? = when (result) { is ExitFormResult -> AppRefusalResponse.fromResult(result) + is FetchSubjectResult -> result.takeUnless { it.found }?.let { AppErrorResponse( if (it.wasOnline) AppErrorReason.GUID_NOT_FOUND_ONLINE @@ -39,6 +46,10 @@ internal class MapRefusalOrErrorResultUseCase @Inject constructor( is ValidateSubjectPoolResult -> result.takeUnless { it.isValid } ?.let { AppIdentifyResponse(emptyList(), eventRepository.getCurrentSessionScope().id) } + is SelectSubjectAgeGroupResult -> result.takeUnless { + projectConfiguration.allowedAgeRanges().any { it.contains(result.ageGroup) } + }?.let { AppErrorResponse(AppErrorReason.AGE_GROUP_NOT_SUPPORTED) } + else -> null } } 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 08ddac626c..8449db8358 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 @@ -34,7 +34,8 @@ internal class MapStepsForLastBiometricEnrolUseCase @Inject constructor() { ) is FingerprintMatchResult -> EnrolLastBiometricStepResult.FingerprintMatchResult( - result.results.map { MatchResult(it.subjectId, it.confidence) } + result.results.map { MatchResult(it.subjectId, it.confidence) }, + result.sdk, ) is FaceCaptureResult -> EnrolLastBiometricStepResult.FaceCaptureResult( diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/UpdateSessionModalitiesUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/UpdateSessionModalitiesUseCase.kt new file mode 100644 index 0000000000..9ffa010539 --- /dev/null +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/UpdateSessionModalitiesUseCase.kt @@ -0,0 +1,27 @@ +package com.simprints.feature.orchestrator.usecases + +import com.simprints.core.ExternalScope +import com.simprints.infra.config.store.models.GeneralConfiguration.Modality +import com.simprints.infra.events.SessionEventRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class UpdateSessionModalitiesUseCase @Inject constructor( + private val sessionEventRepository: SessionEventRepository, + @ExternalScope private val externalScope: CoroutineScope, +) { + operator fun invoke(modalities: List) { + // Empty modalities is invalid so don't update + if (modalities.isEmpty()) return + externalScope.launch { + val sessionScope = sessionEventRepository.getCurrentSessionScope() + val updatedSessionScope = sessionScope.copy( + payload = sessionScope.payload.copy( + modalities = modalities, + ) + ) + sessionEventRepository.saveSessionScope(updatedSessionScope) + } + } +} 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 d72e5e31c1..a4780ce5e1 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 @@ -21,14 +21,10 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( ): AppResponse { val currentSessionId = eventRepository.getCurrentSessionScope().id - val faceDecisionPolicy = projectConfiguration.face?.decisionPolicy - val faceResults = getFaceMatchResults(faceDecisionPolicy, results, projectConfiguration) + val faceResults = getFaceMatchResults(results, projectConfiguration) val bestFaceConfidence = faceResults.firstOrNull()?.confidenceScore ?: 0 - val fingerprintDecisionPolicy = - projectConfiguration.fingerprint?.bioSdkConfiguration?.decisionPolicy - val fingerprintResults = - getFingerprintResults(fingerprintDecisionPolicy, results, projectConfiguration) + val fingerprintResults = getFingerprintResults(results, projectConfiguration) val bestFingerprintConfidence = fingerprintResults.firstOrNull()?.confidenceScore ?: 0 return AppIdentifyResponse( @@ -43,36 +39,39 @@ internal class CreateIdentifyResponseUseCase @Inject constructor( } private fun getFingerprintResults( - fingerprintDecisionPolicy: DecisionPolicy?, results: List, projectConfiguration: ProjectConfiguration, - ) = if (fingerprintDecisionPolicy != null) { - val matches = results.filterIsInstance(FingerprintMatchResult::class.java).lastOrNull()?.results.orEmpty() - val goodResults = matches - .filter { it.confidence >= fingerprintDecisionPolicy.low } - .sortedByDescending { it.confidence } - // Attempt to include only high confidence matches - goodResults - .filter { it.confidence >= fingerprintDecisionPolicy.high } - .ifEmpty { goodResults } - .take(projectConfiguration.identification.maxNbOfReturnedCandidates) - .map { AppMatchResult(it.subjectId, it.confidence, fingerprintDecisionPolicy) } - } else emptyList() + ) = results.filterIsInstance().lastOrNull()?.let { fingerprintMatchResult -> + projectConfiguration.fingerprint?.getSdkConfiguration(fingerprintMatchResult.sdk) + ?.decisionPolicy?.let { fingerprintDecisionPolicy -> + val matches = fingerprintMatchResult.results + val goodResults = matches + .filter { it.confidence >= fingerprintDecisionPolicy.low } + .sortedByDescending { it.confidence } + // Attempt to include only high confidence matches + goodResults + .filter { it.confidence >= fingerprintDecisionPolicy.high } + .ifEmpty { goodResults } + .take(projectConfiguration.identification.maxNbOfReturnedCandidates) + .map { AppMatchResult(it.subjectId, it.confidence, fingerprintDecisionPolicy) } + } + } ?: emptyList() private fun getFaceMatchResults( - faceDecisionPolicy: DecisionPolicy?, results: List, projectConfiguration: ProjectConfiguration, - ) = if (faceDecisionPolicy != null) { - val matches = results.filterIsInstance(FaceMatchResult::class.java).lastOrNull()?.results.orEmpty() - 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) } - } else emptyList() + ) = 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) } + } + } ?: 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 5aa0602d24..400d63c408 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 @@ -1,7 +1,6 @@ package com.simprints.feature.orchestrator.usecases.response import com.simprints.core.domain.response.AppErrorReason -import com.simprints.infra.config.store.models.DecisionPolicy import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppMatchResult @@ -18,34 +17,36 @@ internal class CreateVerifyResponseUseCase @Inject constructor() { projectConfiguration: ProjectConfiguration, results: List, ): AppResponse = listOfNotNull( - getFingerprintMatchResults( - projectConfiguration.fingerprint?.bioSdkConfiguration?.decisionPolicy, - results - ), - getFaceMatchResults(projectConfiguration.face?.decisionPolicy, results), + getFingerprintMatchResults(projectConfiguration, results), + getFaceMatchResults(projectConfiguration, results), ).maxByOrNull { it.confidenceScore } ?.let { AppVerifyResponse(it) } ?: AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) private fun getFingerprintMatchResults( - faceDecisionPolicy: DecisionPolicy?, + projectConfiguration: ProjectConfiguration, results: List, - ) = if (faceDecisionPolicy != null) { - results.filterIsInstance(FingerprintMatchResult::class.java) - .lastOrNull() - ?.results - ?.maxByOrNull { it.confidence } - ?.let { AppMatchResult(it.subjectId, it.confidence, faceDecisionPolicy) } - } else null + ) = results.filterIsInstance() + .lastOrNull()?.let { fingerprintMatchResult -> + projectConfiguration.fingerprint + ?.getSdkConfiguration(fingerprintMatchResult.sdk) + ?.decisionPolicy + ?.let { decisionPolicy -> + fingerprintMatchResult.results + .maxByOrNull { it.confidence } + ?.let { AppMatchResult(it.subjectId, it.confidence, decisionPolicy) } + } + } private fun getFaceMatchResults( - faceDecisionPolicy: DecisionPolicy?, + projectConfiguration: ProjectConfiguration, results: List, - ) = if (faceDecisionPolicy != null) { - results.filterIsInstance(FaceMatchResult::class.java) - .lastOrNull() - ?.results - ?.maxByOrNull { it.confidence } - ?.let { AppMatchResult(it.subjectId, it.confidence, faceDecisionPolicy) } - } else null + ) = results.filterIsInstance() + .lastOrNull()?.let { faceMatchResult -> + projectConfiguration.face?.decisionPolicy?.let { decisionPolicy -> + faceMatchResult.results + .maxByOrNull { it.confidence } + ?.let { AppMatchResult(it.subjectId, it.confidence, decisionPolicy) } + } + } } 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 c6cb1fe7dc..f5959e8e80 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 @@ -34,12 +34,14 @@ internal class IsNewEnrolmentUseCase @Inject constructor() { private fun isValidEnrolmentFingerprintResult( projectConfiguration: ProjectConfiguration, fingerprintResult: FingerprintMatchResult? - ): Boolean = projectConfiguration.fingerprint - ?.bioSdkConfiguration - ?.decisionPolicy - ?.medium?.toFloat() - ?.let { threshold -> fingerprintResult?.results?.all { it.confidence < threshold } } - ?: true + ): Boolean = fingerprintResult?.let { + projectConfiguration.fingerprint + ?.getSdkConfiguration(fingerprintResult.sdk) + ?.decisionPolicy + ?.medium?.toFloat() + ?.let { threshold -> fingerprintResult.results.all { it.confidence < threshold } } + } ?: true + // Missing results and configuration are ignored as "valid" to allow creating new records. private fun isNewEnrolmentFaceResult( 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 e1bfc57779..d1df4c34c6 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 @@ -1,7 +1,6 @@ package com.simprints.feature.orchestrator.usecases.steps import androidx.core.os.bundleOf -import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.common.FlowType import com.simprints.face.capture.FaceCaptureContract import com.simprints.feature.consent.ConsentContract @@ -10,46 +9,124 @@ import com.simprints.feature.enrollast.EnrolLastBiometricContract import com.simprints.feature.fetchsubject.FetchSubjectContract import com.simprints.feature.orchestrator.R import com.simprints.feature.orchestrator.cache.OrchestratorCache +import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.steps.MatchStepStubPayload import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.feature.orchestrator.usecases.MapStepsForLastBiometricEnrolUseCase +import com.simprints.feature.orchestrator.usecases.UpdateSessionModalitiesUseCase import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupContract import com.simprints.feature.selectsubject.SelectSubjectContract import com.simprints.feature.setup.SetupContract import com.simprints.feature.validatepool.ValidateSubjectPoolContract import com.simprints.fingerprint.capture.FingerprintCaptureContract +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration.Modality import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.allowedAgeRanges import com.simprints.infra.config.store.models.fromDomainToModuleApi +import com.simprints.infra.config.store.models.isAgeRestricted import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.matcher.MatchContract import javax.inject.Inject - -@ExcludedFromGeneratedTestCoverageReports("Mapping code for steps") internal class BuildStepsUseCase @Inject constructor( private val buildMatcherSubjectQuery: BuildMatcherSubjectQueryUseCase, private val cache: OrchestratorCache, private val mapStepsForLastBiometrics: MapStepsForLastBiometricEnrolUseCase, + private val updateSessionModalitiesUseCase: UpdateSessionModalitiesUseCase, ) { fun build(action: ActionRequest, projectConfiguration: ProjectConfiguration) = when (action) { is ActionRequest.EnrolActionRequest -> listOf( buildSetupStep(), + buildAgeSelectionStepIfNeeded(action, projectConfiguration), buildConsentStep(ConsentType.ENROL), - buildAgeSelectionStep(action, projectConfiguration), + buildModalityCaptureAndMatchStepsForEnrol(action, projectConfiguration) + ) + + is ActionRequest.IdentifyActionRequest -> { + val subjectQuery = buildMatcherSubjectQuery(projectConfiguration, action) + + listOf( + buildSetupStep(), + buildValidateIdPoolStep(subjectQuery), + buildAgeSelectionStepIfNeeded(action, projectConfiguration), + buildConsentStep(ConsentType.IDENTIFY), + buildModalityCaptureAndMatchStepsForIdentify( + action, + projectConfiguration, + subjectQuery = subjectQuery, + ) + ) + } + + is ActionRequest.VerifyActionRequest -> listOf( + buildSetupStep(), + buildAgeSelectionStepIfNeeded(action, projectConfiguration), + buildFetchGuidStep(action.projectId, action.verifyGuid), + buildConsentStep(ConsentType.VERIFY), + buildModalityCaptureAndMatchStepsForVerify(action, projectConfiguration) + ) + + is ActionRequest.EnrolLastBiometricActionRequest -> listOf( + buildEnrolLastBiometricStep(action), + ) + + is ActionRequest.ConfirmIdentityActionRequest -> listOf( + buildConfirmIdentityStep(action), + ) + }.flatten() + + fun buildCaptureAndMatchStepsForAgeGroup( + action: ActionRequest, + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup, + ): List = when (action) { + is ActionRequest.EnrolActionRequest -> buildModalityCaptureAndMatchStepsForEnrol( + action, + projectConfiguration, + ageGroup, + ) + + is ActionRequest.IdentifyActionRequest -> buildModalityCaptureAndMatchStepsForIdentify( + action, + projectConfiguration, + ageGroup, + subjectQuery = buildMatcherSubjectQuery(projectConfiguration, action), + ) + + is ActionRequest.VerifyActionRequest -> buildModalityCaptureAndMatchStepsForVerify( + action, + projectConfiguration, + ageGroup, + ) + + else -> emptyList() + } + + private fun buildModalityCaptureAndMatchStepsForEnrol( + action: ActionRequest.EnrolActionRequest, + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup? = null, + ): List { + val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) + + return listOf( buildModalityCaptureSteps( projectConfiguration, FlowType.ENROL, + resolvedAgeGroup, ), if (projectConfiguration.general.duplicateBiometricEnrolmentCheck) { buildModalityMatcherSteps( projectConfiguration, FlowType.ENROL, + resolvedAgeGroup, buildMatcherSubjectQuery(projectConfiguration, action), BiometricDataSource.fromString( action.biometricDataSource, @@ -57,78 +134,83 @@ internal class BuildStepsUseCase @Inject constructor( ), ) } else emptyList(), - ) + ).flatten() + } - is ActionRequest.IdentifyActionRequest -> { - val subjectQuery = buildMatcherSubjectQuery(projectConfiguration, action) + private fun buildModalityCaptureAndMatchStepsForIdentify( + action: ActionRequest.IdentifyActionRequest, + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup? = null, + subjectQuery: SubjectQuery, + ): List { + val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) - listOf( - buildSetupStep(), - buildValidateIdPoolStep(subjectQuery), - buildAgeSelectionStep(action, projectConfiguration), - buildConsentStep(ConsentType.IDENTIFY), - buildModalityCaptureSteps( - projectConfiguration, - FlowType.IDENTIFY, + return listOf( + buildModalityCaptureSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + ), + buildModalityMatcherSteps( + projectConfiguration, + FlowType.IDENTIFY, + resolvedAgeGroup, + subjectQuery, + BiometricDataSource.fromString( + action.biometricDataSource, + action.callerPackageName ), - buildModalityMatcherSteps( - projectConfiguration, - FlowType.IDENTIFY, - subjectQuery, - BiometricDataSource.fromString( - action.biometricDataSource, - action.callerPackageName - ), - ) ) - } + ).flatten() + } - is ActionRequest.VerifyActionRequest -> listOf( - buildSetupStep(), - buildAgeSelectionStep(action, projectConfiguration), - buildFetchGuidStep(action.projectId, action.verifyGuid), - buildConsentStep(ConsentType.VERIFY), + private fun buildModalityCaptureAndMatchStepsForVerify( + action: ActionRequest.VerifyActionRequest, + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup? = null, + ): List { + val resolvedAgeGroup = ageGroup ?: ageGroupFromSubjectAge(action, projectConfiguration) + + return listOf( buildModalityCaptureSteps( projectConfiguration, FlowType.VERIFY, + resolvedAgeGroup, ), buildModalityMatcherSteps( projectConfiguration, FlowType.VERIFY, + resolvedAgeGroup, SubjectQuery(subjectId = action.verifyGuid), BiometricDataSource.fromString( action.biometricDataSource, action.callerPackageName ), ) - ) - - is ActionRequest.EnrolLastBiometricActionRequest -> listOf( - buildEnrolLastBiometricStep(action), - ) - - is ActionRequest.ConfirmIdentityActionRequest -> listOf( - buildConfirmIdentityStep(action), - ) - }.flatten() + ).flatten() + } - private fun buildAgeSelectionStep( + private fun buildAgeSelectionStepIfNeeded( action: ActionRequest, projectConfiguration: ProjectConfiguration ): List { - if (projectConfiguration.allowedAgeRanges().isEmpty()) { - return emptyList() + if (projectConfiguration.isAgeRestricted()) { + val subjectAge = action.getSubjectAgeIfAvailable() + if (subjectAge == null) { + return listOf( + Step( + id = StepId.SELECT_SUBJECT_AGE, + navigationActionId = R.id.action_orchestratorFragment_to_age_group_selection, + destinationId = SelectSubjectAgeGroupContract.DESTINATION, + payload = bundleOf() + ) + ) + } else if (projectConfiguration.allowedAgeRanges().none { it.includes(subjectAge) }) { + throw SubjectAgeNotSupportedException() + } } - // Todo check if the action request contains the age parameter - return listOf( - Step( - id = StepId.SELECT_SUBJECT_AGE, - navigationActionId = R.id.action_orchestratorFragment_to_age_group_selection, - destinationId = SelectSubjectAgeGroupContract.DESTINATION, - payload = bundleOf() - ) - ) + return emptyList() } private fun buildSetupStep() = listOf( @@ -170,48 +252,84 @@ internal class BuildStepsUseCase @Inject constructor( private fun buildModalityCaptureSteps( projectConfiguration: ProjectConfiguration, flowType: FlowType, - ) = projectConfiguration.general.modalities.map { - when (it) { + ageGroup: AgeGroup?, + ): List = projectConfiguration.general.modalities.flatMap { modality -> + when (modality) { Modality.FINGERPRINT -> { - val fingersToCollect = - projectConfiguration.fingerprint?.bioSdkConfiguration?.fingersToCapture.orEmpty() + determineFingerprintSDKs(projectConfiguration, ageGroup).map { bioSDK -> + + val sdkConfiguration = projectConfiguration.fingerprint?.getSdkConfiguration(bioSDK) + + //TODO: fingersToCollect can be read directly from FingerprintCapture + val fingersToCollect = sdkConfiguration?.fingersToCapture.orEmpty() .map { finger -> finger.fromDomainToModuleApi() } - Step( - id = StepId.FINGERPRINT_CAPTURE, - navigationActionId = R.id.action_orchestratorFragment_to_fingerprintCapture, - destinationId = FingerprintCaptureContract.DESTINATION, - payload = FingerprintCaptureContract.getArgs(flowType, fingersToCollect), - ) + Step( + id = StepId.FINGERPRINT_CAPTURE, + navigationActionId = R.id.action_orchestratorFragment_to_fingerprintCapture, + destinationId = FingerprintCaptureContract.DESTINATION, + payload = FingerprintCaptureContract.getArgs(flowType, fingersToCollect, bioSDK) + ) + } } Modality.FACE -> { - val samplesToCapture = projectConfiguration.face?.nbOfImagesToCapture ?: 0 - Step( - id = StepId.FACE_CAPTURE, - navigationActionId = R.id.action_orchestratorFragment_to_faceCapture, - destinationId = FaceCaptureContract.DESTINATION, - payload = FaceCaptureContract.getArgs(samplesToCapture), - ) + determineFaceSDKs(projectConfiguration, ageGroup).map { + // Face bio SDK is currently ignored until we add a second one + //TODO: samplesToCapture can be read directly from FaceCapture + val samplesToCapture = projectConfiguration.face?.nbOfImagesToCapture ?: 0 + Step( + id = StepId.FACE_CAPTURE, + navigationActionId = R.id.action_orchestratorFragment_to_faceCapture, + destinationId = FaceCaptureContract.DESTINATION, + payload = FaceCaptureContract.getArgs(samplesToCapture), + ) + } + } + } + }.also { steps -> + // Update session with modalities that will actually be used (if user doesn't exit) + steps.mapNotNull { step -> + when (step.id) { + StepId.FINGERPRINT_CAPTURE -> Modality.FINGERPRINT + StepId.FACE_CAPTURE -> Modality.FACE + else -> null } + }.distinct().takeIf { it.isNotEmpty() }?.let { modalities -> + updateSessionModalitiesUseCase(modalities) } } private fun buildModalityMatcherSteps( projectConfiguration: ProjectConfiguration, flowType: FlowType, + ageGroup: AgeGroup?, subjectQuery: SubjectQuery, biometricDataSource: BiometricDataSource, - ) = projectConfiguration.general.modalities.map { - Step( - id = when (it) { - Modality.FINGERPRINT -> StepId.FINGERPRINT_MATCHER - Modality.FACE -> StepId.FACE_MATCHER - }, - navigationActionId = R.id.action_orchestratorFragment_to_matcher, - destinationId = MatchContract.DESTINATION, - payload = MatchStepStubPayload.asBundle(flowType, subjectQuery, biometricDataSource), - ) + ): List = projectConfiguration.general.modalities.flatMap { modality -> + when (modality) { + Modality.FINGERPRINT -> { + determineFingerprintSDKs(projectConfiguration, ageGroup).map { bioSDK -> + Step( + id = StepId.FINGERPRINT_MATCHER, + navigationActionId = R.id.action_orchestratorFragment_to_matcher, + destinationId = MatchContract.DESTINATION, + payload = MatchStepStubPayload.asBundle(flowType, subjectQuery, biometricDataSource, bioSDK), + ) + } + } + Modality.FACE -> { + determineFaceSDKs(projectConfiguration, ageGroup).map { + // 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), + ) + } + } + } } private fun buildEnrolLastBiometricStep(action: ActionRequest.EnrolLastBiometricActionRequest) = @@ -241,4 +359,51 @@ internal class BuildStepsUseCase @Inject constructor( ), ) ) + + private fun determineFingerprintSDKs( + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup?, + ): List { + val sdks = mutableListOf() + + if (!projectConfiguration.isAgeRestricted()) { + projectConfiguration.fingerprint?.allowedSDKs?.let { sdks.addAll(it) } + } else { + ageGroup?.let { + if (projectConfiguration.fingerprint?.secugenSimMatcher?.allowedAgeRange?.contains(ageGroup) == true) { + sdks.add(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) + } + if (projectConfiguration.fingerprint?.nec?.allowedAgeRange?.contains(ageGroup) == true) { + sdks.add(FingerprintConfiguration.BioSdk.NEC) + } + } + } + + return sdks + } + + private fun determineFaceSDKs( + projectConfiguration: ProjectConfiguration, + ageGroup: AgeGroup?, + ): List { + val sdks = mutableListOf() + + if (!projectConfiguration.isAgeRestricted()) { + projectConfiguration.face?.allowedSDKs?.let { sdks.addAll(it) } + } else { + ageGroup?.let { + if (projectConfiguration.face?.rankOne?.allowedAgeRange?.contains(ageGroup) == true) { + sdks.add(FaceConfiguration.BioSdk.RANK_ONE) + } + } + } + + return sdks + } + + private fun ageGroupFromSubjectAge(action: ActionRequest, projectConfiguration: ProjectConfiguration): AgeGroup? { + return action.getSubjectAgeIfAvailable()?.let { subjectAge -> + projectConfiguration.allowedAgeRanges().firstOrNull{ it.includes(subjectAge) } + } + } } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt index 6e9ae4c462..2c2dd68bf9 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt @@ -6,10 +6,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.jraska.livedata.test import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.domain.response.AppErrorReason import com.simprints.face.capture.FaceCaptureResult import com.simprints.feature.consent.ConsentResult import com.simprints.feature.orchestrator.cache.OrchestratorCache +import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException import com.simprints.feature.orchestrator.steps.MatchStepStubPayload import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId @@ -21,14 +23,20 @@ import com.simprints.feature.orchestrator.usecases.ShouldCreatePersonUseCase import com.simprints.feature.orchestrator.usecases.UpdateDailyActivityUseCase import com.simprints.feature.orchestrator.usecases.response.AppResponseBuilderUseCase import com.simprints.feature.orchestrator.usecases.steps.BuildStepsUseCase +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.LocationStore import com.simprints.feature.setup.SetupResult +import com.simprints.fingerprint.capture.FingerprintCaptureContract import com.simprints.fingerprint.capture.FingerprintCaptureResult +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.NEC +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.orchestration.data.responses.AppErrorResponse +import com.simprints.matcher.MatchParams import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -124,7 +132,7 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) - coEvery { mapRefusalOrErrorResult(any()) } returns null + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false val stepsObserver = viewModel.currentStep.test() @@ -139,7 +147,7 @@ internal class OrchestratorViewModelTest { @Test fun `Creates person if required after step result`() = runTest { every { stepsBuilder.build(any(), any()) } returns emptyList() - coEvery { mapRefusalOrErrorResult(any()) } returns null + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns true coJustRun { createPersonEvent(any()) } @@ -155,7 +163,7 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) - coEvery { mapRefusalOrErrorResult(any()) } returns null + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false coEvery { appResponseBuilder(any(), any(), any()) } returns mockk() coJustRun { dailyActivityUseCase(any()) } @@ -174,7 +182,7 @@ internal class OrchestratorViewModelTest { createMockStep(StepId.SETUP), createMockStep(StepId.CONSENT), ) - coEvery { mapRefusalOrErrorResult(any()) } returns AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) + coEvery { mapRefusalOrErrorResult(any(), any()) } returns AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) viewModel.handleAction(mockk()) viewModel.handleResult(SetupResult(true)) @@ -182,6 +190,44 @@ internal class OrchestratorViewModelTest { viewModel.appResponse.test().assertHasValue() } + @Test + fun `Returns AGE_GROUP_NOT_SUPPORTED response when step builder throws SubjectAgeNotSupportedException`() = runTest { + every { stepsBuilder.build(any(), any()) } throws SubjectAgeNotSupportedException() + + viewModel.handleAction(mockk()) + + val expectedResponse = AppErrorResponse(AppErrorReason.AGE_GROUP_NOT_SUPPORTED) + verify { addCallbackEvent(expectedResponse) } + viewModel.appResponse.test().value().peekContent().let { response -> + assertThat(response.response).isEqualTo(expectedResponse) + } + } + + @Test + fun `Appends capture and match steps upon receiving SelectSubjectAgeGroupResult`() = runTest { + every { stepsBuilder.build(any(), any()) } returns listOf( + createMockStep(StepId.SELECT_SUBJECT_AGE), + ) + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null + every { shouldCreatePerson(any(), any(), any()) } returns false + val captureAndMatchSteps = listOf( + createMockStep(StepId.FACE_CAPTURE), + createMockStep(StepId.FACE_MATCHER, MatchStepStubPayload.asBundle( + FlowType.VERIFY, + SubjectQuery(), + BiometricDataSource.Simprints)), + ) + every { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } returns captureAndMatchSteps + + viewModel.handleAction(mockk()) + viewModel.handleResult(SelectSubjectAgeGroupResult(AgeGroup(0, 1))) + + verify { stepsBuilder.buildCaptureAndMatchStepsForAgeGroup(any(), any(), any()) } + viewModel.currentStep.test().value().peekContent()?.let { step -> + assertThat(step.id).isEqualTo(StepId.FACE_CAPTURE) + } + } + @Test fun `Updates face matcher step payload when receiving face capture`() = runTest { every { stepsBuilder.build(any(), any()) } returns listOf( @@ -191,7 +237,7 @@ internal class OrchestratorViewModelTest { SubjectQuery(), BiometricDataSource.Simprints)), ) - coEvery { mapRefusalOrErrorResult(any()) } returns null + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false viewModel.handleAction(mockk()) @@ -211,7 +257,7 @@ internal class OrchestratorViewModelTest { SubjectQuery(), BiometricDataSource.Simprints)), ) - coEvery { mapRefusalOrErrorResult(any()) } returns null + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false viewModel.handleAction(mockk()) @@ -222,6 +268,68 @@ internal class OrchestratorViewModelTest { } } + @Test + fun `Updates the correct fingerprint match step when multiple fingerprint SDKs are used`() = runTest { + every { stepsBuilder.build(any(), any()) } returns listOf( + createMockStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureContract.getArgs( + flowType = FlowType.VERIFY, + fingers = emptyList(), + fingerprintSDK = SECUGEN_SIM_MATCHER, + )), + createMockStep(StepId.FINGERPRINT_MATCHER, MatchStepStubPayload.asBundle( + flowType = FlowType.VERIFY, + subjectQuery = SubjectQuery(), + biometricDataSource = BiometricDataSource.Simprints, + fingerprintSDK = SECUGEN_SIM_MATCHER, + )), + createMockStep(StepId.FINGERPRINT_CAPTURE, FingerprintCaptureContract.getArgs( + flowType = FlowType.VERIFY, + fingers = emptyList(), + fingerprintSDK = NEC, + )), + createMockStep(StepId.FINGERPRINT_MATCHER, MatchStepStubPayload.asBundle( + flowType = FlowType.VERIFY, + subjectQuery = SubjectQuery(), + biometricDataSource = BiometricDataSource.Simprints, + fingerprintSDK = NEC, + )), + ) + coEvery { mapRefusalOrErrorResult(any(), any()) } returns null + every { shouldCreatePerson(any(), any(), any()) } returns false + val format = "SimMatcher" + val sample1 = FingerprintCaptureResult.Sample( + IFingerIdentifier.LEFT_INDEX_FINGER, + ByteArray(0), + 0, + null, + format, + ) + val sample2 = FingerprintCaptureResult.Sample( + IFingerIdentifier.LEFT_THUMB, + ByteArray(0), + 0, + null, + format, + ) + val captureResults: List = listOf( + FingerprintCaptureResult.Item(null, IFingerIdentifier.LEFT_INDEX_FINGER, sample1), + FingerprintCaptureResult.Item(null, IFingerIdentifier.LEFT_THUMB, sample2), + ) + + viewModel.handleAction(mockk()) + viewModel.handleResult(FingerprintCaptureResult(captureResults)) + + viewModel.currentStep.test().value().peekContent()?.let { step -> + assertThat(step.id).isEqualTo(StepId.FINGERPRINT_MATCHER) + val params = step.payload.getParcelable("params") + assertThat(params).isNotNull() + assertThat(params?.fingerprintSDK).isEqualTo(SECUGEN_SIM_MATCHER) + assertThat(params?.probeFingerprintSamples?.size).isEqualTo(2) + assertThat(params?.probeFingerprintSamples?.get(0)?.format).isEqualTo(format) + assertThat(params?.probeFingerprintSamples?.get(1)?.format).isEqualTo(format) + } + } + @Test fun `Restores steps if empty`() = runTest { every { stepsBuilder.build(any(), any()) } returns emptyList() diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt index 13558b13ea..5b8dc0f45d 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt @@ -5,15 +5,19 @@ import com.simprints.face.capture.FaceCaptureResult import com.simprints.feature.alert.AlertResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.fetchsubject.FetchSubjectResult +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolResult import com.simprints.fingerprint.connect.FingerprintConnectResult +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse import com.simprints.infra.orchestration.data.responses.AppRefusalResponse import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -45,14 +49,14 @@ class MapRefusalOrErrorResultUseCaseTest { FingerprintConnectResult(isSuccess = false) to AppErrorResponse::class.java, AlertResult(buttonKey = "buttonKey") to AppErrorResponse::class.java, ).forEach { (result, responseClass) -> - assertThat(useCase(result)).isInstanceOf(responseClass) + assertThat(useCase(result, mockk())).isInstanceOf(responseClass) } } @Test fun `Maps id pool validation results`() = runTest { - assertThat(useCase(ValidateSubjectPoolResult(isValid = true))).isNull() - assertThat(useCase(ValidateSubjectPoolResult(isValid = false))).isInstanceOf(AppIdentifyResponse::class.java) + assertThat(useCase(ValidateSubjectPoolResult(isValid = true), mockk())).isNull() + assertThat(useCase(ValidateSubjectPoolResult(isValid = false), mockk())).isInstanceOf(AppIdentifyResponse::class.java) } @Test @@ -61,11 +65,24 @@ class MapRefusalOrErrorResultUseCaseTest { FetchSubjectResult(found = true), SetupResult(isSuccess = true), FaceCaptureResult(emptyList()) - ).forEach { result -> assertThat(useCase(result)).isNull() } + ).forEach { result -> assertThat(useCase(result, mockk())).isNull() } } @Test fun `Maps non-result serializable to null`() = runTest { - assertThat(useCase(mockk())).isNull() + assertThat(useCase(mockk(), mockk())).isNull() + } + + @Test + fun `Maps SelectSubjectAgeGroupResult to appropriate response`() = runTest { + val projectConfiguration = mockk(relaxed = true) + + val ageGroupSupported = AgeGroup(0, 10) + val ageGroupNotSupported = AgeGroup(11, 20) + + every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroupSupported + + assertThat(useCase(SelectSubjectAgeGroupResult(ageGroupSupported), projectConfiguration)).isNull() + assertThat(useCase(SelectSubjectAgeGroupResult(ageGroupNotSupported), projectConfiguration)).isInstanceOf(AppErrorResponse::class.java) } } 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 580b7d8f49..4341731921 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 @@ -9,6 +9,7 @@ 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.Finger +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.FingerprintMatchResult @@ -74,10 +75,10 @@ internal class MapStepsForLastBiometricEnrolUseCaseTest { @Test fun `maps FingerprintMatchResult correctly`() { val result = useCase(listOf( - FingerprintMatchResult(emptyList()) + FingerprintMatchResult(emptyList(), FingerprintConfiguration.BioSdk.NEC) )) - assertThat(result.first()).isEqualTo(EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList())) + assertThat(result.first()).isEqualTo(EnrolLastBiometricStepResult.FingerprintMatchResult(emptyList(), FingerprintConfiguration.BioSdk.NEC)) } @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 fa3a58b3c5..1750708592 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.FingerprintConfiguration import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.orchestration.data.responses.AppIdentifyResponse import com.simprints.matcher.FaceMatchResult @@ -37,7 +38,7 @@ class CreateIdentifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)) ) @@ -51,7 +52,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)) ) @@ -66,7 +67,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(20f, 25f, 30f, 40f)) ) @@ -81,7 +82,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(15f, 30f, 100f)) ) @@ -96,7 +97,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 20, 50, 100 @@ -115,7 +116,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 20, 50, 100 @@ -134,7 +135,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 20, 50, 100 @@ -153,7 +154,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 20, 50, 100 @@ -176,7 +177,7 @@ class CreateIdentifyResponseUseCaseTest { mockk { every { identification.maxNbOfReturnedCandidates } returns 2 every { face?.decisionPolicy } returns DecisionPolicy(20, 50, 100) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 20, 50, 100 @@ -197,6 +198,7 @@ class CreateIdentifyResponseUseCaseTest { ) private fun createFingerprintMatchResult(vararg confidences: Float): Serializable = FingerprintMatchResult( - confidences.map { FingerprintMatchResult.Item(subjectId = "1", confidence = it) } + confidences.map { FingerprintMatchResult.Item(subjectId = "1", confidence = it) }, + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER ) } 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 e5ebe16065..9218259363 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 @@ -26,7 +26,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 20f, 30f)) ) @@ -39,7 +39,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns null + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns null }, results = listOf(createFaceMatchResult(10f, 50f, 100f)) ) @@ -52,7 +52,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns null - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 10, 20, 30 @@ -69,7 +69,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 10, 20, 30 @@ -90,7 +90,7 @@ class CreateVerifyResponseUseCaseTest { val result = useCase( mockk { every { face?.decisionPolicy } returns DecisionPolicy(10, 20, 30) - every { fingerprint?.bioSdkConfiguration?.decisionPolicy } returns DecisionPolicy( + every { fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns DecisionPolicy( 10, 20, 30 @@ -106,7 +106,8 @@ class CreateVerifyResponseUseCaseTest { } private fun createFingerprintMatchResult(vararg confidences: Float): Serializable = FingerprintMatchResult( - confidences.map { FingerprintMatchResult.Item(subjectId = "1", confidence = it) } + confidences.map { FingerprintMatchResult.Item(subjectId = "1", confidence = it) }, + mockk(), ) private fun createFaceMatchResult(vararg confidences: Float): Serializable = FaceMatchResult( 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 15b484892c..bc00feec1d 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 @@ -8,6 +8,7 @@ import com.simprints.matcher.FingerprintMatchResult import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import org.junit.Before import org.junit.Test @@ -24,7 +25,7 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.face?.decisionPolicy } returns faceConfidenceDecisionPolicy - every { projectConfiguration.fingerprint?.bioSdkConfiguration?.decisionPolicy } returns fingerprintConfidenceDecisionPolicy + every { projectConfiguration.fingerprint?.getSdkConfiguration((any()))?.decisionPolicy } returns fingerprintConfidenceDecisionPolicy useCase = IsNewEnrolmentUseCase() } @@ -48,7 +49,10 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true assertThat(useCase(projectConfiguration, listOf( - FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore))), + FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore)), + mockk(), + ), ))).isTrue() } @@ -57,7 +61,10 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true assertThat(useCase(projectConfiguration, listOf( - FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", higherThanMediumConfidenceScore))), + FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("", higherThanMediumConfidenceScore)), + mockk(), + ), ))).isFalse() } @@ -84,7 +91,10 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true assertThat(useCase(projectConfiguration, listOf( - FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore))), + FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore)), + mockk(), + ), FaceMatchResult(listOf(FaceMatchResult.Item("", lowerThanMediumConfidenceScore))), ))).isTrue() } @@ -94,7 +104,10 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true assertThat(useCase(projectConfiguration, listOf( - FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore))), + FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("", lowerThanMediumConfidenceScore)), + mockk(), + ), FaceMatchResult(listOf(FaceMatchResult.Item("", higherThanMediumConfidenceScore))), ))).isFalse() } @@ -104,7 +117,10 @@ internal class IsNewEnrolmentUseCaseTest { every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true assertThat(useCase(projectConfiguration, listOf( - FingerprintMatchResult(listOf(FingerprintMatchResult.Item("", higherThanMediumConfidenceScore))), + FingerprintMatchResult( + listOf(FingerprintMatchResult.Item("", higherThanMediumConfidenceScore)), + mockk(), + ), FaceMatchResult(listOf(FaceMatchResult.Item("", lowerThanMediumConfidenceScore))), ))).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 new file mode 100644 index 0000000000..81181828a1 --- /dev/null +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -0,0 +1,526 @@ +package com.simprints.feature.orchestrator.usecases.steps + +import com.simprints.feature.orchestrator.cache.OrchestratorCache +import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException +import com.simprints.feature.orchestrator.steps.Step +import com.simprints.feature.orchestrator.steps.StepId +import com.simprints.feature.orchestrator.usecases.MapStepsForLastBiometricEnrolUseCase +import com.simprints.feature.orchestrator.usecases.UpdateSessionModalitiesUseCase +import com.simprints.infra.config.store.models.AgeGroup +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.config.store.models.FingerprintConfiguration.BioSdk.NEC +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER +import com.simprints.infra.config.store.models.GeneralConfiguration.Modality +import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.orchestration.data.ActionRequest +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test + +class BuildStepsUseCaseTest { + + @RelaxedMockK + private lateinit var buildMatcherSubjectQuery: BuildMatcherSubjectQueryUseCase + + @RelaxedMockK + private lateinit var cache: OrchestratorCache + + @RelaxedMockK + private lateinit var mapStepsForLastBiometrics: MapStepsForLastBiometricEnrolUseCase + + @RelaxedMockK + private lateinit var secugenSimMatcher: FingerprintConfiguration.FingerprintSdkConfiguration + + @RelaxedMockK + private lateinit var nec: FingerprintConfiguration.FingerprintSdkConfiguration + + @RelaxedMockK + private lateinit var updateSessionModalitiesUseCase: UpdateSessionModalitiesUseCase + + private lateinit var useCase: BuildStepsUseCase + + @Before + fun setup() { + MockKAnnotations.init(this) + useCase = BuildStepsUseCase(buildMatcherSubjectQuery, cache, mapStepsForLastBiometrics, updateSessionModalitiesUseCase) + } + + private fun mockCommonProjectConfiguration(): ProjectConfiguration { + val projectConfiguration = mockk(relaxed = true) + every { projectConfiguration.general.modalities } returns listOf(Modality.FINGERPRINT, Modality.FACE) + every { projectConfiguration.fingerprint?.allowedSDKs } returns listOf( + SECUGEN_SIM_MATCHER, + NEC + ) + + every { secugenSimMatcher.fingersToCapture } returns listOf( + Finger.LEFT_THUMB, + Finger.RIGHT_THUMB, + ) + every { secugenSimMatcher.allowedAgeRange } returns null + every { projectConfiguration.fingerprint?.secugenSimMatcher } returns secugenSimMatcher + every { projectConfiguration.fingerprint?.getSdkConfiguration(SECUGEN_SIM_MATCHER) } returns secugenSimMatcher + + every { nec.fingersToCapture } returns listOf( + Finger.LEFT_INDEX_FINGER, + Finger.RIGHT_INDEX_FINGER, + ) + every { nec.allowedAgeRange } returns null + every { projectConfiguration.fingerprint?.nec } returns nec + 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?.allowedAgeRange } returns null + + return projectConfiguration + } + + private fun assertStepOrder(steps: List, vararg expectedStepIds: Int) { + assertEquals("Number of steps does not match the expected number.", expectedStepIds.size, steps.size) + steps.zip(expectedStepIds.toList()).forEachIndexed { index, pair -> + val (step, expectedStepId) = pair + assertEquals("Step at index $index does not match the expected step ID.", expectedStepId, step.id) + } + } + + @Test + fun `build - enrol action - no age restriction - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns false + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + ) + verify { updateSessionModalitiesUseCase(listOf(Modality.FINGERPRINT, Modality.FACE)) } + } + + @Test + fun `build - enrol action - no age restriction - duplicate check - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - identify action - no age restriction - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.VALIDATE_ID_POOL, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - verify action - no age restriction - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.FETCH_GUID, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - confirm identity action - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.CONFIRM_IDENTITY + ) + } + + @Test + fun `build - enrol last biometric action - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.ENROL_LAST_BIOMETRIC + ) + } + + @Test + fun `build - enrol action - age restriction - missing subject age - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.SELECT_SUBJECT_AGE, + StepId.CONSENT + ) + } + + @Test + fun `build - identify action - age restriction - missing subject age - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.VALIDATE_ID_POOL, + StepId.SELECT_SUBJECT_AGE, + StepId.CONSENT + ) + } + + @Test + fun `build - verify action - age restriction - missing subject age - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns null + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.SELECT_SUBJECT_AGE, + StepId.FETCH_GUID, + StepId.CONSENT + ) + } + + @Test + fun `build - enrol action - age restriction - subject age within range - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + ) + } + + @Test + fun `build - enrol action - age restriction - subject age within range - duplicate check - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true + val ageGroup = AgeGroup(18, 60) + every { secugenSimMatcher.allowedAgeRange } returns ageGroup + every { nec.allowedAgeRange } returns ageGroup + every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroup + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - identify action - age restriction - subject age within range - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.VALIDATE_ID_POOL, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - verify action - age restriction - subject age within range - 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 + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 25 // Subject age within the supported range + + val steps = useCase.build(action, projectConfiguration) + + assertStepOrder(steps, + StepId.SETUP, + StepId.FETCH_GUID, + StepId.CONSENT, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build - enrol action - age restriction - subject age not supported - throws SubjectAgeNotSupportedException`() { + val projectConfiguration = mockCommonProjectConfiguration() + val ageGroup = AgeGroup(30, 40) + every { secugenSimMatcher.allowedAgeRange } returns ageGroup + every { nec.allowedAgeRange } returns ageGroup + every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroup + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK + + assertThrows(SubjectAgeNotSupportedException::class.java) { + useCase.build(action, projectConfiguration) + } + } + + @Test + fun `build - identify action - age restriction - subject age not supported - throws SubjectAgeNotSupportedException`() { + val projectConfiguration = mockCommonProjectConfiguration() + val ageGroup = AgeGroup(30, 40) + every { secugenSimMatcher.allowedAgeRange } returns ageGroup + every { nec.allowedAgeRange } returns ageGroup + every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroup + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK + + assertThrows(SubjectAgeNotSupportedException::class.java) { + useCase.build(action, projectConfiguration) + } + } + + @Test + fun `build - verify action - age restriction - subject age not supported - throws SubjectAgeNotSupportedException`() { + val projectConfiguration = mockCommonProjectConfiguration() + val ageGroup = AgeGroup(30, 40) + every { secugenSimMatcher.allowedAgeRange } returns ageGroup + every { nec.allowedAgeRange } returns ageGroup + every { projectConfiguration.face?.rankOne?.allowedAgeRange } returns ageGroup + + val action = mockk(relaxed = true) + every { action.getSubjectAgeIfAvailable() } returns 20 // Subject age not supported by any SDK + + assertThrows(SubjectAgeNotSupportedException::class.java) { + useCase.build(action, projectConfiguration) + } + } + + @Test + fun `build capture and match steps - enrol 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 + + val action = mockk(relaxed = true) + + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + + assertStepOrder(steps, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + ) + } + + @Test + fun `build capture and match steps - enrol action - duplicate check - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + every { projectConfiguration.general.duplicateBiometricEnrolmentCheck } returns true + val ageGroup = AgeGroup(18, 60) + every { secugenSimMatcher.allowedAgeRange } returns ageGroup + every { nec.allowedAgeRange } returns ageGroup + every { projectConfiguration.face?.rankOne?.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.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build capture and match steps - identify 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 + + val action = mockk(relaxed = true) + + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + + assertStepOrder(steps, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build capture and match steps - 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 + + val action = mockk(relaxed = true) + + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, ageGroup) + + assertStepOrder(steps, + StepId.FINGERPRINT_CAPTURE, + StepId.FINGERPRINT_CAPTURE, + StepId.FACE_CAPTURE, + StepId.FINGERPRINT_MATCHER, + StepId.FINGERPRINT_MATCHER, + StepId.FACE_MATCHER + ) + } + + @Test + fun `build capture and match steps - confirm identity action - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + + assertEquals(0, steps.size) + } + + @Test + fun `build capture and match steps - enrol last action - returns expected steps`() { + val projectConfiguration = mockCommonProjectConfiguration() + + val action = mockk(relaxed = true) + + val steps = useCase.buildCaptureAndMatchStepsForAgeGroup(action, projectConfiguration, AgeGroup(18, 60)) + + assertEquals(0, steps.size) + } +} \ No newline at end of file diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureContract.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureContract.kt index 43f8a331bd..7965860677 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureContract.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureContract.kt @@ -3,6 +3,7 @@ package com.simprints.fingerprint.capture import com.simprints.core.domain.common.FlowType import com.simprints.fingerprint.capture.screen.FingerprintCaptureFragmentArgs import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.infra.config.store.models.FingerprintConfiguration object FingerprintCaptureContract { @@ -11,6 +12,7 @@ object FingerprintCaptureContract { fun getArgs( flowType: FlowType, fingers: List, - ) = FingerprintCaptureFragmentArgs(FingerprintCaptureParams(flowType, fingers)).toBundle() + fingerprintSDK: FingerprintConfiguration.BioSdk, + ) = FingerprintCaptureFragmentArgs(FingerprintCaptureParams(flowType, fingers, fingerprintSDK)).toBundle() } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureParams.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureParams.kt index 498646eaeb..67c5037ba0 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureParams.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/FingerprintCaptureParams.kt @@ -4,11 +4,13 @@ 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.FingerprintConfiguration import kotlinx.parcelize.Parcelize @Keep @Parcelize data class FingerprintCaptureParams( val flowType: FlowType, - val fingerprintsToCapture: List + val fingerprintsToCapture: List, + val fingerprintSDK: FingerprintConfiguration.BioSdk, ) : Parcelable diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt index a4121a4a73..fdb42df238 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt @@ -6,7 +6,7 @@ import android.view.View import androidx.activity.addCallback import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -54,7 +54,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri private val args: FingerprintCaptureFragmentArgs by navArgs() private val binding by viewBinding(FragmentFingerprintCaptureBinding::bind) - private val vm: FingerprintCaptureViewModel by activityViewModels() + private val vm: FingerprintCaptureViewModel by viewModels() private lateinit var fingerViewPagerManager: FingerViewPagerManager private var confirmDialog: AlertDialog? = null @@ -99,7 +99,10 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri vm.launchReconnect.observe(viewLifecycleOwner, LiveDataEventObserver { launchConnection() }) - vm.handleOnViewCreated(args.params.fingerprintsToCapture) + vm.handleOnViewCreated( + args.params.fingerprintsToCapture, + args.params.fingerprintSDK, + ) initUI() } @@ -228,7 +231,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri findNavController().navigateSafely( this, R.id.action_fingerprintCaptureFragment_to_graphConnectScanner, - FingerprintConnectContract.getArgs(true) + FingerprintConnectContract.getArgs(args.params.fingerprintSDK) ) } catch (e: Exception) { Simber.tag(FINGER_CAPTURE.name).i("Error launching scanner connection screen", e) diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt index 35bdda5bb5..4b946975fd 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt @@ -160,16 +160,18 @@ internal class FingerprintCaptureViewModel @Inject constructor( } - private fun start(fingerprintsToCapture: List) { + private fun start( + fingerprintsToCapture: List, + fingerprintSdk: FingerprintConfiguration.BioSdk + ) { if (!hasStarted) { hasStarted = true runBlocking { - initBioSdk() // Configuration must be initialised when start returns for UI to be initialised correctly, // and since fetching happens on IO thread execution must be suspended until it is available configuration = configManager.getProjectConfiguration().fingerprint!! - bioSdkConfiguration = configuration.bioSdkConfiguration + initBioSdk(fingerprintSdk) } originalFingerprintsToCapture = fingerprintsToCapture @@ -178,10 +180,11 @@ internal class FingerprintCaptureViewModel @Inject constructor( } } - private suspend fun initBioSdk() { + private suspend fun initBioSdk(fingerprintSdk: FingerprintConfiguration.BioSdk) { try { - bioSdkWrapper = resolveBioSdkWrapperUseCase() + bioSdkWrapper = resolveBioSdkWrapperUseCase(fingerprintSdk) bioSdkWrapper.initialize() + bioSdkConfiguration = configuration.getSdkConfiguration(fingerprintSdk)!! } catch (e: BioSdkException.BioSdkInitializationException) { Simber.e(e) _invalidLicense.send() @@ -344,11 +347,11 @@ internal class FingerprintCaptureViewModel @Inject constructor( try { scannerManager.scanner.setUiIdle() val capturedFingerprint = bioSdkWrapper.acquireFingerprintTemplate( - bioSdkConfiguration.vero2?.captureStrategy?.toInt(), - bioSdkWrapper.scanningTimeoutMs.toInt(), - qualityThreshold(), + capturingResolution = bioSdkConfiguration.vero2?.captureStrategy?.toInt(), + timeOutMs = bioSdkWrapper.scanningTimeoutMs.toInt(), + qualityThreshold = qualityThreshold(), // is this is the last bad scan, we allow low quality extraction - tooManyBadScans(state.currentCaptureState(), plusBadScan = true) + allowLowQualityExtraction = tooManyBadScans(state.currentCaptureState(), plusBadScan = true) ) handleCaptureSuccess(capturedFingerprint) @@ -363,11 +366,11 @@ internal class FingerprintCaptureViewModel @Inject constructor( private fun handleCaptureSuccess(acquireFingerprintTemplateResponse: AcquireFingerprintTemplateResponse) { val scanResult = ScanResult( - acquireFingerprintTemplateResponse.imageQualityScore, - acquireFingerprintTemplateResponse.template, - acquireFingerprintTemplateResponse.templateFormat, - null, - qualityThreshold() + qualityScore = acquireFingerprintTemplateResponse.imageQualityScore, + template = acquireFingerprintTemplateResponse.template, + templateFormat = acquireFingerprintTemplateResponse.templateFormat, + image = null, + qualityThreshold = qualityThreshold() ) _vibrate.send() if (shouldProceedToImageTransfer(scanResult.qualityScore)) { @@ -644,13 +647,16 @@ internal class FingerprintCaptureViewModel @Inject constructor( setStartingState(originalFingerprintsToCapture) } - fun handleOnViewCreated(fingerprintsToCapture: List) { + fun handleOnViewCreated( + fingerprintsToCapture: List, + fingerprintSdk: FingerprintConfiguration.BioSdk + ) { updateState { it.copy( isShowingConnectionScreen = false ) } - start(fingerprintsToCapture) + start(fingerprintsToCapture, fingerprintSdk) } fun handleOnResume() { diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt index 3a2b1d602c..b362daacc4 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt @@ -6,7 +6,7 @@ import android.widget.LinearLayout import android.widget.ProgressBar import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.fingerprint.capture.R import com.simprints.fingerprint.capture.databinding.FragmentFingerBinding @@ -30,7 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint internal class FingerFragment : Fragment(R.layout.fragment_finger) { private val binding by viewBinding(FragmentFingerBinding::bind) - private val vm: FingerprintCaptureViewModel by activityViewModels() + private val vm: FingerprintCaptureViewModel by viewModels(ownerProducer = { requireParentFragment() }) private lateinit var fingerId: IFingerIdentifier diff --git a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt index 773a407964..e69efa9b4b 100644 --- a/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt +++ b/fingerprint/capture/src/test/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModelTest.kt @@ -31,6 +31,7 @@ import com.simprints.fingerprint.infra.scanner.exceptions.safe.NoFingerDetectedE import com.simprints.fingerprint.infra.scanner.exceptions.safe.ScannerDisconnectedException import com.simprints.fingerprint.infra.scanner.wrapper.ScannerWrapper import com.simprints.fingerprint.testtools.FingerprintGenerator +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.Vero1Configuration import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.store.models.Vero2Configuration.ImageSavingStrategy @@ -106,12 +107,12 @@ class FingerprintCaptureViewModelTest { fun setUp() { MockKAnnotations.init(this, relaxed = true) - coEvery {resolveBioSdkWrapperUseCase()} returns bioSdkWrapper + coEvery { resolveBioSdkWrapperUseCase(any()) } returns bioSdkWrapper every { vero2Configuration.qualityThreshold } returns 60 every { vero2Configuration.displayLiveFeedback } returns false every { vero2Configuration.captureStrategy } returns Vero2Configuration.CaptureStrategy.SECUGEN_ISO_1000_DPI every { vero2Configuration.imageSavingStrategy } returns ImageSavingStrategy.NEVER - coEvery { configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration } returns mockk { + coEvery { configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(any()) } returns mockk { every { vero1 } returns Vero1Configuration(60) every { vero2 } returns vero2Configuration } @@ -143,7 +144,7 @@ class FingerprintCaptureViewModelTest { @Test fun viewModel_start_beginsWithCorrectState() = runTest { - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) assertThat(vm.stateLiveData.value).isEqualTo( CollectFingerprintsState( @@ -163,7 +164,7 @@ class FingerprintCaptureViewModelTest { mockScannerSetUiIdle() setupCaptureFingerprintResponses(NEVER_RETURNS) - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) // start and cancel finger scan vm.handleScanButtonPressed() @@ -179,7 +180,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(NEVER_RETURNS) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.Scanning()) @@ -189,7 +190,7 @@ class FingerprintCaptureViewModelTest { fun `test scanner supports image transfer then isImageTransferRequired should be equal to scanningTimeoutMs + imageTransferTimeoutMs`() = runTest { withImageTransfer() every { scanner.isImageTransferSupported() } returns true - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) assertThat(vm.progressBarTimeout()).isEqualTo(bioSdkWrapper.scanningTimeoutMs + bioSdkWrapper.imageTransferTimeoutMs) } @@ -197,7 +198,7 @@ class FingerprintCaptureViewModelTest { fun `test scanner doesn't support imageTransfer then progressBarTimeout should be equal to scanningTimeoutMs`() = runTest { withImageTransfer() every { scanner.isImageTransferSupported() } returns false - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) assertThat(vm.progressBarTimeout()).isEqualTo(bioSdkWrapper.scanningTimeoutMs) } @@ -209,7 +210,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(MockAcquireImageResult.NEVER_RETURNS) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -228,7 +229,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -254,7 +255,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -276,7 +277,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(BAD_SCAN) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -297,7 +298,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(BAD_SCAN) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -318,7 +319,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(NO_FINGER_DETECTED) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotDetected()) @@ -334,7 +335,7 @@ class FingerprintCaptureViewModelTest { } throws ScannerDisconnectedException() withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotCollected) @@ -349,7 +350,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(MockAcquireImageResult.DISCONNECTED) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotCollected) @@ -364,7 +365,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( CaptureState.Collected( @@ -412,7 +413,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) repeat(12) { // 3 times for each of the 4 fingers (2 original + 2 auto-added) vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) @@ -460,7 +461,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) repeat(2) { vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) @@ -510,7 +511,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) repeat(2) { vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) @@ -559,7 +560,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN, DIFFERENT_GOOD_SCAN) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) assertThat(vm.stateLiveData.value?.currentFingerIndex).isEqualTo(1) @@ -587,7 +588,7 @@ class FingerprintCaptureViewModelTest { fun missingFinger_updatesStateCorrectly() = runTest { mockScannerSetUiIdle() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleMissingFingerButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.Skipped) assertThat(vm.stateLiveData.value?.isShowingSplashScreen).isTrue() @@ -604,7 +605,7 @@ class FingerprintCaptureViewModelTest { fun receivesOnlyMissingFingersThenConfirm_showsToastAndResetsState() = runTest { mockScannerSetUiIdle() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) repeat(4) { // 2 original + 2 auto-added vm.handleMissingFingerButtonPressed() advanceTimeBy(TIME_SKIP_MS) @@ -667,7 +668,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) // Finger 1 vm.handleScanButtonPressed() @@ -774,7 +775,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer(strategy = ImageSavingStrategy.EAGER) - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) // Finger 1 vm.handleScanButtonPressed() @@ -880,7 +881,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer(strategy = ImageSavingStrategy.ONLY_GOOD_SCAN) - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) // Finger 1 vm.handleScanButtonPressed() @@ -964,7 +965,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(NEVER_RETURNS) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.Scanning()) vm.handleScanButtonPressed() @@ -979,7 +980,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(MockAcquireImageResult.NEVER_RETURNS) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( CaptureState.TransferringImage( @@ -1006,7 +1007,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(OK) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) vm.handleMissingFingerButtonPressed() @@ -1037,7 +1038,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(NEVER_RETURNS) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.Scanning()) vm.handleOnBackPressed() @@ -1048,7 +1049,7 @@ class FingerprintCaptureViewModelTest { fun shouldLaunch_reconnectActivity_whenScanner_isNotAvailable() = runTest { every { scannerManager.isScannerConnected } returns false - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotCollected) vm.launchReconnect.assertEventReceived() @@ -1063,7 +1064,7 @@ class FingerprintCaptureViewModelTest { acquireImageResponses(MockAcquireImageResult.NEVER_RETURNS) withImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( CaptureState.TransferringImage( @@ -1083,7 +1084,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() vm.handleOnBackPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo( @@ -1102,7 +1103,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(UNKNOWN_ERROR) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotCollected) @@ -1111,7 +1112,7 @@ class FingerprintCaptureViewModelTest { @Test fun onResumeCalled_registersScannerTrigger() = runTest { - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleOnResume() verify { scanner.registerTriggerListener(any()) } @@ -1119,7 +1120,7 @@ class FingerprintCaptureViewModelTest { @Test fun onPauseCalled_unregistersScannerTrigger() = runTest { - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleOnPause() verify { scanner.unregisterTriggerListener(any()) } @@ -1129,7 +1130,7 @@ class FingerprintCaptureViewModelTest { fun whenStart_AndLiveFeedbackIsEnabled_liveFeedbackIsStarted() = runTest { setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) coVerify { scanner.startLiveFeedback() } assertThat(vm.liveFeedbackState).isEqualTo(LiveFeedbackState.START) @@ -1137,7 +1138,7 @@ class FingerprintCaptureViewModelTest { @Test fun whenStart_AndLiveFeedbackIsNotEnabled_liveFeedbackIsNotStarted() = runTest { - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) coVerify(exactly = 0) { scanner.startLiveFeedback() } } @@ -1147,7 +1148,7 @@ class FingerprintCaptureViewModelTest { coEvery { bioSdkWrapper.initialize() } throws BioSdkException.BioSdkInitializationException(Exception()) - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.invalidLicense.assertEventReceived() } @@ -1159,7 +1160,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(NEVER_RETURNS) setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() assertThat(vm.liveFeedbackState).isEqualTo(LiveFeedbackState.PAUSE) @@ -1172,7 +1173,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN) setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() coVerify { scanner.startLiveFeedback() } @@ -1186,7 +1187,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(BAD_SCAN) setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() coVerify { scanner.startLiveFeedback() } @@ -1200,7 +1201,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(GOOD_SCAN, NEVER_RETURNS) setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) vm.handleScanButtonPressed() @@ -1225,7 +1226,7 @@ class FingerprintCaptureViewModelTest { mockScannerSetUiIdle() setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleRestart() coVerify { scanner.startLiveFeedback() } @@ -1236,7 +1237,7 @@ class FingerprintCaptureViewModelTest { fun whenPause_AndLiveFeedbackIsEnabled_liveFeedbackIsStopped() = runTest { setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleOnPause() coVerify { scanner.stopLiveFeedback() } @@ -1247,7 +1248,7 @@ class FingerprintCaptureViewModelTest { fun whenResume_AndLiveFeedbackWasStarted_liveFeedbackIsStarted() = runTest { setupLiveFeedbackOn() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleOnPause() vm.handleOnResume() @@ -1274,7 +1275,7 @@ class FingerprintCaptureViewModelTest { mockScannerSetUiIdle() coEvery { scanner.setUiIdle() } throws ScannerDisconnectedException() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.updateSelectedFinger(1) assertThat(vm.stateLiveData.value?.currentCaptureState()).isEqualTo(CaptureState.NotCollected) @@ -1288,7 +1289,7 @@ class FingerprintCaptureViewModelTest { setupCaptureFingerprintResponses(BAD_SCAN, BAD_SCAN, BAD_SCAN, NO_FINGER_DETECTED) noImageTransfer() - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) vm.handleScanButtonPressed() vm.handleScanButtonPressed() vm.handleScanButtonPressed() @@ -1316,7 +1317,7 @@ class FingerprintCaptureViewModelTest { @ExperimentalTime private fun getToEndOfWorkflow() = runTest { setupCaptureFingerprintResponses(GOOD_SCAN) - vm.handleOnViewCreated(TWO_FINGERS_IDS) + vm.handleOnViewCreated(TWO_FINGERS_IDS, SECUGEN_SIM_MATCHER) repeat(2) { vm.handleScanButtonPressed() advanceTimeBy(TIME_SKIP_MS) diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectContract.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectContract.kt index 0c0f8be74f..067ce044db 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectContract.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectContract.kt @@ -2,13 +2,14 @@ package com.simprints.fingerprint.connect import android.os.Bundle import com.simprints.fingerprint.connect.screens.controller.ConnectScannerControllerFragmentArgs +import com.simprints.infra.config.store.models.FingerprintConfiguration object FingerprintConnectContract { val DESTINATION = R.id.connectScannerControllerFragment - fun getArgs(isReconnect: Boolean): Bundle= ConnectScannerControllerFragmentArgs( - FingerprintConnectParams(isReconnect) - ).toBundle() - + fun getArgs(fingerprintSDK: FingerprintConfiguration.BioSdk): Bundle = + ConnectScannerControllerFragmentArgs( + FingerprintConnectParams(fingerprintSDK) + ).toBundle() } diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectParams.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectParams.kt index dd023c4190..07e8f4cd66 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectParams.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/FingerprintConnectParams.kt @@ -2,10 +2,11 @@ package com.simprints.fingerprint.connect import android.os.Parcelable import androidx.annotation.Keep +import com.simprints.infra.config.store.models.FingerprintConfiguration import kotlinx.parcelize.Parcelize @Keep @Parcelize data class FingerprintConnectParams( - val isReconnect: Boolean + val fingerprintSDK: FingerprintConfiguration.BioSdk, ) : Parcelable diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt index ec4c31d0b8..d7035987fa 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt @@ -41,8 +41,8 @@ internal class ConnectScannerViewModel @Inject constructor( private val saveScannerConnectionEvents: SaveScannerConnectionEventsUseCase, ) : ViewModel() { - private var isReconnect = false - private var allowedGenerations: List = emptyList() + private lateinit var allowedGenerations: List + private lateinit var fingerprintSdk: FingerprintConfiguration.BioSdk private var remainingConnectionAttempts = 0 val currentStep: LiveData @@ -69,7 +69,7 @@ internal class ConnectScannerViewModel @Inject constructor( private val backButtonBehaviour = MutableLiveData(BackButtonBehaviour.EXIT_FORM) fun init(params: FingerprintConnectParams) = viewModelScope.launch { - isReconnect = params.isReconnect + fingerprintSdk = params.fingerprintSDK allowedGenerations = configManager.getProjectConfiguration() .fingerprint ?.allowedScanners @@ -166,7 +166,7 @@ internal class ConnectScannerViewModel @Inject constructor( private suspend fun setupVero() { _currentStep.postValue(Step.SetupScanner) - scannerManager.scanner.setScannerInfoAndCheckAvailableOta() + scannerManager.scanner.setScannerInfoAndCheckAvailableOta(fingerprintSdk) setLastConnectedScannerInfo() logMessageForCrashReport("ScannerManager: setupVero") } diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt index 1015544e10..9864477bb4 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt @@ -161,7 +161,10 @@ internal class ConnectScannerControllerFragment : is ConnectScannerIssueScreen.Ota -> internalNavController?.navigateSafely( currentlyDisplayedInternalFragment, R.id.otaFragment, - OtaFragmentArgs(OtaFragmentParams(screen.availableOtas)).toBundle() + OtaFragmentArgs(OtaFragmentParams( + args.params.fingerprintSDK, + screen.availableOtas + )).toBundle() ) } }) diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt index 78da698cb8..0bffa71032 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt @@ -77,8 +77,9 @@ internal class OtaFragment : Fragment(R.layout.fragment_ota) { connectScannerViewModel.disableBackButton() viewModel.startOta( - args.params.availableOtas, - args.params.currentRetryAttempt + fingerprintSdk = args.params.fingerprintSDK, + availableOtas = args.params.availableOtas, + currentRetryAttempt = args.params.currentRetryAttempt ) } diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragmentParams.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragmentParams.kt index e65b0af41e..a874b70b55 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragmentParams.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragmentParams.kt @@ -3,11 +3,13 @@ package com.simprints.fingerprint.connect.screens.ota import android.os.Parcelable import androidx.annotation.Keep import com.simprints.fingerprint.infra.scanner.domain.ota.AvailableOta +import com.simprints.infra.config.store.models.FingerprintConfiguration import kotlinx.parcelize.Parcelize @Parcelize @Keep data class OtaFragmentParams( + val fingerprintSDK: FingerprintConfiguration.BioSdk, val availableOtas: List, - val currentRetryAttempt: Int = 0 + val currentRetryAttempt: Int = 0, ) : Parcelable diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModel.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModel.kt index e66d2ed045..f7ffe18ef7 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModel.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModel.kt @@ -18,6 +18,7 @@ import com.simprints.fingerprint.infra.scanner.domain.ota.OtaRecoveryStrategy.HA import com.simprints.fingerprint.infra.scanner.domain.ota.OtaRecoveryStrategy.SOFT_RESET import com.simprints.fingerprint.infra.scanner.domain.ota.OtaRecoveryStrategy.SOFT_RESET_AFTER_DELAY import com.simprints.fingerprint.infra.scanner.domain.ota.OtaStep +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.BackendMaintenanceException @@ -58,10 +59,16 @@ internal class OtaViewModel @Inject constructor( private var currentStep: OtaStep? = null private val remainingOtas = mutableListOf() + private lateinit var fingerprintSdk: FingerprintConfiguration.BioSdk @SuppressLint("CheckResult") - fun startOta(availableOtas: List, currentRetryAttempt: Int) { + fun startOta( + fingerprintSdk: FingerprintConfiguration.BioSdk, + availableOtas: List, + currentRetryAttempt: Int + ) { remainingOtas.addAll(availableOtas) + this.fingerprintSdk = fingerprintSdk viewModelScope.launch { try { @@ -95,7 +102,7 @@ internal class OtaViewModel @Inject constructor( private suspend fun targetVersions(availableOta: AvailableOta): String { val scannerVersion = recentUserActivityManager.getRecentUserActivity().lastScannerVersion val availableFirmwareVersions = - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions + configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(fingerprintSdk)?.vero2?.firmwareVersions return when (availableOta) { AvailableOta.CYPRESS -> availableFirmwareVersions?.get(scannerVersion)?.cypress ?: "" AvailableOta.STM -> availableFirmwareVersions?.get(scannerVersion)?.stm ?: "" @@ -132,11 +139,21 @@ internal class OtaViewModel @Inject constructor( _otaFailed.send(FetchOtaResult(isMaintenanceMode = false)) } else { when (val strategy = currentStep?.recoveryStrategy) { - HARD_RESET, SOFT_RESET -> _otaRecovery.send(OtaRecoveryParams(strategy, remainingOtas, currentRetryAttempt)) + HARD_RESET, SOFT_RESET -> _otaRecovery.send(OtaRecoveryParams( + fingerprintSDK = fingerprintSdk, + remainingOtas = remainingOtas, + currentRetryAttempt = currentRetryAttempt, + recoveryStrategy = strategy, + )) SOFT_RESET_AFTER_DELAY -> { delay(OtaRecoveryStrategy.DELAY_TIME_MS) - _otaRecovery.send(OtaRecoveryParams(SOFT_RESET, remainingOtas, currentRetryAttempt)) + _otaRecovery.send(OtaRecoveryParams( + fingerprintSDK = fingerprintSdk, + remainingOtas = remainingOtas, + currentRetryAttempt = currentRetryAttempt, + recoveryStrategy = SOFT_RESET, + )) } null -> _otaFailed.send(FetchOtaResult(isMaintenanceMode = false)) diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt index bfeead2208..e44e0616ef 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt @@ -68,8 +68,9 @@ internal class OtaRecoveryFragment : Fragment(R.layout.fragment_ota_recovery) { findNavController().navigateSafely( this, OtaRecoveryFragmentDirections.actionOtaRecoveryFragmentToOtaFragment(OtaFragmentParams( - args.params.remainingOtas, - args.params.currentRetryAttempt + 1 + fingerprintSDK = args.params.fingerprintSDK, + availableOtas = args.params.remainingOtas, + currentRetryAttempt = args.params.currentRetryAttempt + 1 )) ) } diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryParams.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryParams.kt index 8dcb38e9da..f3ab48c3a4 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryParams.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryParams.kt @@ -4,12 +4,14 @@ import android.os.Parcelable import androidx.annotation.Keep import com.simprints.fingerprint.infra.scanner.domain.ota.AvailableOta import com.simprints.fingerprint.infra.scanner.domain.ota.OtaRecoveryStrategy +import com.simprints.infra.config.store.models.FingerprintConfiguration import kotlinx.parcelize.Parcelize @Parcelize @Keep data class OtaRecoveryParams( - val recoveryStrategy: OtaRecoveryStrategy, + val fingerprintSDK: FingerprintConfiguration.BioSdk, val remainingOtas: List, - val currentRetryAttempt: Int + val currentRetryAttempt: Int, + val recoveryStrategy: OtaRecoveryStrategy, ) : Parcelable diff --git a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModelTest.kt b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModelTest.kt index 040c60dc46..022c5db3a6 100644 --- a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModelTest.kt +++ b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModelTest.kt @@ -27,6 +27,7 @@ import com.simprints.fingerprint.infra.scanner.wrapper.ScannerFactory import com.simprints.fingerprint.infra.scanner.wrapper.ScannerWrapper import com.simprints.fingerprint.scannermock.dummy.DummyBluetoothDevice import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.recent.user.activity.RecentUserActivityManager import com.simprints.infra.recent.user.activity.domain.RecentUserActivity @@ -122,7 +123,7 @@ class ConnectScannerViewModelTest { if (connectFailException != null) throw connectFailException } - coEvery { setScannerInfoAndCheckAvailableOta() } answers {} + coEvery { setScannerInfoAndCheckAvailableOta(fingerprintSdk = SECUGEN_SIM_MATCHER) } answers {} coEvery { sensorWakeUp() } answers {} coEvery { setUiIdle() } answers {} every { versionInformation() } returns when (scannerGeneration) { @@ -139,7 +140,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.BluetoothOff) @@ -155,7 +156,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.BluetoothNotSupported) @@ -172,7 +173,7 @@ class ConnectScannerViewModelTest { val scannerConnectedObserver = viewModel.scannerConnected.testObserver() val scannerStepObserver = viewModel.currentStep.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() scannerConnectedObserver.assertEventReceivedWithContent(true) @@ -194,7 +195,7 @@ class ConnectScannerViewModelTest { val scannerConnectedObserver = viewModel.scannerConnected.testObserver() val scannerStepObserver = viewModel.currentStep.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() scannerConnectedObserver.assertEventReceivedWithContent(true) @@ -216,7 +217,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.SerialEntryPair) @@ -230,7 +231,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.NfcPair) @@ -244,7 +245,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.NfcOff) @@ -258,7 +259,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.SerialEntryPair) @@ -272,7 +273,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.SerialEntryPair) @@ -289,7 +290,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.SerialEntryPair) @@ -303,7 +304,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() connectScannerIssueObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.NfcPair) @@ -319,7 +320,7 @@ class ConnectScannerViewModelTest { val scannerConnectedObserver = viewModel.scannerConnected.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() scannerConnectedObserver.assertEventReceivedWithContent(false) @@ -335,7 +336,7 @@ class ConnectScannerViewModelTest { val scannerConnectedObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() scannerConnectedObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.UnexpectedError) @@ -352,7 +353,7 @@ class ConnectScannerViewModelTest { val scannerConnectedObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() scannerConnectedObserver.assertEventReceivedWithContent(ConnectScannerIssueScreen.LowBattery) @@ -365,7 +366,7 @@ class ConnectScannerViewModelTest { setupBluetooth(numberOfPairedScanners = 1) coEvery { scannerFactory.scannerWrapper } returns mockScannerWrapper(FingerprintConfiguration.VeroGeneration.VERO_2, e) - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() viewModel.showScannerIssueScreen.assertEventReceivedWithContentAssertions { @@ -392,7 +393,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() viewModel.handleScannerDisconnectedNoClick() @@ -406,7 +407,7 @@ class ConnectScannerViewModelTest { val connectScannerIssueObserver = viewModel.showScannerIssueScreen.testObserver() - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.connect() viewModel.handleIncorrectScanner() @@ -428,7 +429,7 @@ class ConnectScannerViewModelTest { val scannerWrapper = mockScannerWrapper(FingerprintConfiguration.VeroGeneration.VERO_1, UnknownScannerIssueException()) every { scannerFactory.scannerWrapper } returns scannerWrapper - viewModel.init(FingerprintConnectParams(isReconnect = false)) + viewModel.init(FingerprintConnectParams(fingerprintSDK = SECUGEN_SIM_MATCHER)) viewModel.startRetryingToConnect() coVerify(exactly = 5) { scannerWrapper.connect() } diff --git a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModelTest.kt b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModelTest.kt index 53db35418b..85f8fbbdb6 100644 --- a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModelTest.kt +++ b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/ota/OtaViewModelTest.kt @@ -14,6 +14,7 @@ import com.simprints.fingerprint.infra.scanner.domain.ota.StmOtaStep import com.simprints.fingerprint.infra.scanner.domain.ota.Un20OtaStep import com.simprints.fingerprint.infra.scanner.exceptions.safe.OtaFailedException import com.simprints.fingerprint.infra.scanner.wrapper.ScannerOtaOperationsWrapper +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.network.exceptions.BackendMaintenanceException @@ -75,7 +76,7 @@ class OtaViewModelTest { coEvery { recentUserActivityManager.getRecentUserActivity() } returns RecentUserActivity(HARDWARE_VERSION, "", "".asTokenizableRaw(), 0, 0, 0, 0) - coEvery { configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions } returns mapOf( + coEvery { configManager.getProjectConfiguration().fingerprint?.getSdkConfiguration(SECUGEN_SIM_MATCHER)?.vero2?.firmwareVersions } returns mapOf( HARDWARE_VERSION to Vero2Configuration.Vero2FirmwareVersions( NEW_CYPRESS_VERSION, NEW_STM_VERSION, @@ -101,7 +102,7 @@ class OtaViewModelTest { fun oneOta_updatesStateCorrectlyAndSavesEvent() = runTest { val progressObserver = otaViewModel.progress.testObserver() - otaViewModel.startOta(listOf(AvailableOta.CYPRESS), 0) + otaViewModel.startOta(SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS), 0) progressObserver.observedValues.assertAlmostContainsExactlyElementsIn( listOf(0f) + CYPRESS_OTA_STEPS.map { it.totalProgress } + listOf(1f) @@ -119,7 +120,7 @@ class OtaViewModelTest { val progressObserver = otaViewModel.progress.testObserver() val completeObserver = otaViewModel.otaComplete.testObserver() - otaViewModel.startOta(listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), 0) + otaViewModel.startOta(SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), 0) progressObserver.observedValues.assertAlmostContainsExactlyElementsIn( listOf(0f) + @@ -144,7 +145,7 @@ class OtaViewModelTest { throw OtaFailedException("oops") } - otaViewModel.startOta(listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), 0) + otaViewModel.startOta(SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), 0) verify { reportFirmwareUpdate.invoke(any(), any(), any(), null) } verify { reportFirmwareUpdate.invoke(any(), any(), any(), any()) } @@ -164,6 +165,7 @@ class OtaViewModelTest { } otaViewModel.startOta( + SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), OtaViewModel.MAX_RETRY_ATTEMPTS ) @@ -183,6 +185,7 @@ class OtaViewModelTest { } otaViewModel.startOta( + SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), OtaViewModel.MAX_RETRY_ATTEMPTS ) @@ -202,6 +205,7 @@ class OtaViewModelTest { } otaViewModel.startOta( + SECUGEN_SIM_MATCHER, listOf(AvailableOta.CYPRESS, AvailableOta.STM, AvailableOta.UN20), 0 ) diff --git a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCase.kt b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCase.kt index 7cfc6eedec..2da7896cf3 100644 --- a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCase.kt +++ b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCase.kt @@ -3,31 +3,17 @@ package com.simprints.fingerprint.infra.biosdk import com.simprints.fingerprint.infra.biosdkimpl.SimprintsSdk import com.simprints.fingerprint.infra.necsdkimpl.NecSdk import com.simprints.infra.config.store.models.FingerprintConfiguration -import com.simprints.infra.config.sync.ConfigManager import javax.inject.Inject class ResolveBioSdkWrapperUseCase @Inject constructor( - private val configManager: ConfigManager, @SimprintsSdk private val simprintsWrapper: BioSdkWrapper, @NecSdk private val necWrapper: BioSdkWrapper, ) { - private lateinit var bioSdkWrapper: BioSdkWrapper - - suspend operator fun invoke(): BioSdkWrapper { - if (::bioSdkWrapper.isInitialized) return bioSdkWrapper - - // Todo we didn't yet implement the logic to select the SDK based on the configuration - // so we are just using the first allowed SDK for now - // See tickets in SIM-81 for more details - bioSdkWrapper = - when (configManager.getProjectConfiguration().fingerprint?.allowedSDKs?.first()) { - FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER -> simprintsWrapper - FingerprintConfiguration.BioSdk.NEC -> necWrapper - else -> error("Unknown fingerprint configuration") - } - - return bioSdkWrapper - } - + operator fun invoke(fingerprintSdk: FingerprintConfiguration.BioSdk): BioSdkWrapper = + when (fingerprintSdk) { + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER -> simprintsWrapper + FingerprintConfiguration.BioSdk.NEC -> necWrapper + else -> error("Unknown fingerprint configuration") + } } diff --git a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCaseTest.kt b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCaseTest.kt index ffe7040457..848fc95727 100644 --- a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCaseTest.kt +++ b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/ResolveBioSdkWrapperUseCaseTest.kt @@ -1,14 +1,10 @@ package com.simprints.fingerprint.infra.biosdk import com.google.common.truth.Truth -import com.simprints.infra.config.store.models.FingerprintConfiguration -import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.NEC +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -23,55 +19,21 @@ class ResolveBioSdkWrapperUseCaseTest { @MockK private lateinit var simprintsBioSdkWrapper: BioSdkWrapper - @MockK - private lateinit var configManager: ConfigManager - - @MockK - private lateinit var fingerprintConfiguration: FingerprintConfiguration - @Before fun setUp() { MockKAnnotations.init(this) - coEvery { configManager.getProjectConfiguration() } returns mockk { - every { fingerprint } returns fingerprintConfiguration - } - bioSdkResolverUseCase = - ResolveBioSdkWrapperUseCase(configManager, simprintsBioSdkWrapper, necBioSdkWrapper) + bioSdkResolverUseCase = ResolveBioSdkWrapperUseCase(simprintsBioSdkWrapper, necBioSdkWrapper) } @Test fun `test with simprints sdk`() = runTest { - every { - fingerprintConfiguration.allowedSDKs - } returns listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) - - - val result = bioSdkResolverUseCase() + val result = bioSdkResolverUseCase(SECUGEN_SIM_MATCHER) Truth.assertThat(simprintsBioSdkWrapper).isEqualTo(result) } @Test fun `test with nec sdk`() = runTest { - every { - fingerprintConfiguration.allowedSDKs - } returns listOf(FingerprintConfiguration.BioSdk.NEC) - - val result = bioSdkResolverUseCase() + val result = bioSdkResolverUseCase(NEC) Truth.assertThat(necBioSdkWrapper).isEqualTo(result) } - - @Test - fun `test if already initialized`() = runTest { - every { - fingerprintConfiguration.allowedSDKs - } returns listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) - - val result = bioSdkResolverUseCase() - Truth.assertThat(simprintsBioSdkWrapper).isEqualTo(result) - - val result2 = bioSdkResolverUseCase() - Truth.assertThat(simprintsBioSdkWrapper).isEqualTo(result2) - - coVerify(exactly = 1) { fingerprintConfiguration.allowedSDKs } - } } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepository.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepository.kt index 2871fe3e01..ee6731f366 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepository.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepository.kt @@ -4,6 +4,8 @@ import com.simprints.fingerprint.infra.scanner.data.local.FirmwareLocalDataSourc import com.simprints.fingerprint.infra.scanner.data.remote.FirmwareRemoteDataSource import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion.Chip +import com.simprints.fingerprint.infra.scanner.domain.versions.getMissingVersionsToDownload +import com.simprints.infra.config.store.models.Vero2Configuration.Vero2FirmwareVersions import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.Simber import javax.inject.Inject @@ -20,35 +22,38 @@ class FirmwareRepository @Inject internal constructor( ) { /** - * This method responsible for updating the firmware versions stored locally on the phone. It - * first checks the local version and matches that against the remote versions, then subsequently updating the rlocal versios that need to be updated. + * This method is responsible for updating the firmware versions stored locally on the phone. It + * first checks the local version and matches that against the remote versions, then subsequently updating the local versions that need to be updated. */ suspend fun updateStoredFirmwareFilesWithLatest() { - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions?.keys?.forEach { hardwareVersion -> - updateStoredFirmwareFilesWithLatest(hardwareVersion) + configManager.getProjectConfiguration().fingerprint?.secugenSimMatcher?.vero2?.firmwareVersions?.let { + updateStoredFirmwareFilesWithLatest(it) + } + configManager.getProjectConfiguration().fingerprint?.nec?.vero2?.firmwareVersions?.let { + updateStoredFirmwareFilesWithLatest(it) + } + } + + private suspend fun updateStoredFirmwareFilesWithLatest(firmwareConfiguration: Map) { + firmwareConfiguration.forEach { (hardwareRevision, firmwareVersions) -> + updateStoredFirmwareFilesWithLatest(firmwareVersions) } } - private suspend fun updateStoredFirmwareFilesWithLatest(hardwareVersion: String) { - val savedVersions = - firmwareLocalDataSource.getAvailableScannerFirmwareVersions() + private suspend fun updateStoredFirmwareFilesWithLatest(firmwareVersions: Vero2FirmwareVersions) { + val savedVersions = firmwareLocalDataSource.getAvailableScannerFirmwareVersions() Simber.d("Saved firmware versions: $savedVersions") - val downloadableFirmwares = firmwareRemoteDataSource.getDownloadableFirmwares( - hardwareVersion, - savedVersions - ) + val downloadableFirmwares = firmwareVersions.getMissingVersionsToDownload(savedVersions) + // issue with timber logging URLs when interpolated in kotlin, check out this article // https://proandroiddev.com/be-careful-what-you-log-it-could-crash-your-app-5fc67a44c842 val versionString = downloadableFirmwares.joinToString() Simber.d("Firmwares available for download: %s", versionString) - val cypressToDownload = - downloadableFirmwares.getVersionToDownloadOrNull(Chip.CYPRESS) - val stmToDownload = - downloadableFirmwares.getVersionToDownloadOrNull(Chip.STM) - val un20ToDownload = - downloadableFirmwares.getVersionToDownloadOrNull(Chip.UN20) + val cypressToDownload = downloadableFirmwares.getVersionToDownloadOrNull(Chip.CYPRESS) + val stmToDownload = downloadableFirmwares.getVersionToDownloadOrNull(Chip.STM) + val un20ToDownload = downloadableFirmwares.getVersionToDownloadOrNull(Chip.UN20) cypressToDownload?.downloadAndSave() stmToDownload?.downloadAndSave() @@ -81,17 +86,17 @@ class FirmwareRepository @Inject internal constructor( val cypressOfficialVersions = mutableSetOf() val stmOfficialVersions = mutableSetOf() val un20OfficialVersions = mutableSetOf() - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions?.entries?.forEach { + + val secuGenFirmwareVersions = configManager.getProjectConfiguration().fingerprint?.secugenSimMatcher?.vero2?.firmwareVersions?.entries + val necFirmwareVersions = configManager.getProjectConfiguration().fingerprint?.nec?.vero2?.firmwareVersions?.entries + (secuGenFirmwareVersions.orEmpty() + necFirmwareVersions.orEmpty()).forEach { cypressOfficialVersions.add(it.value.cypress) stmOfficialVersions.add(it.value.stm) un20OfficialVersions.add(it.value.un20) } locallySavedFiles.entries.forEach { when (it.key) { - Chip.CYPRESS -> obsoleteItems( - it.value, - cypressOfficialVersions - ).forEach { firmwareFile -> + Chip.CYPRESS -> obsoleteItems(it.value, cypressOfficialVersions).forEach { firmwareFile -> firmwareLocalDataSource.deleteCypressFirmware(firmwareFile) } @@ -109,8 +114,5 @@ class FirmwareRepository @Inject internal constructor( private fun obsoleteItems(localVersions: Set, officialVersions: Set) = localVersions.filter { !officialVersions.contains(it) } - suspend fun deleteAllFirmwareFiles() { - firmwareLocalDataSource.deleteAllFirmware() - } - + suspend fun deleteAllFirmwareFiles() = firmwareLocalDataSource.deleteAllFirmware() } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSource.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSource.kt index 2510007dfb..e9db0d2aaf 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSource.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSource.kt @@ -2,8 +2,6 @@ package com.simprints.fingerprint.infra.scanner.data.remote import com.simprints.fingerprint.infra.scanner.data.remote.network.FingerprintFileDownloader import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion -import com.simprints.fingerprint.infra.scanner.domain.versions.getAvailableVersionsForDownload -import com.simprints.infra.config.sync.ConfigManager import com.simprints.infra.logging.Simber import javax.inject.Inject @@ -14,21 +12,7 @@ import javax.inject.Inject */ internal class FirmwareRemoteDataSource @Inject constructor( private val fingerprintFileDownloader: FingerprintFileDownloader, - private val configManager: ConfigManager, ) { - - /** - * Allows for querying whether there are more up-to-date firmware versions by sending the currently saved versions - */ - suspend fun getDownloadableFirmwares( - hardwareVersion: String, - localFirmwareVersions: Map> - ): List = - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions?.getAvailableVersionsForDownload( - hardwareVersion, - localFirmwareVersions - ) ?: listOf() - /** * Downloads the firmware binary at the given URL */ @@ -37,6 +21,4 @@ internal class FirmwareRemoteDataSource @Inject constructor( Simber.d("Downloading firmware file at %s", fileUrl) return fingerprintFileDownloader.download(fileUrl) } - - } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisions.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisions.kt index eab0f9d543..201eff27d7 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisions.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisions.kt @@ -4,77 +4,74 @@ import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion.Chip -internal fun Map.getAvailableVersionsForDownload( - hardwareVersion: String, - localFirmwareVersions: Map> +internal fun Vero2Configuration.Vero2FirmwareVersions.getMissingVersionsToDownload( + savedFirmwareVersions: Map> ): List { - val availableFirmwareVersionsForDownload = ArrayList() - val hardwareFirmwareVersionsToDownload = - get(hardwareVersion) ?: return availableFirmwareVersionsForDownload + val firmwareVersionsToDownload = ArrayList() addAvailableCypressVersions( - localFirmwareVersions, - hardwareFirmwareVersionsToDownload, - availableFirmwareVersionsForDownload + savedFirmwareVersions, + this, + firmwareVersionsToDownload ) addAvailableSTMVersions( - localFirmwareVersions, - hardwareFirmwareVersionsToDownload, - availableFirmwareVersionsForDownload + savedFirmwareVersions, + this, + firmwareVersionsToDownload ) addAvailableUN20Versions( - localFirmwareVersions, - hardwareFirmwareVersionsToDownload, - availableFirmwareVersionsForDownload + savedFirmwareVersions, + this, + firmwareVersionsToDownload ) - return availableFirmwareVersionsForDownload + return firmwareVersionsToDownload } private fun addAvailableCypressVersions( - localFirmwareVersions: Map>, - hardwareFirmwareVersionsToDownload: Vero2Configuration.Vero2FirmwareVersions, - availableFirmwareVersionsForDownload: ArrayList + savedFirmwareVersions: Map>, + configuredFirmwareVersions: Vero2Configuration.Vero2FirmwareVersions, + firmwareVersionsToDownload: ArrayList ) { - val localCypressVersions = localFirmwareVersions[Chip.CYPRESS] + val localCypressVersions = savedFirmwareVersions[Chip.CYPRESS] val hasNotDownloadedCypressVersion = localCypressVersions == null - || !localCypressVersions.contains(hardwareFirmwareVersionsToDownload.cypress) + || !localCypressVersions.contains(configuredFirmwareVersions.cypress) if (hasNotDownloadedCypressVersion) { - availableFirmwareVersionsForDownload.add( - DownloadableFirmwareVersion(Chip.CYPRESS, hardwareFirmwareVersionsToDownload.cypress) + firmwareVersionsToDownload.add( + DownloadableFirmwareVersion(Chip.CYPRESS, configuredFirmwareVersions.cypress) ) } } private fun addAvailableSTMVersions( - localFirmwareVersions: Map>, - hardwareFirmwareVersionsToDownload: Vero2Configuration.Vero2FirmwareVersions, - availableFirmwareVersionsForDownload: ArrayList + savedFirmwareVersions: Map>, + configuredFirmwareVersions: Vero2Configuration.Vero2FirmwareVersions, + firmwareVersionsToDownload: ArrayList ) { - val localSTMVersions = localFirmwareVersions[Chip.STM] + val localSTMVersions = savedFirmwareVersions[Chip.STM] val hasNotDownloadedStmVersion = localSTMVersions == null - || !localSTMVersions.contains(hardwareFirmwareVersionsToDownload.stm) + || !localSTMVersions.contains(configuredFirmwareVersions.stm) if (hasNotDownloadedStmVersion) { - availableFirmwareVersionsForDownload.add( - DownloadableFirmwareVersion(Chip.STM, hardwareFirmwareVersionsToDownload.stm) + firmwareVersionsToDownload.add( + DownloadableFirmwareVersion(Chip.STM, configuredFirmwareVersions.stm) ) } } private fun addAvailableUN20Versions( - localFirmwareVersions: Map>, - hardwareFirmwareVersionsToDownload: Vero2Configuration.Vero2FirmwareVersions, - availableFirmwareVersionsForDownload: ArrayList + savedFirmwareVersions: Map>, + configuredFirmwareVersions: Vero2Configuration.Vero2FirmwareVersions, + firmwareVersionsToDownload: ArrayList ) { - val localUN20Versions = localFirmwareVersions[Chip.UN20] + val localUN20Versions = savedFirmwareVersions[Chip.UN20] val hasNotDownloadedUn20Version = localUN20Versions == null - || !localUN20Versions.contains(hardwareFirmwareVersionsToDownload.un20) + || !localUN20Versions.contains(configuredFirmwareVersions.un20) if (hasNotDownloadedUn20Version) { - availableFirmwareVersionsForDownload.add( - DownloadableFirmwareVersion(Chip.UN20, hardwareFirmwareVersionsToDownload.un20) + firmwareVersionsToDownload.add( + DownloadableFirmwareVersion(Chip.UN20, configuredFirmwareVersions.un20) ) } } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelper.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelper.kt index 07c012378e..9444be651f 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelper.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelper.kt @@ -9,6 +9,7 @@ import com.simprints.fingerprint.infra.scanner.domain.versions.ScannerVersion import com.simprints.fingerprint.infra.scanner.exceptions.safe.OtaAvailableException import com.simprints.fingerprint.infra.scanner.tools.BatteryLevelChecker import com.simprints.fingerprint.infra.scanner.v2.scanner.Scanner +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.sync.ConfigManager import kotlinx.coroutines.delay @@ -40,6 +41,7 @@ internal class ScannerInitialSetupHelper @Inject constructor( * @throws OtaAvailableException If an OTA update is available and the battery is sufficiently charged. */ suspend fun setupScannerWithOtaCheck( + fingerprintSdk: FingerprintConfiguration.BioSdk, scanner: Scanner, macAddress: String, withScannerVersion: (ScannerVersion) -> Unit, @@ -57,6 +59,7 @@ internal class ScannerInitialSetupHelper @Inject constructor( delay(100) // Speculatively needed val batteryInfo = getBatteryInfo(scanner, withBatteryInfo) ifAvailableOtasPrepareScannerThenThrow( + fingerprintSdk, scannerVersion.hardwareVersion, scanner, macAddress, @@ -64,7 +67,6 @@ internal class ScannerInitialSetupHelper @Inject constructor( ) } - private suspend fun getBatteryInfo( scanner: Scanner, withBatteryInfo: (BatteryInfo) -> Unit, @@ -85,16 +87,18 @@ internal class ScannerInitialSetupHelper @Inject constructor( } private suspend fun ifAvailableOtasPrepareScannerThenThrow( + fingerprintSdk: FingerprintConfiguration.BioSdk, hardwareVersion: String, scanner: Scanner, macAddress: String, batteryInfo: BatteryInfo, ) { - val availableVersions = - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions?.get( - hardwareVersion - ) - val availableOtas = determineAvailableOtas(scannerVersion.firmware, availableVersions) + val configuredVersions = configManager.getProjectConfiguration().fingerprint + ?.getSdkConfiguration(fingerprintSdk) + ?.vero2 + ?.firmwareVersions + ?.get(hardwareVersion) + val availableOtas = determineAvailableOtas(scannerVersion.firmware, configuredVersions) val requiresOtaUpdate = availableOtas.isNotEmpty() && !batteryInfo.isLowBattery() && !batteryLevelChecker.isLowBattery() @@ -107,22 +111,22 @@ internal class ScannerInitialSetupHelper @Inject constructor( private suspend fun determineAvailableOtas( current: ScannerFirmwareVersions, - available: Vero2Configuration.Vero2FirmwareVersions?, + configured: Vero2Configuration.Vero2FirmwareVersions?, ): List { - if (available == null) { + if (configured == null) { return emptyList() } val localFiles = firmwareLocalDataSource.getAvailableScannerFirmwareVersions() return listOfNotNull( if ( - localFiles[Chip.CYPRESS]?.contains(available.cypress) == true - && current.cypress != available.cypress + localFiles[Chip.CYPRESS]?.contains(configured.cypress) == true + && current.cypress != configured.cypress ) AvailableOta.CYPRESS else null, - if (localFiles[Chip.STM]?.contains(available.stm) == true && - current.stm != available.stm + if (localFiles[Chip.STM]?.contains(configured.stm) == true && + current.stm != configured.stm ) AvailableOta.STM else null, - if (localFiles[Chip.UN20]?.contains(available.un20) == true && - current.un20 != available.un20 + if (localFiles[Chip.UN20]?.contains(configured.un20) == true && + current.un20 != configured.un20 ) AvailableOta.UN20 else null ) } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapper.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapper.kt index ab8f8e6b20..74b6670149 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapper.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapper.kt @@ -5,6 +5,7 @@ import com.simprints.fingerprint.infra.scanner.domain.ScannerTriggerListener import com.simprints.fingerprint.infra.scanner.domain.versions.ScannerVersion import com.simprints.fingerprint.infra.scanner.exceptions.safe.OtaAvailableException import com.simprints.fingerprint.infra.scanner.exceptions.unexpected.UnavailableVero2FeatureException +import com.simprints.infra.config.store.models.FingerprintConfiguration /** * A common interface for both Vero 1 and Vero 2. Some features are only available on later versions @@ -24,7 +25,7 @@ interface ScannerWrapper { * * @throws OtaAvailableException */ - suspend fun setScannerInfoAndCheckAvailableOta() + suspend fun setScannerInfoAndCheckAvailableOta(fingerprintSdk: FingerprintConfiguration.BioSdk) suspend fun sensorWakeUp() suspend fun sensorShutDown() diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV1.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV1.kt index c325187e5b..0eb19c880c 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV1.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV1.kt @@ -20,6 +20,7 @@ import com.simprints.fingerprint.infra.scanner.v1.SCANNER_ERROR.BUSY import com.simprints.fingerprint.infra.scanner.v1.SCANNER_ERROR.IO_ERROR import com.simprints.fingerprint.infra.scanner.v1.SCANNER_ERROR.SCANNER_UNBONDED import com.simprints.fingerprint.infra.scanner.v1.SCANNER_ERROR.UN20_LOW_VOLTAGE +import com.simprints.infra.config.store.models.FingerprintConfiguration import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import kotlin.coroutines.resume @@ -89,10 +90,10 @@ internal class ScannerWrapperV1( /** * This function does nothing because vero 1 scanner doesn't support firmware updates */ - override suspend fun setScannerInfoAndCheckAvailableOta() = withContext(ioDispatcher) { - //Not implemented - } - + override suspend fun setScannerInfoAndCheckAvailableOta(fingerprintSdk: FingerprintConfiguration.BioSdk) = + withContext(ioDispatcher) { + //Not implemented + } override suspend fun disconnect() = withContext(ioDispatcher) { suspendCoroutine { cont -> diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2.kt index 00307b45ef..11c1c6771f 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2.kt @@ -18,6 +18,7 @@ import com.simprints.fingerprint.infra.scanner.v2.scanner.ScannerExtendedInfoRea import com.simprints.fingerprint.infra.scanner.v2.tools.ScannerUiHelper import com.simprints.fingerprint.infra.scanner.v2.tools.mapPotentialErrorFromScanner import com.simprints.fingerprint.infra.scanner.v2.tools.wrapErrorFromScanner +import com.simprints.infra.config.store.models.FingerprintConfiguration import io.reactivex.Completable import io.reactivex.Observer import io.reactivex.observers.DisposableObserver @@ -70,17 +71,19 @@ internal class ScannerWrapperV2( * @throws UnexpectedScannerException * @throws OtaFailedException */ - override suspend fun setScannerInfoAndCheckAvailableOta() = withContext(ioDispatcher) { - try { - scannerInitialSetupHelper.setupScannerWithOtaCheck( - scannerV2, - macAddress, - { scannerVersion = it }, - { batteryInfo = it } - ) - } catch (ex: Throwable) { - throw wrapErrorFromScanner(ex) - } + override suspend fun setScannerInfoAndCheckAvailableOta(fingerprintSdk: FingerprintConfiguration.BioSdk) = + withContext(ioDispatcher) { + try { + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk, + scannerV2, + macAddress, + { scannerVersion = it }, + { batteryInfo = it } + ) + } catch (ex: Throwable) { + throw wrapErrorFromScanner(ex) + } } override suspend fun disconnect() = withContext(ioDispatcher) { diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepositoryTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepositoryTest.kt index 705b655f92..1a334fc10a 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepositoryTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/FirmwareRepositoryTest.kt @@ -3,8 +3,9 @@ package com.simprints.fingerprint.infra.scanner.data import com.simprints.fingerprint.infra.scanner.data.local.FirmwareLocalDataSource import com.simprints.fingerprint.infra.scanner.data.remote.FirmwareRemoteDataSource import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion -import com.simprints.fingerprint.infra.scanner.domain.versions.getAvailableVersionsForDownload import com.simprints.infra.config.store.models.Vero2Configuration +import com.simprints.infra.config.store.models.Vero2Configuration.Vero2FirmwareVersions +import com.simprints.fingerprint.infra.scanner.domain.versions.getMissingVersionsToDownload import com.simprints.infra.config.sync.ConfigManager import io.mockk.MockKAnnotations import io.mockk.Ordering @@ -12,6 +13,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockkStatic import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -21,7 +23,7 @@ class FirmwareRepositoryTest { @MockK(relaxUnitFun = true) private lateinit var firmwareLocalDataSourceMock: FirmwareLocalDataSource - @MockK + @MockK(relaxed = true) private lateinit var firmwareRemoteDataSourceMock: FirmwareRemoteDataSource @MockK @@ -37,11 +39,11 @@ class FirmwareRepositoryTest { MockKAnnotations.init(this, relaxed = true) coEvery { - configManager.getProjectConfiguration().fingerprint?.bioSdkConfiguration?.vero2 + configManager.getProjectConfiguration().fingerprint?.secugenSimMatcher?.vero2 } returns vero2Configuration every { vero2Configuration.firmwareVersions } returns mapOf( - HARDWARE_VERSION to Vero2Configuration.Vero2FirmwareVersions( + HARDWARE_VERSION to Vero2FirmwareVersions( CYPRESS_VERSION_HIGH, STM_VERSION_HIGH, UN20_VERSION_HIGH @@ -53,6 +55,8 @@ class FirmwareRepositoryTest { firmwareLocalDataSourceMock, configManager ) + + mockkStatic("com.simprints.fingerprint.infra.scanner.domain.versions.ScannerHardwareRevisionsKt") } @Test @@ -62,16 +66,6 @@ class FirmwareRepositoryTest { coEvery { firmwareLocalDataSourceMock.getAvailableScannerFirmwareVersions() } returns SCANNER_VERSIONS_LOW - val availableForDownload = RESPONSE_MAP.getAvailableVersionsForDownload( - HARDWARE_VERSION, - emptyMap() - ) - coEvery { - firmwareRemoteDataSourceMock.getDownloadableFirmwares( - any(), - any() - ) - } returns availableForDownload firmwareRepository.updateStoredFirmwareFilesWithLatest() @@ -79,15 +73,9 @@ class FirmwareRepositoryTest { firmwareRemoteDataSourceMock.downloadFirmware(any()) } coVerify(Ordering.ORDERED) { - firmwareLocalDataSourceMock.saveCypressFirmwareBytes( - any(), any() - ) - firmwareLocalDataSourceMock.saveStmFirmwareBytes( - any(), any() - ) - firmwareLocalDataSourceMock.saveUn20FirmwareBytes( - any(), any() - ) + firmwareLocalDataSourceMock.saveCypressFirmwareBytes(any(), any()) + firmwareLocalDataSourceMock.saveStmFirmwareBytes(any(), any()) + firmwareLocalDataSourceMock.saveUn20FirmwareBytes(any(), any()) } } @@ -97,58 +85,32 @@ class FirmwareRepositoryTest { coEvery { firmwareLocalDataSourceMock.getAvailableScannerFirmwareVersions() } returns SCANNER_VERSIONS_LOW - coEvery { - firmwareRemoteDataSourceMock.getDownloadableFirmwares( - any(), - any() - ) + every { + any().getMissingVersionsToDownload(any()) } returns emptyList() firmwareRepository.updateStoredFirmwareFilesWithLatest() coVerify(exactly = 0) { firmwareRemoteDataSourceMock.downloadFirmware(any()) } - coVerify(exactly = 0) { - firmwareLocalDataSourceMock.saveCypressFirmwareBytes( - any(), - any() - ) - } + coVerify(exactly = 0) { firmwareLocalDataSourceMock.saveCypressFirmwareBytes(any(), any()) } coVerify(exactly = 0) { firmwareLocalDataSourceMock.saveStmFirmwareBytes(any(), any()) } - coVerify(exactly = 0) { - firmwareLocalDataSourceMock.saveUn20FirmwareBytes( - any(), - any() - ) - } + coVerify(exactly = 0) { firmwareLocalDataSourceMock.saveUn20FirmwareBytes(any(), any()) } } @Test fun `updateStoredFirmwareFilesWithLatest downloads other files when only one versions available`() = runTest { + coEvery { firmwareRemoteDataSourceMock.downloadFirmware(any()) } returns CYPRESS_BIN coEvery { firmwareLocalDataSourceMock.getAvailableScannerFirmwareVersions() } returns SCANNER_VERSIONS_LOW_UN20_HIGH - val availableForDownload = RESPONSE_MAP.getAvailableVersionsForDownload( - HARDWARE_VERSION, - SCANNER_VERSIONS_HIGH - ) - coEvery { - firmwareRemoteDataSourceMock.getDownloadableFirmwares( - any(), - any() - ) - } returns availableForDownload firmwareRepository.updateStoredFirmwareFilesWithLatest() - coVerify(exactly = 0) { firmwareRemoteDataSourceMock.downloadFirmware(any()) } - - coVerify(exactly = 0) { - firmwareLocalDataSourceMock.saveUn20FirmwareBytes( - any(), - any() - ) - } + coVerify(exactly = 2) { firmwareRemoteDataSourceMock.downloadFirmware(any()) } + coVerify(exactly = 1) { firmwareLocalDataSourceMock.saveCypressFirmwareBytes(any(), any()) } + coVerify(exactly = 1) { firmwareLocalDataSourceMock.saveStmFirmwareBytes(any(), any()) } + coVerify(exactly = 0) { firmwareLocalDataSourceMock.saveUn20FirmwareBytes(any(), any()) } } @Test @@ -207,13 +169,6 @@ class FirmwareRepositoryTest { private const val UN20_VERSION_LOW = "1.E-1.2" private const val UN20_VERSION_HIGH = "1.E-1.3" - private val RESPONSE_MAP = mapOf( - HARDWARE_VERSION to Vero2Configuration.Vero2FirmwareVersions( - CYPRESS_VERSION_HIGH, - STM_VERSION_HIGH, - UN20_VERSION_HIGH - ) - ) private val SCANNER_VERSIONS_LOW = mapOf( DownloadableFirmwareVersion.Chip.CYPRESS to setOf(CYPRESS_VERSION_LOW), DownloadableFirmwareVersion.Chip.STM to setOf(STM_VERSION_LOW), diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSourceTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSourceTest.kt index d39bb32ab8..446df7033b 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSourceTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/remote/FirmwareRemoteDataSourceTest.kt @@ -1,13 +1,9 @@ package com.simprints.fingerprint.infra.scanner.data.remote import com.google.common.truth.Truth.assertThat -import com.simprints.fingerprint.infra.scanner.data.FirmwareTestData import com.simprints.fingerprint.infra.scanner.data.remote.network.FingerprintFileDownloader import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion -import com.simprints.infra.config.store.models.Vero2Configuration -import com.simprints.infra.config.sync.ConfigManager import io.mockk.coEvery -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Before @@ -15,32 +11,15 @@ import org.junit.Test class FirmwareRemoteDataSourceTest { private val fingerprintFileDownloaderMock: FingerprintFileDownloader = mockk() - private val fingerprintPreferencesMock = mockk { - coEvery { getProjectConfiguration() } returns mockk { - every { fingerprint?.bioSdkConfiguration?.vero2?.firmwareVersions } returns RESPONSE_MAP - } - } - private val firmwareRemoteDataSource = - FirmwareRemoteDataSource(fingerprintFileDownloaderMock, fingerprintPreferencesMock) + FirmwareRemoteDataSource(fingerprintFileDownloaderMock) @Before fun setup() { coEvery { fingerprintFileDownloaderMock.getFileUrl(any()) } returns SOME_URL } - @Test - fun getDownloadableFirmwares_correctlyCallsApiAndTransformsResponse() = runTest { - - val response = firmwareRemoteDataSource.getDownloadableFirmwares( - HARDWARE_VERSION, - emptyMap() - ) - - assertThat(response.size).isEqualTo(3) - } - @Test fun downloadFile_correctlyForwardsDownload() = runTest { coEvery { fingerprintFileDownloaderMock.download(eq(SOME_URL)) } returns SOME_BIN @@ -56,19 +35,7 @@ class FirmwareRemoteDataSourceTest { } companion object { - private const val HARDWARE_VERSION = "E-1" - private const val CYPRESS_VERSION_HIGH = "1.E-1.1" - private const val STM_VERSION_HIGH = "1.E-1.2" - private const val UN20_VERSION_HIGH = "1.E-1.3" - - private val RESPONSE_MAP = mapOf( - FirmwareTestData.HARDWARE_VERSION to Vero2Configuration.Vero2FirmwareVersions( - CYPRESS_VERSION_HIGH, - STM_VERSION_HIGH, - UN20_VERSION_HIGH - ) - ) private const val SOME_URL = "some.url.com" private val SOME_BIN = byteArrayOf(0x00, 0x01, 0x02) diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisionsTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisionsTest.kt index cf59deb4a2..36fa2574db 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisionsTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/domain/versions/ScannerHardwareRevisionsTest.kt @@ -2,9 +2,9 @@ package com.simprints.fingerprint.infra.scanner.domain.versions import com.google.common.truth.Truth import com.simprints.fingerprint.infra.scanner.data.FirmwareTestData +import com.simprints.fingerprint.infra.scanner.data.FirmwareTestData.HARDWARE_VERSION import com.simprints.fingerprint.infra.scanner.data.FirmwareTestData.SCANNER_VERSIONS_HIGH import com.simprints.fingerprint.infra.scanner.domain.ota.DownloadableFirmwareVersion -import com.simprints.infra.config.store.models.Vero2Configuration import org.junit.Test class ScannerHardwareRevisionsTest { @@ -15,32 +15,19 @@ class ScannerHardwareRevisionsTest { val scannerHardwareRevisions = FirmwareTestData.RESPONSE_HARDWARE_REVISIONS_MAP val local = emptyMap>() // When - val result = scannerHardwareRevisions.getAvailableVersionsForDownload(FirmwareTestData.HARDWARE_VERSION, local) + val result = scannerHardwareRevisions[HARDWARE_VERSION]?.getMissingVersionsToDownload(local) // Then - Truth.assertThat(result.size).isEqualTo(3) + Truth.assertThat(result?.size).isEqualTo(3) } @Test fun `test getDownloadableFirmwares with all firmware versions are available locally`() { - //Given val scannerHardwareRevisions = FirmwareTestData.RESPONSE_HARDWARE_REVISIONS_MAP val local = SCANNER_VERSIONS_HIGH // When - val result = scannerHardwareRevisions.getAvailableVersionsForDownload(FirmwareTestData.HARDWARE_VERSION, local) - // Then - Truth.assertThat(result.size).isEqualTo(0) - } - - @Test - fun `test getDownloadableFirmwares with empty ScannerHardwareRevisions`() { - - //Given - val scannerHardwareRevisions = mapOf() - val local = SCANNER_VERSIONS_HIGH - // When - val result = scannerHardwareRevisions.getAvailableVersionsForDownload(FirmwareTestData.HARDWARE_VERSION, local) + val result = scannerHardwareRevisions[HARDWARE_VERSION]?.getMissingVersionsToDownload(local) // Then - Truth.assertThat(result.size).isEqualTo(0) + Truth.assertThat(result?.size).isEqualTo(0) } } diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelperTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelperTest.kt index 68084917fc..b3f3324fd0 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelperTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/helpers/ScannerInitialSetupHelperTest.kt @@ -14,6 +14,7 @@ import com.simprints.fingerprint.infra.scanner.v2.domain.root.models.CypressFirm import com.simprints.fingerprint.infra.scanner.v2.domain.root.models.ScannerInformation import com.simprints.fingerprint.infra.scanner.v2.domain.root.models.UnifiedVersionInformation import com.simprints.fingerprint.infra.scanner.v2.scanner.Scanner +import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.sync.ConfigManager import com.simprints.testtools.common.syntax.assertThrows @@ -35,7 +36,7 @@ class ScannerInitialSetupHelperTest { private val vero2Configuration = mockk() private val configManager = mockk { coEvery { getProjectConfiguration() } returns mockk { - every { fingerprint?.bioSdkConfiguration?.vero2 } returns vero2Configuration + every { fingerprint?.getSdkConfiguration(SECUGEN_SIM_MATCHER)?.vero2 } returns vero2Configuration } } private val firmwareLocalDataSource = mockk() @@ -73,8 +74,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(HIGH_BATTERY_INFO) - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) coVerify(exactly = 0) { connectionHelperMock.reconnect(any(), any()) } } @@ -91,8 +97,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(HIGH_BATTERY_INFO) val exception = assertThrows { - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) } assertThat(exception.availableOtas).isEqualTo(listOf(AvailableOta.CYPRESS)) @@ -113,10 +124,11 @@ class ScannerInitialSetupHelperTest { var batteryInfo: BatteryInfo? = null scannerInitialSetupHelper.setupScannerWithOtaCheck( - scannerMock, - MAC_ADDRESS, - { version = it }, - { batteryInfo = it }) + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = { version = it }, + withBatteryInfo = { batteryInfo = it }) assertThat(version).isEqualTo(SCANNER_VERSION_LOW.toScannerVersion()) assertThat(batteryInfo).isEqualTo(HIGH_BATTERY_INFO) @@ -136,8 +148,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(HIGH_BATTERY_INFO) - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) } @Test @@ -154,8 +171,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(HIGH_BATTERY_INFO) val exception = assertThrows { - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) } assertThat(exception.availableOtas).isEqualTo( @@ -181,8 +203,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(LOW_BATTERY_INFO) - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) } @Test @@ -198,8 +225,13 @@ class ScannerInitialSetupHelperTest { setupScannerWithBatteryInfo(HIGH_BATTERY_INFO) - scannerInitialSetupHelper.setupScannerWithOtaCheck(scannerMock, MAC_ADDRESS, {}, {}) - + scannerInitialSetupHelper.setupScannerWithOtaCheck( + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = {}, + withBatteryInfo = {} + ) } @Test @@ -220,10 +252,11 @@ class ScannerInitialSetupHelperTest { val exception = assertThrows { scannerInitialSetupHelper.setupScannerWithOtaCheck( - scannerMock, - MAC_ADDRESS, - { version = it }, - { batteryInfo = it }) + fingerprintSdk = SECUGEN_SIM_MATCHER, + scanner = scannerMock, + macAddress = MAC_ADDRESS, + withScannerVersion = { version = it }, + withBatteryInfo = { batteryInfo = it }) } assertThat(exception.availableOtas).isEqualTo( diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2Test.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2Test.kt index 9c9b30120a..3618b7135e 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2Test.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerWrapperV2Test.kt @@ -118,16 +118,17 @@ internal class ScannerWrapperV2Test { any(), any(), any(), + any(), any() ) } answers { - val scannerInfoCallback = args[2] as ((ScannerVersion) -> Unit) + val scannerInfoCallback = args[3] as ((ScannerVersion) -> Unit) scannerInfoCallback.invoke(expectedVersion) - val batteryInfoCallback = args[3] as ((BatteryInfo) -> Unit) + val batteryInfoCallback = args[4] as ((BatteryInfo) -> Unit) batteryInfoCallback.invoke(expectedBatteryInfo) } - scannerWrapper.setScannerInfoAndCheckAvailableOta() + scannerWrapper.setScannerInfoAndCheckAvailableOta(mockk()) val actualVersion = scannerWrapper.versionInformation() @@ -139,9 +140,9 @@ internal class ScannerWrapperV2Test { fun `should throw UnexpectedScannerException if setupScannerWithOtaCheck throws IllegalStateException`() = runTest { coEvery { - scannerInitialSetupHelper.setupScannerWithOtaCheck(any(), any(), any(), any()) + scannerInitialSetupHelper.setupScannerWithOtaCheck(any(), any(), any(), any(), any()) } throws IllegalStateException() - scannerWrapper.setScannerInfoAndCheckAvailableOta() + scannerWrapper.setScannerInfoAndCheckAvailableOta(mockk()) } 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 cc380b8247..1d7c092d25 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 @@ -94,16 +94,19 @@ internal data class OldProjectConfig( if (faceQualityThreshold == null) null else FaceConfiguration( - nbOfImagesToCapture = faceNbOfFramesCaptured?.toIntOrNull() - ?: DEFAULT_FACE_FRAMES_TO_CAPTURE, - qualityThreshold = faceQualityThreshold.toInt(), - imageSavingStrategy = if (saveFaceImages.toBoolean()) { - FaceConfiguration.ImageSavingStrategy.ONLY_USED_IN_REFERENCE - } else { - FaceConfiguration.ImageSavingStrategy.NEVER - }, - decisionPolicy = faceConfidenceThresholds?.let { parseDecisionPolicy(it) } - ?: DecisionPolicy(0, 0, 0), + allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE), + rankOne = FaceConfiguration.FaceSdkConfiguration( + nbOfImagesToCapture = faceNbOfFramesCaptured?.toIntOrNull() + ?: DEFAULT_FACE_FRAMES_TO_CAPTURE, + qualityThreshold = faceQualityThreshold.toInt(), + imageSavingStrategy = if (saveFaceImages.toBoolean()) { + FaceConfiguration.ImageSavingStrategy.ONLY_USED_IN_REFERENCE + } else { + FaceConfiguration.ImageSavingStrategy.NEVER + }, + decisionPolicy = faceConfidenceThresholds?.let { parseDecisionPolicy(it) } + ?: DecisionPolicy(0, 0, 0), + ), ) private fun fingerprintConfiguration(): FingerprintConfiguration? = 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 209259b80a..684023928d 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 @@ -5,11 +5,26 @@ 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() + +internal fun FaceConfiguration.BioSdk.toProto() = when (this) { + FaceConfiguration.BioSdk.RANK_ONE -> ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE +} + +internal fun FaceConfiguration.FaceSdkConfiguration.toProto() = + ProtoFaceConfiguration.ProtoFaceSdkConfiguration.newBuilder() .setNbOfImagesToCapture(nbOfImagesToCapture) .setQualityThreshold(qualityThreshold) .setImageSavingStrategy(imageSavingStrategy.toProto()) .setDecisionPolicy(decisionPolicy.toProto()) - .build() + .also { + if (allowedAgeRange != null) it.allowedAgeRange = allowedAgeRange.toProto() + if (verificationMatchThreshold != null) it.verificationMatchThreshold = verificationMatchThreshold + }.build() internal fun FaceConfiguration.ImageSavingStrategy.toProto(): ProtoFaceConfiguration.ImageSavingStrategy = when (this) { @@ -20,10 +35,24 @@ internal fun FaceConfiguration.ImageSavingStrategy.toProto(): ProtoFaceConfigura internal fun ProtoFaceConfiguration.toDomain(): FaceConfiguration = FaceConfiguration( - nbOfImagesToCapture, - qualityThreshold, - imageSavingStrategy.toDomain(), - decisionPolicy.toDomain(), + allowedSDKs = allowedSdksList.map { it.toDomain() }, + if (hasRankOne()) rankOne.toDomain() else null, + ) + +@Suppress("SameReturnValue") +internal fun ProtoFaceConfiguration.ProtoBioSdk.toDomain() = when (this) { + ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE -> FaceConfiguration.BioSdk.RANK_ONE + ProtoFaceConfiguration.ProtoBioSdk.UNRECOGNIZED -> FaceConfiguration.BioSdk.RANK_ONE +} + +internal fun ProtoFaceConfiguration.ProtoFaceSdkConfiguration.toDomain() = + FaceConfiguration.FaceSdkConfiguration( + nbOfImagesToCapture = nbOfImagesToCapture, + qualityThreshold = qualityThreshold, + imageSavingStrategy = imageSavingStrategy.toDomain(), + decisionPolicy = decisionPolicy.toDomain(), + if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null, + if (hasVerificationMatchThreshold()) verificationMatchThreshold else null ) internal fun ProtoFaceConfiguration.ImageSavingStrategy.toDomain(): FaceConfiguration.ImageSavingStrategy = diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt index 839e1106da..5123cc098d 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/FingerprintConfiguration.kt @@ -23,8 +23,8 @@ internal fun FingerprintConfiguration.FingerprintSdkConfiguration.toProto() = .also { if (vero1 != null) it.vero1 = vero1.toProto() if (vero2 != null) it.vero2 = vero2.toProto() - if (allowedAgeRange != null) - it.allowedAgeRange = allowedAgeRange.toProto() + if (allowedAgeRange != null) it.allowedAgeRange = allowedAgeRange.toProto() + if (verificationMatchThreshold != null) it.verificationMatchThreshold = verificationMatchThreshold }.build() @@ -90,7 +90,8 @@ internal fun ProtoFingerprintConfiguration.ProtoFingerprintSdkConfiguration.toDo comparisonStrategyForVerification.toDomain(), if (hasVero1()) vero1.toDomain() else null, if (hasVero2()) vero2.toDomain() else null, - if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null + if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null, + if (hasVerificationMatchThreshold()) verificationMatchThreshold else null, ) @@ -110,10 +111,11 @@ internal fun ProtoFingerprintConfiguration.FingerComparisonStrategy.toDomain() = ) } -internal fun ProtoAllowedAgeRange.toDomain() = AgeGroup(startInclusive, endExclusive) +internal fun ProtoAllowedAgeRange.toDomain() = AgeGroup(startInclusive, if (hasEndExclusive()) endExclusive else null) -internal fun AgeGroup.toProto() = - ProtoAllowedAgeRange.newBuilder().setStartInclusive(startInclusive).let { builder -> - endExclusive?.let { builder.setEndExclusive(it) } - builder.build() +internal fun AgeGroup.toProto() = ProtoAllowedAgeRange.newBuilder() + .also { + it.setStartInclusive(startInclusive) + if (endExclusive != null) it.setEndExclusive(endExclusive) } + .build() diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt index 8a7aa284ea..874b8a57d2 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt @@ -6,4 +6,15 @@ data class AgeGroup( ) { fun isEmpty(): Boolean = startInclusive == 0 && (endExclusive == null || endExclusive == 0) -} + + fun includes(age: Int): Boolean { + val endExclusive = endExclusive ?: Int.MAX_VALUE + return age in startInclusive until endExclusive + } + + fun contains(otherRange: AgeGroup): Boolean { + val thisEndExclusive = this.endExclusive ?: Int.MAX_VALUE + val otherEndExclusive = otherRange.endExclusive ?: Int.MAX_VALUE + return startInclusive <= otherRange.startInclusive && otherEndExclusive <= thisEndExclusive + } +} \ No newline at end of file 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 372ae6c5c1..932af9fa07 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 @@ -1,12 +1,35 @@ package com.simprints.infra.config.store.models data class FaceConfiguration( - val nbOfImagesToCapture: Int, - val qualityThreshold: Int, - val imageSavingStrategy: ImageSavingStrategy, - val decisionPolicy: DecisionPolicy, + val allowedSDKs: List, + val rankOne: FaceSdkConfiguration?, ) { + val nbOfImagesToCapture: Int + get() = rankOne?.nbOfImagesToCapture!! + + val qualityThreshold: Int + get() = rankOne?.qualityThreshold!! + + val imageSavingStrategy: ImageSavingStrategy + get() = rankOne?.imageSavingStrategy!! + + val decisionPolicy: DecisionPolicy + get() = rankOne?.decisionPolicy!! + + data class FaceSdkConfiguration( + val nbOfImagesToCapture: Int, + val qualityThreshold: Int, + val imageSavingStrategy: ImageSavingStrategy, + val decisionPolicy: DecisionPolicy, + val allowedAgeRange: AgeGroup? = null, + val verificationMatchThreshold: Float? = null, + ) + + enum class BioSdk { + RANK_ONE, + } + enum class ImageSavingStrategy { NEVER, ONLY_USED_IN_REFERENCE, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt index 2b1666da55..14c8beeabb 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FingerprintConfiguration.kt @@ -15,6 +15,7 @@ data class FingerprintConfiguration( val vero1: Vero1Configuration? = null, val vero2: Vero2Configuration? = null, val allowedAgeRange: AgeGroup? = null, + val verificationMatchThreshold: Float? = null, ) enum class VeroGeneration { @@ -32,14 +33,8 @@ data class FingerprintConfiguration( CROSS_FINGER_USING_MEAN_OF_MAX; } - // Todo we didn't yet implement the logic to select the SDK based on the configuration - // so we are just using secugenSimMatcher if it is not null or nec otherwise - // See ticket SIM-81 for more details - val bioSdkConfiguration: FingerprintSdkConfiguration - get() = when { - secugenSimMatcher != null -> secugenSimMatcher - nec != null -> nec - else -> throw IllegalStateException("No active BioSdk") - } - + fun getSdkConfiguration(sdk: BioSdk): FingerprintSdkConfiguration? = when (sdk) { + BioSdk.SECUGEN_SIM_MATCHER -> secugenSimMatcher + BioSdk.NEC -> nec + } } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index f20035ddb7..6d3aa3c287 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt @@ -9,8 +9,7 @@ data class ProjectConfiguration( val consent: ConsentConfiguration, val identification: IdentificationConfiguration, val synchronization: SynchronizationConfiguration, -) { -} +) fun ProjectConfiguration.canCoSyncAllData(): Boolean = synchronization.up.coSync.kind == UpSynchronizationConfiguration.UpSynchronizationKind.ALL @@ -44,8 +43,10 @@ fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = fun ProjectConfiguration.allowedAgeRanges(): List { return listOf( - //Todo add face roc sdk , + face?.rankOne?.allowedAgeRange, fingerprint?.secugenSimMatcher?.allowedAgeRange, fingerprint?.nec?.allowedAgeRange ).filterNotNull().filterNot { it.isEmpty() } } + +fun ProjectConfiguration.isAgeRestricted() = allowedAgeRanges().isNotEmpty() diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAgeGroup.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAgeGroup.kt deleted file mode 100644 index d7f674652e..0000000000 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAgeGroup.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.simprints.infra.config.store.remote.models - -import androidx.annotation.Keep -import com.simprints.infra.config.store.models.AgeGroup - -@Keep -internal data class ApiAgeGroup( - val startInclusive: Int?, - val endExclusive: Int?, -) { - fun toDomain() = AgeGroup(startInclusive ?: 0, endExclusive) -} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt new file mode 100644 index 0000000000..ec0c6bee81 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRange.kt @@ -0,0 +1,17 @@ +package com.simprints.infra.config.store.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.AgeGroup + +@Keep +data class ApiAllowedAgeRange( + val startInclusive: Int?, + val endExclusive: Int?, +) { + + fun toDomain() = + // When allowedAgeRange is disabled the API returns an empty object + // which is then parsed as {null, null} + if (startInclusive == null && endExclusive == null) null + else AgeGroup(startInclusive ?: 0, endExclusive) +} 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 ae8b2b933f..bdcb6969e5 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 @@ -5,20 +5,44 @@ import com.simprints.infra.config.store.models.FaceConfiguration @Keep internal data class ApiFaceConfiguration( - val nbOfImagesToCapture: Int, - val qualityThreshold: Int, - val imageSavingStrategy: ImageSavingStrategy, - val decisionPolicy: ApiDecisionPolicy, + val allowedSDKs: List, + val rankOne: ApiFaceSdkConfiguration, ) { fun toDomain(): FaceConfiguration = FaceConfiguration( - nbOfImagesToCapture, - qualityThreshold, - imageSavingStrategy.toDomain(), - decisionPolicy.toDomain() + allowedSDKs = allowedSDKs.map { it.toDomain() }, + rankOne = rankOne.toDomain() ) + @Keep + data class ApiFaceSdkConfiguration( + val nbOfImagesToCapture: Int, + val qualityThreshold: Int, + val decisionPolicy: ApiDecisionPolicy, + val imageSavingStrategy: ImageSavingStrategy, + val allowedAgeRange: ApiAllowedAgeRange?, + val verificationMatchThreshold: Float?, + ) { + fun toDomain() = FaceConfiguration.FaceSdkConfiguration( + nbOfImagesToCapture = nbOfImagesToCapture, + qualityThreshold = qualityThreshold, + decisionPolicy = decisionPolicy.toDomain(), + imageSavingStrategy = imageSavingStrategy.toDomain(), + allowedAgeRange = allowedAgeRange?.toDomain(), + verificationMatchThreshold = verificationMatchThreshold + ) + } + + @Keep + enum class BioSdk { + RANK_ONE; + + fun toDomain() = when (this) { + RANK_ONE -> FaceConfiguration.BioSdk.RANK_ONE + } + } + @Keep enum class ImageSavingStrategy { NEVER, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt index 2d6f953d86..14f8ce516c 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt @@ -28,15 +28,17 @@ internal data class ApiFingerprintConfiguration( val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: ApiVero1Configuration? = null, val vero2: ApiVero2Configuration? = null, - val allowedAgeRange: ApiAgeGroup, + val allowedAgeRange: ApiAllowedAgeRange? = null, + val verificationMatchThreshold: Float? = null, ) { fun toDomain() = FingerprintConfiguration.FingerprintSdkConfiguration( - fingersToCapture.map { it.toDomain() }, - decisionPolicy.toDomain(), - comparisonStrategyForVerification.toDomain(), - vero1?.toDomain(), - vero2?.toDomain(), - allowedAgeRange.toDomain(), + fingersToCapture = fingersToCapture.map { it.toDomain() }, + decisionPolicy = decisionPolicy.toDomain(), + comparisonStrategyForVerification = comparisonStrategyForVerification.toDomain(), + vero1 = vero1?.toDomain(), + vero2 = vero2?.toDomain(), + allowedAgeRange = allowedAgeRange?.toDomain(), + verificationMatchThreshold = verificationMatchThreshold, ) } @@ -99,5 +101,4 @@ internal data class ApiFingerprintConfiguration( CROSS_FINGER_USING_MEAN_OF_MAX -> FingerprintConfiguration.FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX } } - } diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index ea7726b06a..ecc8b4d666 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -29,16 +29,32 @@ message ProtoGeneralConfiguration { } message ProtoFaceConfiguration { - int32 nb_of_images_to_capture = 1; - int32 quality_threshold = 2; - ImageSavingStrategy image_saving_strategy = 3; - ProtoDecisionPolicy decision_policy = 4; + int32 nb_of_images_to_capture = 1; // TODO: remove this field after migration to 2024.2.0 + int32 quality_threshold = 2; // TODO: remove this field after migration to 2024.2.0 + ImageSavingStrategy image_saving_strategy = 3; // TODO: remove this field after migration to 2024.2.0 + ProtoDecisionPolicy decision_policy = 4; // TODO: remove this field after migration to 2024.2.0 + + repeated ProtoBioSdk allowed_sdks = 5; + optional ProtoFaceSdkConfiguration rank_one = 6; + + enum ProtoBioSdk { + RANK_ONE = 0; + } enum ImageSavingStrategy { NEVER = 0; ONLY_GOOD_SCAN = 1; ONLY_USED_IN_REFERENCE = 2; } + + message ProtoFaceSdkConfiguration{ + int32 nb_of_images_to_capture = 1; + int32 quality_threshold = 2; + ImageSavingStrategy image_saving_strategy = 3; + ProtoDecisionPolicy decision_policy = 4; + optional ProtoAllowedAgeRange allowed_age_range = 5; + optional float verification_match_threshold = 6; + } } message ProtoFingerprintConfiguration { @@ -65,6 +81,7 @@ message ProtoFingerprintConfiguration { SAME_FINGER = 0; CROSS_FINGER_USING_MEAN_OF_MAX = 1; } + enum ProtoBioSdk { SECUGEN_SIM_MATCHER = 0; NEC = 1; @@ -77,13 +94,15 @@ message ProtoFingerprintConfiguration { optional ProtoVero2Configuration vero_2 = 4; optional ProtoVero1Configuration vero_1 = 5; optional ProtoAllowedAgeRange allowed_age_range = 6; - + optional float verification_match_threshold = 7; } } + message ProtoAllowedAgeRange { int32 startInclusive = 1; optional int32 endExclusive = 2; } + message ProtoVero1Configuration { int32 quality_threshold = 1; } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt index fde7c07652..4d6ccc9fd4 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt @@ -645,20 +645,30 @@ class ProjectConfigSharedPrefsMigrationTest { "{\"FaceMatchThreshold\":30, \"FaceConfidenceThresholds\":\"{\\\"LOW\\\":\\\"1\\\",\\\"MEDIUM\\\":\\\"20\\\",\\\"HIGH\\\":\\\"100\\\"}\",\"FaceNbOfFramesCaptured\":\"2\",\"FaceQualityThreshold\":\"-1\",\"SaveFaceImages\":\"true\"}" ) private val PROTO_FACE_CONFIGURATION = ProtoFaceConfiguration.newBuilder() - .setNbOfImagesToCapture(2) - .setQualityThreshold(-1) - .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.ONLY_USED_IN_REFERENCE) - .setDecisionPolicy( - ProtoDecisionPolicy.newBuilder().setLow(1).setMedium(20).setHigh(100).build() + .addAllowedSdks(ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE) + .setRankOne( + ProtoFaceConfiguration.ProtoFaceSdkConfiguration.newBuilder() + .setNbOfImagesToCapture(2) + .setQualityThreshold(-1) + .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.ONLY_USED_IN_REFERENCE) + .setDecisionPolicy( + ProtoDecisionPolicy.newBuilder().setLow(1).setMedium(20).setHigh(100).build() + ) + .build() ) .build() private val PROTO_FACE_DEFAULT_CONFIGURATION = ProtoFaceConfiguration.newBuilder() - .setNbOfImagesToCapture(2) - .setQualityThreshold(-1) - .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER) - .setDecisionPolicy( - ProtoDecisionPolicy.newBuilder().setLow(0).setMedium(0).setHigh(0).build() + .addAllowedSdks(ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE) + .setRankOne( + ProtoFaceConfiguration.ProtoFaceSdkConfiguration.newBuilder() + .setNbOfImagesToCapture(2) + .setQualityThreshold(-1) + .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER) + .setDecisionPolicy( + ProtoDecisionPolicy.newBuilder().setLow(0).setMedium(0).setHigh(0).build() + ) + .build() ) .build() diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt new file mode 100644 index 0000000000..3812991234 --- /dev/null +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/AgeGroupTest.kt @@ -0,0 +1,129 @@ +package com.simprints.infra.config.store.models + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AgeGroupTest { + + @Test + fun `should return empty when the age group is 0, 0`() { + val ageGroup = AgeGroup(0, 0) + assertTrue(ageGroup.isEmpty()) + } + @Test + fun `should return empty when the age group is 0, null`() { + val ageGroup = AgeGroup(0, null) + assertTrue(ageGroup.isEmpty()) + } + + @Test + fun `should return not empty when the age group is 0, 1`() { + val ageGroup = AgeGroup(0, 1) + assertFalse(ageGroup.isEmpty()) + } + + @Test + fun `should return not empty when the age group is 1, 0`() { + val ageGroup = AgeGroup(1, 0) + assertFalse(ageGroup.isEmpty()) + } + + @Test + fun `should return not empty when the age group is 1, null`() { + val ageGroup = AgeGroup(1, null) + assertFalse(ageGroup.isEmpty()) + } + + @Test + fun `should return true when the age is included in the age group`() { + val ageGroup = AgeGroup(0, 10) + assertTrue(ageGroup.includes(5)) + } + + @Test + fun `should return false when the age is not included in the age group`() { + val ageGroup = AgeGroup(0, 10) + assertFalse(ageGroup.includes(15)) + } + + @Test + fun `should return true when endExclusive is null and age is greater than startInclusive`() { + val ageGroup = AgeGroup(5, null) + assertTrue(ageGroup.includes(10)) + } + + @Test + fun `should return true when endExclusive is null and age is equal to startInclusive`() { + val ageGroup = AgeGroup(5, null) + assertTrue(ageGroup.includes(5)) + } + + @Test + fun `should return false when endExclusive is null and age is less than startInclusive`() { + val ageGroup = AgeGroup(5, null) + assertFalse(ageGroup.includes(4)) + } + + @Test + fun `should return false when endExclusive is not null and age is equal to endExclusive`() { + val ageGroup = AgeGroup(0, 10) + assertFalse(ageGroup.includes(10)) + } + + @Test + fun `should return true when the age group contains the other age group`() { + val ageGroup = AgeGroup(0, 10) + val otherAgeGroup = AgeGroup(5, 8) + assertTrue(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return true when the age group contains the other age group 2`() { + val ageGroup = AgeGroup(0, 10) + val otherAgeGroup = AgeGroup(0, 9) + assertTrue(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return true when the age group is the same as the other age group`() { + val ageGroup = AgeGroup(0, 10) + val otherAgeGroup = AgeGroup(0, 10) + assertTrue(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return false when the age group does not contain the other age group`() { + val ageGroup = AgeGroup(0, 10) + val otherAgeGroup = AgeGroup(5, 15) + assertFalse(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return false when otherRange endExclusive is null and is contained within ageGroup`() { + val ageGroup = AgeGroup(5, 10) + val otherAgeGroup = AgeGroup(7, null) + assertFalse(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return true when otherRange endExclusive is null and is contained within ageGroup`() { + val ageGroup = AgeGroup(0, null) + val otherAgeGroup = AgeGroup(5, null) + assertTrue(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return false when otherRange startInclusive is less than startInclusive and otherRange endExclusive is within ageGroup range`() { + val ageGroup = AgeGroup(5, 10) + val otherAgeGroup = AgeGroup(4, 9) + assertFalse(ageGroup.contains(otherAgeGroup)) + } + + @Test + fun `should return false when otherRange startInclusive is within ageGroup range and otherRange endExclusive is greater than endExclusive`() { + val ageGroup = AgeGroup(5, 10) + val otherAgeGroup = AgeGroup(6, 11) + assertFalse(ageGroup.contains(otherAgeGroup)) + } +} 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 c322b28b3b..a905024d50 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 @@ -6,7 +6,7 @@ import org.junit.Test class FingerprintConfigurationTest { @Test - fun `should retrieve secugenSimMatcher from bioSdkConfiguration if secugenSimMatcher not null `() { + fun `should retrieve SecugenSimMatcher's configuration when SECUGEN_SIM_MATCHER is requested `() { val fingerprintConfiguration = FingerprintConfiguration( allowedScanners = listOf(FingerprintConfiguration.VeroGeneration.VERO_1), @@ -21,12 +21,12 @@ class FingerprintConfigurationTest { ), nec = null, ) - Truth.assertThat(fingerprintConfiguration.bioSdkConfiguration) + Truth.assertThat(fingerprintConfiguration.getSdkConfiguration(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER)) .isEqualTo(fingerprintConfiguration.secugenSimMatcher) } @Test - fun `should retrieve nec from bioSdkConfiguration if nec not null `() { + fun `should retrieve NEC's configuration when NEC is requested `() { val fingerprintConfiguration = FingerprintConfiguration( allowedScanners = listOf(FingerprintConfiguration.VeroGeneration.VERO_1), @@ -41,22 +41,7 @@ class FingerprintConfigurationTest { vero2 = null, ), ) - Truth.assertThat(fingerprintConfiguration.bioSdkConfiguration) + Truth.assertThat(fingerprintConfiguration.getSdkConfiguration(FingerprintConfiguration.BioSdk.NEC)) .isEqualTo(fingerprintConfiguration.nec) } - - @Test(expected = IllegalStateException::class) - fun `should throw IllegalStateException if nec and secugenSimMatcher are null `() { - - val fingerprintConfiguration = FingerprintConfiguration( - allowedScanners = listOf(FingerprintConfiguration.VeroGeneration.VERO_1), - allowedSDKs = listOf(FingerprintConfiguration.BioSdk.NEC), - displayHandIcons = true, - secugenSimMatcher = null, - nec = null, - ) - fingerprintConfiguration.bioSdkConfiguration - } - - } 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 b3a96ca901..c963aa355d 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 @@ -10,7 +10,10 @@ import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.Up import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.NONE 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.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 @@ -238,4 +241,115 @@ class ProjectConfigurationTest { assertThat(config.imagesUploadRequiresUnmeteredConnection()).isEqualTo(it) } } + + @Test + fun `allowedAgeRanges returns all non-null age ranges`() { + val faceAgeRange = AgeGroup(10, 20) + val secugenSimMatcherAgeRange = AgeGroup(20, 30) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy( + rankOne = faceConfiguration.rankOne?.copy( + allowedAgeRange = faceAgeRange + ) + ), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = secugenSimMatcherAgeRange + ), + nec = null + ) + ) + + // Act + val result = projectConfiguration.allowedAgeRanges() + + // Assert + assertThat(result).containsExactly(faceAgeRange, secugenSimMatcherAgeRange) + } + + @Test + fun `allowedAgeRanges does not return empty age ranges`() { + val faceAgeRange = AgeGroup(10, 20) + val emptyAgeRange = AgeGroup(0, 0) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy( + rankOne = faceConfiguration.rankOne?.copy( + allowedAgeRange = faceAgeRange + ) + ), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy( + allowedAgeRange = emptyAgeRange + ), + nec = null + ) + ) + + // Act + val result = projectConfiguration.allowedAgeRanges() + + // Assert + assertThat(result).containsExactly(faceAgeRange) + } + + @Test + fun `isAgeRestricted should return false when all are null`() { + // Arrange + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = null)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = null), + nec = null + ) + ) + + // Act + val result = projectConfiguration.isAgeRestricted() + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `isAgeRestricted should return false when all age ranges are empty`() { + // Arrange + val emptyAgeRange = AgeGroup(0, 0) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = emptyAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = emptyAgeRange), + nec = null + ) + ) + + // Act + val result = projectConfiguration.isAgeRestricted() + + // Assert + assertThat(result).isFalse() + } + + @Test + fun `isAgeRestricted should return true when all age ranges are non-empty`() { + // Arrange + val faceAgeRange = AgeGroup(10, 20) + val secugenSimMatcherAgeRange = AgeGroup(20, 30) + + val projectConfiguration = projectConfiguration.copy( + face = faceConfiguration.copy(rankOne = rankOneConfiguration.copy(allowedAgeRange = faceAgeRange)), + fingerprint = fingerprintConfiguration.copy( + secugenSimMatcher = fingerprintConfiguration.secugenSimMatcher?.copy(allowedAgeRange = secugenSimMatcherAgeRange), + nec = null + ) + ) + + // Act + val result = projectConfiguration.isAgeRestricted() + + // Assert + assertThat(result).isTrue() + } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt new file mode 100644 index 0000000000..6b5e9080c8 --- /dev/null +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiAllowedAgeRangeTest.kt @@ -0,0 +1,38 @@ +package com.simprints.infra.config.store.remote.models + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.models.AgeGroup +import org.junit.Test + +class ApiAllowedAgeRangeTest { + @Test + fun `should map correctly the model`() { + val apiAllowedAgeRange = ApiAllowedAgeRange(10, 20) + val expectedAgeGroup = AgeGroup(10, 20) + + assertThat(apiAllowedAgeRange.toDomain()).isEqualTo(expectedAgeGroup) + } + + @Test + fun `should return null when startInclusive and endExclusive are null`() { + val apiAllowedAgeRange = ApiAllowedAgeRange(null, null) + + assertThat(apiAllowedAgeRange.toDomain()).isNull() + } + + @Test + fun `should use zero as default value when startInclusive is null`() { + val apiAllowedAgeRange = ApiAllowedAgeRange(null, 20) + val expectedAgeGroup = AgeGroup(0, 20) + + assertThat(apiAllowedAgeRange.toDomain()).isEqualTo(expectedAgeGroup) + } + + @Test + fun `should map null endExclusive correctly`() { + val apiAllowedAgeRange = ApiAllowedAgeRange(0, null) + val expectedAgeGroup = AgeGroup(0, null) + + assertThat(apiAllowedAgeRange.toDomain()).isEqualTo(expectedAgeGroup) + } +} 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 c7d853ed33..b5488343f9 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 @@ -1,11 +1,14 @@ package com.simprints.infra.config.store.testtools import com.simprints.core.domain.tokenization.asTokenizableEncrypted +import com.simprints.infra.config.store.local.models.ProtoAllowedAgeRange import com.simprints.infra.config.store.local.models.ProtoConsentConfiguration import com.simprints.infra.config.store.local.models.ProtoDecisionPolicy import com.simprints.infra.config.store.local.models.ProtoDeviceConfiguration import com.simprints.infra.config.store.local.models.ProtoDownSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoFaceConfiguration +import com.simprints.infra.config.store.local.models.ProtoFinger +import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration import com.simprints.infra.config.store.local.models.ProtoProject @@ -13,35 +16,21 @@ import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoVero1Configuration import com.simprints.infra.config.store.local.models.ProtoVero2Configuration -import com.simprints.infra.config.store.local.models.toProto -import com.simprints.infra.config.store.models.ConsentConfiguration -import com.simprints.infra.config.store.models.DecisionPolicy -import com.simprints.infra.config.store.models.DeviceConfiguration -import com.simprints.infra.config.store.models.DeviceState -import com.simprints.infra.config.store.models.DownSynchronizationConfiguration -import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.GeneralConfiguration -import com.simprints.infra.config.store.models.IdentificationConfiguration -import com.simprints.infra.config.store.models.Project -import com.simprints.infra.config.store.models.ProjectConfiguration -import com.simprints.infra.config.store.models.ProjectState -import com.simprints.infra.config.store.models.SettingsPasswordConfig -import com.simprints.infra.config.store.models.SynchronizationConfiguration -import com.simprints.infra.config.store.models.TokenKeyType -import com.simprints.infra.config.store.models.UpSynchronizationConfiguration -import com.simprints.infra.config.store.models.Vero2Configuration -import com.simprints.infra.config.store.remote.models.ApiAgeGroup +import com.simprints.infra.config.store.models.* +import com.simprints.infra.config.store.remote.models.* +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.FaceConfiguration.FaceSdkConfiguration import com.simprints.infra.config.store.remote.models.ApiConsentConfiguration import com.simprints.infra.config.store.remote.models.ApiDecisionPolicy -import com.simprints.infra.config.store.remote.models.ApiDeviceState import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration +import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration.ApiFaceSdkConfiguration import com.simprints.infra.config.store.remote.models.ApiFingerprintConfiguration import com.simprints.infra.config.store.remote.models.ApiGeneralConfiguration import com.simprints.infra.config.store.remote.models.ApiIdentificationConfiguration import com.simprints.infra.config.store.remote.models.ApiProject import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration -import com.simprints.infra.config.store.remote.models.ApiProjectState import com.simprints.infra.config.store.remote.models.ApiSynchronizationConfiguration import com.simprints.infra.config.store.remote.models.ApiVero1Configuration import com.simprints.infra.config.store.remote.models.ApiVero2Configuration @@ -115,20 +104,48 @@ internal val protoConsentConfiguration = ProtoConsentConfiguration.newBuilder() ) .build() +internal val apiAllowedAgeRange = ApiAllowedAgeRange(2, 10) +internal val allowedAgeRange = AgeGroup(2, 10) +internal val protoAllowedAgeRange = ProtoAllowedAgeRange.newBuilder().setStartInclusive(2).setEndExclusive(10).build() + internal val apiDecisionPolicy = ApiDecisionPolicy(10, 30, 40) internal val decisionPolicy = DecisionPolicy(10, 30, 40) internal val protoDecisionPolicy = ProtoDecisionPolicy.newBuilder().setLow(10).setMedium(30).setHigh(40).build() +internal val rankOneConfiguration = FaceSdkConfiguration( + nbOfImagesToCapture = 2, + qualityThreshold = -1, + imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER, + decisionPolicy = decisionPolicy, + allowedAgeRange = null, + verificationMatchThreshold = null, +) -internal val apiFaceConfiguration = - ApiFaceConfiguration(2, -1, ApiFaceConfiguration.ImageSavingStrategy.NEVER, apiDecisionPolicy) -internal val faceConfiguration = - FaceConfiguration(2, -1, FaceConfiguration.ImageSavingStrategy.NEVER, decisionPolicy) +internal val apiFaceConfiguration = ApiFaceConfiguration( + allowedSDKs = listOf(ApiFaceConfiguration.BioSdk.RANK_ONE), + rankOne = ApiFaceSdkConfiguration( + nbOfImagesToCapture = 2, + qualityThreshold = -1, + decisionPolicy = apiDecisionPolicy, + imageSavingStrategy = ApiFaceConfiguration.ImageSavingStrategy.NEVER, + allowedAgeRange = null, + verificationMatchThreshold = null, + ) + ) +internal val faceConfiguration = FaceConfiguration( + allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE), + rankOne = rankOneConfiguration +) internal val protoFaceConfiguration = ProtoFaceConfiguration.newBuilder() - .setNbOfImagesToCapture(2) - .setQualityThreshold(-1) - .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER) - .setDecisionPolicy(protoDecisionPolicy) + .addAllowedSdks(ProtoFaceConfiguration.ProtoBioSdk.RANK_ONE) + .setRankOne( + ProtoFaceConfiguration.ProtoFaceSdkConfiguration.newBuilder() + .setNbOfImagesToCapture(2) + .setQualityThreshold(-1) + .setImageSavingStrategy(ProtoFaceConfiguration.ImageSavingStrategy.NEVER) + .setDecisionPolicy(protoDecisionPolicy) + .build() + ) .build() internal val apiVero2Configuration = ApiVero2Configuration( @@ -138,7 +155,6 @@ internal val apiVero2Configuration = ApiVero2Configuration( false, mapOf("E-1" to ApiVero2Configuration.ApiVero2FirmwareVersions("1.1", "1.2", "1.4")) ) -internal val apiAgeGroup = ApiAgeGroup(18, 65) internal val vero2Configuration = Vero2Configuration( 30, @@ -162,24 +178,53 @@ internal val protoVero2Configuration = ProtoVero2Configuration.newBuilder() .build() internal val apiFingerprintConfiguration = ApiFingerprintConfiguration( - listOf(ApiFingerprintConfiguration.VeroGeneration.VERO_2), - listOf(ApiFingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER), - true, - ApiFingerprintConfiguration.ApiFingerprintSdkConfiguration( + allowedScanners = listOf(ApiFingerprintConfiguration.VeroGeneration.VERO_2), + allowedSDKs = listOf(ApiFingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER), + displayHandIcons = true, + secugenSimMatcher = ApiFingerprintConfiguration.ApiFingerprintSdkConfiguration( listOf(ApiFingerprintConfiguration.Finger.LEFT_3RD_FINGER), apiDecisionPolicy, ApiFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, ApiVero1Configuration(10), apiVero2Configuration, - apiAgeGroup , - + apiAllowedAgeRange, + 42.0f, ), - null, + nec = null, ) -internal val fingerprintConfiguration = apiFingerprintConfiguration.toDomain() +internal val fingerprintConfiguration = FingerprintConfiguration( + allowedScanners = listOf(FingerprintConfiguration.VeroGeneration.VERO_2), + allowedSDKs = listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER), + displayHandIcons = true, + secugenSimMatcher = FingerprintConfiguration.FingerprintSdkConfiguration( + fingersToCapture = listOf(Finger.LEFT_3RD_FINGER), + decisionPolicy = decisionPolicy, + comparisonStrategyForVerification = FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, + vero1 = Vero1Configuration(qualityThreshold = 10), + vero2 = vero2Configuration, + allowedAgeRange = allowedAgeRange, + verificationMatchThreshold = 42.0f, + ), + nec = null, +) -internal val protoFingerprintConfiguration = fingerprintConfiguration.toProto() +internal val protoFingerprintConfiguration = ProtoFingerprintConfiguration.newBuilder() + .addAllowedScanners(ProtoFingerprintConfiguration.VeroGeneration.VERO_2) + .addAllowedSdks(ProtoFingerprintConfiguration.ProtoBioSdk.SECUGEN_SIM_MATCHER) + .setDisplayHandIcons(true) + .setSecugenSimMatcher( + ProtoFingerprintConfiguration.ProtoFingerprintSdkConfiguration.newBuilder() + .addFingersToCapture(ProtoFinger.LEFT_3RD_FINGER) + .setDecisionPolicy(protoDecisionPolicy) + .setComparisonStrategyForVerification(ProtoFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER) + .setVero1(ProtoVero1Configuration.newBuilder().setQualityThreshold(10).build()) + .setVero2(protoVero2Configuration) + .setAllowedAgeRange(protoAllowedAgeRange) + .setVerificationMatchThreshold(42.0f) + .build() + ) + .build() internal val apiGeneralConfiguration = ApiGeneralConfiguration( listOf(ApiGeneralConfiguration.Modality.FACE), @@ -389,6 +434,6 @@ internal val apiDeviceState = ApiDeviceState( ) internal val deviceState = DeviceState( "deviceId", - false, + false, null ) diff --git a/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt b/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt index 5cbc2d8acc..33d0f48536 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt @@ -22,5 +22,6 @@ enum class AppErrorReason { BACKEND_MAINTENANCE_ERROR, PROJECT_PAUSED, BLUETOOTH_NO_PERMISSION, - PROJECT_ENDING + PROJECT_ENDING, + AGE_GROUP_NOT_SUPPORTED, } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt index c086a3a09f..30e9186fbf 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt @@ -17,6 +17,7 @@ import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEven import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.LOGIN_NOT_COMPLETE import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.PROJECT_ENDING import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.PROJECT_PAUSED +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.AGE_GROUP_NOT_SUPPORTED import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.UNEXPECTED_ERROR import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason.SCANNER_LOW_BATTERY @@ -44,7 +45,8 @@ internal data class ApiErrorCallback(val reason: ApiReason) : ApiCallback(ApiCal LICENSE_INVALID, PROJECT_ENDING, PROJECT_PAUSED, - BLUETOOTH_NO_PERMISSION + BLUETOOTH_NO_PERMISSION, + AGE_GROUP_NOT_SUPPORTED, } } @@ -66,6 +68,7 @@ internal fun Reason.fromDomainToApi() = PROJECT_ENDING -> ApiReason.PROJECT_ENDING PROJECT_PAUSED -> ApiReason.PROJECT_PAUSED BLUETOOTH_NO_PERMISSION -> ApiReason.BLUETOOTH_NO_PERMISSION + AGE_GROUP_NOT_SUPPORTED -> ApiReason.AGE_GROUP_NOT_SUPPORTED } @@ -86,4 +89,5 @@ internal fun ApiReason.fromApiToDomain(): Reason = ApiReason.PROJECT_ENDING -> PROJECT_ENDING ApiReason.PROJECT_PAUSED -> PROJECT_PAUSED ApiReason.BLUETOOTH_NO_PERMISSION -> BLUETOOTH_NO_PERMISSION + ApiReason.AGE_GROUP_NOT_SUPPORTED -> AGE_GROUP_NOT_SUPPORTED } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt index 87ab3ced26..0266c26e35 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt @@ -10,7 +10,6 @@ import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEven class ApiErrorCallbackTest { - @Test fun `ApiReason correctly mapped to domain`() { mapOf( @@ -28,7 +27,8 @@ class ApiErrorCallbackTest { ApiErrorCallback.ApiReason.BACKEND_MAINTENANCE_ERROR to ErrorReason.BACKEND_MAINTENANCE_ERROR, ApiErrorCallback.ApiReason.PROJECT_ENDING to ErrorReason.PROJECT_ENDING, ApiErrorCallback.ApiReason.PROJECT_PAUSED to ErrorReason.PROJECT_PAUSED, - ApiErrorCallback.ApiReason.BLUETOOTH_NO_PERMISSION to ErrorReason.BLUETOOTH_NO_PERMISSION + ApiErrorCallback.ApiReason.BLUETOOTH_NO_PERMISSION to ErrorReason.BLUETOOTH_NO_PERMISSION, + ApiErrorCallback.ApiReason.AGE_GROUP_NOT_SUPPORTED to ErrorReason.AGE_GROUP_NOT_SUPPORTED, ).forEach { assertThat(it.key.fromApiToDomain()).isEqualTo(it.value) } @@ -53,6 +53,7 @@ class ApiErrorCallbackTest { ErrorReason.PROJECT_ENDING to ApiErrorCallback.ApiReason.PROJECT_ENDING, ErrorReason.PROJECT_PAUSED to ApiErrorCallback.ApiReason.PROJECT_PAUSED, ErrorReason.BLUETOOTH_NO_PERMISSION to ApiErrorCallback.ApiReason.BLUETOOTH_NO_PERMISSION, + ErrorReason.AGE_GROUP_NOT_SUPPORTED to ApiErrorCallback.ApiReason.AGE_GROUP_NOT_SUPPORTED, ).forEach { assertThat(it.key.fromDomainToApi()).isEqualTo(it.value) } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt index 3e701beba3..3d87d08fff 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt @@ -61,7 +61,8 @@ data class ErrorCallbackEvent( PROJECT_ENDING, PROJECT_PAUSED, BLUETOOTH_NO_PERMISSION, - FACE_CONFIGURATION_ERROR; + FACE_CONFIGURATION_ERROR, + AGE_GROUP_NOT_SUPPORTED; companion object { @@ -84,6 +85,7 @@ data class ErrorCallbackEvent( AppErrorReason.ROOTED_DEVICE -> throw Throwable("Can't convert from rooted device") AppErrorReason.PROJECT_ENDING -> PROJECT_ENDING AppErrorReason.BLUETOOTH_NO_PERMISSION -> BLUETOOTH_NO_PERMISSION + AppErrorReason.AGE_GROUP_NOT_SUPPORTED -> AGE_GROUP_NOT_SUPPORTED } } } diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt index 2078f68d63..9863764271 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt @@ -57,6 +57,7 @@ class ErrorCallbackEventTest { AppErrorReason.PROJECT_PAUSED to ErrorReason.PROJECT_PAUSED, AppErrorReason.PROJECT_ENDING to ErrorReason.PROJECT_ENDING, AppErrorReason.BLUETOOTH_NO_PERMISSION to ErrorReason.BLUETOOTH_NO_PERMISSION, + AppErrorReason.AGE_GROUP_NOT_SUPPORTED to ErrorReason.AGE_GROUP_NOT_SUPPORTED, ).forEach { assertThat(fromAppResponseErrorReasonToEventReason(it.key)).isEqualTo(it.value) } diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt index ac49583ac9..09d2ec0775 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt @@ -3,9 +3,11 @@ package com.simprints.infra.orchestration.data import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.tokenization.TokenizableString import java.io.Serializable +@ExcludedFromGeneratedTestCoverageReports("This is a simple data class") @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -25,6 +27,14 @@ sealed class ActionRequest( open val unknownExtras: Map, ) : Serializable { + fun getSubjectAgeIfAvailable(): Int? = + when (this) { + is EnrolActionRequest -> subjectAge + is IdentifyActionRequest -> subjectAge + is VerifyActionRequest ->subjectAge + else -> null + } + @Keep data class EnrolActionRequest( override val actionIdentifier: ActionRequestIdentifier, @@ -32,6 +42,7 @@ sealed class ActionRequest( override val userId: TokenizableString, override val moduleId: TokenizableString, val biometricDataSource: String, + val subjectAge: Int? = null, val callerPackageName: String, val metadata: String, override val unknownExtras: Map, @@ -44,6 +55,7 @@ sealed class ActionRequest( override val userId: TokenizableString, override val moduleId: TokenizableString, val biometricDataSource: String, + val subjectAge: Int? = null, val callerPackageName: String, val metadata: String, override val unknownExtras: Map, @@ -56,6 +68,7 @@ sealed class ActionRequest( override val userId: TokenizableString, override val moduleId: TokenizableString, val biometricDataSource: String, + val subjectAge: Int? = null, val callerPackageName: String, val metadata: String, val verifyGuid: String, 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 8eb0d9cf24..a2f26f20da 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 @@ -3,7 +3,6 @@ package com.simprints.infra.sync.config.testtools import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.models.ConsentConfiguration import com.simprints.infra.config.store.models.DecisionPolicy -import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.config.store.models.DownSynchronizationConfiguration import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.config.store.models.Finger @@ -38,7 +37,15 @@ internal val vero2Configuration = Vero2Configuration( mapOf("E-1" to Vero2Configuration.Vero2FirmwareVersions("1.1", "1.2", "1.4")) ) internal val faceConfiguration = - FaceConfiguration(2, -1, FaceConfiguration.ImageSavingStrategy.NEVER, decisionPolicy) + FaceConfiguration( + allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE), + rankOne = FaceConfiguration.FaceSdkConfiguration( + nbOfImagesToCapture = 2, + qualityThreshold = -1, + imageSavingStrategy = FaceConfiguration.ImageSavingStrategy.NEVER, + decisionPolicy = decisionPolicy + ) + ) internal val fingerprintConfiguration = FingerprintConfiguration( allowedScanners = listOf(FingerprintConfiguration.VeroGeneration.VERO_2), @@ -126,11 +133,4 @@ internal val project = Project( imageBucket = "url", baseUrl = "baseUrl", tokenizationKeys = tokenizationKeysDomain -) - -internal val deviceConfiguration = - DeviceConfiguration( - "en", - listOf("module1".asTokenizableEncrypted(), "module2".asTokenizableEncrypted()), - "instruction" - ) +) \ No newline at end of file