diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 519b5ce60b..7e7f478798 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -61,6 +61,7 @@ jobs: feature:login-check feature:alert feature:exit-form + feature:select-subject-age-group reportsId: feature1 feature-unit-tests2: diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index ccfadaf454..61614a46f8 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -13,10 +13,9 @@ extra.apply { * CI. Read more about our versioning here: * https://simprints.atlassian.net/wiki/spaces/KB/pages/1761378305/Releasing+Simprints+ID * - * Dev version >= 2023.4.1 is required for receiving encryption Tokens from BFSID [CORE-2502] - * Dev version >= 2023.4.0 is required for receiving new fingerprint configurations [CORE-3033] + * Dev version >= 2024.2.1 is required for receiving biometric sdk age restrictions */ - set("VERSION_NAME", "2024.1.1") + set("VERSION_NAME", "2024.2.1") /** * Build type. The version code describes which build type was used for the build. diff --git a/build-logic/proguard-rules.pro b/build-logic/proguard-rules.pro index 1b2c8d836e..76e755116e 100644 --- a/build-logic/proguard-rules.pro +++ b/build-logic/proguard-rules.pro @@ -1,5 +1,2 @@ # Dont warn about the missing files during the obfuscation --dontwarn java.lang.invoke.StringConcatFactory --dontwarn com.simprints.infra.resources.R$string --dontwarn com.simprints.infra.resources.R$color --dontwarn com.simprints.infra.resources.R$drawable +-dontwarn com.simprints.** diff --git a/feature/orchestrator/build.gradle.kts b/feature/orchestrator/build.gradle.kts index 8143a2ff92..ac0a8aa4b6 100644 --- a/feature/orchestrator/build.gradle.kts +++ b/feature/orchestrator/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":feature:exit-form")) implementation(project(":feature:matcher")) implementation(project(":feature:validate-subject-pool")) + implementation(project(":feature:select-subject-age-group")) implementation(project(":face:capture")) diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt index 97a0422b96..bf02fd574e 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt @@ -24,6 +24,7 @@ import com.simprints.feature.login.LoginContract import com.simprints.feature.login.LoginResult import com.simprints.feature.logincheck.LoginCheckViewModel import com.simprints.feature.orchestrator.cache.OrchestratorCache +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupContract import com.simprints.feature.selectsubject.SelectSubjectContract import com.simprints.feature.setup.SetupContract import com.simprints.feature.validatepool.ValidateSubjectPoolContract @@ -116,6 +117,7 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { handleResult(FingerprintCaptureContract.DESTINATION, orchestratorVm::handleResult) handleResult(FetchSubjectContract.DESTINATION, orchestratorVm::handleResult) handleResult(ValidateSubjectPoolContract.DESTINATION, orchestratorVm::handleResult) + handleResult(SelectSubjectAgeGroupContract.DESTINATION, orchestratorVm::handleResult) } private fun handleResult(destination: Int, block: (T) -> Unit) { diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt index e16465a9c1..04c69a16b8 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/Step.kt @@ -12,6 +12,7 @@ import com.simprints.feature.enrollast.EnrolLastBiometricResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.fetchsubject.FetchSubjectResult import com.simprints.feature.login.LoginResult +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.selectsubject.SelectSubjectResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolResult @@ -56,6 +57,7 @@ import java.io.Serializable JsonSubTypes.Type(value = AlertResult::class, name = "AlertResult"), JsonSubTypes.Type(value = ExitFormResult::class, name = "ExitFormResult"), JsonSubTypes.Type(value = ValidateSubjectPoolResult::class, name = "ValidateSubjectPoolResult"), + JsonSubTypes.Type(value = SelectSubjectAgeGroupResult::class, name = "SelectSubjectAgeResult"), ) abstract class SerializableMixin : Serializable diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt index 835cd3b969..64c6d1c739 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/StepId.kt @@ -16,6 +16,7 @@ internal object StepId { const val ENROL_LAST_BIOMETRIC = STEP_BASE_CORE + 4 const val CONFIRM_IDENTITY = STEP_BASE_CORE + 5 const val VALIDATE_ID_POOL = STEP_BASE_CORE + 6 + const val SELECT_SUBJECT_AGE = STEP_BASE_CORE + 7 // Face step ids private const val STEP_BASE_FINGERPRINT = 300 @@ -24,7 +25,6 @@ internal object StepId { // Face step ids private const val STEP_BASE_FACE = 500 - const val FACE_CONFIGURATION = STEP_BASE_FACE + 1 const val FACE_CAPTURE = STEP_BASE_FACE + 2 const val FACE_MATCHER = STEP_BASE_FACE + 3 } 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 1d937fd3ec..39915fe78a 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 @@ -14,12 +14,14 @@ 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.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.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.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery @@ -27,6 +29,7 @@ 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, @@ -38,6 +41,7 @@ internal class BuildStepsUseCase @Inject constructor( is ActionRequest.EnrolActionRequest -> listOf( buildSetupStep(), buildConsentStep(ConsentType.ENROL), + buildAgeSelectionStep(action, projectConfiguration), buildModalityCaptureSteps( projectConfiguration, FlowType.ENROL, @@ -58,6 +62,7 @@ internal class BuildStepsUseCase @Inject constructor( listOf( buildSetupStep(), buildValidateIdPoolStep(subjectQuery), + buildAgeSelectionStep(action, projectConfiguration), buildConsentStep(ConsentType.IDENTIFY), buildModalityCaptureSteps( projectConfiguration, @@ -74,6 +79,7 @@ internal class BuildStepsUseCase @Inject constructor( is ActionRequest.VerifyActionRequest -> listOf( buildSetupStep(), + buildAgeSelectionStep(action, projectConfiguration), buildFetchGuidStep(action.projectId, action.verifyGuid), buildConsentStep(ConsentType.VERIFY), buildModalityCaptureSteps( @@ -97,26 +103,51 @@ internal class BuildStepsUseCase @Inject constructor( ) }.flatten() - private fun buildSetupStep() = listOf(Step( - id = StepId.SETUP, - navigationActionId = R.id.action_orchestratorFragment_to_setup, - destinationId = SetupContract.DESTINATION, - payload = bundleOf(), - )) + private fun buildAgeSelectionStep( + action: ActionRequest, + projectConfiguration: ProjectConfiguration + ): List { + if (projectConfiguration.allowedAgeRanges().isEmpty()) { + return emptyList() + } + // 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() + ) + ) - private fun buildFetchGuidStep(projectId: String, subjectId: String) = listOf(Step( - id = StepId.FETCH_GUID, - navigationActionId = R.id.action_orchestratorFragment_to_fetchSubject, - destinationId = FetchSubjectContract.DESTINATION, - payload = FetchSubjectContract.getArgs(projectId, subjectId), - )) + } - private fun buildConsentStep(consentType: ConsentType) = listOf(Step( - id = StepId.CONSENT, - navigationActionId = R.id.action_orchestratorFragment_to_consent, - destinationId = ConsentContract.DESTINATION, - payload = ConsentContract.getArgs(consentType), - )) + private fun buildSetupStep() = listOf( + Step( + id = StepId.SETUP, + navigationActionId = R.id.action_orchestratorFragment_to_setup, + destinationId = SetupContract.DESTINATION, + payload = bundleOf(), + ) + ) + + private fun buildFetchGuidStep(projectId: String, subjectId: String) = listOf( + Step( + id = StepId.FETCH_GUID, + navigationActionId = R.id.action_orchestratorFragment_to_fetchSubject, + destinationId = FetchSubjectContract.DESTINATION, + payload = FetchSubjectContract.getArgs(projectId, subjectId), + ) + ) + + private fun buildConsentStep(consentType: ConsentType) = listOf( + Step( + id = StepId.CONSENT, + navigationActionId = R.id.action_orchestratorFragment_to_consent, + destinationId = ConsentContract.DESTINATION, + payload = ConsentContract.getArgs(consentType), + ) + ) private fun buildValidateIdPoolStep(subjectQuery: SubjectQuery) = listOf(Step( id = StepId.VALIDATE_ID_POOL, @@ -126,8 +157,8 @@ internal class BuildStepsUseCase @Inject constructor( )) private fun buildModalityCaptureSteps( - projectConfiguration: ProjectConfiguration, - flowType: FlowType, + projectConfiguration: ProjectConfiguration, + flowType: FlowType, ) = projectConfiguration.general.modalities.map { when (it) { Modality.FINGERPRINT -> { @@ -156,10 +187,10 @@ internal class BuildStepsUseCase @Inject constructor( } private fun buildModalityMatcherSteps( - projectConfiguration: ProjectConfiguration, - flowType: FlowType, - subjectQuery: SubjectQuery, - biometricDataSource: BiometricDataSource, + projectConfiguration: ProjectConfiguration, + flowType: FlowType, + subjectQuery: SubjectQuery, + biometricDataSource: BiometricDataSource, ) = projectConfiguration.general.modalities.map { Step( id = when (it) { @@ -172,25 +203,31 @@ internal class BuildStepsUseCase @Inject constructor( ) } - private fun buildEnrolLastBiometricStep(action: ActionRequest.EnrolLastBiometricActionRequest) = listOf(Step( - id = StepId.ENROL_LAST_BIOMETRIC, - navigationActionId = R.id.action_orchestratorFragment_to_enrolLast, - destinationId = EnrolLastBiometricContract.DESTINATION, - payload = EnrolLastBiometricContract.getArgs( - projectId = action.projectId, - userId = action.userId, - moduleId = action.moduleId, - steps = mapStepsForLastBiometrics(cache.steps.mapNotNull { it.result }), - ), - )) + private fun buildEnrolLastBiometricStep(action: ActionRequest.EnrolLastBiometricActionRequest) = + listOf( + Step( + id = StepId.ENROL_LAST_BIOMETRIC, + navigationActionId = R.id.action_orchestratorFragment_to_enrolLast, + destinationId = EnrolLastBiometricContract.DESTINATION, + payload = EnrolLastBiometricContract.getArgs( + projectId = action.projectId, + userId = action.userId, + moduleId = action.moduleId, + steps = mapStepsForLastBiometrics(cache.steps.mapNotNull { it.result }), + ), + ) + ) - private fun buildConfirmIdentityStep(action: ActionRequest.ConfirmIdentityActionRequest) = listOf(Step( - id = StepId.CONFIRM_IDENTITY, - navigationActionId = R.id.action_orchestratorFragment_to_selectSubject, - destinationId = SelectSubjectContract.DESTINATION, - payload = SelectSubjectContract.getArgs( - projectId = action.projectId, - subjectId = action.selectedGuid, - ), - )) + private fun buildConfirmIdentityStep(action: ActionRequest.ConfirmIdentityActionRequest) = + listOf( + Step( + id = StepId.CONFIRM_IDENTITY, + navigationActionId = R.id.action_orchestratorFragment_to_selectSubject, + destinationId = SelectSubjectContract.DESTINATION, + payload = SelectSubjectContract.getArgs( + projectId = action.projectId, + subjectId = action.selectedGuid, + ), + ) + ) } diff --git a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml index f98da93764..56a33ef8da 100644 --- a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml +++ b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml @@ -34,6 +34,10 @@ android:id="@+id/action_orchestratorFragment_to_setup" app:destination="@id/graph_setup" /> + + + diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupContract.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupContract.kt new file mode 100644 index 0000000000..8b98a34cbf --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupContract.kt @@ -0,0 +1,6 @@ +package com.simprints.feature.selectagegroup + + +object SelectSubjectAgeGroupContract { + val DESTINATION = R.id.selectSubjectAgeGroupFragment +} diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt new file mode 100644 index 0000000000..e75eb0a8c2 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt @@ -0,0 +1,11 @@ +package com.simprints.feature.selectagegroup + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.AgeGroup +import java.io.Serializable + +@Keep +data class SelectSubjectAgeGroupResult( + val ageGroup: AgeGroup, +) : Serializable + diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt new file mode 100644 index 0000000000..867d06275e --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt @@ -0,0 +1,56 @@ +package com.simprints.feature.selectagegroup.screen + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.feature.selectagegroup.R +import com.simprints.infra.resources.R as IDR + + +// The age groups should be sorted as follows: Newborn, Baby, Child, Adult +// because the icons are in that order +@ExcludedFromGeneratedTestCoverageReports("UI classes are not unit tested") +internal class AgeGroupAdapter( + private val ageGroups: List, + private val onClick: (AgeGroupDisplayModel) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val view = layoutInflater.inflate(R.layout.age_group_item, parent, false) + return ViewHolder(view) + + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val ageGroup = ageGroups[position] + holder.bind(ageGroup, position) + } + + override fun getItemCount() = ageGroups.size + + private val icons = intArrayOf( + IDR.drawable.ic_age_group_selection_new_born, + IDR.drawable.ic_age_group_selection_baby, + IDR.drawable.ic_age_group_selection_child, + IDR.drawable.ic_age_group_selection_adult + ) + + @ExcludedFromGeneratedTestCoverageReports("UI classes are not unit tested") + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val ageGroupTextView: TextView = itemView.findViewById(R.id.item_label) + private val ageGroupIcon: ImageView = itemView.findViewById(R.id.item_icon) + fun bind(ageGroupDisplayModel: AgeGroupDisplayModel, position: Int) { + ageGroupTextView.text = ageGroupDisplayModel.displayString + // if the position is greater than the number of icons, use the last icon + ageGroupIcon.setImageResource(icons.getOrNull(position) ?: icons.last()) + + itemView.setOnClickListener { + onClick(ageGroupDisplayModel) + } + } + } +} diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModel.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModel.kt new file mode 100644 index 0000000000..a13aab523b --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModel.kt @@ -0,0 +1,5 @@ +package com.simprints.feature.selectagegroup.screen + +import com.simprints.infra.config.store.models.AgeGroup + +internal data class AgeGroupDisplayModel(val displayString: String, val range: AgeGroup) diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt new file mode 100644 index 0000000000..b5d1b90d82 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt @@ -0,0 +1,75 @@ +package com.simprints.feature.selectagegroup.screen + +import android.content.Context +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.allowedAgeRanges +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import com.simprints.infra.resources.R as IDR + +internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( + private val configurationRepo: ConfigRepository, + @ApplicationContext private val context: Context, +) { + + /** + * Builds a list of age groups for display + * it reads the allowed age ranges from the configuration and formats them for display + * it also adds a 0- and -above if they are not present + */ + suspend operator fun invoke(): List { + val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() + val processedAgeRanges = generateSortedUniqueAgeGroups(allowedAgeRanges) + return processedAgeRanges.map { ageGroup -> + AgeGroupDisplayModel(ageGroup.getDisplayName(), ageGroup) + } + } + + private fun generateSortedUniqueAgeGroups(ageGroups: List): List { + // Handle empty list case by returning a single age group starting at 0 and ending with null + if (ageGroups.isEmpty()) return listOf(AgeGroup(0, null)) + + // Flatten all start and end ages into a single list, removing nulls, duplicates, and sorting. + var sortedUniqueAges = + ageGroups.flatMap { listOf(it.startInclusive, it.endExclusive) }.filterNotNull() + // Ensure the first age group starts at 0 + sortedUniqueAges = (listOf(0) + sortedUniqueAges).sorted().distinct() + // Create age groups based on sorted unique ages + return sortedUniqueAges.zipWithNext { start, end -> AgeGroup(start, end) } + AgeGroup( + sortedUniqueAges.last(), null + ) + } + + // Helper function to convert months to readable format + private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { + val years = ageInMonths / 12 + val remainingMonths = ageInMonths % 12 + + val yearsString = context.resources.getQuantityString( + IDR.plurals.age_group_selection_age_in_years, years, years + ) + val monthsString = context.resources.getQuantityString( + IDR.plurals.age_group_selection_age_in_months, ageInMonths, ageInMonths + ) + + val remainingMonthsString = context.resources.getQuantityString( + IDR.plurals.age_group_selection_age_in_months, remainingMonths, remainingMonths + ) + return when { + years == 0 -> monthsString + remainingMonths == 0 -> yearsString + else -> "$yearsString, $remainingMonthsString" + } + } + + private fun AgeGroup.getDisplayName(): String { + val start = formatAgeInMonthsForDisplay(startInclusive) + val end = endExclusive?.let { + "${context.getString(IDR.string.age_group_selection_age_range_to)} ${ + formatAgeInMonthsForDisplay(it) + }" + } ?: context.getString(IDR.string.age_group_selection_age_range_and_above) + return "$start $end" + } +} diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt new file mode 100644 index 0000000000..be9a81b665 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt @@ -0,0 +1,66 @@ +package com.simprints.feature.selectagegroup.screen + +import android.os.Bundle +import android.view.View +import androidx.activity.addCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.simprints.feature.exitform.toArgs +import com.simprints.feature.selectagegroup.R +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult +import com.simprints.feature.selectagegroup.databinding.FragmentAgeGroupSelectionBinding +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.uibase.navigation.finishWithResult +import com.simprints.infra.uibase.navigation.navigateSafely +import com.simprints.infra.uibase.viewbinding.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_group_selection) { + + private val viewModel: SelectSubjectAgeGroupViewModel by viewModels() + private val binding by viewBinding(FragmentAgeGroupSelectionBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.ageGroups.observe(viewLifecycleOwner) { ageGroupsList -> + fillRecyclerView(ageGroupsList) + } + + viewModel.showExitForm.observe(viewLifecycleOwner) { exitFormConfig -> + exitFormConfig.getContentIfNotHandled()?.let { + findNavController().navigateSafely( + this, R.id.action_selectSubjectAgeGroupFragment_to_refusalFragment, it.toArgs() + ) + } + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { + viewModel.onBackPressed() + } + + viewModel.finish.observe(viewLifecycleOwner) { + it.getContentIfNotHandled()?.let(::finishWithResult) + } + viewModel.start() + } + + private fun fillRecyclerView(ageGroupsList: List) { + with(binding.ageGroupRecyclerView) { + layoutManager = LinearLayoutManager(requireContext()) + val dividerItemDecoration = DividerItemDecoration( + this.context, (layoutManager as LinearLayoutManager).orientation + ) + this.addItemDecoration(dividerItemDecoration) + adapter = AgeGroupAdapter(ageGroupsList) { ageGroup -> + viewModel.saveAgeGroupSelection(ageGroup.range) + } + } + } + + private fun finishWithResult(ageGroup: AgeGroup) { + findNavController().finishWithResult(this, SelectSubjectAgeGroupResult(ageGroup)) + } +} diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt new file mode 100644 index 0000000000..cad850726c --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt @@ -0,0 +1,92 @@ +package com.simprints.feature.selectagegroup.screen + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.simprints.core.ExternalScope +import com.simprints.core.livedata.LiveDataEventWithContent +import com.simprints.core.livedata.send +import com.simprints.core.tools.time.TimeHelper +import com.simprints.core.tools.time.Timestamp +import com.simprints.feature.exitform.ExitFormConfigurationBuilder +import com.simprints.feature.exitform.exitFormConfiguration +import com.simprints.feature.exitform.scannerOptions +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.events.SessionEventRepository +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION +import com.simprints.infra.logging.Simber +import com.simprints.infra.resources.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class SelectSubjectAgeGroupViewModel @Inject constructor( + private val timeHelper: TimeHelper, + private val eventRepository: SessionEventRepository, + private val buildAgeGroups: BuildAgeGroupsDescriptionUseCase, + private val configurationRepo: ConfigRepository, + @ExternalScope private val externalScope: CoroutineScope, +) : ViewModel() { + + val finish: LiveData> + get() = _finish + private var _finish = MutableLiveData>() + val ageGroups: LiveData> + get() = _ageGroupsDisplayModel + private var _ageGroupsDisplayModel = MutableLiveData>() + private lateinit var startTime: Timestamp + + val showExitForm: LiveData> + get() = _showExitForm + private val _showExitForm = + MutableLiveData>() + + fun start() = viewModelScope.launch { + startTime = timeHelper.now() + val ageGroups = buildAgeGroups() + // notify the adapter + _ageGroupsDisplayModel.value = ageGroups + } + + fun saveAgeGroupSelection(ageRange: AgeGroup) = externalScope.launch { + val event = AgeGroupSelectionEvent( + startTime, + timeHelper.now(), + AgeGroupSelectionEvent.AgeGroup(ageRange.startInclusive, ageRange.endExclusive) + ) + eventRepository.addOrUpdateEvent(event) + Simber.tag(SESSION.name).i("Added Age Group Selection Event") + _finish.send(ageRange) + } + + fun onBackPressed() { + viewModelScope.launch { + val projectConfig = configurationRepo.getProjectConfiguration() + _showExitForm.send(getExitFormFromModalities(projectConfig.general.modalities)) + } + } + + private fun getExitFormFromModalities(modalities: List) = when { + modalities.size != 1 -> exitFormConfiguration { + titleRes = R.string.exit_form_title_biometrics + backButtonRes = R.string.exit_form_continue_fingerprints_button + } + + modalities.first() == GeneralConfiguration.Modality.FACE -> exitFormConfiguration { + titleRes = R.string.exit_form_title_face + backButtonRes = R.string.exit_form_continue_face_button + } + + else -> exitFormConfiguration { + titleRes = R.string.exit_form_title_fingerprinting + backButtonRes = R.string.exit_form_continue_fingerprints_button + visibleOptions = scannerOptions() + } + } +} diff --git a/feature/select-subject-age-group/src/main/res/layout/age_group_item.xml b/feature/select-subject-age-group/src/main/res/layout/age_group_item.xml new file mode 100644 index 0000000000..02a0ab3350 --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/layout/age_group_item.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/feature/select-subject-age-group/src/main/res/layout/fragment_age_group_selection.xml b/feature/select-subject-age-group/src/main/res/layout/fragment_age_group_selection.xml new file mode 100644 index 0000000000..9b129d0fb4 --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/layout/fragment_age_group_selection.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml b/feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml new file mode 100644 index 0000000000..8a7eccd685 --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt new file mode 100644 index 0000000000..4378184013 --- /dev/null +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt @@ -0,0 +1,112 @@ +package com.simprints.feature.selectagegroup.screen + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.allowedAgeRanges +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.mockkStatic +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +@Config(application = HiltTestApplication::class) +class BuildAgeGroupsDescriptionUseCaseTest { + private lateinit var buildAgeGroupsDescription: BuildAgeGroupsDescriptionUseCase + + private val context = InstrumentationRegistry.getInstrumentation().context + + @MockK + private lateinit var configurationRepo: ConfigRepository + + @Before + fun setUp() { + MockKAnnotations.init(this) + mockkStatic("com.simprints.infra.config.store.models.ProjectConfigurationKt") + buildAgeGroupsDescription = BuildAgeGroupsDescriptionUseCase(configurationRepo, context) + } + + @Test + fun testAgeGroupDescriptions() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf( + AgeGroup(6, 60), AgeGroup(120, null), AgeGroup(60, 120) + ) + val result = buildAgeGroupsDescription() + val expected = arrayOf( + "0 months to 6 months", + "6 months to 5 years", + "5 years to 10 years", + "10 years and above" + ) + assertAgeGroupDescriptionsMatchExpected(result, expected) + } + + @Test + fun testAgeGroupsOverlapping() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf( + AgeGroup(6, 60), AgeGroup(36, null), AgeGroup(60, null) + ) + + val result = buildAgeGroupsDescription() + val expected = arrayOf( + "0 months to 6 months", "6 months to 3 years", "3 years to 5 years", "5 years and above" + ) + + assertAgeGroupDescriptionsMatchExpected(result, expected) + } + + @Test + fun testAgeGroupWithInitialAndFinalMissing() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf( + AgeGroup(6, 60) + ) + + val result = buildAgeGroupsDescription() + val expected = arrayOf("0 months to 6 months", "6 months to 5 years", "5 years and above") + + assertAgeGroupDescriptionsMatchExpected(result, expected) + } + + @Test + fun testEmptyAgeGroup() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf() + val result = buildAgeGroupsDescription() + + val expected = arrayOf("0 months and above") + + assertAgeGroupDescriptionsMatchExpected(result, expected) + } + + @Test + fun testAgeGroupsWithMonthsFraction() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf( + AgeGroup(6, 63), AgeGroup(125, null), AgeGroup(63, 125) + ) + val result = buildAgeGroupsDescription() + val expected = arrayOf( + "0 months to 6 months", + "6 months to 5 years, 3 months", + "5 years, 3 months to 10 years, 5 months", + "10 years, 5 months and above" + ) + + assertAgeGroupDescriptionsMatchExpected(result, expected) + } + + private fun assertAgeGroupDescriptionsMatchExpected( + result: List, expected: Array + ) = assertThat(result.map { it.displayString }).containsExactlyElementsIn(expected) + + +} + diff --git a/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt new file mode 100644 index 0000000000..572df68f1a --- /dev/null +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt @@ -0,0 +1,120 @@ +package com.simprints.feature.selectagegroup.screen + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.common.truth.Truth +import com.jraska.livedata.test +import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.events.SessionEventRepository +import com.simprints.infra.resources.R +import com.simprints.testtools.common.coroutines.TestCoroutineRule +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class SelectSubjectAgeGroupViewModelTest { + + private lateinit var viewModel: SelectSubjectAgeGroupViewModel + + @RelaxedMockK + private lateinit var timeHelper: TimeHelper + + @RelaxedMockK + private lateinit var eventRepository: SessionEventRepository + + @MockK + private lateinit var buildAgeGroups: BuildAgeGroupsDescriptionUseCase + + @RelaxedMockK + private lateinit var configurationRepo: ConfigRepository + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val testCoroutineRule = TestCoroutineRule() + + private val ageGroupViewModels = listOf( + AgeGroupDisplayModel("0-6 months", AgeGroup(0, 6)), + AgeGroupDisplayModel("6-12 months", AgeGroup(6, 12)), + ) + + @Before + fun setUp() { + MockKAnnotations.init(this) + coEvery { buildAgeGroups() } returns ageGroupViewModels + + viewModel = SelectSubjectAgeGroupViewModel( + timeHelper, + eventRepository, + buildAgeGroups, + configurationRepo, + CoroutineScope(testCoroutineRule.testCoroutineDispatcher) + ) + } + + @Test + fun `test start`() = runTest { + viewModel.start() + val ageGroups = viewModel.ageGroups.test().value() + + Truth.assertThat(ageGroups.size).isEqualTo(ageGroupViewModels.size) + } + + @Test + fun `test saveAgeGroupSelection`() = runTest { + viewModel.start() + val selectedAgeGroup = ageGroupViewModels.first().range + viewModel.saveAgeGroupSelection(selectedAgeGroup) + val result = viewModel.finish.test().value() + Truth.assertThat(result.peekContent()) + .isEqualTo(selectedAgeGroup) + + } + + @Test + fun `test onBackPressed fingerPrint modality`() = runTest { + coEvery { configurationRepo.getProjectConfiguration().general.modalities } returns listOf( + GeneralConfiguration.Modality.FINGERPRINT + ) + viewModel.onBackPressed() + val result = viewModel.showExitForm.test().value().peekContent() + + // Assert that the titleRes and backButtonRes are equal to the fingerPrint modality + Truth.assertThat(result.titleRes).isEqualTo(R.string.exit_form_title_fingerprinting) + Truth.assertThat(result.backButtonRes).isEqualTo(R.string.exit_form_continue_fingerprints_button) + } + @Test + fun `test onBackPressed face modality`() = runTest { + coEvery { configurationRepo.getProjectConfiguration().general.modalities } returns listOf( + GeneralConfiguration.Modality.FACE + ) + viewModel.onBackPressed() + val result = viewModel.showExitForm.test().value().peekContent() + + // Assert that the titleRes and backButtonRes are equal to the face modality + Truth.assertThat(result.titleRes).isEqualTo(R.string.exit_form_title_face) + Truth.assertThat( result.backButtonRes).isEqualTo(R.string.exit_form_continue_face_button) + } + @Test + fun `test onBackPressed multiple modalities`() = runTest { + coEvery { configurationRepo.getProjectConfiguration().general.modalities } returns listOf( + GeneralConfiguration.Modality.FACE, + GeneralConfiguration.Modality.FINGERPRINT + ) + viewModel.onBackPressed() + val result = viewModel.showExitForm.test().value().peekContent() + + // Assert that the titleRes and backButtonRes are equal to the biometrics modality + Truth.assertThat(result.titleRes).isEqualTo(R.string.exit_form_title_biometrics) + Truth.assertThat(result.backButtonRes).isEqualTo(R.string.exit_form_continue_fingerprints_button) + } +} 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 df4cbda510..839e1106da 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 @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.local.models import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration internal fun FingerprintConfiguration.toProto(): ProtoFingerprintConfiguration = @@ -22,8 +23,10 @@ internal fun FingerprintConfiguration.FingerprintSdkConfiguration.toProto() = .also { if (vero1 != null) it.vero1 = vero1.toProto() if (vero2 != null) it.vero2 = vero2.toProto() - } - .build() + if (allowedAgeRange != null) + it.allowedAgeRange = allowedAgeRange.toProto() + }.build() + internal fun FingerprintConfiguration.VeroGeneration.toProto() = when (this) { FingerprintConfiguration.VeroGeneration.VERO_1 -> ProtoFingerprintConfiguration.VeroGeneration.VERO_1 @@ -87,6 +90,7 @@ 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 ) @@ -105,3 +109,11 @@ internal fun ProtoFingerprintConfiguration.FingerComparisonStrategy.toDomain() = "invalid FingerComparisonStrategy $name" ) } + +internal fun ProtoAllowedAgeRange.toDomain() = AgeGroup(startInclusive, endExclusive) + +internal fun AgeGroup.toProto() = + ProtoAllowedAgeRange.newBuilder().setStartInclusive(startInclusive).let { builder -> + endExclusive?.let { builder.setEndExclusive(it) } + builder.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 new file mode 100644 index 0000000000..9cae84d647 --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt @@ -0,0 +1,9 @@ +package com.simprints.infra.config.store.models + +data class AgeGroup( + val startInclusive: Int, + val endExclusive: Int?, +) { + fun isEmpty(): Boolean = + startInclusive == 0 && endExclusive == null +} 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 5aad9da4df..2b1666da55 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 @@ -13,7 +13,8 @@ data class FingerprintConfiguration( val decisionPolicy: DecisionPolicy, val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: Vero1Configuration? = null, - val vero2: Vero2Configuration? = null + val vero2: Vero2Configuration? = null, + val allowedAgeRange: AgeGroup? = null, ) enum class VeroGeneration { 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 9d30491d8a..f20035ddb7 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 @@ -41,3 +41,11 @@ fun ProjectConfiguration.isEventDownSyncAllowed(): Boolean = fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = synchronization.up.simprints.imagesRequireUnmeteredConnection + +fun ProjectConfiguration.allowedAgeRanges(): List { + return listOf( + //Todo add face roc sdk , + fingerprint?.secugenSimMatcher?.allowedAgeRange, + fingerprint?.nec?.allowedAgeRange + ).filterNotNull().filterNot { it.isEmpty() } +} diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/ConfigRemoteInterface.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/ConfigRemoteInterface.kt index bcaba4b5ea..6abbb3f5cc 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/ConfigRemoteInterface.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/ConfigRemoteInterface.kt @@ -3,7 +3,6 @@ package com.simprints.infra.config.store.remote import com.simprints.infra.config.store.remote.models.ApiDeviceState import com.simprints.infra.config.store.remote.models.ApiFileUrl import com.simprints.infra.config.store.remote.models.ApiProject -import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration import com.simprints.infra.network.SimRemoteInterface import retrofit2.http.GET import retrofit2.http.Path @@ -11,9 +10,6 @@ import retrofit2.http.Query internal interface ConfigRemoteInterface : SimRemoteInterface { - @GET("projects/{projectId}/configuration") - suspend fun getConfiguration(@Path("projectId") projectId: String): ApiProjectConfiguration - @GET("projects/{projectId}") suspend fun getProject(@Path("projectId") projectId: String): ApiProject 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 new file mode 100644 index 0000000000..d7f674652e --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAgeGroup.kt @@ -0,0 +1,12 @@ +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/ApiFingerprintConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfiguration.kt index 1f95f3f66f..2d6f953d86 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 @@ -27,15 +27,16 @@ internal data class ApiFingerprintConfiguration( val decisionPolicy: ApiDecisionPolicy, val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: ApiVero1Configuration? = null, - val vero2: ApiVero2Configuration? = null + val vero2: ApiVero2Configuration? = null, + val allowedAgeRange: ApiAgeGroup, ) { fun toDomain() = FingerprintConfiguration.FingerprintSdkConfiguration( fingersToCapture.map { it.toDomain() }, decisionPolicy.toDomain(), comparisonStrategyForVerification.toDomain(), vero1?.toDomain(), - vero2?.toDomain() - + vero2?.toDomain(), + allowedAgeRange.toDomain(), ) } @@ -98,4 +99,5 @@ 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 0c7f3f5ddf..ea7726b06a 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -76,9 +76,14 @@ message ProtoFingerprintConfiguration { FingerComparisonStrategy comparison_strategy_for_verification = 3; optional ProtoVero2Configuration vero_2 = 4; optional ProtoVero1Configuration vero_1 = 5; + optional ProtoAllowedAgeRange allowed_age_range = 6; } } +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/models/FingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt index 803cfd68ce..e9169892f4 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/FingerprintConfigurationTest.kt @@ -58,6 +58,18 @@ class FingerprintConfigurationTest { assertThat(it.value.toProto()).isEqualTo(it.key) } } + @Test + fun `should map correctly the allowedAgeRange`() { + val mapping = mapOf( + ProtoFingerprintConfiguration.VeroGeneration.VERO_1 to FingerprintConfiguration.VeroGeneration.VERO_1, + ProtoFingerprintConfiguration.VeroGeneration.VERO_2 to FingerprintConfiguration.VeroGeneration.VERO_2, + ) + + mapping.forEach { + assertThat(it.key.toDomain()).isEqualTo(it.value) + assertThat(it.value.toProto()).isEqualTo(it.key) + } + } @Test fun `should map correctly the FingerComparisonStrategy enums`() { diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt index a040a115ce..f8e2759874 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/remote/models/ApiFingerprintConfigurationTest.kt @@ -29,6 +29,7 @@ class ApiFingerprintConfigurationTest { ApiFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, null, apiFingerprintConfiguration.secugenSimMatcher?.vero2, + apiFingerprintConfiguration.secugenSimMatcher?.allowedAgeRange!!, ), null, ) @@ -42,6 +43,7 @@ class ApiFingerprintConfigurationTest { FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, null, fingerprintConfiguration.secugenSimMatcher?.vero2, + fingerprintConfiguration.secugenSimMatcher?.allowedAgeRange!!, ), null, ) @@ -62,6 +64,7 @@ class ApiFingerprintConfigurationTest { ApiFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, ApiVero1Configuration(10), null, + apiFingerprintConfiguration.secugenSimMatcher?.allowedAgeRange!! ), ) @@ -76,6 +79,7 @@ class ApiFingerprintConfigurationTest { FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, Vero1Configuration(10), null, + fingerprintConfiguration.secugenSimMatcher?.allowedAgeRange!! ), ) 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 955261492f..c7d853ed33 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 @@ -31,6 +31,7 @@ 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.remote.models.ApiConsentConfiguration import com.simprints.infra.config.store.remote.models.ApiDecisionPolicy import com.simprints.infra.config.store.remote.models.ApiDeviceState @@ -137,6 +138,7 @@ 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, @@ -168,7 +170,9 @@ internal val apiFingerprintConfiguration = ApiFingerprintConfiguration( apiDecisionPolicy, ApiFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, ApiVero1Configuration(10), - apiVero2Configuration + apiVero2Configuration, + apiAgeGroup , + ), null, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayload.kt new file mode 100644 index 0000000000..381d32827e --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayload.kt @@ -0,0 +1,35 @@ +package com.simprints.infra.eventsync.event.remote.models + +import androidx.annotation.Keep +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent + +@Keep +internal data class ApiAgeGroupSelectionPayload( + override val startTime: ApiTimestamp, + val endTime: ApiTimestamp, + val subjectAgeGroup: ApiAgeGroup, +) : ApiEventPayload(startTime) { + + constructor(domainPayload: AgeGroupSelectionEvent.AgeGroupSelectionPayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.endedAt?.fromDomainToApi() ?: ApiTimestamp(0), + domainPayload.subjectAgeGroup.fromDomainToApi(), + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = + null // this payload doesn't have tokenizable fields + + data class ApiAgeGroup( + val startInclusive: Int, + val endExclusive: Int?, + ) + +} + +private fun AgeGroupSelectionEvent.AgeGroup.fromDomainToApi() = + ApiAgeGroupSelectionPayload.ApiAgeGroup( + startInclusive = startInclusive, + endExclusive = endExclusive, + ) + diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt index 58d7b873fe..edb57c62a8 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayload.kt @@ -4,6 +4,7 @@ import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload @@ -14,6 +15,7 @@ import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayloa import com.simprints.infra.events.event.domain.models.EnrolmentEventV1 import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 import com.simprints.infra.events.event.domain.models.EventPayload +import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECTION import com.simprints.infra.events.event.domain.models.EventType.ALERT_SCREEN import com.simprints.infra.events.event.domain.models.EventType.AUTHENTICATION import com.simprints.infra.events.event.domain.models.EventType.AUTHORIZATION @@ -183,4 +185,5 @@ internal fun EventPayload.fromDomainToApi(): ApiEventPayload = EVENT_DOWN_SYNC_REQUEST -> ApiEventDownSyncRequestPayload(this as EventDownSyncRequestEvent.EventDownSyncRequestPayload) EVENT_UP_SYNC_REQUEST -> ApiEventUpSyncRequestPayload(this as EventUpSyncRequestEvent.EventUpSyncRequestPayload) LICENSE_CHECK -> ApiLicenseCheckEventPayload(this as LicenseCheckEvent.LicenseCheckEventPayload) + AGE_GROUP_SELECTION -> ApiAgeGroupSelectionPayload(this as AgeGroupSelectionEvent.AgeGroupSelectionPayload) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt index 73c44940d8..7410881621 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventPayloadType.kt @@ -43,6 +43,7 @@ import com.simprints.infra.events.event.domain.models.EventType.SCANNER_CONNECTI import com.simprints.infra.events.event.domain.models.EventType.SCANNER_FIRMWARE_UPDATE import com.simprints.infra.events.event.domain.models.EventType.SUSPICIOUS_INTENT import com.simprints.infra.events.event.domain.models.EventType.VERO_2_INFO_SNAPSHOT +import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECTION @Keep internal enum class ApiEventPayloadType { @@ -143,6 +144,9 @@ internal enum class ApiEventPayloadType { /* key added: LICENSE_CHECK_KEY */ LicenseCheck, + /* key added: AGE_GROUP_SELECTION_KEY */ + AgeGroupSelection, + ; companion object { @@ -225,6 +229,7 @@ internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { EVENT_DOWN_SYNC_REQUEST -> ApiEventPayloadType.EventDownSyncRequest EVENT_UP_SYNC_REQUEST -> ApiEventPayloadType.EventUpSyncRequest LICENSE_CHECK -> ApiEventPayloadType.LicenseCheck + AGE_GROUP_SELECTION -> ApiEventPayloadType.AgeGroupSelection } @@ -258,6 +263,7 @@ internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.EventDownSyncRequest -> EVENT_DOWN_SYNC_REQUEST ApiEventPayloadType.EventUpSyncRequest -> EVENT_UP_SYNC_REQUEST ApiEventPayloadType.LicenseCheck -> LICENSE_CHECK + ApiEventPayloadType.AgeGroupSelection -> AGE_GROUP_SELECTION ApiEventPayloadType.Callout -> throw UnsupportedOperationException("") ApiEventPayloadType.Callback -> throw UnsupportedOperationException("") } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt index 6474e1d20e..4e22729cb1 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/EventValidationUtils.kt @@ -605,4 +605,12 @@ fun validateUpSyncRequestEventApiModel(json: JSONObject) { } } +fun validateAgeGroupSelectionEventApiModel(json: JSONObject) { + validateCommonParams(json, "AgeGroupSelection", 1) + with(json.getJSONObject("payload")) { + validateTimestamp(getJSONObject("startTime")) + validateTimestamp(getJSONObject("endTime")) + assertThat(getString("subjectAgeGroup")).isNotNull() + } +} private fun Array.valuesAsStrings(): List = this.map { it.toString() } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/ApiEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/ApiEventTest.kt index 76596a3b17..14114d708c 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/ApiEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/ApiEventTest.kt @@ -7,6 +7,7 @@ import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.extentions.safeSealedWhens import com.simprints.core.tools.json.JsonHelper +import com.simprints.infra.events.sampledata.createAgeGroupSelectionEvent import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createAuthenticationEvent import com.simprints.infra.events.sampledata.createAuthorizationEvent @@ -49,6 +50,7 @@ import com.simprints.infra.events.sampledata.createVerificationCallbackEventV2 import com.simprints.infra.events.sampledata.createVerificationCalloutEvent import com.simprints.infra.events.sampledata.createVero2InfoSnapshotEvent import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.AgeGroupSelection import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.AlertScreen import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Authentication import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Authorization @@ -81,6 +83,7 @@ import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Sca import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.SuspiciousIntent import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Vero2InfoSnapshot import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi +import com.simprints.infra.eventsync.event.validateAgeGroupSelectionEventApiModel import com.simprints.infra.eventsync.event.validateAlertScreenEventApiModel import com.simprints.infra.eventsync.event.validateAuthenticationEventApiModel import com.simprints.infra.eventsync.event.validateAuthorizationEventApiModel @@ -492,6 +495,14 @@ class ApiEventTest { validateUpSyncRequestEventApiModel(json) } + @Test + fun validate_ageGroupSelectionEventApiModel() { + val event = createAgeGroupSelectionEvent() + val apiEvent = event.fromDomainToApi() + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + + validateAgeGroupSelectionEventApiModel(json) + } @Test fun `when event contains tokenized attendant id, then ApiEvent should contain tokenizedField`() { @@ -589,6 +600,7 @@ class ApiEventTest { EventDownSyncRequest -> validate_DownSyncRequestEventApiModel() EventUpSyncRequest -> validate_UpSyncRequestEventApiModel() LicenseCheck -> validate_licenseCheckEventApiModel() + AgeGroupSelection-> validate_ageGroupSelectionEventApiModel() null -> TODO() }.safeSealedWhens } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt similarity index 99% rename from infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayloadTest.kt rename to infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt index f4a9d2a542..893085bd1c 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayloadTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt @@ -14,4 +14,4 @@ class ApiGuidSelectionPayloadTest { assertThat(payload.getTokenizedFieldJsonPath(it)).isNull() } } -} \ No newline at end of file +} diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt index fa1d2697fe..cc49f0fb2b 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/testtools/RemoteTestingHelper.kt @@ -14,7 +14,7 @@ internal class RemoteTestingHelper { ApiEventPayloadType.CompletionCheck, ApiEventPayloadType.FaceOnboardingComplete, ApiEventPayloadType.FaceFallbackCapture, ApiEventPayloadType.FaceCapture, ApiEventPayloadType.FaceCaptureConfirmation, ApiEventPayloadType.FingerprintCaptureBiometrics, ApiEventPayloadType.FaceCaptureBiometrics, ApiEventPayloadType.EventDownSyncRequest, ApiEventPayloadType.EventUpSyncRequest, - ApiEventPayloadType.LicenseCheck, + ApiEventPayloadType.LicenseCheck,ApiEventPayloadType.AgeGroupSelection, null, -> { // ADD TEST FOR NEW EVENT IN THIS CLASS } diff --git a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt index dc25a94217..b07ef11246 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -8,6 +8,7 @@ import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.SimNetworkUtils import com.simprints.core.tools.utils.SimNetworkUtils.Connection import com.simprints.infra.config.store.models.GeneralConfiguration.Modality +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent import com.simprints.infra.events.event.domain.models.AlertScreenEvent import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.BLUETOOTH_NOT_ENABLED import com.simprints.infra.events.event.domain.models.AuthenticationEvent @@ -442,3 +443,9 @@ fun createLicenseCheckEvent() = LicenseCheckEvent( vendor = "NEC_FINGERPRINT", ) + +fun createAgeGroupSelectionEvent() = AgeGroupSelectionEvent( + createdAt = CREATED_AT, + endedAt = ENDED_AT, + subjectAgeGroup = AgeGroupSelectionEvent.AgeGroup(1, 2) +) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AgeGroupSelectionEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AgeGroupSelectionEvent.kt new file mode 100644 index 0000000000..41de698c1a --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AgeGroupSelectionEvent.kt @@ -0,0 +1,52 @@ +package com.simprints.infra.events.event.domain.models + +import androidx.annotation.Keep +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.AGE_GROUP_SELECTION +import java.util.UUID + +@Keep +data class AgeGroupSelectionEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: AgeGroupSelectionPayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + + constructor( + createdAt: Timestamp, + endedAt: Timestamp, + subjectAgeGroup: AgeGroup, + ) : this( + UUID.randomUUID().toString(), + AgeGroupSelectionPayload(createdAt, EVENT_VERSION, endedAt, subjectAgeGroup), + AGE_GROUP_SELECTION + ) + + override fun getTokenizedFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map) = + this // No tokenized fields + + @Keep + data class AgeGroupSelectionPayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + override val endedAt: Timestamp?, + val subjectAgeGroup: AgeGroup, + override val type: EventType = AGE_GROUP_SELECTION, + ) : EventPayload() + + @Keep + data class AgeGroup( + val startInclusive: Int, + val endExclusive: Int?, + ) + + companion object { + const val EVENT_VERSION = 1 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt index 09509c3ab1..bf287cf85a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/Event.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.events.event.domain.models.EventType.Companion.AGE_GROUP_SELECTION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ALERT_SCREEN_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.AUTHENTICATION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.AUTHORIZATION_KEY @@ -137,6 +138,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = EventDownSyncRequestEvent::class, name = EVENT_DOWN_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = EventUpSyncRequestEvent::class, name = EVENT_UP_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = LicenseCheckEvent::class, name = LICENSE_CHECK_KEY), + JsonSubTypes.Type(value = AgeGroupSelectionEvent::class, name = AGE_GROUP_SELECTION_KEY), ) abstract class Event { diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt index 7e1195cd41..e516bd2651 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventPayload.kt @@ -3,6 +3,7 @@ package com.simprints.infra.events.event.domain.models import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent.AgeGroupSelectionPayload import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload @@ -87,6 +88,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = EventDownSyncRequestPayload::class, name = Companion.EVENT_DOWN_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = EventUpSyncRequestPayload::class, name = Companion.EVENT_UP_SYNC_REQUEST_KEY), JsonSubTypes.Type(value = LicenseCheckEventPayload::class, name = Companion.LICENSE_CHECK_KEY), + JsonSubTypes.Type(value = AgeGroupSelectionPayload::class, name = Companion.AGE_GROUP_SELECTION_KEY), ) abstract class EventPayload { abstract val type: EventType diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt index 67b58fa474..172358ccab 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/EventType.kt @@ -131,6 +131,8 @@ enum class EventType { /* key added: LICENSE_CHECK_KEY */ LICENSE_CHECK, + /* key added: AGE_GROUP_SELECTION_KEY */ + AGE_GROUP_SELECTION, ; companion object { @@ -175,5 +177,6 @@ enum class EventType { const val EVENT_DOWN_SYNC_REQUEST_KEY = "EVENT_DOWN_SYNC_REQUEST" const val EVENT_UP_SYNC_REQUEST_KEY = "EVENT_UP_SYNC_REQUEST" const val LICENSE_CHECK_KEY = "LICENSE_CHECK" + const val AGE_GROUP_SELECTION_KEY = "AGE_GROUP_SELECTION" } } diff --git a/infra/events/src/test/resources/all-events/age_group_selection_v1.json b/infra/events/src/test/resources/all-events/age_group_selection_v1.json new file mode 100644 index 0000000000..ab03c9d7d3 --- /dev/null +++ b/infra/events/src/test/resources/all-events/age_group_selection_v1.json @@ -0,0 +1,19 @@ +{ + "id": "c3670beb-4d01-4fc3-ae4e-19e8c6b687f3", + "type": "AGE_GROUP_SELECTION", + "labels": { + "projectId": "TEST6Oai41ps1pBNrzBL", + "sessionId": "e35c39f9-b81e-48f2-97e7-46ecc8399bb3", + "deviceId": "f2fd8393c0a0be67" + }, + "payload": { + "type": "AGE_GROUP_SELECTION", + "eventVersion": 1, + "createdAt": 221675670, + "endedAt": 9141278930, + "subjectAgeGroup": { + "startInclusive": 10, + "endExclusive": 210 + } + } +} diff --git a/infra/resources/src/main/res/drawable/ic_age_group_selection_adult.xml b/infra/resources/src/main/res/drawable/ic_age_group_selection_adult.xml new file mode 100644 index 0000000000..a153823ee8 --- /dev/null +++ b/infra/resources/src/main/res/drawable/ic_age_group_selection_adult.xml @@ -0,0 +1,9 @@ + + + diff --git a/infra/resources/src/main/res/drawable/ic_age_group_selection_baby.xml b/infra/resources/src/main/res/drawable/ic_age_group_selection_baby.xml new file mode 100644 index 0000000000..6e7678b52e --- /dev/null +++ b/infra/resources/src/main/res/drawable/ic_age_group_selection_baby.xml @@ -0,0 +1,9 @@ + + + diff --git a/infra/resources/src/main/res/drawable/ic_age_group_selection_child.xml b/infra/resources/src/main/res/drawable/ic_age_group_selection_child.xml new file mode 100644 index 0000000000..0afe80fca7 --- /dev/null +++ b/infra/resources/src/main/res/drawable/ic_age_group_selection_child.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/infra/resources/src/main/res/drawable/ic_age_group_selection_new_born.xml b/infra/resources/src/main/res/drawable/ic_age_group_selection_new_born.xml new file mode 100644 index 0000000000..5fe874d7df --- /dev/null +++ b/infra/resources/src/main/res/drawable/ic_age_group_selection_new_born.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/infra/resources/src/main/res/drawable/ripple_effect.xml b/infra/resources/src/main/res/drawable/ripple_effect.xml new file mode 100644 index 0000000000..48012089a6 --- /dev/null +++ b/infra/resources/src/main/res/drawable/ripple_effect.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/infra/resources/src/main/res/drawable/shadow_rounded_bg.xml b/infra/resources/src/main/res/drawable/shadow_rounded_bg.xml new file mode 100644 index 0000000000..e65ccc8d2d --- /dev/null +++ b/infra/resources/src/main/res/drawable/shadow_rounded_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/infra/resources/src/main/res/values/colors.xml b/infra/resources/src/main/res/values/colors.xml index 5d73616f72..366961247b 100644 --- a/infra/resources/src/main/res/values/colors.xml +++ b/infra/resources/src/main/res/values/colors.xml @@ -1,6 +1,7 @@ #00B3D1 + #00B0C1 #009CB6 #40C057 diff --git a/infra/resources/src/main/res/values/dimens.xml b/infra/resources/src/main/res/values/dimens.xml index 85483227e5..f09194828b 100644 --- a/infra/resources/src/main/res/values/dimens.xml +++ b/infra/resources/src/main/res/values/dimens.xml @@ -24,8 +24,6 @@ - 16sp - 24sp 16dp diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index 5da39923ba..9f8fac8229 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -436,4 +436,17 @@ Close Sync and retry + + Select Subject\'s Age Group + to + and above + + %d month + %d months + + + %d year + %d years + + diff --git a/infra/resources/src/main/res/values/theme.xml b/infra/resources/src/main/res/values/theme.xml index 0e93ef2f06..8c9665d206 100644 --- a/infra/resources/src/main/res/values/theme.xml +++ b/infra/resources/src/main/res/values/theme.xml @@ -60,6 +60,7 @@ ?colorPrimaryVariant false true + @color/simprints_off_white diff --git a/settings.gradle.kts b/settings.gradle.kts index 66a97ae9c1..03923abb58 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,7 +110,8 @@ include( ":feature:consent", ":feature:setup", ":feature:matcher", - ":feature:validate-subject-pool" + ":feature:validate-subject-pool", + ":feature:select-subject-age-group", ) // Infra modules