From ee8906960fc6a99567ca4956ae82ca84ac5e1bd7 Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 4 Jun 2024 00:22:32 +0300 Subject: [PATCH 01/12] Add Age group selection screen --- feature/orchestrator/build.gradle.kts | 1 + .../feature/orchestrator/steps/StepId.kt | 2 +- .../usecases/steps/BuildStepsUseCase.kt | 127 +++++++++++------- .../res/navigation/graph_orchestration.xml | 4 + feature/select-subject-age-group/.gitignore | 1 + .../select-subject-age-group/build.gradle.kts | 15 +++ .../src/main/AndroidManifest.xml | 2 + .../SelectSubjectAgeGroupContract.kt | 8 ++ .../selectagegroup/SelectSubjectAgeResult.kt | 10 ++ .../feature/selectagegroup/screen/AgeGroup.kt | 3 + .../selectagegroup/screen/AgeGroupAdapter.kt | 55 ++++++++ .../screen/BuildAgeGroupsUseCase.kt | 67 +++++++++ .../screen/SelectSubjectAgeGroupFragment.kt | 54 ++++++++ .../screen/SelectSubjectAgeGroupViewModel.kt | 58 ++++++++ .../src/main/res/layout/age_group_item.xml | 30 +++++ .../layout/fragment_age_group_selection.xml | 30 +++++ .../navigation/graph_age_group_selection.xml | 16 +++ .../config/store/models/FaceConfiguration.kt | 1 + .../store/models/FingerprintConfiguration.kt | 3 +- .../store/models/ProjectConfiguration.kt | 12 ++ .../drawable/ic_age_group_selection_adult.xml | 9 ++ .../drawable/ic_age_group_selection_baby.xml | 9 ++ .../drawable/ic_age_group_selection_child.xml | 11 ++ .../ic_age_group_selection_new_born.xml | 18 +++ .../src/main/res/drawable/ripple_effect.xml | 9 ++ .../main/res/drawable/shadow_rounded_bg.xml | 5 + .../resources/src/main/res/values/colors.xml | 1 + .../resources/src/main/res/values/dimens.xml | 1 + .../resources/src/main/res/values/strings.xml | 7 + infra/resources/src/main/res/values/theme.xml | 1 + settings.gradle.kts | 1 + 31 files changed, 524 insertions(+), 47 deletions(-) create mode 100644 feature/select-subject-age-group/.gitignore create mode 100644 feature/select-subject-age-group/build.gradle.kts create mode 100644 feature/select-subject-age-group/src/main/AndroidManifest.xml create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupContract.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt create mode 100644 feature/select-subject-age-group/src/main/res/layout/age_group_item.xml create mode 100644 feature/select-subject-age-group/src/main/res/layout/fragment_age_group_selection.xml create mode 100644 feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml create mode 100644 infra/resources/src/main/res/drawable/ic_age_group_selection_adult.xml create mode 100644 infra/resources/src/main/res/drawable/ic_age_group_selection_baby.xml create mode 100644 infra/resources/src/main/res/drawable/ic_age_group_selection_child.xml create mode 100644 infra/resources/src/main/res/drawable/ic_age_group_selection_new_born.xml create mode 100644 infra/resources/src/main/res/drawable/ripple_effect.xml create mode 100644 infra/resources/src/main/res/drawable/shadow_rounded_bg.xml diff --git a/feature/orchestrator/build.gradle.kts b/feature/orchestrator/build.gradle.kts index d0d00a7817..45eccd61fd 100644 --- a/feature/orchestrator/build.gradle.kts +++ b/feature/orchestrator/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(project(":feature:select-subject")) implementation(project(":feature:exit-form")) implementation(project(":feature:matcher")) + implementation(project(":feature:select-subject-age-group")) implementation(project(":face:capture")) 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 9f4dab4b88..4078545250 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 @@ -15,6 +15,7 @@ internal object StepId { const val CONSENT = STEP_BASE_CORE + 3 const val ENROL_LAST_BIOMETRIC = STEP_BASE_CORE + 4 const val CONFIRM_IDENTITY = STEP_BASE_CORE + 5 + const val SELECT_SUBJECT_AGE = STEP_BASE_CORE + 6 // Face step ids private const val STEP_BASE_FINGERPRINT = 300 @@ -23,7 +24,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 447a1ca7e1..ef84c95e64 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,11 +14,13 @@ 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.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 @@ -26,6 +28,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, @@ -37,6 +40,7 @@ internal class BuildStepsUseCase @Inject constructor( is ActionRequest.EnrolActionRequest -> listOf( buildSetupStep(), buildConsentStep(ConsentType.ENROL), + buildAgeSelectionStep(action, projectConfiguration), buildModalityCaptureSteps( projectConfiguration, FlowType.ENROL, @@ -53,6 +57,7 @@ internal class BuildStepsUseCase @Inject constructor( is ActionRequest.IdentifyActionRequest -> listOf( buildSetupStep(), + buildAgeSelectionStep(action, projectConfiguration), buildConsentStep(ConsentType.IDENTIFY), buildModalityCaptureSteps( projectConfiguration, @@ -68,6 +73,7 @@ internal class BuildStepsUseCase @Inject constructor( is ActionRequest.VerifyActionRequest -> listOf( buildSetupStep(), + buildAgeSelectionStep(action, projectConfiguration), buildFetchGuidStep(action.projectId, action.verifyGuid), buildConsentStep(ConsentType.VERIFY), buildModalityCaptureSteps( @@ -91,31 +97,56 @@ 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 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 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 buildConsentStep(consentType: ConsentType) = listOf( + Step( + id = StepId.CONSENT, + navigationActionId = R.id.action_orchestratorFragment_to_consent, + destinationId = ConsentContract.DESTINATION, + payload = ConsentContract.getArgs(consentType), + ) + ) private fun buildModalityCaptureSteps( - projectConfiguration: ProjectConfiguration, - flowType: FlowType, + projectConfiguration: ProjectConfiguration, + flowType: FlowType, ) = projectConfiguration.general.modalities.map { when (it) { Modality.FINGERPRINT -> { @@ -144,10 +175,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) { @@ -160,25 +191,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 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 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, + ), + ) + ) } diff --git a/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml b/feature/orchestrator/src/main/res/navigation/graph_orchestration.xml index f9f0cb4cc4..c00864b24b 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..a657c86131 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupContract.kt @@ -0,0 +1,8 @@ +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/SelectSubjectAgeResult.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt new file mode 100644 index 0000000000..639e6802ee --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt @@ -0,0 +1,10 @@ +package com.simprints.feature.selectagegroup + +import androidx.annotation.Keep +import java.io.Serializable + +@Keep +data class SelectSubjectAgeResult( + val success: Boolean, +) : Serializable + diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt new file mode 100644 index 0000000000..3b91c14be6 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt @@ -0,0 +1,3 @@ +package com.simprints.feature.selectagegroup.screen + +internal data class AgeGroup(val displayString: String, val range: IntRange) 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..848dd769e9 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupAdapter.kt @@ -0,0 +1,55 @@ +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.feature.selectagegroup.R +import com.simprints.infra.logging.Simber +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 +internal class AgeGroupAdapter( + private val ageGroups: List, + private val onClick: (AgeGroup) -> 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 + ) + + 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(ageGroup: AgeGroup, position: Int) { + ageGroupTextView.text = ageGroup.displayString + ageGroupIcon.setImageResource(icons[position]) + Simber.i("Age group: $ageGroup") + itemView.setOnClickListener { + onClick(ageGroup) + Simber.i("Age group clicked: $ageGroup") + } + } + } +} diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt new file mode 100644 index 0000000000..2716ebb1d7 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt @@ -0,0 +1,67 @@ +package com.simprints.feature.selectagegroup.screen + +import android.content.Context +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.allowedAgeRanges +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import com.simprints.infra.resources.R.string as IDR + +internal class BuildAgeGroupsUseCase @Inject constructor( + private val configurationRepo: ConfigRepository, + @ApplicationContext private val context: Context, +) { + + suspend operator fun invoke(): List { + val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() + return formatAgeRangesForDisplay(allowedAgeRanges) + } + + private fun formatAgeRangesForDisplay(allowedAgeRanges: List): List { + val result = mutableListOf() + + // Sorting the age ranges + val sortedRanges = allowedAgeRanges.sortedBy { it.first } + // Starting point + var startAge = 0 + + for (range in sortedRanges) { + if (range.first > startAge) { + result.add( + AgeGroup( + "${formatAgeInMonthsForDisplay(startAge)} ${getString(IDR.age_group_selection_age_range_to)} ${formatAgeInMonthsForDisplay(range.first) + }", range + ) + ) + } + if (range.last != Int.MAX_VALUE) { + startAge = range.last + 1 + } else { + startAge = range.first + break + } + } + + result.add( + AgeGroup( + "${formatAgeInMonthsForDisplay(startAge)} ${getString(IDR.age_group_selection_age_range_and_above)}", + startAge..Int.MAX_VALUE + ) + ) + + return result + } + + private fun getString(id: Int): String { + return context.getString(id) + } + + // Helper function to convert months to readable format + private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { + return when { + ageInMonths < 12 -> "$ageInMonths ${getString(IDR.age_group_selection_months)}" + ageInMonths < 24 -> "1 ${getString(IDR.age_group_selection_year)}" + else -> "${ageInMonths / 12} ${getString(IDR.age_group_selection_years)}" + } + } +} 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..98ca7b274b --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt @@ -0,0 +1,54 @@ +package com.simprints.feature.selectagegroup.screen + +import android.os.Bundle +import android.view.View +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.selectagegroup.R +import com.simprints.feature.selectagegroup.databinding.FragmentAgeGroupSelectionBinding +import com.simprints.feature.selectagegroup.SelectSubjectAgeResult +import com.simprints.infra.uibase.navigation.finishWithResult +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.createAgeGroups() + + viewModel.finish.observe(viewLifecycleOwner) { + it.getContentIfNotHandled()?.let(::finishWithResult) + } + // viewModel.saveGuidSelection(args.projectId, args.subjectId) + } + + private fun fillRecyclerView(ageGroupsList: List) { + // fill the recycler view with the age groups + // 0 to 6 months, 6 months to 5 years, 5 to 10 years, 10 years and above + // on click of an age group, call viewModel.saveGuidSelection(args.projectId, args.subjectId) + 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(success: Boolean) { + findNavController().finishWithResult(this, SelectSubjectAgeResult(success)) + } +} 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..7d5a38f874 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModel.kt @@ -0,0 +1,58 @@ +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.infra.events.SessionEventRepository +import com.simprints.infra.events.event.domain.models.GuidSelectionEvent +import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION +import com.simprints.infra.logging.Simber +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: BuildAgeGroupsUseCase, + @ExternalScope private val externalScope: CoroutineScope, + + ) : ViewModel() { + + val finish: LiveData> + get() = _finish + private var _finish = MutableLiveData>() + val ageGroups: LiveData> + get() = _ageGroups + private var _ageGroups = MutableLiveData>() + + fun createAgeGroups() = + viewModelScope.launch { + + val ageGroups = buildAgeGroups() + // notify the adapter + _ageGroups.value = ageGroups + + } + + fun saveAgeGroupSelection(ageRange: IntRange) = externalScope.launch { +// try { +// val event = GuidSelectionEvent(timeHelper.now(), subjectId) +// eventRepository.addOrUpdateEvent(event) +// +// Simber.tag(SESSION.name).i("Added Guid Selection Event") +// _finish.send(true) +// } catch (t: Throwable) { +// // It doesn't matter if it was an error, we always return a result +// Simber.tag(SESSION.name).e(t) +// _finish.send(false) +// } + } +} 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..65e2ae6526 --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/layout/age_group_item.xml @@ -0,0 +1,30 @@ + + + + + + + + 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..518b67d3cb --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/layout/fragment_age_group_selection.xml @@ -0,0 +1,30 @@ + + + + + + + + + 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..83bcd39e48 --- /dev/null +++ b/feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml @@ -0,0 +1,16 @@ + + + + + + + + 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..06ce0d0392 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt @@ -5,6 +5,7 @@ data class FaceConfiguration( val qualityThreshold: Int, val imageSavingStrategy: ImageSavingStrategy, val decisionPolicy: DecisionPolicy, + val allowedAgeRange: IntRange? = null, ) { enum class ImageSavingStrategy { 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..1b7f50b75b 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: IntRange? = 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..be2e9d888d 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 @@ -1,5 +1,7 @@ package com.simprints.infra.config.store.models +import com.simprints.infra.logging.Simber + data class ProjectConfiguration( val projectId: String, val updatedAt: String, @@ -41,3 +43,13 @@ fun ProjectConfiguration.isEventDownSyncAllowed(): Boolean = fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = synchronization.up.simprints.imagesRequireUnmeteredConnection + +fun ProjectConfiguration.allowedAgeRanges(): List { + return listOf( + face?.allowedAgeRange, + fingerprint?.secugenSimMatcher?.allowedAgeRange, + fingerprint?.nec?.allowedAgeRange + ).mapNotNull { it }.also { + Simber.i("Allowed age ranges: $it") + } +} 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..b01f2c9fe7 100644 --- a/infra/resources/src/main/res/values/dimens.xml +++ b/infra/resources/src/main/res/values/dimens.xml @@ -25,6 +25,7 @@ 16sp + 20sp 24sp 16dp diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index 45b15c5543..58c35a9264 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -424,4 +424,11 @@ Sync in progress + + to + and above + months + year + 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 19ffd7b56b..4c81118853 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -110,6 +110,7 @@ include( ":feature:consent", ":feature:setup", ":feature:matcher", + ":feature:select-subject-age-group", ) // Infra modules From dab1da7c4f75a9709ef4ff64505ac96bacb620fd Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 4 Jun 2024 08:40:00 +0300 Subject: [PATCH 02/12] Add Age group selection event --- .../screen/SelectSubjectAgeGroupFragment.kt | 2 +- .../screen/SelectSubjectAgeGroupViewModel.kt | 34 +++++++----- .../models/ApiAgeGroupSelectionPayload.kt | 35 +++++++++++++ .../event/remote/models/ApiEventPayload.kt | 3 ++ .../remote/models/ApiEventPayloadType.kt | 5 ++ ....kt => ApiAgeGroupSelectionPayloadTest.kt} | 4 +- .../domain/models/AgeGroupSelectionEvent.kt | 52 +++++++++++++++++++ .../infra/events/event/domain/models/Event.kt | 2 + .../event/domain/models/EventPayload.kt | 2 + .../events/event/domain/models/EventType.kt | 3 ++ 10 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayload.kt rename infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/{ApiGuidSelectionPayloadTest.kt => ApiAgeGroupSelectionPayloadTest.kt} (92%) create mode 100644 infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AgeGroupSelectionEvent.kt 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 index 98ca7b274b..ea81694752 100644 --- 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 @@ -24,7 +24,7 @@ internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_gr viewModel.ageGroups.observe(viewLifecycleOwner) { ageGroupsList -> fillRecyclerView(ageGroupsList) } - viewModel.createAgeGroups() + viewModel.start() viewModel.finish.observe(viewLifecycleOwner) { it.getContentIfNotHandled()?.let(::finishWithResult) 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 index 7d5a38f874..6be1aaf833 100644 --- 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 @@ -8,8 +8,9 @@ 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.infra.events.SessionEventRepository -import com.simprints.infra.events.event.domain.models.GuidSelectionEvent +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SESSION import com.simprints.infra.logging.Simber import dagger.hilt.android.lifecycle.HiltViewModel @@ -32,10 +33,11 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( val ageGroups: LiveData> get() = _ageGroups private var _ageGroups = MutableLiveData>() + private lateinit var startTime : Timestamp - fun createAgeGroups() = + fun start() = viewModelScope.launch { - + startTime = timeHelper.now() val ageGroups = buildAgeGroups() // notify the adapter _ageGroups.value = ageGroups @@ -43,16 +45,20 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( } fun saveAgeGroupSelection(ageRange: IntRange) = externalScope.launch { -// try { -// val event = GuidSelectionEvent(timeHelper.now(), subjectId) -// eventRepository.addOrUpdateEvent(event) -// -// Simber.tag(SESSION.name).i("Added Guid Selection Event") -// _finish.send(true) -// } catch (t: Throwable) { -// // It doesn't matter if it was an error, we always return a result -// Simber.tag(SESSION.name).e(t) -// _finish.send(false) -// } + try { + val event = AgeGroupSelectionEvent( + startTime, + timeHelper.now(), + AgeGroupSelectionEvent.AgeGroup(ageRange.first, ageRange.last) + ) + eventRepository.addOrUpdateEvent(event) + + Simber.tag(SESSION.name).i("Added Age Group Selection Event") + _finish.send(true) + } catch (t: Throwable) { + // It doesn't matter if it was an error, we always return a result + Simber.tag(SESSION.name).e(t) + _finish.send(false) + } } } 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..a7de6fe831 --- /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 from: Int, + val to: Int, + ) + +} + +private fun AgeGroupSelectionEvent.AgeGroup.fromDomainToApi() = + ApiAgeGroupSelectionPayload.ApiAgeGroup( + from = from, + to = to, + ) + 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 dadba08d2e..78d3c3c142 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 @@ -103,6 +103,9 @@ internal enum class ApiEventPayloadType { /* key added: LICENSE_CHECK_KEY */ LicenseCheck, + /* key added: AGE_GROUP_SELECTION_KEY */ + AGE_GROUP_SELECTION, + ; companion object { @@ -185,6 +188,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.AGE_GROUP_SELECTION } @@ -218,6 +222,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.AGE_GROUP_SELECTION -> 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/remote/models/ApiGuidSelectionPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt similarity index 92% 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..5ecd326767 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 @@ -5,7 +5,7 @@ import com.simprints.infra.config.store.models.TokenKeyType import io.mockk.mockk import org.junit.Test -class ApiGuidSelectionPayloadTest { +class ApiAgeGroupSelectionPayloadTest { @Test fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { @@ -14,4 +14,4 @@ class ApiGuidSelectionPayloadTest { assertThat(payload.getTokenizedFieldJsonPath(it)).isNull() } } -} \ No newline at end of file +} 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..8aa96ef706 --- /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 from: Int, + val to: 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 d7a4f371d6..3a2b5704c8 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 2bcd574e6a..84d990569d 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 @@ -43,6 +43,7 @@ import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCap import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent.EventUpSyncRequestPayload import com.simprints.infra.events.event.domain.models.LicenseCheckEvent.LicenseCheckEventPayload +import com.simprints.infra.events.event.domain.models.AgeGroupSelectionEvent.AgeGroupSelectionPayload @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) @JsonSubTypes( @@ -87,6 +88,7 @@ import com.simprints.infra.events.event.domain.models.LicenseCheckEvent.LicenseC 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" } } From ad312eca7b699c70882cb439d43c22a998cd581a Mon Sep 17 00:00:00 2001 From: melad Date: Wed, 5 Jun 2024 14:14:34 +0300 Subject: [PATCH 03/12] [MS-479] handle selection result --- .github/workflows/pr-checks.yml | 1 + build-logic/proguard-rules.pro | 5 +- .../orchestrator/OrchestratorFragment.kt | 2 + .../feature/orchestrator/steps/Step.kt | 2 + .../select-subject-age-group/build.gradle.kts | 2 + .../selectagegroup/SelectSubjectAgeResult.kt | 3 +- .../feature/selectagegroup/screen/AgeGroup.kt | 3 - .../selectagegroup/screen/AgeGroupAdapter.kt | 14 ++-- .../screen/AgeGroupDisplayModel.kt | 5 ++ .../BuildAgeGroupsDescriptionUseCase.kt | 69 +++++++++++++++++ .../screen/BuildAgeGroupsUseCase.kt | 67 ---------------- .../screen/SelectSubjectAgeGroupFragment.kt | 31 ++++++-- .../screen/SelectSubjectAgeGroupViewModel.kt | 76 +++++++++++++------ .../navigation/graph_age_group_selection.xml | 9 +++ .../local/models/FingerprintConfiguration.kt | 2 + .../infra/config/store/models/AgeGroup.kt | 6 ++ .../config/store/models/FaceConfiguration.kt | 1 - .../store/models/FingerprintConfiguration.kt | 2 +- .../store/models/ProjectConfiguration.kt | 8 +- .../models/ApiAgeGroupSelectionPayload.kt | 8 +- .../remote/models/ApiEventPayloadType.kt | 6 +- .../domain/models/AgeGroupSelectionEvent.kt | 4 +- 22 files changed, 197 insertions(+), 129 deletions(-) delete mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroupDisplayModel.kt create mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt delete mode 100644 feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt 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/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/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt index f993bc145f..1c1df75fbc 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..43c6a3706b 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.SelectSubjectAgeResult 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 = SelectSubjectAgeResult::class, name = "SelectSubjectAgeResult"), ) abstract class SerializableMixin : Serializable diff --git a/feature/select-subject-age-group/build.gradle.kts b/feature/select-subject-age-group/build.gradle.kts index 4e089a8cf9..758cda6c86 100644 --- a/feature/select-subject-age-group/build.gradle.kts +++ b/feature/select-subject-age-group/build.gradle.kts @@ -12,4 +12,6 @@ dependencies { implementation(project(":infra:auth-store")) implementation(project(":infra:logging")) implementation(project(":infra:config-store")) + implementation(project(":feature:exit-form")) + } diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt index 639e6802ee..175cddb3c4 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt @@ -1,10 +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 SelectSubjectAgeResult( - val success: Boolean, + val ageGroup: AgeGroup, ) : Serializable diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt deleted file mode 100644 index 3b91c14be6..0000000000 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/AgeGroup.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.simprints.feature.selectagegroup.screen - -internal data class AgeGroup(val displayString: String, val range: IntRange) 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 index 848dd769e9..c800012ecc 100644 --- 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 @@ -14,8 +14,8 @@ 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 internal class AgeGroupAdapter( - private val ageGroups: List, - private val onClick: (AgeGroup) -> Unit + private val ageGroups: List, + private val onClick: (AgeGroupDisplayModel) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -42,13 +42,13 @@ internal class AgeGroupAdapter( 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(ageGroup: AgeGroup, position: Int) { - ageGroupTextView.text = ageGroup.displayString + fun bind(ageGroupDisplayModel: AgeGroupDisplayModel, position: Int) { + ageGroupTextView.text = ageGroupDisplayModel.displayString ageGroupIcon.setImageResource(icons[position]) - Simber.i("Age group: $ageGroup") + Simber.i("Age group: $ageGroupDisplayModel") itemView.setOnClickListener { - onClick(ageGroup) - Simber.i("Age group clicked: $ageGroup") + onClick(ageGroupDisplayModel) + Simber.i("Age group clicked: $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..bee2954f66 --- /dev/null +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCase.kt @@ -0,0 +1,69 @@ +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.string 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 + * overlapping age ranges are not yet supported + */ + suspend operator fun invoke(): List { + val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() + return formatAgeRangesForDisplay(allowedAgeRanges) + } + + private fun formatAgeRangesForDisplay(ageGroups: List): List { + + // Sorting the age ranges + val sortedRanges = ageGroups.sortedBy { it.startInclusive }.toMutableList() + + // Add initial item if no age group starts with 0 + if (sortedRanges.isEmpty() || sortedRanges.first().startInclusive != 0) { + sortedRanges.add(0, AgeGroup(0, sortedRanges.firstOrNull()?.startInclusive)) + } + // Add final item if no age group ends with null + if (sortedRanges.none { it.endExclusive == null }) { + sortedRanges.add(AgeGroup(sortedRanges.last().endExclusive ?: 0, null)) + } + return sortedRanges.map { ageGroup -> + AgeGroupDisplayModel(ageGroup.getDisplayName(), ageGroup) + } + + } + + private fun getString(id: Int): String { + return context.getString(id) + } + + // Helper function to convert months to readable format + private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { + return when { + ageInMonths < 12 -> "$ageInMonths ${getString(IDR.age_group_selection_months)}" + ageInMonths < 24 -> "1 ${getString(IDR.age_group_selection_year)}" + else -> "${ageInMonths / 12} ${getString(IDR.age_group_selection_years)}" + } + } + + private fun AgeGroup.getDisplayName(): String { + val start = formatAgeInMonthsForDisplay(startInclusive) + val end = endExclusive?.let { + "${getString(IDR.age_group_selection_age_range_to)} ${ + formatAgeInMonthsForDisplay(it) + }" + } ?: getString(IDR.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/BuildAgeGroupsUseCase.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt deleted file mode 100644 index 2716ebb1d7..0000000000 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsUseCase.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.simprints.feature.selectagegroup.screen - -import android.content.Context -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.allowedAgeRanges -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import com.simprints.infra.resources.R.string as IDR - -internal class BuildAgeGroupsUseCase @Inject constructor( - private val configurationRepo: ConfigRepository, - @ApplicationContext private val context: Context, -) { - - suspend operator fun invoke(): List { - val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() - return formatAgeRangesForDisplay(allowedAgeRanges) - } - - private fun formatAgeRangesForDisplay(allowedAgeRanges: List): List { - val result = mutableListOf() - - // Sorting the age ranges - val sortedRanges = allowedAgeRanges.sortedBy { it.first } - // Starting point - var startAge = 0 - - for (range in sortedRanges) { - if (range.first > startAge) { - result.add( - AgeGroup( - "${formatAgeInMonthsForDisplay(startAge)} ${getString(IDR.age_group_selection_age_range_to)} ${formatAgeInMonthsForDisplay(range.first) - }", range - ) - ) - } - if (range.last != Int.MAX_VALUE) { - startAge = range.last + 1 - } else { - startAge = range.first - break - } - } - - result.add( - AgeGroup( - "${formatAgeInMonthsForDisplay(startAge)} ${getString(IDR.age_group_selection_age_range_and_above)}", - startAge..Int.MAX_VALUE - ) - ) - - return result - } - - private fun getString(id: Int): String { - return context.getString(id) - } - - // Helper function to convert months to readable format - private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { - return when { - ageInMonths < 12 -> "$ageInMonths ${getString(IDR.age_group_selection_months)}" - ageInMonths < 24 -> "1 ${getString(IDR.age_group_selection_year)}" - else -> "${ageInMonths / 12} ${getString(IDR.age_group_selection_years)}" - } - } -} 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 index ea81694752..c43742fa2e 100644 --- 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 @@ -2,15 +2,19 @@ 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.databinding.FragmentAgeGroupSelectionBinding import com.simprints.feature.selectagegroup.SelectSubjectAgeResult +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 @@ -21,18 +25,29 @@ internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_gr 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.start() + + 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.saveGuidSelection(args.projectId, args.subjectId) + viewModel.start() } - private fun fillRecyclerView(ageGroupsList: List) { + private fun fillRecyclerView(ageGroupsList: List) { // fill the recycler view with the age groups // 0 to 6 months, 6 months to 5 years, 5 to 10 years, 10 years and above // on click of an age group, call viewModel.saveGuidSelection(args.projectId, args.subjectId) @@ -42,13 +57,13 @@ internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_gr this.context, (layoutManager as LinearLayoutManager).orientation ) this.addItemDecoration(dividerItemDecoration) - adapter = AgeGroupAdapter(ageGroupsList) {ageGroup -> - viewModel.saveAgeGroupSelection(ageGroup.range) + adapter = AgeGroupAdapter(ageGroupsList) { ageGroup -> + viewModel.saveAgeGroupSelection(ageGroup.range) } } } - private fun finishWithResult(success: Boolean) { - findNavController().finishWithResult(this, SelectSubjectAgeResult(success)) + private fun finishWithResult(ageGroup: AgeGroup) { + findNavController().finishWithResult(this, SelectSubjectAgeResult(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 index 6be1aaf833..c3a54d16a8 100644 --- 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 @@ -9,10 +9,17 @@ 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 @@ -22,43 +29,66 @@ import javax.inject.Inject internal class SelectSubjectAgeGroupViewModel @Inject constructor( private val timeHelper: TimeHelper, private val eventRepository: SessionEventRepository, - private val buildAgeGroups: BuildAgeGroupsUseCase, + private val buildAgeGroups: BuildAgeGroupsDescriptionUseCase, + private val configurationRepo: ConfigRepository, @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { - val finish: LiveData> + val finish: LiveData> get() = _finish - private var _finish = MutableLiveData>() - val ageGroups: LiveData> - get() = _ageGroups - private var _ageGroups = MutableLiveData>() - private lateinit var startTime : Timestamp + private var _finish = MutableLiveData>() + val ageGroups: LiveData> + get() = _ageGroupsDisplayModel + private var _ageGroupsDisplayModel = MutableLiveData>() + private lateinit var startTime: Timestamp - fun start() = - viewModelScope.launch { - startTime = timeHelper.now() - val ageGroups = buildAgeGroups() - // notify the adapter - _ageGroups.value = ageGroups + 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 { - fun saveAgeGroupSelection(ageRange: IntRange) = externalScope.launch { - try { val event = AgeGroupSelectionEvent( - startTime, + startTime, timeHelper.now(), - AgeGroupSelectionEvent.AgeGroup(ageRange.first, ageRange.last) + AgeGroupSelectionEvent.AgeGroup(ageRange.startInclusive, ageRange.endExclusive) ) eventRepository.addOrUpdateEvent(event) Simber.tag(SESSION.name).i("Added Age Group Selection Event") - _finish.send(true) - } catch (t: Throwable) { - // It doesn't matter if it was an error, we always return a result - Simber.tag(SESSION.name).e(t) - _finish.send(false) + _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/navigation/graph_age_group_selection.xml b/feature/select-subject-age-group/src/main/res/navigation/graph_age_group_selection.xml index 83bcd39e48..1408d140c6 100644 --- 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 @@ -13,4 +13,13 @@ + + + + + + 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..1b832cc887 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 = @@ -87,6 +88,7 @@ internal fun ProtoFingerprintConfiguration.ProtoFingerprintSdkConfiguration.toDo comparisonStrategyForVerification.toDomain(), if (hasVero1()) vero1.toDomain() else null, if (hasVero2()) vero2.toDomain() else null, + AgeGroup(130, 300), ) 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..d6a38c0f6c --- /dev/null +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/AgeGroup.kt @@ -0,0 +1,6 @@ +package com.simprints.infra.config.store.models + +data class AgeGroup( + val startInclusive: Int, + val endExclusive: Int?, +) 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 06ce0d0392..372ae6c5c1 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/FaceConfiguration.kt @@ -5,7 +5,6 @@ data class FaceConfiguration( val qualityThreshold: Int, val imageSavingStrategy: ImageSavingStrategy, val decisionPolicy: DecisionPolicy, - val allowedAgeRange: IntRange? = null, ) { enum class ImageSavingStrategy { 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 1b7f50b75b..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 @@ -14,7 +14,7 @@ data class FingerprintConfiguration( val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: Vero1Configuration? = null, val vero2: Vero2Configuration? = null, - val allowedAgeRange: IntRange? = 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 be2e9d888d..c4dbb773c9 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 @@ -44,12 +44,10 @@ fun ProjectConfiguration.isEventDownSyncAllowed(): Boolean = fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = synchronization.up.simprints.imagesRequireUnmeteredConnection -fun ProjectConfiguration.allowedAgeRanges(): List { +fun ProjectConfiguration.allowedAgeRanges(): List { return listOf( - face?.allowedAgeRange, + //Todo add face roc sdk , fingerprint?.secugenSimMatcher?.allowedAgeRange, fingerprint?.nec?.allowedAgeRange - ).mapNotNull { it }.also { - Simber.i("Allowed age ranges: $it") - } + ).mapNotNull { it } } 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 index a7de6fe831..381d32827e 100644 --- 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 @@ -21,15 +21,15 @@ internal data class ApiAgeGroupSelectionPayload( null // this payload doesn't have tokenizable fields data class ApiAgeGroup( - val from: Int, - val to: Int, + val startInclusive: Int, + val endExclusive: Int?, ) } private fun AgeGroupSelectionEvent.AgeGroup.fromDomainToApi() = ApiAgeGroupSelectionPayload.ApiAgeGroup( - from = from, - to = to, + startInclusive = startInclusive, + endExclusive = endExclusive, ) 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 78d3c3c142..151943b23a 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 @@ -104,7 +104,7 @@ internal enum class ApiEventPayloadType { /* key added: LICENSE_CHECK_KEY */ LicenseCheck, /* key added: AGE_GROUP_SELECTION_KEY */ - AGE_GROUP_SELECTION, + AgeGroupSelection, ; @@ -188,7 +188,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.AGE_GROUP_SELECTION + AGE_GROUP_SELECTION -> ApiEventPayloadType.AgeGroupSelection } @@ -222,7 +222,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.AGE_GROUP_SELECTION -> AGE_GROUP_SELECTION + ApiEventPayloadType.AgeGroupSelection -> AGE_GROUP_SELECTION ApiEventPayloadType.Callout -> throw UnsupportedOperationException("") ApiEventPayloadType.Callback -> throw UnsupportedOperationException("") } 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 index 8aa96ef706..41de698c1a 100644 --- 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 @@ -42,8 +42,8 @@ data class AgeGroupSelectionEvent( @Keep data class AgeGroup( - val from: Int, - val to: Int?, + val startInclusive: Int, + val endExclusive: Int?, ) companion object { From f6937bd8409c60979868ea98cbb811f32c8bde32 Mon Sep 17 00:00:00 2001 From: melad Date: Thu, 6 Jun 2024 08:29:25 +0300 Subject: [PATCH 04/12] [MS-479] Use the new fingerprint sdk config --- build-logic/build_properties.gradle.kts | 3 +- .../selectagegroup/screen/AgeGroupAdapter.kt | 10 +- .../BuildAgeGroupsDescriptionUseCaseTest.kt | 94 +++++++++++++++++++ .../local/models/FingerprintConfiguration.kt | 16 +++- .../src/main/proto/project_config.proto | 5 + .../eventsync/event/EventValidationUtils.kt | 8 ++ .../eventsync/event/remote/ApiEventTest.kt | 12 +++ .../testtools/RemoteTestingHelper.kt | 2 +- .../events/sampledata/EventFactoryUtils.kt | 6 ++ .../all-events/age_group_selection_v1.json | 19 ++++ 10 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt create mode 100644 infra/events/src/test/resources/all-events/age_group_selection_v1.json diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index ccfadaf454..7ca92d3728 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -13,8 +13,7 @@ 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") 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 index c800012ecc..84944d84d9 100644 --- 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 @@ -7,7 +7,6 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.simprints.feature.selectagegroup.R -import com.simprints.infra.logging.Simber import com.simprints.infra.resources.R as IDR @@ -16,8 +15,7 @@ import com.simprints.infra.resources.R as IDR internal class AgeGroupAdapter( private val ageGroups: List, private val onClick: (AgeGroupDisplayModel) -> Unit -) : - RecyclerView.Adapter() { +) : 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) @@ -44,11 +42,11 @@ internal class AgeGroupAdapter( private val ageGroupIcon: ImageView = itemView.findViewById(R.id.item_icon) fun bind(ageGroupDisplayModel: AgeGroupDisplayModel, position: Int) { ageGroupTextView.text = ageGroupDisplayModel.displayString - ageGroupIcon.setImageResource(icons[position]) - Simber.i("Age group: $ageGroupDisplayModel") + // 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) - Simber.i("Age group clicked: $ageGroupDisplayModel") } } } 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..5136b70bd2 --- /dev/null +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/BuildAgeGroupsDescriptionUseCaseTest.kt @@ -0,0 +1,94 @@ +package com.simprints.feature.selectagegroup.screen + +import android.content.Context +import com.google.common.truth.Truth +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.AgeGroup +import com.simprints.infra.config.store.models.allowedAgeRanges +import io.mockk.MockKAnnotations +import io.mockk.coEvery +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 +import com.simprints.infra.resources.R.string as IDR + + +class BuildAgeGroupsDescriptionUseCaseTest { + private lateinit var buildAgeGroupsDescription: BuildAgeGroupsDescriptionUseCase + + @MockK + private lateinit var context: 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) + with(context) { + every { getString(IDR.age_group_selection_months) } returns "months" + every { getString(IDR.age_group_selection_year) } returns "year" + every { getString(IDR.age_group_selection_years) } returns "years" + every { getString(IDR.age_group_selection_age_range_to) } returns "to" + every { getString(IDR.age_group_selection_age_range_and_above) } returns "and above" + } + } + + @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 testAgeGroupWithInitialAndFinalMissing() = runTest { + coEvery { configurationRepo.getProjectConfiguration().allowedAgeRanges() } returns listOf( + AgeGroup(6, 60), AgeGroup(60, null) + ) + + 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) + } + + private fun assertAgeGroupDescriptionsMatchExpected( + result: List, + expected: Array + ) { + result.forEachIndexed { index, ageGroupDisplayModel -> + Truth.assertThat( + ageGroupDisplayModel.displayString + ).isEqualTo(expected[index]) + } + + } + +} + 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 1b832cc887..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 @@ -23,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 @@ -88,7 +90,7 @@ internal fun ProtoFingerprintConfiguration.ProtoFingerprintSdkConfiguration.toDo comparisonStrategyForVerification.toDomain(), if (hasVero1()) vero1.toDomain() else null, if (hasVero2()) vero2.toDomain() else null, - AgeGroup(130, 300), + if (hasAllowedAgeRange()) allowedAgeRange.toDomain() else null ) @@ -107,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/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/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/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 3fdf619719..13371ab414 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 @@ -418,3 +418,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/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 + } + } +} From 2f7ec3802fe24ea3f4aa1144aad46c2d440db4ff Mon Sep 17 00:00:00 2001 From: melad Date: Thu, 6 Jun 2024 18:01:59 +0300 Subject: [PATCH 05/12] [MS-479] Handle age groups overlapping --- .../feature/orchestrator/steps/Step.kt | 4 +- ...sult.kt => SelectSubjectAgeGroupResult.kt} | 2 +- .../BuildAgeGroupsDescriptionUseCase.kt | 43 +++++++++++++++---- .../screen/SelectSubjectAgeGroupFragment.kt | 4 +- .../BuildAgeGroupsDescriptionUseCaseTest.kt | 32 +++++++++----- .../models/ApiAgeGroupSelectionPayloadTest.kt | 2 +- 6 files changed, 61 insertions(+), 26 deletions(-) rename feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/{SelectSubjectAgeResult.kt => SelectSubjectAgeGroupResult.kt} (84%) 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 43c6a3706b..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,7 +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.SelectSubjectAgeResult +import com.simprints.feature.selectagegroup.SelectSubjectAgeGroupResult import com.simprints.feature.selectsubject.SelectSubjectResult import com.simprints.feature.setup.SetupResult import com.simprints.feature.validatepool.ValidateSubjectPoolResult @@ -57,7 +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 = SelectSubjectAgeResult::class, name = "SelectSubjectAgeResult"), + JsonSubTypes.Type(value = SelectSubjectAgeGroupResult::class, name = "SelectSubjectAgeResult"), ) abstract class SerializableMixin : Serializable diff --git a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt similarity index 84% rename from feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt rename to feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt index 175cddb3c4..e75eb0a8c2 100644 --- a/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeResult.kt +++ b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/SelectSubjectAgeGroupResult.kt @@ -5,7 +5,7 @@ import com.simprints.infra.config.store.models.AgeGroup import java.io.Serializable @Keep -data class SelectSubjectAgeResult( +data class SelectSubjectAgeGroupResult( val ageGroup: AgeGroup, ) : Serializable 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 index bee2954f66..a61bfa52fb 100644 --- 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 @@ -27,22 +27,47 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( private fun formatAgeRangesForDisplay(ageGroups: List): List { // Sorting the age ranges - val sortedRanges = ageGroups.sortedBy { it.startInclusive }.toMutableList() + val sortedRanges = processAgeGroups(ageGroups).toMutableList() - // Add initial item if no age group starts with 0 - if (sortedRanges.isEmpty() || sortedRanges.first().startInclusive != 0) { - sortedRanges.add(0, AgeGroup(0, sortedRanges.firstOrNull()?.startInclusive)) - } - // Add final item if no age group ends with null - if (sortedRanges.none { it.endExclusive == null }) { - sortedRanges.add(AgeGroup(sortedRanges.last().endExclusive ?: 0, null)) - } return sortedRanges.map { ageGroup -> AgeGroupDisplayModel(ageGroup.getDisplayName(), ageGroup) } } + private fun processAgeGroups(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 + val sortedUniqueAges = + ageGroups.flatMap { listOf(it.startInclusive, it.endExclusive) }.filterNotNull() + .sorted().distinct() + + val processedAgeGroups = mutableListOf() + + // Ensure the first age group starts at 0 + if (sortedUniqueAges.first() != 0) { + processedAgeGroups.add(AgeGroup(0, sortedUniqueAges.first())) + } + + var startAge = sortedUniqueAges.first() + + // Create age groups based on sorted unique ages + for (i in 1 until sortedUniqueAges.size) { + val endAge = sortedUniqueAges[i] + processedAgeGroups.add(AgeGroup(startAge, endAge)) + startAge = endAge + } + + // Ensure the final age group ends with null + processedAgeGroups.add(AgeGroup(sortedUniqueAges.last(), null)) + + return processedAgeGroups + + } + + private fun getString(id: Int): String { return context.getString(id) } 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 index c43742fa2e..846c3152b2 100644 --- 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 @@ -10,7 +10,7 @@ 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.SelectSubjectAgeResult +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 @@ -64,6 +64,6 @@ internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_gr } private fun finishWithResult(ageGroup: AgeGroup) { - findNavController().finishWithResult(this, SelectSubjectAgeResult(ageGroup)) + findNavController().finishWithResult(this, SelectSubjectAgeGroupResult(ageGroup)) } } 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 index 5136b70bd2..be1db49c87 100644 --- 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 @@ -41,29 +41,40 @@ class BuildAgeGroupsDescriptionUseCaseTest { @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") - + 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), AgeGroup(60, null) + AgeGroup(6, 60) ) val result = buildAgeGroupsDescription() - val expected = - arrayOf("0 months to 6 months", "6 months to 5 years", "5 years and above") + val expected = arrayOf("0 months to 6 months", "6 months to 5 years", "5 years and above") assertAgeGroupDescriptionsMatchExpected(result, expected) } @@ -79,8 +90,7 @@ class BuildAgeGroupsDescriptionUseCaseTest { } private fun assertAgeGroupDescriptionsMatchExpected( - result: List, - expected: Array + result: List, expected: Array ) { result.forEachIndexed { index, ageGroupDisplayModel -> Truth.assertThat( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt index 5ecd326767..893085bd1c 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiAgeGroupSelectionPayloadTest.kt @@ -5,7 +5,7 @@ import com.simprints.infra.config.store.models.TokenKeyType import io.mockk.mockk import org.junit.Test -class ApiAgeGroupSelectionPayloadTest { +class ApiGuidSelectionPayloadTest { @Test fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { From ca12fa8b90f00d20ac8daee1b7148960eece783b Mon Sep 17 00:00:00 2001 From: melad Date: Thu, 6 Jun 2024 18:33:05 +0300 Subject: [PATCH 06/12] [MS-479] add SelectSubjectAgeGroupViewModelTest --- .../selectagegroup/screen/AgeGroupAdapter.kt | 3 + .../BuildAgeGroupsDescriptionUseCase.kt | 27 +++--- .../SelectSubjectAgeGroupViewModelTest.kt | 82 +++++++++++++++++++ 3 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt 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 index 84944d84d9..867d06275e 100644 --- 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 @@ -6,12 +6,14 @@ 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 @@ -37,6 +39,7 @@ internal class AgeGroupAdapter( 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) 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 index a61bfa52fb..5523124e03 100644 --- 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 @@ -17,25 +17,18 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( * 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 - * overlapping age ranges are not yet supported */ suspend operator fun invoke(): List { - val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() - return formatAgeRangesForDisplay(allowedAgeRanges) - } - - private fun formatAgeRangesForDisplay(ageGroups: List): List { - // Sorting the age ranges - val sortedRanges = processAgeGroups(ageGroups).toMutableList() + val allowedAgeRanges = configurationRepo.getProjectConfiguration().allowedAgeRanges() + val processedAgeRanges = generateSortedUniqueAgeGroups(allowedAgeRanges) - return sortedRanges.map { ageGroup -> + return processedAgeRanges.map { ageGroup -> AgeGroupDisplayModel(ageGroup.getDisplayName(), ageGroup) } - } - private fun processAgeGroups(ageGroups: List): List { + 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)) @@ -68,26 +61,26 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( } - private fun getString(id: Int): String { + private fun getResourceString(id: Int): String { return context.getString(id) } // Helper function to convert months to readable format private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { return when { - ageInMonths < 12 -> "$ageInMonths ${getString(IDR.age_group_selection_months)}" - ageInMonths < 24 -> "1 ${getString(IDR.age_group_selection_year)}" - else -> "${ageInMonths / 12} ${getString(IDR.age_group_selection_years)}" + ageInMonths < 12 -> "$ageInMonths ${getResourceString(IDR.age_group_selection_months)}" + ageInMonths < 24 -> "1 ${getResourceString(IDR.age_group_selection_year)}" + else -> "${ageInMonths / 12} ${getResourceString(IDR.age_group_selection_years)}" } } private fun AgeGroup.getDisplayName(): String { val start = formatAgeInMonthsForDisplay(startInclusive) val end = endExclusive?.let { - "${getString(IDR.age_group_selection_age_range_to)} ${ + "${getResourceString(IDR.age_group_selection_age_range_to)} ${ formatAgeInMonthsForDisplay(it) }" - } ?: getString(IDR.age_group_selection_age_range_and_above) + } ?: getResourceString(IDR.age_group_selection_age_range_and_above) return "$start $end" } } 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..2d08aa2a73 --- /dev/null +++ b/feature/select-subject-age-group/src/test/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupViewModelTest.kt @@ -0,0 +1,82 @@ +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.events.SessionEventRepository +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) + + } + + +} From b060479da2f411c8d1f988c2e403a9dc6cc8280e Mon Sep 17 00:00:00 2001 From: melad Date: Sat, 15 Jun 2024 14:53:26 +0300 Subject: [PATCH 07/12] [MS-479] add ApiAgeGroup.kt to ApiFingerprintConfiguration --- .../screen/SelectSubjectAgeGroupViewModel.kt | 22 +++++++++++-------- .../store/remote/ConfigRemoteInterface.kt | 3 --- .../config/store/remote/models/ApiAgeGroup.kt | 12 ++++++++++ .../models/ApiFingerprintConfiguration.kt | 9 +++++--- .../models/FingerprintConfigurationTest.kt | 12 ++++++++++ .../infra/config/store/testtools/Models.kt | 5 ++++- 6 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiAgeGroup.kt 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 index c3a54d16a8..9eb19afd42 100644 --- 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.ExternalScope import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send @@ -45,7 +46,8 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( val showExitForm: LiveData> get() = _showExitForm - private val _showExitForm = MutableLiveData>() + private val _showExitForm = + MutableLiveData>() fun start() = viewModelScope.launch { startTime = timeHelper.now() @@ -56,15 +58,15 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( fun saveAgeGroupSelection(ageRange: AgeGroup) = externalScope.launch { - val event = AgeGroupSelectionEvent( - startTime, - timeHelper.now(), - AgeGroupSelectionEvent.AgeGroup(ageRange.startInclusive, ageRange.endExclusive) - ) - eventRepository.addOrUpdateEvent(event) + 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) + Simber.tag(SESSION.name).i("Added Age Group Selection Event") + _finish.send(ageRange) } fun onBackPressed() { @@ -74,6 +76,8 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( _showExitForm.send(getExitFormFromModalities(projectConfig.general.modalities)) } } + + @ExcludedFromGeneratedTestCoverageReports(reason = "UI code") private fun getExitFormFromModalities(modalities: List) = when { modalities.size != 1 -> exitFormConfiguration { titleRes = R.string.exit_form_title_biometrics 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 6e94b87879..68f38e27d0 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 @@ -12,9 +12,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..fd82d8c40e --- /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, 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..1e747fe9ce 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 @@ -1,6 +1,7 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep +import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Finger as DomainFingerprint @@ -27,15 +28,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? = null, ) { fun toDomain() = FingerprintConfiguration.FingerprintSdkConfiguration( fingersToCapture.map { it.toDomain() }, decisionPolicy.toDomain(), comparisonStrategyForVerification.toDomain(), vero1?.toDomain(), - vero2?.toDomain() - + vero2?.toDomain(), + allowedAgeRange?.toDomain(), ) } @@ -98,4 +100,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/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/testtools/Models.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/testtools/Models.kt index 51ece29ca7..af64c049ed 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 @@ -121,6 +121,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, @@ -152,7 +153,9 @@ internal val apiFingerprintConfiguration = ApiFingerprintConfiguration( apiDecisionPolicy, ApiFingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, ApiVero1Configuration(10), - apiVero2Configuration + apiVero2Configuration, + apiAgeGroup , + ), null, ) From 7f13b41142a3e56119ab96741b69c68e7ec9b8a9 Mon Sep 17 00:00:00 2001 From: melad Date: Sun, 16 Jun 2024 09:13:57 +0300 Subject: [PATCH 08/12] [MS-479] Code reformat --- .../selectagegroup/SelectSubjectAgeGroupContract.kt | 2 -- .../screen/SelectSubjectAgeGroupFragment.kt | 3 --- .../screen/SelectSubjectAgeGroupViewModel.kt | 6 +----- .../src/main/res/layout/age_group_item.xml | 2 +- .../main/res/navigation/graph_age_group_selection.xml | 10 +--------- 5 files changed, 3 insertions(+), 20 deletions(-) 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 index a657c86131..8b98a34cbf 100644 --- 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 @@ -2,7 +2,5 @@ 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/screen/SelectSubjectAgeGroupFragment.kt b/feature/select-subject-age-group/src/main/java/com/simprints/feature/selectagegroup/screen/SelectSubjectAgeGroupFragment.kt index 846c3152b2..be9a81b665 100644 --- 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 @@ -48,9 +48,6 @@ internal class SelectSubjectAgeGroupFragment : Fragment(R.layout.fragment_age_gr } private fun fillRecyclerView(ageGroupsList: List) { - // fill the recycler view with the age groups - // 0 to 6 months, 6 months to 5 years, 5 to 10 years, 10 years and above - // on click of an age group, call viewModel.saveGuidSelection(args.projectId, args.subjectId) with(binding.ageGroupRecyclerView) { layoutManager = LinearLayoutManager(requireContext()) val dividerItemDecoration = DividerItemDecoration( 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 index 9eb19afd42..9053cfc0d1 100644 --- 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 @@ -33,8 +33,7 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( private val buildAgeGroups: BuildAgeGroupsDescriptionUseCase, private val configurationRepo: ConfigRepository, @ExternalScope private val externalScope: CoroutineScope, - - ) : ViewModel() { +) : ViewModel() { val finish: LiveData> get() = _finish @@ -57,20 +56,17 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( } 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)) 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 index 65e2ae6526..2d6f625174 100644 --- 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 @@ -15,7 +15,7 @@ android:background="@drawable/shadow_rounded_bg" android:elevation="4dp" android:padding="8dp" - android:src="@drawable/ic_age_group_selection_child" /> + tools:src=" @drawable/ic_age_group_selection_child" /> - - - - - + tools:layout="@layout/fragment_age_group_selection" /> - - - From f34cbc8017e175d4bfa686cfe08edaedee25b1bd Mon Sep 17 00:00:00 2001 From: melad Date: Sun, 16 Jun 2024 09:45:55 +0300 Subject: [PATCH 09/12] [MS-479] Add tests to SelectSubjectAgeGroupViewModelTest --- .../screen/SelectSubjectAgeGroupViewModel.kt | 2 - .../SelectSubjectAgeGroupViewModelTest.kt | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) 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 index 9053cfc0d1..cad850726c 100644 --- 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 @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.ExternalScope import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send @@ -73,7 +72,6 @@ internal class SelectSubjectAgeGroupViewModel @Inject constructor( } } - @ExcludedFromGeneratedTestCoverageReports(reason = "UI code") private fun getExitFormFromModalities(modalities: List) = when { modalities.size != 1 -> exitFormConfiguration { titleRes = R.string.exit_form_title_biometrics 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 index 2d08aa2a73..572df68f1a 100644 --- 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 @@ -6,7 +6,9 @@ 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 @@ -78,5 +80,41 @@ class SelectSubjectAgeGroupViewModelTest { } + @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) + } } From 33a68876c7deffc9cb5552008f3efc71c3eeecb7 Mon Sep 17 00:00:00 2001 From: melad Date: Sun, 16 Jun 2024 14:54:12 +0300 Subject: [PATCH 10/12] [MS-479] support years fraction groups --- .../screen/BuildAgeGroupsDescriptionUseCase.kt | 11 +++++++++-- .../BuildAgeGroupsDescriptionUseCaseTest.kt | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) 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 index 5523124e03..95ddf87b24 100644 --- 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 @@ -69,8 +69,15 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { return when { ageInMonths < 12 -> "$ageInMonths ${getResourceString(IDR.age_group_selection_months)}" - ageInMonths < 24 -> "1 ${getResourceString(IDR.age_group_selection_year)}" - else -> "${ageInMonths / 12} ${getResourceString(IDR.age_group_selection_years)}" + else -> { + val years = ageInMonths / 12 + val remainingMonths = ageInMonths % 12 + if (remainingMonths == 0) { + "$years ${getResourceString(IDR.age_group_selection_years)}" + } else { + "$years ${getResourceString(IDR.age_group_selection_years)}, $remainingMonths ${getResourceString(IDR.age_group_selection_months)}" + } + } } } 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 index be1db49c87..430402798d 100644 --- 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 @@ -89,6 +89,22 @@ class BuildAgeGroupsDescriptionUseCaseTest { 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 ) { From 0c67b092abfb2d726932582aa4e1ffcb7df94f53 Mon Sep 17 00:00:00 2001 From: melad Date: Mon, 17 Jun 2024 10:55:51 +0300 Subject: [PATCH 11/12] [MS-479] retrieve null startInclusive from the api --- build-logic/build_properties.gradle.kts | 2 +- .../com/simprints/infra/config/store/models/AgeGroup.kt | 5 ++++- .../infra/config/store/models/ProjectConfiguration.kt | 4 +--- .../infra/config/store/remote/models/ApiAgeGroup.kt | 6 +++--- .../store/remote/models/ApiFingerprintConfiguration.kt | 5 ++--- .../store/remote/models/ApiFingerprintConfigurationTest.kt | 4 ++++ 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index 7ca92d3728..61614a46f8 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -15,7 +15,7 @@ extra.apply { * * 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/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 d6a38c0f6c..9cae84d647 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 @@ -3,4 +3,7 @@ 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/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ProjectConfiguration.kt index c4dbb773c9..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 @@ -1,7 +1,5 @@ package com.simprints.infra.config.store.models -import com.simprints.infra.logging.Simber - data class ProjectConfiguration( val projectId: String, val updatedAt: String, @@ -49,5 +47,5 @@ fun ProjectConfiguration.allowedAgeRanges(): List { //Todo add face roc sdk , fingerprint?.secugenSimMatcher?.allowedAgeRange, fingerprint?.nec?.allowedAgeRange - ).mapNotNull { it } + ).filterNotNull().filterNot { it.isEmpty() } } 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 index fd82d8c40e..d7f674652e 100644 --- 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 @@ -5,8 +5,8 @@ import com.simprints.infra.config.store.models.AgeGroup @Keep internal data class ApiAgeGroup( - val startInclusive: Int, + val startInclusive: Int?, val endExclusive: Int?, -){ - fun toDomain() = AgeGroup(startInclusive, endExclusive) +) { + 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 1e747fe9ce..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 @@ -1,7 +1,6 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep -import com.simprints.infra.config.store.models.AgeGroup import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.Finger as DomainFingerprint @@ -29,7 +28,7 @@ internal data class ApiFingerprintConfiguration( val comparisonStrategyForVerification: FingerComparisonStrategy, val vero1: ApiVero1Configuration? = null, val vero2: ApiVero2Configuration? = null, - val allowedAgeRange: ApiAgeGroup? = null, + val allowedAgeRange: ApiAgeGroup, ) { fun toDomain() = FingerprintConfiguration.FingerprintSdkConfiguration( fingersToCapture.map { it.toDomain() }, @@ -37,7 +36,7 @@ internal data class ApiFingerprintConfiguration( comparisonStrategyForVerification.toDomain(), vero1?.toDomain(), vero2?.toDomain(), - allowedAgeRange?.toDomain(), + allowedAgeRange.toDomain(), ) } 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!! ), ) From 7bd6974a6c312b1e1af65a7cd139d36437322dc3 Mon Sep 17 00:00:00 2001 From: melad Date: Wed, 19 Jun 2024 17:59:54 +0300 Subject: [PATCH 12/12] [MS-479] Apply code review comments --- .../BuildAgeGroupsDescriptionUseCase.kt | 72 ++++++++----------- .../src/main/res/layout/age_group_item.xml | 3 +- .../layout/fragment_age_group_selection.xml | 3 +- .../BuildAgeGroupsDescriptionUseCaseTest.kt | 36 ++++------ .../resources/src/main/res/values/strings.xml | 12 +++- 5 files changed, 55 insertions(+), 71 deletions(-) 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 index 95ddf87b24..adf1e0b125 100644 --- 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 @@ -6,7 +6,7 @@ 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.string as IDR +import com.simprints.infra.resources.R as IDR internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( private val configurationRepo: ConfigRepository, @@ -19,10 +19,8 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( * 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) } @@ -32,51 +30,40 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( // 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 - val sortedUniqueAges = + // 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() - .sorted().distinct() - - val processedAgeGroups = mutableListOf() - // Ensure the first age group starts at 0 - if (sortedUniqueAges.first() != 0) { - processedAgeGroups.add(AgeGroup(0, sortedUniqueAges.first())) - } - - var startAge = sortedUniqueAges.first() - + sortedUniqueAges = (listOf(0) + sortedUniqueAges).sorted().distinct() // Create age groups based on sorted unique ages - for (i in 1 until sortedUniqueAges.size) { - val endAge = sortedUniqueAges[i] - processedAgeGroups.add(AgeGroup(startAge, endAge)) - startAge = endAge - } - - // Ensure the final age group ends with null - processedAgeGroups.add(AgeGroup(sortedUniqueAges.last(), null)) - - return processedAgeGroups - + return sortedUniqueAges.zipWithNext { start, end -> AgeGroup(start, end) } + AgeGroup( + sortedUniqueAges.last(), null + ) } + private fun getResourceString(id: Int) = context.getString(id) - private fun getResourceString(id: Int): String { - return context.getString(id) - } + private fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any) = + context.resources.getQuantityString(id, quantity, *formatArgs) // Helper function to convert months to readable format - private fun formatAgeInMonthsForDisplay(ageInMonths: Int): String { - return when { - ageInMonths < 12 -> "$ageInMonths ${getResourceString(IDR.age_group_selection_months)}" - else -> { - val years = ageInMonths / 12 - val remainingMonths = ageInMonths % 12 - if (remainingMonths == 0) { - "$years ${getResourceString(IDR.age_group_selection_years)}" - } else { - "$years ${getResourceString(IDR.age_group_selection_years)}, $remainingMonths ${getResourceString(IDR.age_group_selection_months)}" - } + private fun formatAgeInMonthsForDisplay(ageInMonths: Int) = when { + ageInMonths < 12 -> getQuantityString( + IDR.plurals.age_group_selection_age_in_months, ageInMonths, ageInMonths + ) + + else -> { + val years = ageInMonths / 12 + val remainingMonths = ageInMonths % 12 + if (remainingMonths == 0) { + getQuantityString(IDR.plurals.age_group_selection_age_in_years, years, years) + } else { + val yearsString = + getQuantityString(IDR.plurals.age_group_selection_age_in_years, years, years) + val monthsString = getQuantityString( + IDR.plurals.age_group_selection_age_in_months, remainingMonths, remainingMonths + ) + "$yearsString, $monthsString" } } } @@ -84,11 +71,10 @@ internal class BuildAgeGroupsDescriptionUseCase @Inject constructor( private fun AgeGroup.getDisplayName(): String { val start = formatAgeInMonthsForDisplay(startInclusive) val end = endExclusive?.let { - "${getResourceString(IDR.age_group_selection_age_range_to)} ${ + "${getResourceString(IDR.string.age_group_selection_age_range_to)} ${ formatAgeInMonthsForDisplay(it) }" - } ?: getResourceString(IDR.age_group_selection_age_range_and_above) + } ?: getResourceString(IDR.string.age_group_selection_age_range_and_above) return "$start $end" } } - 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 index 2d6f625174..02a0ab3350 100644 --- 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 @@ -19,12 +19,11 @@ 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 index 518b67d3cb..5865b3194e 100644 --- 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 @@ -10,7 +10,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/margin_huge" - android:text="Select Subject's Age Group" + style="@style/Text.Headline5.White" + android:text="@string/age_group_selection_title" android:textColor="@android:color/white" android:textSize="@dimen/text_size_title" app:layout_constraintEnd_toEndOf="parent" 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 index 430402798d..4378184013 100644 --- 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 @@ -1,26 +1,30 @@ package com.simprints.feature.selectagegroup.screen -import android.content.Context -import com.google.common.truth.Truth +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.every import io.mockk.impl.annotations.MockK import io.mockk.mockkStatic import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import com.simprints.infra.resources.R.string as IDR - +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 - @MockK - private lateinit var context: Context + private val context = InstrumentationRegistry.getInstrumentation().context @MockK private lateinit var configurationRepo: ConfigRepository @@ -30,13 +34,6 @@ class BuildAgeGroupsDescriptionUseCaseTest { MockKAnnotations.init(this) mockkStatic("com.simprints.infra.config.store.models.ProjectConfigurationKt") buildAgeGroupsDescription = BuildAgeGroupsDescriptionUseCase(configurationRepo, context) - with(context) { - every { getString(IDR.age_group_selection_months) } returns "months" - every { getString(IDR.age_group_selection_year) } returns "year" - every { getString(IDR.age_group_selection_years) } returns "years" - every { getString(IDR.age_group_selection_age_range_to) } returns "to" - every { getString(IDR.age_group_selection_age_range_and_above) } returns "and above" - } } @Test @@ -64,6 +61,7 @@ class BuildAgeGroupsDescriptionUseCaseTest { 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) } @@ -101,20 +99,14 @@ class BuildAgeGroupsDescriptionUseCaseTest { "5 years, 3 months to 10 years, 5 months", "10 years, 5 months and above" ) - assertAgeGroupDescriptionsMatchExpected(result, expected) + assertAgeGroupDescriptionsMatchExpected(result, expected) } private fun assertAgeGroupDescriptionsMatchExpected( result: List, expected: Array - ) { - result.forEachIndexed { index, ageGroupDisplayModel -> - Truth.assertThat( - ageGroupDisplayModel.displayString - ).isEqualTo(expected[index]) - } + ) = assertThat(result.map { it.displayString }).containsExactlyElementsIn(expected) - } } diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index 879f33c58d..9f8fac8229 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -437,10 +437,16 @@ Sync and retry + Select Subject\'s Age Group to and above - months - year - years + + %d month + %d months + + + %d year + %d years +