diff --git a/.clabot b/.clabot index 21b7dd4d53..0871eaff72 100644 --- a/.clabot +++ b/.clabot @@ -21,7 +21,7 @@ "ybourgery", "simprints-cloud-ci-bot", "dependabot[bot]", - "transifex-integration" + "transifex-integration[bot]" ], "message": "We require contributors to sign our Contributor License Agreement. For us to review and merge your code, don't hesitate to get in touch with @chrisjroyce, @TristramN or @mharvey83 to get yourself added.", "label": "...", diff --git a/.github/workflows/deploy-to-firebase-distribution.yml b/.github/workflows/deploy-to-firebase-distribution.yml index e51956b28f..b04984ec77 100644 --- a/.github/workflows/deploy-to-firebase-distribution.yml +++ b/.github/workflows/deploy-to-firebase-distribution.yml @@ -11,6 +11,7 @@ on: jobs: deploy-to-firebase: runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} concurrency: group: firebase-${{ inputs.buildType }}-workflow #only one instance of this workflow can run at a time diff --git a/.github/workflows/deploy-to-internal.yml b/.github/workflows/deploy-to-internal.yml index 0bcc912ba5..ce409b8dc3 100644 --- a/.github/workflows/deploy-to-internal.yml +++ b/.github/workflows/deploy-to-internal.yml @@ -15,14 +15,12 @@ jobs: deploy-to-internal: runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} concurrency: group: release-internal-workflow #only one instance of this workflow can run at a time cancel-in-progress: true - permissions: - contents: write # A write permission For Auto tagging the releases - environment: internal env: @@ -66,9 +64,12 @@ jobs: - name: Build the Release bundle run: ./gradlew id:bundleRelease + - name: Upload Release bundle + uses: actions/upload-artifact@v4 + with: + name: release-bundle + path: id/build/outputs/bundle/release/*.aab - name: Publish Release bundle run: ./gradlew id:publishReleaseBundle - - - name: Set release tag - run: bash ci/deployment/release_tag_setup + continue-on-error: true diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml deleted file mode 100644 index a615b94fd3..0000000000 --- a/.github/workflows/jira.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Auto create jira task for github issues -name: Jira Sync - -on: - workflow_dispatch: - - -# [TODO] enable this workflow once the jira integration is ready -# issues: -# types: [ opened, labeled, unlabeled ] -# issue_comment: -# types: [ created ] -jobs: - sync: - name: Sync Items - runs-on: ubuntu-latest - steps: - - name: Sync - uses: mheap/github-action-issue-to-jira@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - jiraHost: ${{ secrets.JIRA_HOST }} - jiraUsername: ${{ secrets.JIRA_USERNAME }} - jiraPassword: ${{ secrets.JIRA_PASSWORD }} - project: ${{ vars.JIRA_PROJECT_KEY }} - assignee: ${{ vars.JIRA_ASSIGNEE }} diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 4aaa02570f..efdfd37c3a 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -14,7 +14,6 @@ jobs: secrets: inherit with: modules: | - id infra:core infra:ui-base infra:network @@ -32,7 +31,7 @@ jobs: infra:realm infra:recent-user-activity infra:config-store - infra:config-sync + infra:sync infra:auth-store infra:auth-logic reportsId: infra1 @@ -46,7 +45,6 @@ jobs: infra:events infra:event-sync infra:enrolment-records-store - infra:enrolment-records-sync infra:license infra:images reportsId: infra2 diff --git a/.github/workflows/promote-artifact.yml b/.github/workflows/promote-artifact.yml index 1a6532e822..4152374b57 100644 --- a/.github/workflows/promote-artifact.yml +++ b/.github/workflows/promote-artifact.yml @@ -19,9 +19,13 @@ on: jobs: promote-artifact: runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} environment: ${{inputs.deployment-track}} # Dynamically set the job environment based on the input + permissions: + contents: write # A write permission For Auto tagging the releases + concurrency: group: promote-release-workflow #only one instance of this workflow can run at a time @@ -55,6 +59,10 @@ jobs: if: ${{inputs.deployment-track == 'Prod-25-Percent-Rollout'}} run: ./gradlew promoteArtifact --from-track alpha --promote-track production --release-status inProgress --user-fraction .25 + - name: Set release tag + if: ${{inputs.deployment-track == 'Prod-25-Percent-Rollout'}} + run: bash ci/deployment/release_tag_setup + - name: Promote to production 50% if: ${{inputs.deployment-track == 'Prod-50-Percent-Rollout'}} run: ./gradlew promoteArtifact --update production --user-fraction .5 diff --git a/.github/workflows/refresh-gradle-cache.yml b/.github/workflows/refresh-gradle-cache.yml index 3fdcb2b6b5..eebbf6b1c4 100644 --- a/.github/workflows/refresh-gradle-cache.yml +++ b/.github/workflows/refresh-gradle-cache.yml @@ -9,6 +9,7 @@ on: jobs: refresh-caches: runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} env: GOOGLE_SERVICES_FILE: ${{ secrets.GOOGLE_SERVICES_FILE}} diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 43c64193ef..f6e607b334 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -18,6 +18,7 @@ jobs: test-modules: runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} env: GOOGLE_SERVICES_FILE: ${{ secrets.GOOGLE_SERVICES_FILE}} diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml index 9e10ae1ea0..c4a560c4a9 100644 --- a/.github/workflows/sonar-scan.yml +++ b/.github/workflows/sonar-scan.yml @@ -10,6 +10,7 @@ jobs: GOOGLE_SERVICES_FILE: ${{ secrets.GOOGLE_SERVICES_FILE}} runs-on: ubuntu-latest + timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES) }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/build-logic/build_properties.gradle.kts b/build-logic/build_properties.gradle.kts index 2dee0372a8..ccfadaf454 100644 --- a/build-logic/build_properties.gradle.kts +++ b/build-logic/build_properties.gradle.kts @@ -16,7 +16,7 @@ extra.apply { * 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] */ - set("VERSION_NAME", "2023.4.0") + set("VERSION_NAME", "2024.1.1") /** * Build type. The version code describes which build type was used for the build. diff --git a/build-logic/convention/src/main/kotlin/PipelineSonarConventionPlugin.kt b/build-logic/convention/src/main/kotlin/PipelineSonarConventionPlugin.kt index 8095621a71..f7a3ad631e 100644 --- a/build-logic/convention/src/main/kotlin/PipelineSonarConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/PipelineSonarConventionPlugin.kt @@ -38,6 +38,6 @@ class PipelineSonarConventionPlugin : Plugin { private val cacheDir = "${File.separator}build${File.separator}.sonar" private val jacocoDir = "${File.separator}build${File.separator}reports${File.separator}jacoco${File.separator}jacocoTestReport${File.separator}jacocoTestReport.xml" private val sourceDir = "${File.separator}src${File.separator}main${File.separator}java${File.separator}com${File.separator}simprints" - private val binariesDir = "${File.separator}build${File.separator}intermediates${File.separator}**" + private val binariesDir = "${File.separator}build${File.separator}" } } diff --git a/build-logic/convention/src/main/kotlin/common/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/common/KotlinAndroid.kt index 3d0afd22ca..e55b1769df 100644 --- a/build-logic/convention/src/main/kotlin/common/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/common/KotlinAndroid.kt @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions * Configure base Kotlin with Android options */ internal fun Project.configureKotlinAndroid( - commonExtension: CommonExtension<*, *, *, *, *>, + commonExtension: CommonExtension<*, *, *, *, *, *>, ) { commonExtension.apply { compileSdk = SdkVersions.TARGET @@ -39,6 +39,6 @@ internal fun Project.configureKotlinAndroid( } } -fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { +fun CommonExtension<*, *, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { (this as ExtensionAware).extensions.configure("kotlinOptions", block) } diff --git a/build-logic/sonarqube.gradle b/build-logic/sonarqube.gradle index 80670fe8fb..f72d43bf85 100644 --- a/build-logic/sonarqube.gradle +++ b/build-logic/sonarqube.gradle @@ -2,7 +2,7 @@ apply plugin: "org.sonarqube" def jacocoDir = "${File.separator}build${File.separator}reports${File.separator}jacoco${File.separator}jacocoTestReport${File.separator}jacocoTestReport.xml" def sourceDir = "${File.separator}src${File.separator}main${File.separator}java${File.separator}com${File.separator}simprints" -def binariesDir = "${File.separator}build${File.separator}intermediates${File.separator}**" +def binariesDir = "${File.separator}build${File.separator}" sonarqube { properties { diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt index 3335d1f66d..486b221a39 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/FaceCaptureViewModel.kt @@ -18,7 +18,10 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.FaceConfiguration import com.simprints.infra.facebiosdk.initialization.FaceBioSdkInitializer import com.simprints.infra.license.LicenseRepository +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor +import com.simprints.infra.license.determineLicenseStatus import com.simprints.infra.logging.LoggingConstants.CrashReportTag import com.simprints.infra.logging.Simber import dagger.hilt.android.lifecycle.HiltViewModel @@ -33,7 +36,8 @@ internal class FaceCaptureViewModel @Inject constructor( private val eventReporter: SimpleCaptureEventReporter, private val bitmapToByteArray: BitmapToByteArrayUseCase, private val licenseRepository: LicenseRepository, - private val faceBioSdkInitializer: FaceBioSdkInitializer + private val faceBioSdkInitializer: FaceBioSdkInitializer, + private val saveLicenseCheckEvent: SaveLicenseCheckEventUseCase, ) : ViewModel() { // Updated in live feedback screen @@ -73,11 +77,22 @@ internal class FaceCaptureViewModel @Inject constructor( fun initFaceBioSdk(activity: Activity) = viewModelScope.launch { val license = licenseRepository.getCachedLicense(Vendor.RANK_ONE) - if (!faceBioSdkInitializer.tryInitWithLicense(activity, license)) { - Simber.tag(CrashReportTag.LICENSE.name).i("License is invalid") + var licenseStatus = license.determineLicenseStatus() + + if (licenseStatus == LicenseStatus.VALID) { + if (!faceBioSdkInitializer.tryInitWithLicense(activity, license!!.data)) { + // License is valid but the SDK failed to initialize + // This is should reported as an error + licenseStatus = LicenseStatus.ERROR + } + } + if (licenseStatus != LicenseStatus.VALID) { + Simber.tag(CrashReportTag.LICENSE.name).i("Face license is $licenseStatus") licenseRepository.deleteCachedLicense(Vendor.RANK_ONE) _invalidLicense.send() } + saveLicenseCheckEvent(Vendor.RANK_ONE, licenseStatus) + } fun getSampleDetection() = faceDetections.firstOrNull() diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt index 0f03bc9e7a..934538e4de 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/FaceCaptureControllerFragment.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.activity.addCallback import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.simprints.core.livedata.LiveDataEventObserver @@ -19,6 +20,7 @@ import com.simprints.feature.exitform.exitFormConfiguration import com.simprints.feature.exitform.toArgs import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -29,9 +31,17 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c private val viewModel: FaceCaptureViewModel by activityViewModels() - private fun internalNavController() = childFragmentManager - .findFragmentById(R.id.orchestrator_host_fragment) - ?.findNavController() + + private val hostFragment: Fragment? + get() = childFragmentManager + .findFragmentById(R.id.orchestrator_host_fragment) + + private val internalNavController: NavController? + get() = hostFragment?.findNavController() + + private val currentlyDisplayedInternalFragment: Fragment? + get() = hostFragment?.childFragmentManager?.fragments?.first() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -45,7 +55,7 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c if (option != null) { findNavController().finishWithResult(this, it) } else { - internalNavController()?.navigate(R.id.action_global_faceLiveFeedback) + internalNavController?.navigateSafely(currentlyDisplayedInternalFragment, R.id.action_global_faceLiveFeedback) } } @@ -60,11 +70,12 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c viewModel.setupCapture(args.samplesToCapture) initFaceBioSdk() viewModel.recaptureEvent.observe(viewLifecycleOwner, LiveDataEventObserver { - internalNavController()?.navigate(R.id.action_global_faceLiveFeedback) + internalNavController?.navigateSafely(currentlyDisplayedInternalFragment, R.id.action_global_faceLiveFeedback) }) viewModel.exitFormEvent.observe(viewLifecycleOwner, LiveDataEventObserver { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_global_refusalFragment, exitFormConfiguration { titleRes = IDR.string.exit_form_title_face @@ -82,7 +93,7 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c }) requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { - when (internalNavController()?.currentDestination?.id) { + when (internalNavController?.currentDestination?.id) { R.id.facePreparationFragment, R.id.faceLiveFeedbackFragment -> viewModel.handleBackButton() @@ -90,12 +101,13 @@ internal class FaceCaptureControllerFragment : Fragment(R.layout.fragment_face_c } } - internalNavController()?.setGraph(R.navigation.graph_face_capture_internal) + internalNavController?.setGraph(R.navigation.graph_face_capture_internal) } private fun initFaceBioSdk() { viewModel.invalidLicense.observe(viewLifecycleOwner) { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_global_errorFragment, InvalidFaceLicenseAlert.toAlertArgs() ) diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/InvalidFaceLicenseAlert.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/InvalidFaceLicenseAlert.kt index c08e3650d5..6980de121e 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/controller/InvalidFaceLicenseAlert.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/controller/InvalidFaceLicenseAlert.kt @@ -1,13 +1,12 @@ package com.simprints.face.capture.screens.controller import androidx.annotation.Keep -import androidx.core.os.bundleOf import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.alertConfiguration import com.simprints.feature.alert.config.AlertButtonConfig import com.simprints.feature.alert.config.AlertColor import com.simprints.feature.alert.toArgs -import com.simprints.infra.events.event.domain.models.AlertScreenEvent +import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType import com.simprints.infra.resources.R import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports @@ -20,9 +19,7 @@ internal object InvalidFaceLicenseAlert { messageRes = R.string.configuration_licence_invalid_message image = R.drawable.ic_exclamation leftButton = AlertButtonConfig.Close - payload = bundleOf(PAYLOAD_TYPE_KEY to AppErrorReason.FACE_LICENSE_INVALID) - eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.FACE_LICENSE_INVALID + appErrorReason = AppErrorReason.LICENSE_INVALID + eventType = AlertScreenEventType.LICENSE_INVALID }.toArgs() - - private const val PAYLOAD_TYPE_KEY = "error_type" } diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt index 08157aca1b..03b8a1bfea 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragment.kt @@ -26,6 +26,7 @@ import com.simprints.face.capture.databinding.FragmentLiveFeedbackBinding import com.simprints.face.capture.models.FaceDetection import com.simprints.face.capture.screens.FaceCaptureViewModel import com.simprints.infra.logging.Simber +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.view.setCheckedWithLeftDrawable import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -36,7 +37,7 @@ import com.simprints.infra.resources.R as IDR /** - * This is the class presented as the user is capturing theface, they are presented with this fragment, which displays + * As the user is capturing subject's face, they are presented with this fragment, which displays * live information about distance and whether the face is ready to be captured or not. * It also displays the capture process of the face and then sends this result to * [com.simprints.face.capture.screens.confirmation.ConfirmationFragment] @@ -65,18 +66,14 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) IDR.string.face_capturing_permission_denied, Toast.LENGTH_LONG ).show() + } else { + setUpCamera() } - // init fragment anyway - initFragment() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (requireActivity().hasPermission(Manifest.permission.CAMERA)) { - initFragment() - } else { - launchPermissionRequest.launch(Manifest.permission.CAMERA) - } + initFragment() } private fun initFragment() { @@ -95,13 +92,15 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) binding.captureOverlay.rectInCanvas, Size(binding.captureOverlay.width, binding.captureOverlay.height), ) - setUpCamera() } } } /** Initialize CameraX, and prepare to bind the camera use cases */ private fun setUpCamera() = lifecycleScope.launch { + if (::cameraExecutor.isInitialized) { + return@launch + } // Initialize our background executor cameraExecutor = Executors.newSingleThreadExecutor() // ImageAnalysis @@ -120,6 +119,18 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) preview.setSurfaceProvider(binding.faceCaptureCamera.surfaceProvider) } + override fun onStart() { + super.onStart() + + // Check permission in onStart() so that if user left the app to go to Settings + // and give the permission, it's reflected when they come back to SID + if (requireActivity().hasPermission(Manifest.permission.CAMERA)) { + setUpCamera() + } else { + launchPermissionRequest.launch(Manifest.permission.CAMERA) + } + } + override fun onStop() { // Shut down our background executor if(::cameraExecutor.isInitialized) { @@ -141,7 +152,7 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback) LiveFeedbackFragmentViewModel.CapturingState.FINISHED -> { mainVm.captureFinished(vm.sortedQualifyingCaptures) - findNavController().navigate(R.id.action_faceLiveFeedbackFragment_to_faceConfirmationFragment) + findNavController().navigateSafely(this, R.id.action_faceLiveFeedbackFragment_to_faceConfirmationFragment) } } diff --git a/face/capture/src/main/java/com/simprints/face/capture/screens/preparation/PreparationFragment.kt b/face/capture/src/main/java/com/simprints/face/capture/screens/preparation/PreparationFragment.kt index 5161235be3..9bd173bc4b 100644 --- a/face/capture/src/main/java/com/simprints/face/capture/screens/preparation/PreparationFragment.kt +++ b/face/capture/src/main/java/com/simprints/face/capture/screens/preparation/PreparationFragment.kt @@ -9,8 +9,9 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.face.capture.R import com.simprints.face.capture.databinding.FragmentPreparationBinding -import com.simprints.infra.uibase.viewbinding.viewBinding import com.simprints.face.capture.screens.FaceCaptureViewModel +import com.simprints.infra.uibase.navigation.navigateSafely +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -33,7 +34,7 @@ internal class PreparationFragment : Fragment(R.layout.fragment_preparation) { binding.detectionOnboardingFrame.setOnClickListener { mainVm.addOnboardingComplete(startTime) - findNavController().navigate(R.id.action_facePreparationFragment_to_faceLiveFeedbackFragment) + findNavController().navigateSafely(this, R.id.action_facePreparationFragment_to_faceLiveFeedbackFragment) } } } diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt index f6debc5a86..60c0c6a154 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/FaceCaptureViewModelTest.kt @@ -11,17 +11,22 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.FaceConfiguration.ImageSavingStrategy import com.simprints.infra.facebiosdk.initialization.FaceBioSdkInitializer import com.simprints.infra.license.LicenseRepository +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor +import com.simprints.infra.license.remote.License import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.assertEventReceived import com.simprints.testtools.common.livedata.getOrAwaitValue import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import org.junit.Before import org.junit.Rule @@ -53,6 +58,9 @@ class FaceCaptureViewModelTest { @MockK private lateinit var licenseRepository: LicenseRepository + @MockK + private lateinit var saveLicenseCheckEvent: SaveLicenseCheckEventUseCase + private lateinit var viewModel: FaceCaptureViewModel private val faceDetections = listOf( @@ -75,6 +83,7 @@ class FaceCaptureViewModelTest { bitmapToByteArrayUseCase, licenseRepository, faceBioSdkInitializer, + saveLicenseCheckEvent ) } @@ -137,24 +146,53 @@ class FaceCaptureViewModelTest { fun `test initFaceBioSdk should initialize faceBioSdk`() { // Given val license = "license" - coEvery { licenseRepository.getCachedLicense(Vendor.RANK_ONE) } returns license + every { faceBioSdkInitializer.tryInitWithLicense(any(), license) } returns true + coEvery { + licenseRepository.getCachedLicense(Vendor.RANK_ONE) + } returns License("2133-12-30T17:32:28Z", license) + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheckEvent(Vendor.RANK_ONE, capture(licenseStatusSlot)) } // When viewModel.initFaceBioSdk(mockk()) // Then coVerify { faceBioSdkInitializer.tryInitWithLicense(any(), license) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.VALID) } @Test fun `test initFaceBioSdk should post invalid license when faceBioSdkInitializer returns false`() { // Given val license = "license" - coEvery { licenseRepository.getCachedLicense(Vendor.RANK_ONE) } returns license + coEvery { + licenseRepository.getCachedLicense(Vendor.RANK_ONE) + } returns License("2133-12-30T17:32:28Z", license) + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheckEvent(Vendor.RANK_ONE, capture(licenseStatusSlot)) } + coEvery { faceBioSdkInitializer.tryInitWithLicense(any(), license) } returns false // When viewModel.initFaceBioSdk(mockk()) // Then viewModel.invalidLicense.assertEventReceived() coVerify { licenseRepository.deleteCachedLicense(Vendor.RANK_ONE) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.ERROR) + } + @Test + fun `test initFaceBioSdk should post invalid license when license is expired`() { + // Given + val license = "license" + coEvery { + licenseRepository.getCachedLicense(Vendor.RANK_ONE) + } returns License("2011-12-30T17:32:28Z", license) + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheckEvent(Vendor.RANK_ONE, capture(licenseStatusSlot)) } + + // When + viewModel.initFaceBioSdk(mockk()) + // Then + viewModel.invalidLicense.assertEventReceived() + coVerify { licenseRepository.deleteCachedLicense(Vendor.RANK_ONE) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.EXPIRED) } } diff --git a/face/infra/face-bio-sdk/src/main/java/com/simprints/infra/facebiosdk/matching/FaceIdentity.kt b/face/infra/face-bio-sdk/src/main/java/com/simprints/infra/facebiosdk/matching/FaceIdentity.kt index e78034a652..deaadea50a 100644 --- a/face/infra/face-bio-sdk/src/main/java/com/simprints/infra/facebiosdk/matching/FaceIdentity.kt +++ b/face/infra/face-bio-sdk/src/main/java/com/simprints/infra/facebiosdk/matching/FaceIdentity.kt @@ -4,4 +4,4 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class FaceIdentity(val faceId: String, val faces: List) : Parcelable +data class FaceIdentity(val subjectId: String, val faces: List) : Parcelable diff --git a/feature/alert/src/main/java/com/simprints/feature/alert/AlertConfigurationBuilder.kt b/feature/alert/src/main/java/com/simprints/feature/alert/AlertConfigurationBuilder.kt index bf8d4be72f..f0ba96fe0e 100644 --- a/feature/alert/src/main/java/com/simprints/feature/alert/AlertConfigurationBuilder.kt +++ b/feature/alert/src/main/java/com/simprints/feature/alert/AlertConfigurationBuilder.kt @@ -1,9 +1,8 @@ package com.simprints.feature.alert -import android.os.Bundle import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.core.os.bundleOf +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.config.AlertButtonConfig import com.simprints.feature.alert.config.AlertColor import com.simprints.feature.alert.config.AlertConfiguration @@ -22,7 +21,7 @@ data class AlertConfigurationBuilder( var leftButton: AlertButtonConfig = AlertButtonConfig.Close, var rightButton: AlertButtonConfig? = null, var eventType: AlertScreenEvent.AlertScreenPayload.AlertScreenEventType? = null, - var payload: Bundle = Bundle(), + var appErrorReason: AppErrorReason? = null, ) /** @@ -38,22 +37,12 @@ data class AlertConfigurationBuilder( * messageIcon - Optional icon to show next to the message, default - view is not visible * leftButton - Left/main button configuration, default - basic "Close" button * rightButton - Optional right button configuration, default - view is not visible + * appErrorReason - Error code that will be returned in app result if the alert is terminal, default - null * eventType - Event type to be logged on alert opening, default - nothing - * payload - Custom bundle that will be provided in result callback * ``` */ fun alertConfiguration(block: AlertConfigurationBuilder.() -> Unit) = AlertConfigurationBuilder().apply(block) -/** - * Convenience function to provide payload as pairs of values instead of bundle: - * - * ``` - * alertConfiguration { }.withPayload("key" to "value").toArgs() - * ``` - */ -fun AlertConfigurationBuilder.withPayload(vararg pairs: Pair) = - this.also { it.payload = bundleOf(*pairs) } - fun AlertConfigurationBuilder.toArgs() = AlertFragmentArgs(AlertConfiguration( color = this.color, title = this.title, @@ -65,7 +54,7 @@ fun AlertConfigurationBuilder.toArgs() = AlertFragmentArgs(AlertConfiguration( leftButton = this.leftButton, rightButton = this.rightButton, eventType = this.eventType, - payload = this.payload, + appErrorReason = this.appErrorReason, )).toBundle() data class AlertButtonBuilder( diff --git a/feature/alert/src/main/java/com/simprints/feature/alert/AlertContract.kt b/feature/alert/src/main/java/com/simprints/feature/alert/AlertContract.kt index 02cd35d2bd..fa973ae834 100644 --- a/feature/alert/src/main/java/com/simprints/feature/alert/AlertContract.kt +++ b/feature/alert/src/main/java/com/simprints/feature/alert/AlertContract.kt @@ -4,5 +4,6 @@ object AlertContract { val DESTINATION = R.id.alertFragment const val ALERT_BUTTON_PRESSED_BACK = "alert_fragment_back" + const val ALERT_REASON_PAYLOAD = "alert_reason_payload" } diff --git a/feature/alert/src/main/java/com/simprints/feature/alert/AlertResult.kt b/feature/alert/src/main/java/com/simprints/feature/alert/AlertResult.kt index 53c71a5086..6aecefde1c 100644 --- a/feature/alert/src/main/java/com/simprints/feature/alert/AlertResult.kt +++ b/feature/alert/src/main/java/com/simprints/feature/alert/AlertResult.kt @@ -1,11 +1,11 @@ package com.simprints.feature.alert -import android.os.Bundle import androidx.annotation.Keep +import com.simprints.core.domain.response.AppErrorReason import java.io.Serializable @Keep data class AlertResult( val buttonKey: String, - val payload: Bundle, + val appErrorReason: AppErrorReason? = null, ) : Serializable diff --git a/feature/alert/src/main/java/com/simprints/feature/alert/config/AlertConfiguration.kt b/feature/alert/src/main/java/com/simprints/feature/alert/config/AlertConfiguration.kt index 532742a000..c9cbb46a00 100644 --- a/feature/alert/src/main/java/com/simprints/feature/alert/config/AlertConfiguration.kt +++ b/feature/alert/src/main/java/com/simprints/feature/alert/config/AlertConfiguration.kt @@ -1,10 +1,10 @@ package com.simprints.feature.alert.config -import android.os.Bundle import android.os.Parcelable import androidx.annotation.DrawableRes import androidx.annotation.Keep import androidx.annotation.StringRes +import com.simprints.core.domain.response.AppErrorReason import com.simprints.infra.events.event.domain.models.AlertScreenEvent import kotlinx.parcelize.Parcelize @@ -21,5 +21,5 @@ data class AlertConfiguration( val leftButton: AlertButtonConfig, val rightButton: AlertButtonConfig?, val eventType: AlertScreenEvent.AlertScreenPayload.AlertScreenEventType?, - val payload: Bundle, + val appErrorReason: AppErrorReason? = null, ) : Parcelable diff --git a/feature/alert/src/main/java/com/simprints/feature/alert/screen/AlertFragment.kt b/feature/alert/src/main/java/com/simprints/feature/alert/screen/AlertFragment.kt index a2afb72472..4a7312aa74 100644 --- a/feature/alert/src/main/java/com/simprints/feature/alert/screen/AlertFragment.kt +++ b/feature/alert/src/main/java/com/simprints/feature/alert/screen/AlertFragment.kt @@ -10,8 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.simprints.infra.uibase.view.setTextWithFallbacks -import com.simprints.infra.uibase.viewbinding.viewBinding +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.AlertContract import com.simprints.feature.alert.AlertResult import com.simprints.feature.alert.R @@ -21,6 +20,8 @@ import com.simprints.feature.alert.databinding.FragmentAlertBinding import com.simprints.infra.logging.LoggingConstants.CrashReportTag.ALERT import com.simprints.infra.logging.Simber import com.simprints.infra.uibase.navigation.setResult +import com.simprints.infra.uibase.view.setTextWithFallbacks +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -54,15 +55,15 @@ internal class AlertFragment : Fragment(R.layout.fragment_alert) { binding.alertMessage.setCompoundDrawablesWithIntrinsicBounds(config.messageIcon, 0, 0, 0) } - binding.alertLeftButton.setupButton(config.leftButton, config.payload) + binding.alertLeftButton.setupButton(config.leftButton, config.appErrorReason) binding.alertRightButton.isVisible = config.rightButton != null if (config.rightButton != null) { - binding.alertRightButton.setupButton(config.rightButton, config.payload) + binding.alertRightButton.setupButton(config.rightButton, config.appErrorReason) } requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { Simber.tag(ALERT.name).i("Alert back button clicked") - setPressedButtonResult(AlertContract.ALERT_BUTTON_PRESSED_BACK, config.payload) + setPressedButtonResult(AlertContract.ALERT_BUTTON_PRESSED_BACK, config.appErrorReason) findNavController().popBackStack() } config.eventType?.let { vm.saveAlertEvent(it) } @@ -70,12 +71,12 @@ internal class AlertFragment : Fragment(R.layout.fragment_alert) { Simber.tag(ALERT.name).i("${binding.alertTitle.text}") } - private fun TextView.setupButton(config: AlertButtonConfig, payload: Bundle) { + private fun TextView.setupButton(config: AlertButtonConfig, appErrorReason: AppErrorReason?) { setTextWithFallbacks(config.text, config.textRes) setOnClickListener { config.resultKey?.let { Simber.tag(ALERT.name).i("Alert button clicked: $it") - setPressedButtonResult(it, payload) + setPressedButtonResult(it, appErrorReason) } if (config.closeOnClick) { @@ -85,7 +86,7 @@ internal class AlertFragment : Fragment(R.layout.fragment_alert) { } } - private fun setPressedButtonResult(key: String, payload: Bundle) { - findNavController().setResult(this, AlertResult(key, payload)) + private fun setPressedButtonResult(key: String, appErrorReason: AppErrorReason?) { + findNavController().setResult(this, AlertResult(key, appErrorReason)) } } diff --git a/feature/alert/src/test/java/com/simprints/feature/alert/screen/AlertFragmentTest.kt b/feature/alert/src/test/java/com/simprints/feature/alert/screen/AlertFragmentTest.kt index 6be55ffbdc..6acc007f7b 100644 --- a/feature/alert/src/test/java/com/simprints/feature/alert/screen/AlertFragmentTest.kt +++ b/feature/alert/src/test/java/com/simprints/feature/alert/screen/AlertFragmentTest.kt @@ -5,9 +5,13 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.* import com.simprints.feature.alert.config.AlertColor import com.simprints.infra.uibase.navigation.handleResultDirectly @@ -142,55 +146,61 @@ class AlertFragmentTest { } @Test - fun `passed back the custom payload on back press`() { - var payload = 0 + fun `passed back the custom appErrorReason on back press`() { + var payload: AppErrorReason? = null launchFragmentInHiltContainer( navController = navController, - fragmentArgs = alertConfiguration {}.withPayload("testKey" to 42).toArgs() + fragmentArgs = alertConfiguration { + appErrorReason = AppErrorReason.LICENSE_INVALID + }.toArgs() ) { handleResultDirectly(AlertContract.DESTINATION) { result -> - payload = result.payload.getInt("testKey") + payload = result.appErrorReason } } pressBack() - Truth.assertThat(payload).isEqualTo(42) + Truth.assertThat(payload).isEqualTo(AppErrorReason.LICENSE_INVALID) } @Test fun `passed back the custom payload on left button press`() { - var payload = 0 + var payload: AppErrorReason? = null launchFragmentInHiltContainer( navController = navController, - fragmentArgs = alertConfiguration {}.withPayload("testKey" to 42).toArgs() + fragmentArgs = alertConfiguration { + appErrorReason = AppErrorReason.LICENSE_INVALID + }.toArgs() ) { handleResultDirectly(AlertContract.DESTINATION) { result -> - payload = result.payload.getInt("testKey") + payload = result.appErrorReason } } onView(withId(R.id.alertLeftButton)).perform(click()) - Truth.assertThat(payload).isEqualTo(42) + Truth.assertThat(payload).isEqualTo(AppErrorReason.LICENSE_INVALID) } @Test fun `passed back the custom payload on right button press`() { - var payload = 0 + var payload: AppErrorReason? = null + launchFragmentInHiltContainer( navController = navController, fragmentArgs = alertConfiguration { + appErrorReason = AppErrorReason.LICENSE_INVALID rightButton = alertButton { text = "Right" resultKey = "test" } - }.withPayload("testKey" to 42).toArgs() + }.toArgs() ) { handleResultDirectly(AlertContract.DESTINATION) { result -> - payload = result.payload.getInt("testKey") + payload = result.appErrorReason } } onView(withId(R.id.alertRightButton)).perform(click()) - Truth.assertThat(payload).isEqualTo(42) + Truth.assertThat(payload).isEqualTo(AppErrorReason.LICENSE_INVALID) } @Test @@ -235,7 +245,6 @@ class AlertFragmentTest { Truth.assertThat(resultKey).isEqualTo("test") } - @Test fun `button does not close screen if not configured`() { val navSpy = spyk(navController) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt index 9fa06283a1..f2cc5d549b 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/EnrolRequestBuilder.kt @@ -40,6 +40,7 @@ internal class EnrolRequestBuilder( actionIdentifier = actionIdentifier, projectId = extractor.getProjectId(), userId = extractor.getUserId().asTokenizableRaw(), + biometricDataSource = extractor.getBiometricDataSource(), metadata = extractor.getMetadata(), moduleId = extractor.getModuleId().asTokenizableRaw(), unknownExtras = extractor.getUnknownExtras() diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt index facc100378..fe13a64870 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/IdentifyRequestBuilder.kt @@ -43,6 +43,7 @@ internal class IdentifyRequestBuilder( projectId = extractor.getProjectId(), userId = extractor.getUserId().asTokenizableRaw(), moduleId = extractor.getModuleId().asTokenizableRaw(), + biometricDataSource = extractor.getBiometricDataSource(), metadata = extractor.getMetadata(), unknownExtras = extractor.getUnknownExtras() ) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt index 62aebee902..ad3f8e0f35 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilder.kt @@ -42,6 +42,7 @@ internal class VerifyRequestBuilder( projectId = extractor.getProjectId(), userId = extractor.getUserId().asTokenizableRaw(), moduleId = extractor.getModuleId().asTokenizableRaw(), + biometricDataSource = extractor.getBiometricDataSource(), metadata = extractor.getMetadata(), verifyGuid = extractor.getVerifyGuid(), unknownExtras = extractor.getUnknownExtras() diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt index 4582b136c3..dd8c9e8f34 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/mappers/request/extractors/ActionRequestExtractor.kt @@ -21,6 +21,8 @@ internal abstract class ActionRequestExtractor(private val extras: Map bundleOf( Constants.SIMPRINTS_SESSION_ID to response.sessionId, Constants.SIMPRINTS_BIOMETRICS_COMPLETE_CHECK to true, - Constants.SIMPRINTS_IDENTIFICATIONS to response.identifications - .map { Identification(it.guid, it.confidenceScore, Tier.valueOf(it.tier.name)) } - .toTypedArray(), + Constants.SIMPRINTS_IDENTIFICATIONS to ArrayList( + response.identifications.map { + Identification(it.guid, it.confidenceScore, Tier.valueOf(it.tier.name)) + } + ), ).appendCoSyncData(response.eventsJson) is ActionResponse.ConfirmActionResponse -> bundleOf( @@ -76,8 +78,8 @@ internal class LibSimprintsResponseMapper @Inject constructor() { AppErrorReason.BLUETOOTH_NO_PERMISSION -> Constants.SIMPRINTS_BLUETOOTH_NO_PERMISSION AppErrorReason.FINGERPRINT_CONFIGURATION_ERROR -> Constants.SIMPRINTS_FINGERPRINT_CONFIGURATION_ERROR AppErrorReason.FACE_CONFIGURATION_ERROR -> Constants.SIMPRINTS_FACE_CONFIGURATION_ERROR - AppErrorReason.FACE_LICENSE_MISSING -> Constants.SIMPRINTS_FACE_LICENSE_MISSING - AppErrorReason.FACE_LICENSE_INVALID -> Constants.SIMPRINTS_FACE_LICENSE_INVALID + AppErrorReason.LICENSE_MISSING -> Constants.SIMPRINTS_LICENSE_MISSING + AppErrorReason.LICENSE_INVALID -> Constants.SIMPRINTS_LICENSE_INVALID AppErrorReason.BACKEND_MAINTENANCE_ERROR -> Constants.SIMPRINTS_BACKEND_MAINTENANCE_ERROR AppErrorReason.PROJECT_PAUSED -> Constants.SIMPRINTS_PROJECT_PAUSED AppErrorReason.PROJECT_ENDING -> Constants.SIMPRINTS_PROJECT_ENDING @@ -97,6 +99,7 @@ internal class LibSimprintsResponseMapper @Inject constructor() { } companion object { + internal const val RESULT_CODE_OVERRIDE = "result_code_override" } } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/Constants.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/Constants.kt index 6612908c0b..bbd1f5390c 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/Constants.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/Constants.kt @@ -56,8 +56,6 @@ internal object CommCareConstants { const val EXIT_REASON = "exitReason" const val EXIT_EXTRA = "exitExtra" const val SIMPRINTS_SESSION_ID = "sessionId" - const val SIMPRINTS_EVENTS = "events" - const val SIMPRINTS_SUBJECT_ACTIONS = "subjectActions" const val COMMCARE_BUNDLE_KEY = "odk_intent_bundle" const val COMMCARE_DATA_KEY = "odk_intent_data" diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt index 3642eb7a45..ee00668a6f 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForSubjectUseCase.kt @@ -2,13 +2,13 @@ package com.simprints.feature.clientapi.usecases import com.simprints.core.tools.json.JsonHelper import com.simprints.core.tools.utils.EncodingUtils -import com.simprints.feature.clientapi.models.CoSyncEnrolmentRecordEvents import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.canCoSyncAllData import com.simprints.infra.config.store.models.canCoSyncBiometricData import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.store.domain.models.Subject import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery +import com.simprints.infra.events.event.cosync.CoSyncEnrolmentRecordEvents import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent import javax.inject.Inject diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCase.kt index 606516a71c..36ff6824b4 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCase.kt @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.domain.tokenization.serialization.TokenizationAsStringSerializer import com.simprints.core.tools.json.JsonHelper -import com.simprints.feature.clientapi.models.CoSyncEvents import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.canCoSyncAllData @@ -13,6 +12,7 @@ import com.simprints.infra.config.store.models.canCoSyncBiometricData import com.simprints.infra.config.store.models.canCoSyncData import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.cosync.CoSyncEvents import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 import com.simprints.infra.events.event.domain.models.Event import com.simprints.infra.events.event.domain.models.PersonCreationEvent diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt index 844b19a8a7..494dd7ca75 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/IsFlowCompletedWithErrorUseCase.kt @@ -24,8 +24,8 @@ internal class IsFlowCompletedWithErrorUseCase @Inject constructor() { AppErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED, AppErrorReason.LOGIN_NOT_COMPLETE, AppErrorReason.FINGERPRINT_CONFIGURATION_ERROR, - AppErrorReason.FACE_LICENSE_MISSING, - AppErrorReason.FACE_LICENSE_INVALID, + AppErrorReason.LICENSE_MISSING, + AppErrorReason.LICENSE_INVALID, AppErrorReason.FACE_CONFIGURATION_ERROR, AppErrorReason.BACKEND_MAINTENANCE_ERROR, -> false diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt index 39462c900a..df33cb4d28 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/builders/VerifyRequestBuilderTest.kt @@ -1,7 +1,6 @@ package com.simprints.feature.clientapi.mappers.request.builders import com.google.common.truth.Truth.assertThat -import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory.Companion.MOCK_MODULE_ID import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory.Companion.MOCK_PACKAGE import com.simprints.feature.clientapi.mappers.request.requestFactories.RequestActionFactory.Companion.MOCK_PROJECT_ID diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolActionFactory.kt index 24d5f9d979..edebd79e22 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/EnrolActionFactory.kt @@ -23,6 +23,7 @@ internal object EnrolActionFactory : RequestActionFactory() { userId = MOCK_USER_ID.asTokenizableRaw(), metadata = MOCK_METADATA, moduleId = MOCK_MODULE_ID.asTokenizableRaw(), + biometricDataSource = MOCK_BIOMETRIC_DATA_SOURCE, unknownExtras = emptyMap() ) diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/IdentifyRequestActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/IdentifyRequestActionFactory.kt index 4853d12800..deaed922f4 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/IdentifyRequestActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/IdentifyRequestActionFactory.kt @@ -23,6 +23,7 @@ internal object IdentifyRequestActionFactory : RequestActionFactory() { moduleId = MOCK_MODULE_ID.asTokenizableRaw(), userId = MOCK_USER_ID.asTokenizableRaw(), metadata = MOCK_METADATA, + biometricDataSource = MOCK_BIOMETRIC_DATA_SOURCE, unknownExtras = emptyMap() ) diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt index e00430bf0d..ff7f591e6e 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/RequestActionFactory.kt @@ -18,6 +18,7 @@ internal abstract class RequestActionFactory { const val MOCK_VERIFY_GUID = "1d3a92c1-3410-40fb-9e88-4570c9abd150" const val MOCK_SESSION_ID = "ddf01a3c-3081-4d3e-b872-538731517cb9" const val MOCK_SELECTED_GUID = "5390ef82-9c1f-40a9-b833-2e97ab369208" + const val MOCK_BIOMETRIC_DATA_SOURCE = "" } abstract fun getIdentifier(): ActionRequestIdentifier @@ -35,6 +36,7 @@ internal abstract class RequestActionFactory { every { mockExtractor.getUserId() } returns MOCK_USER_ID every { mockExtractor.getModuleId() } returns MOCK_MODULE_ID every { mockExtractor.getMetadata() } returns MOCK_METADATA + every { mockExtractor.getBiometricDataSource() } returns MOCK_BIOMETRIC_DATA_SOURCE every { mockExtractor.getUnknownExtras() } returns emptyMap() } diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/VerifyActionFactory.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/VerifyActionFactory.kt index 51d88d4349..bf5af2b18b 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/VerifyActionFactory.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/requestFactories/VerifyActionFactory.kt @@ -25,6 +25,7 @@ internal object VerifyActionFactory : RequestActionFactory() { userId = MOCK_USER_ID.asTokenizableRaw(), metadata = MOCK_METADATA, verifyGuid = MOCK_VERIFY_GUID, + biometricDataSource = MOCK_BIOMETRIC_DATA_SOURCE, unknownExtras = emptyMap() ) diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt index 7b2b29a42a..51f4b0fe2a 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/response/LibSimprintsResponseMapperTest.kt @@ -70,11 +70,9 @@ class LibSimprintsResponseMapperTest { ) assertThat(extras.getString(Constants.SIMPRINTS_SESSION_ID)).isEqualTo("sessionId") - assertThat(extras.getParcelableArray(Constants.SIMPRINTS_IDENTIFICATIONS)).isEqualTo( - arrayOf( - Identification("guid-1", 100, Tier.TIER_5), - Identification("guid-2", 75, Tier.TIER_3), - ) + assertThat(extras.getParcelableArrayList(Constants.SIMPRINTS_IDENTIFICATIONS)).containsExactly( + Identification("guid-1", 100, Tier.TIER_5), + Identification("guid-2", 75, Tier.TIER_3), ) } @@ -172,8 +170,8 @@ class LibSimprintsResponseMapperTest { AppErrorReason.BLUETOOTH_NO_PERMISSION to Constants.SIMPRINTS_BLUETOOTH_NO_PERMISSION, AppErrorReason.FINGERPRINT_CONFIGURATION_ERROR to Constants.SIMPRINTS_FINGERPRINT_CONFIGURATION_ERROR, AppErrorReason.FACE_CONFIGURATION_ERROR to Constants.SIMPRINTS_FACE_CONFIGURATION_ERROR, - AppErrorReason.FACE_LICENSE_MISSING to Constants.SIMPRINTS_FACE_LICENSE_MISSING, - AppErrorReason.FACE_LICENSE_INVALID to Constants.SIMPRINTS_FACE_LICENSE_INVALID, + AppErrorReason.LICENSE_MISSING to Constants.SIMPRINTS_LICENSE_MISSING, + AppErrorReason.LICENSE_INVALID to Constants.SIMPRINTS_LICENSE_INVALID, AppErrorReason.BACKEND_MAINTENANCE_ERROR to Constants.SIMPRINTS_BACKEND_MAINTENANCE_ERROR, AppErrorReason.PROJECT_PAUSED to Constants.SIMPRINTS_PROJECT_PAUSED, AppErrorReason.PROJECT_ENDING to Constants.SIMPRINTS_PROJECT_ENDING, diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCaseTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCaseTest.kt index 54a6bf1a08..e4f0154a1d 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCaseTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEventsForCoSyncUseCaseTest.kt @@ -2,11 +2,11 @@ package com.simprints.feature.clientapi.usecases import com.google.common.truth.Truth.assertThat import com.simprints.core.tools.json.JsonHelper -import com.simprints.feature.clientapi.models.CoSyncEvents import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.UpSynchronizationConfiguration import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.events.EventRepository +import com.simprints.infra.events.event.cosync.CoSyncEvents import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 import com.simprints.infra.events.event.domain.models.PersonCreationEvent import com.simprints.infra.events.event.domain.models.face.FaceCaptureBiometricsEvent diff --git a/feature/consent/src/main/java/com/simprints/feature/consent/screens/consent/ConsentFragment.kt b/feature/consent/src/main/java/com/simprints/feature/consent/screens/consent/ConsentFragment.kt index e0f8f4d124..0ed60f93c2 100644 --- a/feature/consent/src/main/java/com/simprints/feature/consent/screens/consent/ConsentFragment.kt +++ b/feature/consent/src/main/java/com/simprints/feature/consent/screens/consent/ConsentFragment.kt @@ -19,6 +19,7 @@ import com.simprints.feature.exitform.toArgs import com.simprints.infra.uibase.listeners.OnTabSelectedListener import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -33,7 +34,8 @@ internal class ConsentFragment : Fragment(R.layout.fragment_consent) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.consentPrivacyNotice.paintFlags = binding.consentPrivacyNotice.paintFlags or Paint.UNDERLINE_TEXT_FLAG + binding.consentPrivacyNotice.paintFlags = + binding.consentPrivacyNotice.paintFlags or Paint.UNDERLINE_TEXT_FLAG binding.consentTextHolderView.movementMethod = ScrollingMovementMethod() handleClicks() @@ -53,8 +55,16 @@ internal class ConsentFragment : Fragment(R.layout.fragment_consent) { viewModel.declineClicked(getCurrentConsentTab()) } - binding.consentAcceptButton.setOnClickListener { viewModel.acceptClicked(getCurrentConsentTab()) } - binding.consentDeclineButton.setOnClickListener { viewModel.declineClicked(getCurrentConsentTab()) } + binding.consentAcceptButton.setOnClickListener { + viewModel.acceptClicked( + getCurrentConsentTab() + ) + } + binding.consentDeclineButton.setOnClickListener { + viewModel.declineClicked( + getCurrentConsentTab() + ) + } binding.consentPrivacyNotice.setOnClickListener { openPrivacyNotice() } } @@ -64,16 +74,24 @@ internal class ConsentFragment : Fragment(R.layout.fragment_consent) { } viewModel.showExitForm.observe(viewLifecycleOwner) { exitFormConfig -> exitFormConfig.getContentIfNotHandled()?.let { - findNavController().navigate(R.id.action_consentFragment_to_refusalFragment, it.toArgs()) + findNavController().navigateSafely( + currentFragment = this, + actionId = R.id.action_consentFragment_to_refusalFragment, + args = it.toArgs() + ) } } viewModel.returnConsentResult.observe(viewLifecycleOwner) { isApproved -> - isApproved.getContentIfNotHandled()?.let { findNavController().finishWithResult(this, it) } + isApproved.getContentIfNotHandled() + ?.let { findNavController().finishWithResult(this, it) } } } private fun openPrivacyNotice() { - findNavController().navigate(R.id.action_consentFragment_to_privacyNoticeFragment) + findNavController().navigateSafely( + this, + R.id.action_consentFragment_to_privacyNoticeFragment + ) } private fun updateUiWithState(state: ConsentViewState) { @@ -93,7 +111,10 @@ internal class ConsentFragment : Fragment(R.layout.fragment_consent) { } } - private fun TabLayout.addParentalConsentTab(generalConsentText: String, parentalConsentText: String) { + private fun TabLayout.addParentalConsentTab( + generalConsentText: String, + parentalConsentText: String + ) { addTab(newTab().setText(IDR.string.consent_parental_title), PARENTAL_CONSENT_TAB) addOnTabSelectedListener(OnTabSelectedListener { tab -> val position = tab.position diff --git a/feature/dashboard/build.gradle.kts b/feature/dashboard/build.gradle.kts index d1f5dfcc12..c4399d9326 100644 --- a/feature/dashboard/build.gradle.kts +++ b/feature/dashboard/build.gradle.kts @@ -11,13 +11,14 @@ dependencies { implementation(project(":infra:events")) implementation(project(":infra:event-sync")) implementation(project(":infra:config-store")) - implementation(project(":infra:config-sync")) + implementation(project(":infra:sync")) implementation(project(":infra:enrolment-records-store")) implementation(project(":infra:images")) implementation(project(":infra:auth-store")) implementation(project(":infra:auth-logic")) implementation(project(":infra:recent-user-activity")) implementation(project(":feature:consent")) + implementation(project(":feature:login")) implementation(libs.fuzzywuzzy.core) diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt index b789f443b3..26d2e7ad58 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt @@ -15,11 +15,11 @@ import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentDebugBinding import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.sync.ProjectConfigurationScheduler import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.EventSyncManager import com.simprints.infra.eventsync.status.models.EventSyncWorkerState +import com.simprints.infra.sync.SyncOrchestrator import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher @@ -38,7 +38,7 @@ internal class DebugFragment : Fragment(R.layout.fragment_debug) { lateinit var configRepository: ConfigRepository @Inject - lateinit var configScheduler: ProjectConfigurationScheduler + lateinit var syncOrchestrator: SyncOrchestrator @Inject lateinit var authStore: AuthStore @@ -79,15 +79,20 @@ internal class DebugFragment : Fragment(R.layout.fragment_debug) { } binding.syncStart.setOnClickListener { - eventSyncManager.sync() + syncOrchestrator.startEventSync() } binding.syncStop.setOnClickListener { - eventSyncManager.stop() + syncOrchestrator.stopEventSync() } binding.syncSchedule.setOnClickListener { - eventSyncManager.scheduleSync() + syncOrchestrator.rescheduleEventSync() + } + + binding.clearFirebaseToken.setOnClickListener { + authStore.clearFirebaseToken() + binding.logs.append("\nFirebase token deleted") } binding.syncConfig.setOnClickListener { @@ -105,7 +110,7 @@ internal class DebugFragment : Fragment(R.layout.fragment_debug) { } binding.syncDevice.setOnClickListener { - configScheduler.startDeviceSync() + syncOrchestrator.startDeviceSync() } binding.printRoomDb.setOnClickListener { @@ -125,8 +130,9 @@ internal class DebugFragment : Fragment(R.layout.fragment_debug) { binding.cleanAll.setOnClickListener { lifecycleScope.launch(dispatcher) { - eventSyncManager.cancelScheduledSync() - eventSyncManager.stop() + syncOrchestrator.stopEventSync() + syncOrchestrator.cancelEventSync() + eventRepository.deleteAll() eventSyncManager.resetDownSyncInfo() enrolmentRecordRepository.deleteAll() diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModel.kt index e1bfc2c2cb..b654cb957a 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModel.kt @@ -4,22 +4,17 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope -import com.simprints.core.ExternalScope import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase -import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.SettingsPasswordConfig import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel internal class LogoutSyncViewModel @Inject constructor( private val configRepository: ConfigRepository, private val logoutUseCase: LogoutUseCase, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { val settingsLocked: LiveData> @@ -29,6 +24,7 @@ internal class LogoutSyncViewModel @Inject constructor( fun logout() { - externalScope.launch { logoutUseCase() } + logoutUseCase() } } + diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt index 64004c264c..ed46d76baf 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt @@ -9,12 +9,16 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import com.simprints.infra.uibase.viewbinding.viewBinding +import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentLogoutSyncBinding import com.simprints.feature.dashboard.logout.LogoutSyncViewModel -import com.simprints.feature.dashboard.views.SyncCardState import com.simprints.feature.dashboard.main.sync.SyncViewModel +import com.simprints.feature.dashboard.views.SyncCardState +import com.simprints.feature.login.LoginContract +import com.simprints.feature.login.LoginResult +import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -28,6 +32,12 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { super.onViewCreated(view, savedInstanceState) initViews() observeLiveData() + + findNavController().handleResult( + viewLifecycleOwner, + R.id.logOutSyncFragment, + LoginContract.DESTINATION, + ) { result -> syncViewModel.handleLoginResult(result) } } private fun initViews() = with(binding) { @@ -36,6 +46,7 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { { startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) } logoutSyncCard.onSelectNoModulesButtonClick = { findNavController().navigate(R.id.action_logoutSyncFragment_to_moduleSelectionFragment) } + logoutSyncCard.onLoginButtonClick = { syncViewModel.login() } logoutSyncToolbar.setNavigationOnClickListener { findNavController().popBackStack() } @@ -56,6 +67,12 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { logoutWithoutSyncButton.isVisible = isLogoutButtonVisible.not() logoutSyncInfo.isInvisible = isLogoutButtonVisible } + syncViewModel.loginRequestedEventLiveData.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { loginArgs -> + findNavController().navigate( + R.id.action_logOutSyncFragment_to_login, + loginArgs + ) + }) } /** diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt index 83636e5983..514d959125 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt @@ -8,11 +8,11 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.simprints.core.livedata.LiveDataEventWithContentObserver -import com.simprints.infra.uibase.viewbinding.viewBinding import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentLogoutSyncDeclineBinding import com.simprints.feature.dashboard.logout.LogoutSyncViewModel import com.simprints.feature.dashboard.settings.password.SettingsPasswordDialogFragment +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt index 62f6a45942..182c576b4c 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt @@ -1,26 +1,21 @@ package com.simprints.feature.dashboard.logout.usecase import com.simprints.infra.authlogic.AuthManager -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler +import com.simprints.infra.sync.SyncOrchestrator +import kotlinx.coroutines.runBlocking import javax.inject.Inject internal class LogoutUseCase @Inject constructor( - private val configScheduler: ProjectConfigurationScheduler, - private val imageUpSyncScheduler: ImageUpSyncScheduler, - private val eventSyncManager: EventSyncManager, + private val syncOrchestrator: SyncOrchestrator, private val authManager: AuthManager, ) { - suspend operator fun invoke() { + operator fun invoke() = runBlocking { // Cancel all background sync - eventSyncManager.cancelScheduledSync() - imageUpSyncScheduler.cancelImageUpSync() - configScheduler.cancelProjectSync() - configScheduler.cancelDeviceSync() - - eventSyncManager.deleteSyncInfo() + syncOrchestrator.cancelBackgroundWork() + syncOrchestrator.deleteEventSyncInfo() + // sign out the user authManager.signOut() } + } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncFragment.kt index b9bcdd43ca..e48c66b042 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncFragment.kt @@ -7,13 +7,17 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import com.simprints.infra.uibase.viewbinding.viewBinding +import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentDashboardCardSyncBinding import com.simprints.feature.dashboard.requestlogin.LogoutReason import com.simprints.feature.dashboard.requestlogin.RequestLoginFragmentArgs -import com.simprints.infra.resources.R as IDR +import com.simprints.feature.login.LoginContract +import com.simprints.feature.login.LoginResult +import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import com.simprints.infra.resources.R as IDR @AndroidEntryPoint internal class SyncFragment : Fragment(R.layout.fragment_dashboard_card_sync) { @@ -25,6 +29,12 @@ internal class SyncFragment : Fragment(R.layout.fragment_dashboard_card_sync) { super.onViewCreated(view, savedInstanceState) initViews() observeLiveData() + + findNavController().handleResult( + viewLifecycleOwner, + R.id.mainFragment, + LoginContract.DESTINATION, + ) { result -> viewModel.handleLoginResult(result) } } private fun initViews() = with(binding.dashboardSyncCard) { @@ -32,6 +42,7 @@ internal class SyncFragment : Fragment(R.layout.fragment_dashboard_card_sync) { onOfflineButtonClick = { startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) } onSelectNoModulesButtonClick = { findNavController().navigate(R.id.action_mainFragment_to_moduleSelectionFragment) } + onLoginButtonClick = { viewModel.login() } } private fun observeLiveData() { @@ -55,5 +66,11 @@ internal class SyncFragment : Fragment(R.layout.fragment_dashboard_card_sync) { RequestLoginFragmentArgs(logoutReason = logoutReason).toBundle() ) } + viewModel.loginRequestedEventLiveData.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { loginArgs -> + findNavController().navigate( + R.id.action_mainFragment_to_login, + loginArgs + ) + }) } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncViewModel.kt index dfd71f8f7b..0e37cfe292 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/main/sync/SyncViewModel.kt @@ -1,12 +1,14 @@ package com.simprints.feature.dashboard.main.sync +import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.simprints.core.ExternalScope import com.simprints.core.livedata.LiveDataEvent +import com.simprints.core.livedata.LiveDataEventWithContent +import com.simprints.core.livedata.send import com.simprints.core.tools.time.TimeHelper import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase import com.simprints.feature.dashboard.views.SyncCardState @@ -15,12 +17,15 @@ import com.simprints.feature.dashboard.views.SyncCardState.SyncConnecting import com.simprints.feature.dashboard.views.SyncCardState.SyncDefault import com.simprints.feature.dashboard.views.SyncCardState.SyncFailed import com.simprints.feature.dashboard.views.SyncCardState.SyncFailedBackendMaintenance +import com.simprints.feature.dashboard.views.SyncCardState.SyncFailedReloginRequired import com.simprints.feature.dashboard.views.SyncCardState.SyncHasNoModules import com.simprints.feature.dashboard.views.SyncCardState.SyncOffline import com.simprints.feature.dashboard.views.SyncCardState.SyncPendingUpload import com.simprints.feature.dashboard.views.SyncCardState.SyncProgress import com.simprints.feature.dashboard.views.SyncCardState.SyncTooManyRequests import com.simprints.feature.dashboard.views.SyncCardState.SyncTryAgain +import com.simprints.feature.login.LoginContract +import com.simprints.feature.login.LoginResult import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.DownSynchronizationConfiguration @@ -31,8 +36,9 @@ import com.simprints.infra.config.store.models.isEventDownSyncAllowed import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.eventsync.EventSyncManager import com.simprints.infra.eventsync.status.models.EventSyncState -import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.network.ConnectivityTracker +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.sync.SyncOrchestrator import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -44,12 +50,13 @@ import javax.inject.Inject @HiltViewModel internal class SyncViewModel @Inject constructor( private val eventSyncManager: EventSyncManager, + private val syncOrchestrator: SyncOrchestrator, private val connectivityTracker: ConnectivityTracker, private val configRepository: ConfigRepository, private val timeHelper: TimeHelper, private val authStore: AuthStore, - private val logoutUseCase: LogoutUseCase, - @ExternalScope private val externalScope: CoroutineScope, + private val logout: LogoutUseCase, + private val recentUserActivityManager: RecentUserActivityManager, ) : ViewModel() { companion object { @@ -69,6 +76,10 @@ internal class SyncViewModel @Inject constructor( get() = _signOutEventLiveData private val _signOutEventLiveData = MediatorLiveData() + val loginRequestedEventLiveData: LiveData> + get() = _loginRequestedEventLiveData + private val _loginRequestedEventLiveData = MutableLiveData>() + private val upSyncCountLiveData = MutableLiveData(0) private val syncStateLiveData = eventSyncManager.getLastSyncState() @@ -78,7 +89,6 @@ internal class SyncViewModel @Inject constructor( } private var lastTimeSyncRun: Date? = null - private var estimatedOutage: Long? = null init { viewModelScope.launch { @@ -94,8 +104,8 @@ internal class SyncViewModel @Inject constructor( configRepository.getProject(authStore.signedInProjectId).state == ProjectState.PROJECT_ENDING if (isSyncComplete && isProjectEnding) { - externalScope.launch { - logoutUseCase() + viewModelScope.launch { + logout() _signOutEventLiveData.postValue(LiveDataEvent()) } } @@ -108,12 +118,29 @@ internal class SyncViewModel @Inject constructor( fun sync() { _syncCardLiveData.postValue(SyncConnecting(null, 0, null)) - eventSyncManager.sync() + syncOrchestrator.startEventSync() + } + + fun login() { + viewModelScope.launch { + val loginArgs = LoginContract.toArgs( + authStore.signedInProjectId, + authStore.signedInUserId + ?: recentUserActivityManager.getRecentUserActivity().lastUserUsed + ) + _loginRequestedEventLiveData.send(loginArgs) + } + } + + fun handleLoginResult(result: LoginResult) { + if (result.isSuccess) { + sync() + } } private fun startInitialSyncIfRequired() { viewModelScope.launch { - val lastUpdate = lastTimeSyncRun ?: eventSyncManager.getLastSyncTime() + val lastUpdate = lastTimeSyncRun ?: eventSyncManager.getLastSyncTime() val isRunning = syncStateLiveData.value?.isSyncRunning() ?: false @@ -189,73 +216,49 @@ internal class SyncViewModel @Inject constructor( } } - private suspend fun processRecentSyncState(syncState: EventSyncState, itemsToUpSync: Int): SyncCardState { - - val downSyncStates = syncState.downSyncWorkersInfo - val upSyncStates = syncState.upSyncWorkersInfo - val allSyncStates = downSyncStates + upSyncStates + private suspend fun processRecentSyncState( + syncState: EventSyncState, + itemsToUpSync: Int, + ): SyncCardState { return when { - isThereNotSyncHistory(allSyncStates) -> SyncDefault(lastTimeSyncSucceed()) - isSyncCompleted(allSyncStates) -> { + syncState.isThereNotSyncHistory() -> SyncDefault(lastTimeSyncSucceed()) + syncState.isSyncCompleted() -> { if (itemsToUpSync == 0) SyncComplete(lastTimeSyncSucceed()) else SyncPendingUpload(lastTimeSyncSucceed(), itemsToUpSync) } - isSyncProcess(allSyncStates) -> SyncProgress( + + syncState.isSyncInProgress() -> SyncProgress( lastTimeSyncSucceed(), syncState.progress, syncState.total ) - isSyncConnecting(allSyncStates) -> SyncConnecting( + + syncState.isSyncConnecting() -> SyncConnecting( lastTimeSyncSucceed(), syncState.progress, syncState.total ) - isSyncFailedBecauseTooManyRequests(allSyncStates) -> SyncTooManyRequests( + + syncState.isSyncFailedBecauseReloginRequired() -> SyncFailedReloginRequired( + lastTimeSyncSucceed() + ) + + syncState.isSyncFailedBecauseTooManyRequests() -> SyncTooManyRequests( lastTimeSyncSucceed() ) - isSyncFailedBecauseCloudIntegration(allSyncStates) -> SyncFailed(lastTimeSyncSucceed()) - isSyncFailedBecauseBackendMaintenance(allSyncStates) -> SyncFailedBackendMaintenance( + + syncState.isSyncFailedBecauseCloudIntegration() -> SyncFailed(lastTimeSyncSucceed()) + syncState.isSyncFailedBecauseBackendMaintenance() -> SyncFailedBackendMaintenance( lastTimeSyncSucceed(), - estimatedOutage + syncState.getEstimatedBackendMaintenanceOutage() ) - isSyncFailed(allSyncStates) -> SyncTryAgain(lastTimeSyncSucceed()) - else -> SyncProgress(lastTimeSyncSucceed(), syncState.progress, syncState.total) - } - } - private fun isThereNotSyncHistory(allSyncStates: List) = - allSyncStates.isEmpty() - - private fun isSyncCompleted(allSyncStates: List) = - allSyncStates.all { it.state is EventSyncWorkerState.Succeeded } - - private fun isSyncProcess(allSyncStates: List) = - allSyncStates.any { it.state is EventSyncWorkerState.Running } - - private fun isSyncConnecting(allSyncStates: List) = - allSyncStates.any { it.state is EventSyncWorkerState.Enqueued } - - private fun isSyncFailedBecauseTooManyRequests(allSyncStates: List) = - allSyncStates.any { it.state is EventSyncWorkerState.Failed && (it.state as EventSyncWorkerState.Failed).failedBecauseTooManyRequest } - - private fun isSyncFailedBecauseCloudIntegration(allSyncStates: List) = - allSyncStates.any { it.state is EventSyncWorkerState.Failed && (it.state as EventSyncWorkerState.Failed).failedBecauseCloudIntegration } - - private fun isSyncFailedBecauseBackendMaintenance(allSyncStates: List): Boolean { - val isBackendMaintenance = - allSyncStates.any { it.state is EventSyncWorkerState.Failed && (it.state as EventSyncWorkerState.Failed).failedBecauseBackendMaintenance } - if (isBackendMaintenance) { - val syncWorkerInfo = - allSyncStates.find { it.state is EventSyncWorkerState.Failed && (it.state as EventSyncWorkerState.Failed).estimatedOutage != 0L } - val failedWorkerState = syncWorkerInfo?.state as EventSyncWorkerState.Failed? - estimatedOutage = failedWorkerState?.estimatedOutage + syncState.isSyncFailed() -> SyncTryAgain(lastTimeSyncSucceed()) + else -> SyncProgress(lastTimeSyncSucceed(), syncState.progress, syncState.total) } - return isBackendMaintenance } - private fun isSyncFailed(allSyncStates: List) = - allSyncStates.any { it.state is EventSyncWorkerState.Failed || it.state is EventSyncWorkerState.Blocked || it.state is EventSyncWorkerState.Cancelled } private suspend fun isModuleSelectionRequired() = isDownSyncAllowed() && isSelectedModulesEmpty() && isModuleSync() diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/SettingsFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/SettingsFragment.kt index 370e6df71c..09a748882b 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/SettingsFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/SettingsFragment.kt @@ -12,13 +12,13 @@ import androidx.navigation.fragment.findNavController import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.simprints.infra.uibase.viewbinding.viewBinding import com.simprints.feature.dashboard.DashboardActivity import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentSettingsBinding import com.simprints.feature.dashboard.settings.password.SettingsPasswordDialogFragment import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration.Modality.FINGERPRINT +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/about/AboutViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/about/AboutViewModel.kt index 1dc0de0431..c870e03e99 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/about/AboutViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/about/AboutViewModel.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.ExternalScope import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase @@ -16,7 +15,6 @@ import com.simprints.infra.eventsync.EventSyncManager import com.simprints.infra.recent.user.activity.RecentUserActivityManager import com.simprints.infra.recent.user.activity.domain.RecentUserActivity import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,10 +22,9 @@ import javax.inject.Inject @HiltViewModel internal class AboutViewModel @Inject constructor( private val configRepository: ConfigRepository, - private val logoutUseCase: LogoutUseCase, + private val logout: LogoutUseCase, private val eventSyncManager: EventSyncManager, private val recentUserActivityManager: RecentUserActivityManager, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { val syncAndSearchConfig: LiveData @@ -75,9 +72,6 @@ internal class AboutViewModel @Inject constructor( private suspend fun canSyncDataToSimprints(): Boolean = configRepository.getProjectConfiguration().canSyncDataToSimprints() - private fun logout() { - externalScope.launch { logoutUseCase() } - } private fun load() = viewModelScope.launch { val configuration = configRepository.getProjectConfiguration() diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt index ac2a7833c6..4e882cb624 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoFragment.kt @@ -7,15 +7,19 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController -import com.simprints.infra.uibase.viewbinding.viewBinding +import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.dashboard.R import com.simprints.feature.dashboard.databinding.FragmentSyncInfoBinding import com.simprints.feature.dashboard.settings.syncinfo.modulecount.ModuleCount import com.simprints.feature.dashboard.settings.syncinfo.modulecount.ModuleCountAdapter +import com.simprints.feature.login.LoginContract +import com.simprints.feature.login.LoginResult import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.SynchronizationConfiguration import com.simprints.infra.config.store.models.canSyncDataToSimprints import com.simprints.infra.config.store.models.isEventDownSyncAllowed +import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -23,6 +27,7 @@ import com.simprints.infra.resources.R as IDR internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { companion object { + private const val TOTAL_RECORDS_INDEX = 0 } @@ -37,6 +42,12 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { setupClickListeners() observeUI() viewModel.refreshInformation() + + findNavController().handleResult( + viewLifecycleOwner, + R.id.syncInfoFragment, + LoginContract.DESTINATION, + ) { result -> viewModel.handleLoginResult(result) } } private fun setupClickListeners() { @@ -54,6 +65,9 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { viewModel.forceSync() updateSyncButton(isSyncInProgress = true) } + binding.syncReloginRequiredLoginButton.setOnClickListener { + viewModel.login() + } } private fun observeUI() { @@ -77,13 +91,10 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { } viewModel.recordsToDownSync.observe(viewLifecycleOwner) { - binding.recordsToDownloadCount.text = it?.toString() ?: "" - setProgressBar(it, binding.recordsToDownloadCount, binding.recordsToDownloadProgress) - } - - viewModel.recordsToDelete.observe(viewLifecycleOwner) { - binding.recordsToDeleteCount.text = it?.toString() ?: "" - setProgressBar(it, binding.recordsToDeleteCount, binding.recordsToDeleteProgress) + binding.recordsToDownloadCount.text = it?.let { + if (it.isLowerBound) "${it.count}+" else "${it.count}" + } ?: "" + setProgressBar(it?.count, binding.recordsToDownloadCount, binding.recordsToDownloadProgress) } viewModel.moduleCounts.observe(viewLifecycleOwner) { @@ -97,6 +108,21 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { viewModel.isSyncAvailable.observe(viewLifecycleOwner) { binding.syncButton.isEnabled = it } + viewModel.isReloginRequired.observe(viewLifecycleOwner) { reloginRequired -> + if (reloginRequired) { + binding.syncReloginRequiredSection.visibility = View.VISIBLE + binding.syncButton.visibility = View.GONE + } else { + binding.syncReloginRequiredSection.visibility = View.GONE + binding.syncButton.visibility = View.VISIBLE + } + } + viewModel.loginRequestedEventLiveData.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { loginArgs -> + findNavController().navigate( + R.id.action_syncInfoFragment_to_login, + loginArgs + ) + }) } private fun updateSyncButton(isSyncInProgress: Boolean) { @@ -119,7 +145,6 @@ internal class SyncInfoFragment : Fragment(R.layout.fragment_sync_info) { private fun setupRecordsCountCards(configuration: ProjectConfiguration) { if (!configuration.isEventDownSyncAllowed()) { binding.recordsToDownloadCardView.visibility = View.GONE - binding.recordsToDeleteCardView.visibility = View.GONE } if (!configuration.canSyncDataToSimprints()) { diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt index c3cbb1a5a4..d7d1c20be1 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt @@ -1,16 +1,26 @@ package com.simprints.feature.dashboard.settings.syncinfo +import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.livedata.LiveDataEventWithContent +import com.simprints.core.livedata.send import com.simprints.feature.dashboard.settings.syncinfo.modulecount.ModuleCount +import com.simprints.feature.login.LoginContract +import com.simprints.feature.login.LoginResult +import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.DownSynchronizationConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.SynchronizationConfiguration +import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.models.isEventDownSyncAllowed +import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.eventsync.EventSyncManager @@ -19,12 +29,9 @@ import com.simprints.infra.eventsync.status.models.EventSyncState import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.images.ImageRepository import com.simprints.infra.logging.Simber -import com.simprints.infra.authstore.AuthStore -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.TokenKeyType -import com.simprints.infra.config.store.tokenization.TokenizationProcessor -import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.network.ConnectivityTracker +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.sync.SyncOrchestrator import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -40,7 +47,9 @@ internal class SyncInfoViewModel @Inject constructor( private val authStore: AuthStore, private val imageRepository: ImageRepository, private val eventSyncManager: EventSyncManager, - private val tokenizationProcessor: TokenizationProcessor + private val syncOrchestrator: SyncOrchestrator, + private val tokenizationProcessor: TokenizationProcessor, + private val recentUserActivityManager: RecentUserActivityManager ) : ViewModel() { val recordsInLocal: LiveData @@ -55,13 +64,9 @@ internal class SyncInfoViewModel @Inject constructor( get() = _imagesToUpload private val _imagesToUpload = MutableLiveData(null) - val recordsToDownSync: LiveData + val recordsToDownSync: LiveData get() = _recordsToDownSync - private val _recordsToDownSync = MutableLiveData(null) - - val recordsToDelete: LiveData - get() = _recordsToDelete - private val _recordsToDelete = MutableLiveData(null) + private val _recordsToDownSync = MutableLiveData(null) val moduleCounts: LiveData> get() = _moduleCounts @@ -71,7 +76,7 @@ internal class SyncInfoViewModel @Inject constructor( get() = _configuration private val _configuration = MutableLiveData() - val isConnected: LiveData = connectivityTracker.observeIsConnected() + private val isConnected: LiveData = connectivityTracker.observeIsConnected() val lastSyncState = eventSyncManager.getLastSyncState() private var lastKnownEventSyncState: EventSyncState? = null @@ -80,6 +85,14 @@ internal class SyncInfoViewModel @Inject constructor( get() = _isSyncAvailable private val _isSyncAvailable = MediatorLiveData() + val isReloginRequired: LiveData + get() = _isReloginRequired + private val _isReloginRequired = MediatorLiveData() + + val loginRequestedEventLiveData: LiveData> + get() = _loginRequestedEventLiveData + private val _loginRequestedEventLiveData = MutableLiveData>() + init { _isSyncAvailable.addSource(lastSyncState) { lastSyncStateValue -> _isSyncAvailable.postValue( @@ -108,20 +121,22 @@ internal class SyncInfoViewModel @Inject constructor( ) ) } + _isReloginRequired.addSource(lastSyncState) { lastSyncStateValue -> + _isReloginRequired.postValue(lastSyncStateValue.isSyncFailedBecauseReloginRequired()) + } } fun refreshInformation() { _recordsInLocal.postValue(null) _recordsToUpSync.postValue(null) _recordsToDownSync.postValue(null) - _recordsToDelete.postValue(null) _imagesToUpload.postValue(null) _moduleCounts.postValue(listOf()) load() } fun forceSync() { - eventSyncManager.sync() + syncOrchestrator.startEventSync() // There is a delay between starting sync and lastSyncState // reporting it so this prevents starting multiple syncs by accident _isSyncAvailable.postValue(false) @@ -145,6 +160,22 @@ internal class SyncInfoViewModel @Inject constructor( } } + fun login() { + viewModelScope.launch { + val loginArgs = LoginContract.toArgs( + authStore.signedInProjectId, + authStore.signedInUserId ?: recentUserActivityManager.getRecentUserActivity().lastUserUsed + ) + _loginRequestedEventLiveData.send(loginArgs) + } + } + + fun handleLoginResult(result: LoginResult) { + if (result.isSuccess) { + forceSync() + } + } + private fun load() = viewModelScope.launch { val projectId = authStore.signedInProjectId @@ -152,12 +183,7 @@ internal class SyncInfoViewModel @Inject constructor( async { _configuration.postValue(configRepository.getProjectConfiguration()) }, async { _recordsInLocal.postValue(getRecordsInLocal(projectId)) }, async { _recordsToUpSync.postValue(getRecordsToUpSync()) }, - async { - fetchRecordsToCreateAndDeleteCount().let { - _recordsToDownSync.postValue(it.toCreate) - _recordsToDelete.postValue(it.toDelete) - } - }, + async { _recordsToDownSync.postValue(fetchRecordsToCreateAndDeleteCount()) }, async { _imagesToUpload.postValue(imageRepository.getNumberOfImagesToUpload(projectId)) }, async { _moduleCounts.postValue(getModuleCounts(projectId)) } ) @@ -168,8 +194,8 @@ internal class SyncInfoViewModel @Inject constructor( isConnected: Boolean?, syncConfiguration: SynchronizationConfiguration? = configuration.value?.synchronization, ) = isConnected == true - && isSyncRunning == false - && syncConfiguration?.let { + && isSyncRunning == false + && syncConfiguration?.let { !isModuleSync(it.down) || isModuleSyncAndModuleIdOptionsNotEmpty( it ) @@ -193,7 +219,7 @@ internal class SyncInfoViewModel @Inject constructor( if (configRepository.getProjectConfiguration().isEventDownSyncAllowed()) { fetchAndUpdateRecordsToDownSyncAndDeleteCount() } else { - DownSyncCounts(0, 0) + DownSyncCounts(0, isLowerBound = false) } private suspend fun fetchAndUpdateRecordsToDownSyncAndDeleteCount(): DownSyncCounts = @@ -201,7 +227,7 @@ internal class SyncInfoViewModel @Inject constructor( eventSyncManager.countEventsToDownload() } catch (t: Throwable) { Simber.d(t) - DownSyncCounts(0, 0) + DownSyncCounts(0, isLowerBound = false) } private suspend fun getModuleCounts(projectId: String): List = diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModel.kt index 5685f2bd3c..229ebaba67 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModel.kt @@ -15,7 +15,7 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor -import com.simprints.infra.eventsync.EventSyncManager +import com.simprints.infra.sync.SyncOrchestrator import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -25,7 +25,7 @@ import javax.inject.Inject internal class ModuleSelectionViewModel @Inject constructor( private val authStore: AuthStore, private val moduleRepository: ModuleRepository, - private val eventSyncManager: EventSyncManager, + private val syncOrchestrator: SyncOrchestrator, private val configRepository: ConfigRepository, private val tokenizationProcessor: TokenizationProcessor, @ExternalScope private val externalScope: CoroutineScope, @@ -108,8 +108,8 @@ internal class ModuleSelectionViewModel @Inject constructor( module.copy(name = encryptedName) } moduleRepository.saveModules(modules) - eventSyncManager.stop() - eventSyncManager.sync() + syncOrchestrator.stopEventSync() + syncOrchestrator.startEventSync() } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardState.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardState.kt index 6052c858a8..af4c686d30 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardState.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardState.kt @@ -27,6 +27,10 @@ internal sealed class SyncCardState(open val lastTimeSyncSucceed: String?) { override val lastTimeSyncSucceed: String?, ) : SyncCardState(lastTimeSyncSucceed) + data class SyncFailedReloginRequired( + override val lastTimeSyncSucceed: String?, + ) : SyncCardState(lastTimeSyncSucceed) + data class SyncFailedBackendMaintenance( override val lastTimeSyncSucceed: String?, val estimatedOutage: Long? = null diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardView.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardView.kt index ab2a005272..1495b45f93 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardView.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/views/SyncCardView.kt @@ -26,6 +26,7 @@ internal class SyncCardView : MaterialCardView { var onSyncButtonClick: () -> Unit = {} var onSelectNoModulesButtonClick: () -> Unit = {} var onOfflineButtonClick: () -> Unit = {} + var onLoginButtonClick: () -> Unit = {} private val binding = LayoutCardSyncBinding.inflate(LayoutInflater.from(context), this) init { @@ -38,10 +39,8 @@ internal class SyncCardView : MaterialCardView { is SyncCardState.SyncDefault -> prepareSyncDefaultStateView() is SyncCardState.SyncPendingUpload -> prepareSyncDefaultStateView(state.itemsToUpSync) is SyncCardState.SyncFailed -> prepareSyncFailedStateView() - is SyncCardState.SyncFailedBackendMaintenance -> prepareSyncFailedBecauseBackendMaintenanceView( - state - ) - + is SyncCardState.SyncFailedReloginRequired -> prepareSyncFailedBecauseReloginRequired() + is SyncCardState.SyncFailedBackendMaintenance -> prepareSyncFailedBecauseBackendMaintenanceView(state) is SyncCardState.SyncTooManyRequests -> prepareSyncTooManyRequestsView() is SyncCardState.SyncTryAgain -> prepareTryAgainStateView() is SyncCardState.SyncHasNoModules -> prepareNoModulesStateView() @@ -60,6 +59,7 @@ internal class SyncCardView : MaterialCardView { binding.syncCardOffline.visibility = View.GONE binding.syncCardProgress.visibility = View.GONE binding.syncCardTryAgain.visibility = View.GONE + binding.syncCardReloginRequired.visibility = View.GONE } private fun prepareSyncDefaultStateView(itemsToSync: Int = 0) { @@ -82,6 +82,11 @@ internal class SyncCardView : MaterialCardView { resources.getString(R.string.dashboard_sync_card_failed_message) } + private fun prepareSyncFailedBecauseReloginRequired() { + binding.syncCardReloginRequired.visibility = View.VISIBLE + binding.syncCardReloginRequiredLoginButton.setOnClickListener { onLoginButtonClick() } + } + private fun prepareSyncFailedBecauseBackendMaintenanceView(state: SyncCardState.SyncFailedBackendMaintenance) { binding.syncCardFailedMessage.visibility = View.VISIBLE binding.syncCardFailedMessage.text = @@ -102,9 +107,7 @@ internal class SyncCardView : MaterialCardView { private fun prepareTryAgainStateView() { binding.syncCardTryAgain.visibility = View.VISIBLE - binding.syncCardTryAgainSyncButton.setOnClickListener { - onSyncButtonClick() - } + binding.syncCardTryAgainSyncButton.setOnClickListener { onSyncButtonClick() } } private fun prepareNoModulesStateView() { @@ -116,9 +119,7 @@ internal class SyncCardView : MaterialCardView { private fun prepareSyncOfflineView() { binding.syncCardOffline.visibility = View.VISIBLE - binding.syncCardOfflineButton.setOnClickListener { - onOfflineButtonClick() - } + binding.syncCardOfflineButton.setOnClickListener { onOfflineButtonClick() } } private fun prepareProgressView(state: SyncCardState.SyncProgress) { diff --git a/feature/dashboard/src/main/res/layout/fragment_debug.xml b/feature/dashboard/src/main/res/layout/fragment_debug.xml index c558624a43..7d313dcf14 100644 --- a/feature/dashboard/src/main/res/layout/fragment_debug.xml +++ b/feature/dashboard/src/main/res/layout/fragment_debug.xml @@ -71,8 +71,15 @@ android:text="Schedule Event Sync" tools:ignore="HardcodedText" /> - + + diff --git a/feature/dashboard/src/main/res/layout/fragment_sync_info.xml b/feature/dashboard/src/main/res/layout/fragment_sync_info.xml index 90ec6e3ae6..4b89154bf8 100644 --- a/feature/dashboard/src/main/res/layout/fragment_sync_info.xml +++ b/feature/dashboard/src/main/res/layout/fragment_sync_info.xml @@ -31,48 +31,46 @@ + app:layout_constraintTop_toBottomOf="@+id/appBarLayout"> + android:padding="8dp"> + android:padding="4dp" + android:text="@string/dashboard_sync_info_total_records_on_device" /> - + app:layout_constraintTop_toBottomOf="@+id/recordsInLocalCardView"> + android:orientation="vertical" + android:padding="8dp"> + android:gravity="center" + android:padding="4dp" + android:text="@string/dashboard_sync_info_records_to_upload" /> + tools:text="0" /> - - + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/recordsToUploadCardView" + app:layout_constraintTop_toTopOf="@id/recordsToUploadCardView"> + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="8dp"> + android:gravity="center" + android:padding="4dp" + android:text="@string/dashboard_sync_info_images_to_upload" /> + tools:text="0" /> + app:layout_constraintTop_toBottomOf="@id/recordsToUploadCardView"> + android:orientation="horizontal" + android:padding="8dp"> + android:padding="4dp" + android:text="@string/dashboard_sync_info_records_to_download" /> + android:layout_weight="0.2" + android:gravity="center" + android:textSize="16sp" + tools:text="137" /> - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="16dp" + android:gravity="center" + android:orientation="vertical" + app:layout_constraintBottom_toTopOf="@+id/modulesTabHost" + app:layout_constraintEnd_toEndOf="@+id/modulesTabHost" + app:layout_constraintStart_toStartOf="@+id/modulesTabHost" + app:layout_constraintTop_toBottomOf="@+id/recordsToDownloadCardView"> + + + android:visibility="gone"> - - - - + android:layout_marginBottom="8dp" + android:text="@string/dashboard_sync_card_relogin_required" /> + - + + app:layout_constraintTop_toBottomOf="@+id/recordsCardsBarrier" + tools:ignore="SpeakableTextPresentCheck" + tools:visibility="visible"> @@ -336,7 +318,7 @@ android:layout_height="match_parent" android:fadeScrollbars="false" android:orientation="vertical" - android:padding="10dp" + android:padding="8dp" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="100" @@ -348,22 +330,22 @@ android:id="@+id/moduleSelectionButton" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="10dp" - android:layout_marginTop="15dp" - android:layout_marginEnd="10dp" - android:layout_marginBottom="15dp" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="16dp" android:text="@string/dashboard_sync_info_select_modules_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/modulesTabHost" app:layout_constraintStart_toStartOf="@+id/modulesTabHost" - app:layout_constraintTop_toBottomOf="@+id/modulesTabHost" /> + tools:visibility="visible" /> + app:constraint_referenced_ids="recordsToUploadCardView,recordsToDownloadCardView,recordsToDownloadCardView,syncActionSection" /> \ No newline at end of file diff --git a/feature/dashboard/src/main/res/layout/layout_card_sync.xml b/feature/dashboard/src/main/res/layout/layout_card_sync.xml index 2d66248e64..0b94c25346 100644 --- a/feature/dashboard/src/main/res/layout/layout_card_sync.xml +++ b/feature/dashboard/src/main/res/layout/layout_card_sync.xml @@ -191,6 +191,32 @@ android:text="@string/dashboard_sync_card_try_again_button" /> + + + + + + + + + + + + + - - - + (relaxed = true) private val context = InstrumentationRegistry.getInstrumentation().context + private val navController = testNavController(R.navigation.graph_dashboard) @Test fun `should not hide the sync card view if it can't sync to BFSID`() { mockSyncToBFSIDAllowed(false) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.logoutSyncCard)).check(matches(isDisplayed())) } @@ -66,7 +67,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncDefault(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -98,7 +99,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncPendingUpload(LAST_SYNC_TIME, 2)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -138,7 +139,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncFailed(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -166,7 +167,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncFailedBackendMaintenance(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -198,7 +199,7 @@ internal class LogoutSyncFragmentTest { ) ) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -231,7 +232,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncTooManyRequests(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -259,7 +260,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncTryAgain(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -322,7 +323,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncProgress(LAST_SYNC_TIME, 20, 40)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -358,7 +359,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncConnecting(LAST_SYNC_TIME, 20, 40)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -391,7 +392,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncComplete(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -439,7 +440,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncProgress(LAST_SYNC_TIME, 20, 40)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.logoutButton)).check(matches(not(isDisplayed()))) onView(withId(R.id.logout_sync_info)).check(matches(isDisplayed())) onView(withId(R.id.logoutWithoutSyncButton)).check(matches(isDisplayed())) @@ -450,7 +451,7 @@ internal class LogoutSyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncComplete(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.logoutButton)).check(matches(isDisplayed())) onView(withId(R.id.logout_sync_info)).check(matches(not(isDisplayed()))) onView(withId(R.id.logoutWithoutSyncButton)).check(matches(not(isDisplayed()))) diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCaseTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCaseTest.kt new file mode 100644 index 0000000000..93b95ca6cc --- /dev/null +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCaseTest.kt @@ -0,0 +1,51 @@ +package com.simprints.feature.dashboard.logout.usecase + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.simprints.infra.authlogic.AuthManager +import com.simprints.infra.sync.SyncOrchestrator +import com.simprints.testtools.common.coroutines.TestCoroutineRule +import io.mockk.MockKAnnotations +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class LogoutUseCaseTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val testCoroutineRule = TestCoroutineRule() + + @MockK + private lateinit var syncOrchestrator: SyncOrchestrator + + @MockK + private lateinit var authManager: AuthManager + + private lateinit var useCase: LogoutUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxed = true) + + useCase = LogoutUseCase( + syncOrchestrator = syncOrchestrator, + authManager = authManager, + ) + } + + @Test + fun `Fully logs out when called`() = runTest { + useCase.invoke() + + coVerify { + syncOrchestrator.cancelBackgroundWork() + syncOrchestrator.deleteEventSyncInfo() + authManager.signOut() + } + } +} diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/MainFragmentTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/MainFragmentTest.kt index a6760d700a..6619cb98c8 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/MainFragmentTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/MainFragmentTest.kt @@ -1,11 +1,13 @@ package com.simprints.feature.dashboard.main import androidx.lifecycle.Observer -import androidx.test.espresso.Espresso.* +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.simprints.feature.dashboard.R @@ -51,11 +53,13 @@ class MainFragmentTest { @JvmField internal val syncViewModel = mockk(relaxed = true) + private val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) + @Test fun `should hide the privacy notice menu if the consent is not required`() { mockConsentRequired(false) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) openContextualActionModeOverflowMenu() onView(withText("Privacy Notice")).check(doesNotExist()) @@ -65,7 +69,7 @@ class MainFragmentTest { fun `should display the privacy notice menu if the consent is required`() { mockConsentRequired(true) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) openContextualActionModeOverflowMenu() onView(withText("Privacy Notice")).check(matches(isDisplayed())) @@ -75,8 +79,6 @@ class MainFragmentTest { fun `should redirect to the settings fragment when clicking on settings`() { mockConsentRequired(true) - val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) - launchFragmentInHiltContainer(navController = navController) openContextualActionModeOverflowMenu() @@ -88,8 +90,6 @@ class MainFragmentTest { fun `should redirect to the privacy notices fragment when clicking on privacy notices`() { mockConsentRequired(true) - val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) - launchFragmentInHiltContainer(navController = navController) openContextualActionModeOverflowMenu() @@ -101,8 +101,6 @@ class MainFragmentTest { fun `should redirect to the debug fragment when clicking on debug`() { mockConsentRequired(true) - val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) - launchFragmentInHiltContainer(navController = navController) openContextualActionModeOverflowMenu() diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncFragmentTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncFragmentTest.kt index 5be2bdb4af..312fcc32a2 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncFragmentTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncFragmentTest.kt @@ -3,8 +3,10 @@ package com.simprints.feature.dashboard.main.sync import androidx.lifecycle.Observer import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.* -import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth @@ -43,12 +45,13 @@ class SyncFragmentTest { internal val viewModel = mockk(relaxed = true) private val context = InstrumentationRegistry.getInstrumentation().context + private val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) @Test fun `should hide the sync card view if it can't sync to BFSID`() { mockSyncToBFSIDAllowed(false) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.dashboardSyncCard)).check(matches(not(isDisplayed()))) } @@ -57,7 +60,7 @@ class SyncFragmentTest { fun `should display the sync card view if it can sync to BFSID`() { mockSyncToBFSIDAllowed(true) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.dashboardSyncCard)).check(matches(isDisplayed())) } @@ -67,7 +70,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncDefault(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -76,6 +79,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) val lastSyncText = context.getString(IDR.string.dashboard_sync_card_last_sync, LAST_SYNC_TIME) @@ -94,7 +98,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncPendingUpload(LAST_SYNC_TIME, 2)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -103,6 +107,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) val lastSyncText = context.getString(IDR.string.dashboard_sync_card_last_sync, LAST_SYNC_TIME) @@ -116,13 +121,12 @@ class SyncFragmentTest { verify(exactly = 1) { viewModel.sync() } } - @Test fun `should display the correct sync card view for the SyncFailed state`() { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncFailed(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -131,6 +135,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) @@ -139,12 +144,37 @@ class SyncFragmentTest { onView(withId(R.id.sync_card_failed_message)).check(matches(withText(IDR.string.dashboard_sync_card_failed_message))) } + @Test + fun `should display the correct sync card view for the SyncFailedReloginRequired state`() { + mockSyncToBFSIDAllowed(true) + mockSyncCardLiveData(SyncCardState.SyncFailedReloginRequired(LAST_SYNC_TIME)) + + launchFragmentInHiltContainer(navController = navController) + + checkHiddenViews( + listOf( + R.id.sync_card_default_state_sync_button, + R.id.sync_card_select_no_modules, + R.id.sync_card_offline, + R.id.sync_card_progress, + R.id.sync_card_try_again, + ) + ) + + val lastSyncText = context.getString(IDR.string.dashboard_sync_card_last_sync, LAST_SYNC_TIME) + onView(withId(R.id.sync_card_last_sync)).check(matches(withText(lastSyncText))) + onView(withId(R.id.sync_card_relogin_required)).check(matches(isDisplayed())) + onView(withId(R.id.sync_card_relogin_required_login_button)).check(matches(isDisplayed())) + .perform(click()) + verify(exactly = 1) { viewModel.login() } + } + @Test fun `should display the correct sync card view for the SyncFailedBackendMaintenance state without estimated outage`() { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncFailedBackendMaintenance(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -153,6 +183,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) val lastSyncText = context.getString(IDR.string.dashboard_sync_card_last_sync, LAST_SYNC_TIME) @@ -170,7 +201,7 @@ class SyncFragmentTest { ) ) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -179,6 +210,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) @@ -194,7 +226,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncTooManyRequests(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -203,6 +235,7 @@ class SyncFragmentTest { R.id.sync_card_offline, R.id.sync_card_progress, R.id.sync_card_try_again, + R.id.sync_card_relogin_required, ) ) @@ -216,7 +249,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncTryAgain(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -225,6 +258,7 @@ class SyncFragmentTest { R.id.sync_card_select_no_modules, R.id.sync_card_offline, R.id.sync_card_progress, + R.id.sync_card_relogin_required, ) ) @@ -241,8 +275,6 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncHasNoModules(LAST_SYNC_TIME)) - val navController = testNavController(R.navigation.graph_dashboard, R.id.mainFragment) - launchFragmentInHiltContainer(navController = navController) checkHiddenViews( @@ -252,6 +284,7 @@ class SyncFragmentTest { R.id.sync_card_try_again, R.id.sync_card_offline, R.id.sync_card_progress, + R.id.sync_card_relogin_required, ) ) @@ -269,7 +302,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncOffline(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -278,6 +311,7 @@ class SyncFragmentTest { R.id.sync_card_try_again, R.id.sync_card_select_no_modules_button, R.id.sync_card_progress, + R.id.sync_card_relogin_required, ) ) @@ -293,7 +327,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncProgress(LAST_SYNC_TIME, 20, 40)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -302,6 +336,7 @@ class SyncFragmentTest { R.id.sync_card_try_again, R.id.sync_card_select_no_modules_button, R.id.sync_card_offline, + R.id.sync_card_relogin_required, ) ) @@ -323,7 +358,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncConnecting(LAST_SYNC_TIME, 20, 40)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -332,6 +367,7 @@ class SyncFragmentTest { R.id.sync_card_try_again, R.id.sync_card_select_no_modules_button, R.id.sync_card_offline, + R.id.sync_card_relogin_required, ) ) @@ -350,7 +386,7 @@ class SyncFragmentTest { mockSyncToBFSIDAllowed(true) mockSyncCardLiveData(SyncCardState.SyncComplete(LAST_SYNC_TIME)) - launchFragmentInHiltContainer() + launchFragmentInHiltContainer(navController = navController) checkHiddenViews( listOf( @@ -359,6 +395,7 @@ class SyncFragmentTest { R.id.sync_card_try_again, R.id.sync_card_select_no_modules_button, R.id.sync_card_offline, + R.id.sync_card_relogin_required, ) ) diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncViewModelTest.kt index 4a38191de7..12da030ad2 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncViewModelTest.kt @@ -6,6 +6,7 @@ import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.tools.time.TimeHelper import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase +import com.simprints.feature.dashboard.views.SyncCardState import com.simprints.feature.dashboard.views.SyncCardState.SyncComplete import com.simprints.feature.dashboard.views.SyncCardState.SyncConnecting import com.simprints.feature.dashboard.views.SyncCardState.SyncDefault @@ -17,12 +18,14 @@ import com.simprints.feature.dashboard.views.SyncCardState.SyncPendingUpload import com.simprints.feature.dashboard.views.SyncCardState.SyncProgress import com.simprints.feature.dashboard.views.SyncCardState.SyncTooManyRequests import com.simprints.feature.dashboard.views.SyncCardState.SyncTryAgain +import com.simprints.feature.login.LoginResult import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.config.store.models.DownSynchronizationConfiguration import com.simprints.infra.config.store.models.ProjectState import com.simprints.infra.config.store.models.SynchronizationConfiguration +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ALL import com.simprints.infra.eventsync.EventSyncManager @@ -30,6 +33,8 @@ import com.simprints.infra.eventsync.status.models.EventSyncState import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerType import com.simprints.infra.network.ConnectivityTracker +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.getOrAwaitValue import io.mockk.MockKAnnotations @@ -39,7 +44,6 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Rule @@ -69,6 +73,9 @@ internal class SyncViewModelTest { @MockK lateinit var eventSyncManager: EventSyncManager + @MockK + lateinit var syncOrchestrator: SyncOrchestrator + @MockK lateinit var connectivityTracker: ConnectivityTracker @@ -84,6 +91,9 @@ internal class SyncViewModelTest { @MockK lateinit var logoutUseCase: LogoutUseCase + @MockK + lateinit var recentUserActivityManager: RecentUserActivityManager + @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) @@ -91,7 +101,11 @@ internal class SyncViewModelTest { every { eventSyncManager.getLastSyncState() } returns syncState every { connectivityTracker.observeIsConnected() } returns isConnected coEvery { configRepository.getProjectConfiguration().synchronization } returns mockk { - every { up.simprints } returns SimprintsUpSynchronizationConfiguration(kind = ALL) + every { up.simprints } returns SimprintsUpSynchronizationConfiguration( + kind = ALL, + batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes.default(), + false, + ) every { frequency } returns SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START every { down.partitionType } returns DownSynchronizationConfiguration.PartitionType.MODULE } @@ -117,7 +131,7 @@ internal class SyncViewModelTest { val viewModel = initViewModel() - verify(exactly = 1) { eventSyncManager.sync() } + verify(exactly = 1) { syncOrchestrator.startEventSync() } assertThat(viewModel.syncCardLiveData.value).isEqualTo(SyncConnecting(null, 0, null)) } @@ -129,7 +143,7 @@ internal class SyncViewModelTest { val viewModel = initViewModel() - verify(exactly = 1) { eventSyncManager.sync() } + verify(exactly = 1) { syncOrchestrator.startEventSync() } assertThat(viewModel.syncCardLiveData.value).isEqualTo(SyncConnecting(null, 0, null)) } @@ -147,7 +161,7 @@ internal class SyncViewModelTest { initViewModel() - verify(exactly = 0) { eventSyncManager.sync() } + verify(exactly = 0) { syncOrchestrator.startEventSync() } } @Test @@ -299,6 +313,51 @@ internal class SyncViewModelTest { assertThat(syncCardLiveData).isEqualTo(SyncFailed(DATE)) } + @Test + fun `should post a ReloginRequired card state if the sync fails with such problem`() { + coEvery { configRepository.getDeviceConfiguration() } returns deviceConfiguration + isConnected.value = true + syncState.value = EventSyncState( + "", 10, 40, listOf(), listOf( + EventSyncState.SyncWorkerInfo( + EventSyncWorkerType.DOWNLOADER, + EventSyncWorkerState.Failed(failedBecauseReloginRequired = true) + ) + ) + ) + val syncCardLiveData = initViewModel().syncCardLiveData.getOrAwaitValue() + + assertThat(syncCardLiveData).isEqualTo(SyncCardState.SyncFailedReloginRequired(DATE)) + } + + @Test + fun `calling login() sends respective event to the view`() { + val viewModel = initViewModel() + + viewModel.login() + + val loginRequestedEvent = viewModel.loginRequestedEventLiveData.getOrAwaitValue() + assertThat(loginRequestedEvent).isNotNull() + } + + @Test + fun `calling handleLoginResult() triggers sync if result is success`() { + val viewModel = initViewModel() + + viewModel.handleLoginResult(LoginResult(true)) + + verify(exactly = 1) { syncOrchestrator.startEventSync() } + } + + @Test + fun `calling handleLoginResult() does not trigger sync if result is not success`() { + val viewModel = initViewModel() + + viewModel.handleLoginResult(LoginResult(false)) + + verify(exactly = 0) { syncOrchestrator.startEventSync() } + } + @Test fun `should post a SyncFailedBackendMaintenance card state if the sync fails because of cloud maintenance`() { coEvery { configRepository.getDeviceConfiguration() } returns deviceConfiguration @@ -379,11 +438,12 @@ internal class SyncViewModelTest { private fun initViewModel(): SyncViewModel = SyncViewModel( eventSyncManager = eventSyncManager, + syncOrchestrator = syncOrchestrator, connectivityTracker = connectivityTracker, configRepository = configRepository, timeHelper = timeHelper, authStore = authStore, - logoutUseCase = logoutUseCase, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher) + logout = logoutUseCase, + recentUserActivityManager = recentUserActivityManager, ) } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt index d2d52fa66b..7367f5bb55 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt @@ -20,7 +20,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -66,8 +65,7 @@ class AboutViewModelTest { configRepository = configRepository, eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, - logoutUseCase = logoutUseCase, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), + logout = logoutUseCase, ) assertThat(viewModel.modalities.value).isEqualTo(MODALITIES) @@ -186,8 +184,7 @@ class AboutViewModelTest { configRepository = configRepository, eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), - logoutUseCase = logoutUseCase, + logout = logoutUseCase, ) } } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt index 42cd10010b..b38f933069 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt @@ -6,6 +6,7 @@ import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.feature.dashboard.settings.syncinfo.modulecount.ModuleCount +import com.simprints.feature.login.LoginResult import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.DownSynchronizationConfiguration @@ -24,6 +25,8 @@ import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerType import com.simprints.infra.images.ImageRepository import com.simprints.infra.network.ConnectivityTracker +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.getOrAwaitValue import com.simprints.testtools.common.livedata.getOrAwaitValues @@ -71,6 +74,12 @@ class SyncInfoViewModelTest { @MockK private lateinit var eventSyncManager: EventSyncManager + @MockK + private lateinit var syncOrchestrator: SyncOrchestrator + + @MockK + lateinit var recentUserActivityManager: RecentUserActivityManager + @MockK private lateinit var project: Project @@ -101,7 +110,9 @@ class SyncInfoViewModelTest { authStore = authStore, imageRepository = imageRepository, eventSyncManager = eventSyncManager, - tokenizationProcessor = tokenizationProcessor + syncOrchestrator = syncOrchestrator, + tokenizationProcessor = tokenizationProcessor, + recentUserActivityManager = recentUserActivityManager ) } @@ -193,22 +204,35 @@ class SyncInfoViewModelTest { } @Test - fun `should initialize the recordsToDownSync and recordsToDelete live data to the count otherwise`() = + fun `should initialize the recordsToDownSync live data to the count otherwise`() = runTest { val module1 = "module1".asTokenizableEncrypted() - val creationForModules = 10 - val deletionForModules = 5 coEvery { configRepo.getDeviceConfiguration() } returns mockk { every { selectedModules } returns listOf(module1) } coEvery { eventSyncManager.countEventsToDownload() - } returns DownSyncCounts(creationForModules, deletionForModules) + } returns DownSyncCounts(15, isLowerBound = false) viewModel.refreshInformation() - assertThat(viewModel.recordsToDownSync.getOrAwaitValue()).isEqualTo(creationForModules) - assertThat(viewModel.recordsToDelete.getOrAwaitValue()).isEqualTo(deletionForModules) + assertThat(viewModel.recordsToDownSync.getOrAwaitValue()?.count).isEqualTo(15) + } + + @Test + fun `should initialize the recordsToDownSync live data to the default count value if fetch fails`() = + runTest { + val module1 = "module1".asTokenizableEncrypted() + coEvery { configRepo.getDeviceConfiguration() } returns mockk { + every { selectedModules } returns listOf(module1) + } + coEvery { + eventSyncManager.countEventsToDownload() + } throws Exception() + + viewModel.refreshInformation() + + assertThat(viewModel.recordsToDownSync.getOrAwaitValue()?.count).isEqualTo(0) } @Test @@ -295,7 +319,7 @@ class SyncInfoViewModelTest { fun `should invoke sync manager when sync is requested`() = runTest { viewModel.forceSync() - verify(exactly = 1) { eventSyncManager.sync() } + verify(exactly = 1) { syncOrchestrator.startEventSync() } assertThat(viewModel.isSyncAvailable.getOrAwaitValue()).isEqualTo(false) } @@ -419,6 +443,57 @@ class SyncInfoViewModelTest { assertThat(viewModel.isSyncAvailable.getOrAwaitValue()).isFalse() } + @Test + fun `emit ReloginRequired = false when lastSyncState updates with different status`() = + runTest { + stateLiveData.value = EventSyncState( + "", 0, 0, listOf(), listOf( + EventSyncState.SyncWorkerInfo( + EventSyncWorkerType.DOWNLOADER, + EventSyncWorkerState.Failed(failedBecauseBackendMaintenance = true) + ) + ) + ) + + assertThat(viewModel.isReloginRequired.getOrAwaitValue()).isFalse() + } + + @Test + fun `emit ReloginRequired = true when lastSyncState updates with such status`() = runTest { + stateLiveData.value = EventSyncState( + "", 0, 0, listOf(), listOf( + EventSyncState.SyncWorkerInfo( + EventSyncWorkerType.DOWNLOADER, + EventSyncWorkerState.Failed(failedBecauseReloginRequired = true) + ) + ) + ) + + assertThat(viewModel.isReloginRequired.getOrAwaitValue()).isTrue() + } + + @Test + fun `calling login() sends respective event to the view`() { + viewModel.login() + + val loginRequestedEvent = viewModel.loginRequestedEventLiveData.getOrAwaitValue() + assertThat(loginRequestedEvent).isNotNull() + } + + @Test + fun `calling handleLoginResult() triggers sync if result is success`() { + viewModel.handleLoginResult(LoginResult(true)) + + verify(exactly = 1) { syncOrchestrator.startEventSync() } + } + + @Test + fun `calling handleLoginResult() does not trigger sync if result is not success`() { + viewModel.handleLoginResult(LoginResult(false)) + + verify(exactly = 0) { syncOrchestrator.startEventSync() } + } + private fun createMockDownSyncConfig( partitionType: DownSynchronizationConfiguration.PartitionType, modules: List = emptyList(), diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModelTest.kt index 449998094c..f8b26812ad 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/moduleselection/ModuleSelectionViewModelTest.kt @@ -17,11 +17,16 @@ import com.simprints.infra.config.store.models.SettingsPasswordConfig import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.eventsync.EventSyncManager +import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.getOrAwaitValue import com.simprints.testtools.common.syntax.assertThrows -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import org.junit.Before import org.junit.Rule @@ -37,19 +42,22 @@ class ModuleSelectionViewModelTest { @get:Rule val testCoroutineRule = TestCoroutineRule() - @MockK(relaxed = true) + @MockK private lateinit var repository: ModuleRepository - @MockK(relaxed = true) + @MockK private lateinit var eventSyncManager: EventSyncManager - @MockK(relaxed = true) + @MockK + private lateinit var syncOrchestrator: SyncOrchestrator + + @MockK private lateinit var configRepository: ConfigRepository - @MockK(relaxed = true) + @MockK private lateinit var tokenizationProcessor: TokenizationProcessor - @MockK(relaxed = true) + @MockK private lateinit var authStore: AuthStore @MockK @@ -59,7 +67,7 @@ class ModuleSelectionViewModelTest { @Before fun setUp() { - MockKAnnotations.init(this) + MockKAnnotations.init(this, relaxed = true) val modulesDefault = listOf( Module("a".asTokenizableEncrypted(), false), @@ -87,7 +95,7 @@ class ModuleSelectionViewModelTest { viewModel = ModuleSelectionViewModel( authStore = authStore, moduleRepository = repository, - eventSyncManager = eventSyncManager, + syncOrchestrator = syncOrchestrator, configRepository = configRepository, tokenizationProcessor = tokenizationProcessor, externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), @@ -180,8 +188,8 @@ class ModuleSelectionViewModelTest { viewModel.saveModules() coVerify(exactly = 1) { repository.saveModules(updatedModules) } - coVerify(exactly = 1) { eventSyncManager.stop() } - coVerify(exactly = 1) { eventSyncManager.sync() } + coVerify(exactly = 1) { syncOrchestrator.stopEventSync() } + coVerify(exactly = 1) { syncOrchestrator.startEventSync() } } @Test diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/tools/di/FakeCoreModule.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/tools/di/FakeCoreModule.kt index 3bd1635264..efe6abcf6b 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/tools/di/FakeCoreModule.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/tools/di/FakeCoreModule.kt @@ -1,7 +1,9 @@ package com.simprints.feature.dashboard.tools.di +import com.simprints.core.AppScope import com.simprints.core.CoreModule import com.simprints.core.DeviceID +import com.simprints.core.DispatcherBG import com.simprints.core.DispatcherIO import com.simprints.core.ExternalScope import com.simprints.core.PackageVersionName @@ -37,6 +39,10 @@ object FakeCoreModule { @Singleton fun provideTimeHelper(): TimeHelper = mockk() + @Provides + @Singleton + fun provideJsonHelper(): JsonHelper = mockk() + @DeviceID @Provides fun provideDeviceId(): String = DEVICE_ID @@ -47,22 +53,26 @@ object FakeCoreModule { @DispatcherIO @Provides - fun provideCoroutineDispatcher(): CoroutineDispatcher = StandardTestDispatcher() + fun provideCoroutineDispatcherIo(): CoroutineDispatcher = StandardTestDispatcher() + + @DispatcherBG + @Provides + fun provideCoroutineDispatcherBg(): CoroutineDispatcher = StandardTestDispatcher() @ExternalScope @Provides fun provideExternalScope(): CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + @AppScope + @Provides + fun provideAppScope(): CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + @Provides @Singleton fun provideStringTokenizer(): StringTokenizer = mockk() - @Provides @Singleton fun provideEncodingUtils(): EncodingUtils = EncodingUtilsImplForTests - @Provides - @Singleton - fun provideJsonHelper() = JsonHelper } diff --git a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricFragment.kt b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricFragment.kt index f75867ce35..6138c79259 100644 --- a/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricFragment.kt +++ b/feature/enrol-last-biometric/src/main/java/com/simprints/feature/enrollast/screen/EnrolLastBiometricFragment.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.simprints.core.domain.response.AppErrorReason import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.alert.AlertContract import com.simprints.feature.alert.AlertResult @@ -23,6 +24,7 @@ import com.simprints.infra.config.store.models.GeneralConfiguration.Modality import com.simprints.infra.events.event.domain.models.AlertScreenEvent import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -54,7 +56,8 @@ internal class EnrolLastBiometricFragment : Fragment(R.layout.fragment_enrol_las } private fun showError(errorType: ErrorType, modalities: List) { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_enrolLastBiometricFragment_to_errorFragment, createAlertConfiguration(errorType, modalities).toArgs() ) @@ -68,8 +71,8 @@ internal class EnrolLastBiometricFragment : Fragment(R.layout.fragment_enrol_las titleRes = IDR.string.enrol_last_biometrics_alert_title message = getString(getAlertMessage(errorType), getModalityName(modalities)) leftButton = AlertButtonConfig.Close - eventType = - AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED + appErrorReason = AppErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED + eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED } private fun getAlertMessage(errorType: ErrorType) = when (errorType) { diff --git a/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlerts.kt b/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlerts.kt index a0035e18f9..0e19d8e114 100644 --- a/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlerts.kt +++ b/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlerts.kt @@ -1,31 +1,29 @@ package com.simprints.feature.fetchsubject.screen -import android.os.Bundle -import androidx.core.os.bundleOf +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.alertButton import com.simprints.feature.alert.alertConfiguration import com.simprints.feature.alert.config.AlertButtonConfig import com.simprints.feature.alert.config.AlertColor import com.simprints.infra.events.event.domain.models.AlertScreenEvent import com.simprints.infra.resources.R +import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports +@ExcludedFromGeneratedTestCoverageReports("UI code") internal object FetchSubjectAlerts { const val ACTION_CLOSE = "action_close" const val ACTION_RETRY = "action_retry" - private const val WAS_ONLINE = "was_online" - fun subjectNotFoundOnline() = alertConfiguration { color = AlertColor.Gray titleRes = R.string.fetch_subject_guid_not_found_alert_title image = R.drawable.ic_alert_default messageRes = R.string.fetch_subject_guid_not_found_online_message + appErrorReason = AppErrorReason.GUID_NOT_FOUND_ONLINE eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.GUID_NOT_FOUND_ONLINE leftButton = AlertButtonConfig.Close.copy(resultKey = ACTION_CLOSE) - - payload = bundleOf(WAS_ONLINE to true) } fun subjectNotFoundOffline() = alertConfiguration { @@ -34,6 +32,7 @@ internal object FetchSubjectAlerts { image = R.drawable.ic_alert_default messageRes = R.string.fetch_subject_guid_not_found_offline_message messageIcon = R.drawable.ic_alert_hint_no_network + appErrorReason = AppErrorReason.GUID_NOT_FOUND_OFFLINE eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.GUID_NOT_FOUND_OFFLINE leftButton = alertButton { @@ -42,10 +41,5 @@ internal object FetchSubjectAlerts { resultKey = ACTION_RETRY } rightButton = AlertButtonConfig.Close.copy(resultKey = ACTION_CLOSE) - - payload = bundleOf(WAS_ONLINE to false) } - - fun wasOnline(bundle: Bundle) = bundle.getBoolean(WAS_ONLINE, false) - } diff --git a/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectFragment.kt b/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectFragment.kt index 4d618835c9..b924952a32 100644 --- a/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectFragment.kt +++ b/feature/fetch-subject/src/main/java/com/simprints/feature/fetchsubject/screen/FetchSubjectFragment.kt @@ -6,7 +6,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.simprints.feature.fetchsubject.R import com.simprints.feature.alert.AlertContract import com.simprints.feature.alert.AlertResult import com.simprints.feature.alert.toArgs @@ -16,9 +15,11 @@ import com.simprints.feature.exitform.exitFormConfiguration import com.simprints.feature.exitform.scannerOptions import com.simprints.feature.exitform.toArgs import com.simprints.feature.fetchsubject.FetchSubjectResult +import com.simprints.feature.fetchsubject.R import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -45,7 +46,9 @@ internal class FetchSubjectFragment : Fragment(R.layout.fragment_subject_fetch) private fun handleAlertResult(alertResult: AlertResult) { when (alertResult.buttonKey) { - FetchSubjectAlerts.ACTION_CLOSE -> finishWithResult(false, FetchSubjectAlerts.wasOnline(alertResult.payload)) + FetchSubjectAlerts.ACTION_CLOSE -> { + findNavController().finishWithResult(this, alertResult) + } FetchSubjectAlerts.ACTION_RETRY -> tryFetchSubject() AlertContract.ALERT_BUTTON_PRESSED_BACK -> viewModel.startExitForm() } @@ -84,11 +87,11 @@ internal class FetchSubjectFragment : Fragment(R.layout.fragment_subject_fetch) }.toArgs() private fun openAlert(alertArgs: Bundle) { - findNavController().navigate(R.id.action_fetchSubjectFragment_to_errorFragment, alertArgs) + findNavController().navigateSafely(this, R.id.action_fetchSubjectFragment_to_errorFragment, alertArgs) } private fun openExitForm(exitFormArgs: Bundle) { - findNavController().navigate(R.id.action_fetchSubjectFragment_to_exitFormFragment, exitFormArgs) + findNavController().navigateSafely(this, R.id.action_fetchSubjectFragment_to_exitFormFragment, exitFormArgs) } private fun finishWithResult(found: Boolean, wasOnline: Boolean = false) { @@ -98,4 +101,5 @@ internal class FetchSubjectFragment : Fragment(R.layout.fragment_subject_fetch) private fun handleExitFormResult(exiFormResult: ExitFormResult) { findNavController().finishWithResult(this, exiFormResult) } + } diff --git a/feature/fetch-subject/src/test/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlertsTest.kt b/feature/fetch-subject/src/test/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlertsTest.kt deleted file mode 100644 index a68f9a31cf..0000000000 --- a/feature/fetch-subject/src/test/java/com/simprints/feature/fetchsubject/screen/FetchSubjectAlertsTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.simprints.feature.fetchsubject.screen - - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class FetchSubjectAlertsTest { - - @Test - fun `when returning from NotFoundOnline alert should set wasOnline flag to true`() { - val config = FetchSubjectAlerts.subjectNotFoundOnline() - val wasOnline = FetchSubjectAlerts.wasOnline(config.payload) - assertThat(wasOnline).isTrue() - } - - @Test - fun `when returning from NotFoundOffline alert should set wasOnline flag to false`() { - val config = FetchSubjectAlerts.subjectNotFoundOffline() - val wasOnline = FetchSubjectAlerts.wasOnline(config.payload) - assertThat(wasOnline).isFalse() - } -} diff --git a/feature/login-check/build.gradle.kts b/feature/login-check/build.gradle.kts index dfd6b8370e..6063495f71 100644 --- a/feature/login-check/build.gradle.kts +++ b/feature/login-check/build.gradle.kts @@ -12,10 +12,9 @@ dependencies { implementation(project(":infra:orchestrator-data")) implementation(project(":infra:config-store")) - implementation(project(":infra:config-sync")) + implementation(project(":infra:sync")) implementation(project(":infra:events")) implementation(project(":infra:event-sync")) - implementation(project(":infra:images")) implementation(project(":infra:auth-store")) implementation(project(":infra:auth-logic")) implementation(project(":infra:recent-user-activity")) diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt index 1aab827a99..f8952f60d5 100644 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/LoginCheckViewModel.kt @@ -9,9 +9,7 @@ import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send import com.simprints.feature.login.LoginError import com.simprints.feature.login.LoginResult -import com.simprints.feature.logincheck.usecases.* import com.simprints.feature.logincheck.usecases.AddAuthorizationEventUseCase -import com.simprints.feature.logincheck.usecases.CancelBackgroundSyncUseCase import com.simprints.feature.logincheck.usecases.ExtractCrashKeysUseCase import com.simprints.feature.logincheck.usecases.ExtractParametersForAnalyticsUseCase import com.simprints.feature.logincheck.usecases.IsUserSignedInUseCase @@ -22,12 +20,14 @@ import com.simprints.feature.logincheck.usecases.ReportActionRequestEventsUseCas import com.simprints.feature.logincheck.usecases.StartBackgroundSyncUseCase import com.simprints.feature.logincheck.usecases.UpdateProjectInCurrentSessionUseCase import com.simprints.feature.logincheck.usecases.UpdateSessionScopePayloadUseCase +import com.simprints.feature.logincheck.usecases.UpdateStoredUserIdUseCase import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.ProjectState import com.simprints.infra.logging.Simber import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.infra.security.SecurityManager import com.simprints.infra.security.exceptions.RootedDeviceException +import com.simprints.infra.sync.SyncOrchestrator import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -45,7 +45,7 @@ class LoginCheckViewModel @Inject internal constructor( private val isUserSignedIn: IsUserSignedInUseCase, private val configRepository: ConfigRepository, private val startBackgroundSync: StartBackgroundSyncUseCase, - private val cancelBackgroundSync: CancelBackgroundSyncUseCase, + private val syncOrchestrator: SyncOrchestrator, private val updateDatabaseCountsInCurrentSession: UpdateSessionScopePayloadUseCase, private val updateProjectInCurrentSession: UpdateProjectInCurrentSessionUseCase, private val updateStoredUserId: UpdateStoredUserIdUseCase, @@ -101,7 +101,7 @@ class LoginCheckViewModel @Inject internal constructor( cachedRequest = actionRequest loginAlreadyTried.set(true) - cancelBackgroundSync.invoke() + syncOrchestrator.cancelBackgroundWork() _showLoginFlow.send(actionRequest) } diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCase.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCase.kt deleted file mode 100644 index 3532814748..0000000000 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCase.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.simprints.feature.logincheck.usecases - -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler -import javax.inject.Inject - -internal class CancelBackgroundSyncUseCase @Inject constructor( - private val eventSyncManager: EventSyncManager, - private val imageUpSyncScheduler: ImageUpSyncScheduler, - private val configScheduler: ProjectConfigurationScheduler, -) { - - operator fun invoke() { - eventSyncManager.cancelScheduledSync() - imageUpSyncScheduler.cancelImageUpSync() - configScheduler.cancelProjectSync() - configScheduler.cancelDeviceSync() - } -} diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncIfNeededUseCase.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncIfNeededUseCase.kt index 0495056746..cccb152678 100644 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncIfNeededUseCase.kt +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncIfNeededUseCase.kt @@ -2,27 +2,20 @@ package com.simprints.feature.logincheck.usecases import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.SynchronizationConfiguration -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler +import com.simprints.infra.sync.SyncOrchestrator import javax.inject.Inject internal class StartBackgroundSyncUseCase @Inject constructor( - private val eventSyncManager: EventSyncManager, - private val imageUpSyncScheduler: ImageUpSyncScheduler, - private val configScheduler: ProjectConfigurationScheduler, + private val syncOrchestrator: SyncOrchestrator, private val configRepository: ConfigRepository, ) { suspend operator fun invoke() { - eventSyncManager.scheduleSync() - imageUpSyncScheduler.scheduleImageUpSync() - configScheduler.scheduleProjectSync() - configScheduler.scheduleDeviceSync() + syncOrchestrator.scheduleBackgroundWork() val frequency = configRepository.getProjectConfiguration().synchronization.frequency if (frequency == SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START) { - eventSyncManager.sync() + syncOrchestrator.startEventSync() } } } diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt index e4034af61d..1e26fa8796 100644 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/LoginCheckViewModelTest.kt @@ -7,7 +7,6 @@ import com.simprints.feature.login.LoginError import com.simprints.feature.login.LoginResult import com.simprints.feature.logincheck.usecases.ActionFactory import com.simprints.feature.logincheck.usecases.AddAuthorizationEventUseCase -import com.simprints.feature.logincheck.usecases.CancelBackgroundSyncUseCase import com.simprints.feature.logincheck.usecases.ExtractCrashKeysUseCase import com.simprints.feature.logincheck.usecases.ExtractParametersForAnalyticsUseCase import com.simprints.feature.logincheck.usecases.IsUserSignedInUseCase @@ -20,6 +19,7 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.ProjectState import com.simprints.infra.security.SecurityManager import com.simprints.infra.security.exceptions.RootedDeviceException +import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -65,7 +65,7 @@ internal class LoginCheckViewModelTest { lateinit var startBackgroundSync: StartBackgroundSyncUseCase @MockK - lateinit var cancelBackgroundSync: CancelBackgroundSyncUseCase + lateinit var syncOrchestrator: SyncOrchestrator @MockK lateinit var updateSessionScopePayloadUseCase: UpdateSessionScopePayloadUseCase @@ -91,7 +91,7 @@ internal class LoginCheckViewModelTest { isUserSignedInUseCase, configRepository, startBackgroundSync, - cancelBackgroundSync, + syncOrchestrator, updateSessionScopePayloadUseCase, updateProjectStateUseCase, updateStoredUserIdUseCase, @@ -161,7 +161,7 @@ internal class LoginCheckViewModelTest { coVerify { addAuthorizationEventUseCase.invoke(any(), eq(false)) - cancelBackgroundSync.invoke() + syncOrchestrator.cancelBackgroundWork() } viewModel.showLoginFlow.test() .assertValue { it.peekContent() == ActionFactory.getFlowRequest() } diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/ActionFactory.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/ActionFactory.kt index 3c9e1f897f..b57343f72b 100644 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/ActionFactory.kt +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/ActionFactory.kt @@ -18,6 +18,7 @@ internal object ActionFactory { userId = MOCK_USER_ID, moduleId = MOCK_MODULE_ID, unknownExtras = extras, + biometricDataSource = MOCK_BIOMETRIC_DATA_SOURCE, metadata = "", ) @@ -36,4 +37,5 @@ internal object ActionFactory { val MOCK_USER_ID = "userId".asTokenizableRaw() val MOCK_MODULE_ID = "moduleId".asTokenizableRaw() const val MOCK_PROJECT_ID = "projectId" + private const val MOCK_BIOMETRIC_DATA_SOURCE = "" } diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCaseTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCaseTest.kt deleted file mode 100644 index 469c03e70b..0000000000 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/CancelBackgroundSyncUseCaseTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.simprints.feature.logincheck.usecases - -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import io.mockk.verify -import org.junit.Before -import org.junit.Test - -class CancelBackgroundSyncUseCaseTest { - - @MockK - lateinit var eventSyncManager: EventSyncManager - - @MockK - lateinit var imageUpSyncScheduler: ImageUpSyncScheduler - - @MockK - lateinit var configScheduler: ProjectConfigurationScheduler - - private lateinit var useCase: CancelBackgroundSyncUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - useCase = CancelBackgroundSyncUseCase( - eventSyncManager, - imageUpSyncScheduler, - configScheduler - ) - } - - @Test - fun `Cancels all syncs when called`() { - useCase.invoke() - - verify { - eventSyncManager.cancelScheduledSync() - imageUpSyncScheduler.cancelImageUpSync() - configScheduler.cancelProjectSync() - configScheduler.cancelDeviceSync() - } - } -} diff --git a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt index cc960eb265..d995e4e301 100644 --- a/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt +++ b/feature/login-check/src/test/java/com/simprints/feature/logincheck/usecases/StartBackgroundSyncUseCaseTest.kt @@ -2,9 +2,7 @@ package com.simprints.feature.logincheck.usecases import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.SynchronizationConfiguration -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler +import com.simprints.infra.sync.SyncOrchestrator import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -17,13 +15,7 @@ import org.junit.Test class StartBackgroundSyncUseCaseTest { @MockK - lateinit var eventSyncManager: EventSyncManager - - @MockK - lateinit var imageUpSyncScheduler: ImageUpSyncScheduler - - @MockK - lateinit var configScheduler: ProjectConfigurationScheduler + lateinit var syncOrchestrator: SyncOrchestrator @MockK lateinit var configRepository: ConfigRepository @@ -35,9 +27,7 @@ class StartBackgroundSyncUseCaseTest { MockKAnnotations.init(this, relaxed = true) useCase = StartBackgroundSyncUseCase( - eventSyncManager, - imageUpSyncScheduler, - configScheduler, + syncOrchestrator, configRepository, ) } @@ -48,13 +38,8 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - verify { - eventSyncManager.scheduleSync() - configScheduler.scheduleProjectSync() - configScheduler.scheduleDeviceSync() - } coVerify { - imageUpSyncScheduler.scheduleImageUpSync() + syncOrchestrator.scheduleBackgroundWork() } } @@ -64,7 +49,7 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - verify { eventSyncManager.sync() } + verify { syncOrchestrator.startEventSync() } } @Test @@ -73,7 +58,7 @@ class StartBackgroundSyncUseCaseTest { useCase.invoke() - verify(exactly = 0) { eventSyncManager.sync() } + verify(exactly = 0) { syncOrchestrator.startEventSync() } } } diff --git a/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormFragment.kt b/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormFragment.kt index 122803a286..3dfba509da 100644 --- a/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormFragment.kt +++ b/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormFragment.kt @@ -8,15 +8,18 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.simprints.feature.login.LoginError import com.simprints.feature.login.LoginResult import com.simprints.feature.login.R import com.simprints.feature.login.databinding.FragmentLoginFormBinding +import com.simprints.feature.login.databinding.ViewUrlChangeInputBinding import com.simprints.feature.login.screens.form.SignInState.BackendMaintenanceError import com.simprints.feature.login.screens.form.SignInState.BadCredentials import com.simprints.feature.login.screens.form.SignInState.IntegrityException @@ -30,6 +33,7 @@ import com.simprints.feature.login.screens.form.SignInState.QrCodeValid import com.simprints.feature.login.screens.form.SignInState.QrGenericError import com.simprints.feature.login.screens.form.SignInState.QrInvalidCode import com.simprints.feature.login.screens.form.SignInState.QrNoCameraPermission +import com.simprints.feature.login.screens.form.SignInState.ShowUrlChangeDialog import com.simprints.feature.login.screens.form.SignInState.Success import com.simprints.feature.login.screens.form.SignInState.TechnicalFailure import com.simprints.feature.login.screens.form.SignInState.Unknown @@ -39,6 +43,7 @@ import com.simprints.infra.logging.LoggingConstants import com.simprints.infra.logging.Simber import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -77,7 +82,7 @@ internal class LoginFormFragment : Fragment(R.layout.fragment_login_form) { viewLifecycleOwner, R.id.loginFormFragment, R.id.loginQrScanner - ) { viewModel.handleQrResult(it) } + ) { viewModel.handleQrResult(args.loginParams.projectId, it) } initUi() observeUiState() @@ -88,9 +93,16 @@ internal class LoginFormFragment : Fragment(R.layout.fragment_login_form) { private fun initUi() { binding.loginUserId.setText(args.loginParams.userId.value) + binding.loginProjectId.setText(args.loginParams.projectId) + + binding.loginChangeUrlButton.setOnClickListener { + Simber.tag(LoggingConstants.CrashReportTag.LOGIN.name).i("Change URL button clicked") + viewModel.changeUrlClicked() + } + binding.loginButtonScanQr.setOnClickListener { Simber.tag(LoggingConstants.CrashReportTag.LOGIN.name).i("Scan QR button clicked") - findNavController().navigate(R.id.action_loginFormFragment_to_loginQrScanner) + findNavController().navigateSafely(this, R.id.action_loginFormFragment_to_loginQrScanner) } binding.loginButtonSignIn.setOnClickListener { Simber.tag(LoggingConstants.CrashReportTag.LOGIN.name).i("Login button clicked") @@ -129,6 +141,8 @@ internal class LoginFormFragment : Fragment(R.layout.fragment_login_form) { QrInvalidCode -> showToast(IDR.string.login_invalid_qr_code_error) QrNoCameraPermission -> showToast(IDR.string.login_qr_code_scanning_camera_permission_error) + is ShowUrlChangeDialog -> createChangeUrlDialog(result).show() + // Showing error card is BackendMaintenanceError -> showOutageErrorCard(result.estimatedOutage) @@ -144,7 +158,6 @@ internal class LoginFormFragment : Fragment(R.layout.fragment_login_form) { } private fun updateFields(result: QrCodeValid) { - binding.loginProjectId.setText(result.projectId) binding.loginProjectSecret.setText(result.projectSecret) } @@ -164,6 +177,24 @@ internal class LoginFormFragment : Fragment(R.layout.fragment_login_form) { Toast.makeText(requireContext(), getString(messageId), Toast.LENGTH_LONG).show() } + private fun createChangeUrlDialog(result: ShowUrlChangeDialog): AlertDialog { + val binding = ViewUrlChangeInputBinding.inflate(layoutInflater) + .apply { loginUrlChangeInput.setText(result.currentUrl) } + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(IDR.string.login_change_url) + .setView(binding.root) + .setNeutralButton(IDR.string.login_change_url_reset) { di, _ -> + viewModel.saveNewUrl(null) + di.dismiss() + } + .setPositiveButton(IDR.string.login_change_url_save) { di, _ -> + viewModel.saveNewUrl(binding.loginUrlChangeInput.text.toString()) + di.dismiss() + } + .setNegativeButton(IDR.string.login_change_url_cancel) { di, _ -> di.dismiss() } + .create() + } + private fun finishWithSuccess() { findNavController().finishWithResult(this, LoginResult(true)) } diff --git a/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormViewModel.kt b/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormViewModel.kt index 1df40e932c..06a2ff77f7 100644 --- a/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormViewModel.kt +++ b/feature/login/src/main/java/com/simprints/feature/login/screens/form/LoginFormViewModel.kt @@ -79,24 +79,28 @@ internal class LoginFormViewModel @Inject constructor( private fun areMandatoryCredentialsPresent( projectId: String, projectSecret: String, - userId: String + userId: String, ) = projectId.isNotEmpty() && projectSecret.isNotEmpty() && userId.isNotEmpty() - fun handleQrResult(result: QrScannerResult) { + fun handleQrResult(projectId: String, result: QrScannerResult) { if (result.error != null) { _signInState.send(mapQrError(result.error)) } else if (!result.content.isNullOrEmpty()) { try { val qrContent = jsonHelper.fromJson(result.content) - Simber.tag(CrashReportTag.LOGIN.name).i("QR scanning successful") - qrContent.apiBaseUrl?.let { simNetwork.setApiBaseUrl(it) } - _signInState.send( - SignInState.QrCodeValid( - qrContent.projectId, - qrContent.projectSecret + + if (projectId != qrContent.projectId) { + _signInState.send(SignInState.ProjectIdMismatch) + } else { + qrContent.apiBaseUrl?.let { simNetwork.setApiBaseUrl(it) } + _signInState.send( + SignInState.QrCodeValid( + qrContent.projectId, + qrContent.projectSecret + ) ) - ) + } } catch (e: Exception) { Simber.tag(CrashReportTag.LOGIN.name).i("QR scanning unsuccessful") _signInState.send(SignInState.QrInvalidCode) @@ -113,4 +117,13 @@ internal class LoginFormViewModel @Inject constructor( QrScannerError.UnknownError -> SignInState.QrGenericError } + fun changeUrlClicked() { + _signInState.send(SignInState.ShowUrlChangeDialog(simNetwork.getApiBaseUrlPrefix())) + } + + fun saveNewUrl(newUrl: String?) = if (newUrl.isNullOrEmpty()) { + simNetwork.resetApiBaseUrl() + } else { + simNetwork.setApiBaseUrl(newUrl) + } } diff --git a/feature/login/src/main/java/com/simprints/feature/login/screens/form/SignInState.kt b/feature/login/src/main/java/com/simprints/feature/login/screens/form/SignInState.kt index 14961452b6..088c47b58d 100644 --- a/feature/login/src/main/java/com/simprints/feature/login/screens/form/SignInState.kt +++ b/feature/login/src/main/java/com/simprints/feature/login/screens/form/SignInState.kt @@ -17,6 +17,8 @@ internal sealed class SignInState { data object QrInvalidCode : SignInState() data object QrGenericError : SignInState() + data class ShowUrlChangeDialog(val currentUrl: String) : SignInState() + data object BadCredentials : SignInState() data object Offline : SignInState() data object TechnicalFailure : SignInState() diff --git a/feature/login/src/main/res/layout/fragment_login_form.xml b/feature/login/src/main/res/layout/fragment_login_form.xml index fdab83d200..4221701a68 100644 --- a/feature/login/src/main/res/layout/fragment_login_form.xml +++ b/feature/login/src/main/res/layout/fragment_login_form.xml @@ -27,32 +27,30 @@ + android:enabled="false"> - - + android:layout_marginTop="8dp" + android:enabled="false"> + + + + \ No newline at end of file diff --git a/feature/login/src/main/res/layout/view_url_change_input.xml b/feature/login/src/main/res/layout/view_url_change_input.xml new file mode 100644 index 0000000000..031d19bd63 --- /dev/null +++ b/feature/login/src/main/res/layout/view_url_change_input.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/feature/login/src/test/java/com/simprints/feature/login/screens/form/LoginFormViewModelTest.kt b/feature/login/src/test/java/com/simprints/feature/login/screens/form/LoginFormViewModelTest.kt index ad5f8ac137..55ede02d83 100644 --- a/feature/login/src/test/java/com/simprints/feature/login/screens/form/LoginFormViewModelTest.kt +++ b/feature/login/src/test/java/com/simprints/feature/login/screens/form/LoginFormViewModelTest.kt @@ -156,7 +156,7 @@ internal class LoginFormViewModelTest { QrScannerError.CameraNotAvailable to SignInState.QrCameraUnavailable::class.java, QrScannerError.UnknownError to SignInState.QrGenericError::class.java, ).forEach { (error, expected) -> - viewModel.handleQrResult(QrScannerResult(null, error)) + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(null, error)) val result = viewModel.signInState.getOrAwaitValue() assertThat(result.getContentIfNotHandled()).isInstanceOf(expected) @@ -165,7 +165,7 @@ internal class LoginFormViewModelTest { @Test fun `returns correct SignInState when empty QR result`() { - viewModel.handleQrResult(QrScannerResult(null, null)) + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(null, null)) val result = viewModel.signInState.getOrAwaitValue() assertThat(result.getContentIfNotHandled()).isInstanceOf(SignInState.QrInvalidCode::class.java) @@ -175,17 +175,27 @@ internal class LoginFormViewModelTest { fun `returns correct SignInState when QR code parsing fails`() { every { jsonHelper.fromJson(any()) } throws RuntimeException("parsing fail") - viewModel.handleQrResult(QrScannerResult(QR_CONTENT, null)) + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(QR_CONTENT, null)) val result = viewModel.signInState.getOrAwaitValue() assertThat(result.getContentIfNotHandled()).isInstanceOf(SignInState.QrInvalidCode::class.java) } + @Test + fun `returns correct SignInState when QR contains wrong project ID`() { + every { jsonHelper.fromJson(eq(QR_CONTENT)) } returns QrCodeContent("differentProjectId", PROJECT_SECRET) + + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(QR_CONTENT, null)) + val result = viewModel.signInState.getOrAwaitValue() + + assertThat(result.getContentIfNotHandled()).isInstanceOf(SignInState.ProjectIdMismatch::class.java) + } + @Test fun `returns correct SignInState when QR code parsing success`() { every { jsonHelper.fromJson(eq(QR_CONTENT)) } returns QrCodeContent(PROJECT_ID, PROJECT_SECRET) - viewModel.handleQrResult(QrScannerResult(QR_CONTENT, null)) + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(QR_CONTENT, null)) val result = viewModel.signInState.getOrAwaitValue() assertThat(result.getContentIfNotHandled()).isInstanceOf(SignInState.QrCodeValid::class.java) @@ -197,11 +207,33 @@ internal class LoginFormViewModelTest { fun `updates base API url when QR code parsing success`() { every { jsonHelper.fromJson(eq(QR_CONTENT)) } returns QrCodeContent(PROJECT_ID, PROJECT_SECRET, URL) - viewModel.handleQrResult(QrScannerResult(QR_CONTENT, null)) + viewModel.handleQrResult(PROJECT_ID, QrScannerResult(QR_CONTENT, null)) verify { simNetwork.setApiBaseUrl(eq(URL)) } } + @Test + fun `updates UI state when change URL clicked`() { + viewModel.changeUrlClicked() + + val result = viewModel.signInState.getOrAwaitValue() + assertThat(result.getContentIfNotHandled()).isInstanceOf(SignInState.ShowUrlChangeDialog::class.java) + } + + @Test + fun `saves provided base URL`() { + viewModel.saveNewUrl(URL) + + verify { simNetwork.setApiBaseUrl(URL) } + } + + @Test + fun `resets provided base URL`() { + viewModel.saveNewUrl(null) + + verify { simNetwork.resetApiBaseUrl() } + } + companion object { diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt index cf9d404f3a..fd2a7f223d 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchContract.kt @@ -1,6 +1,7 @@ package com.simprints.matcher import com.simprints.core.domain.common.FlowType +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.matcher.screen.MatchFragmentArgs @@ -9,14 +10,16 @@ object MatchContract { val DESTINATION = R.id.matcherFragment fun getArgs( - fingerprintSamples: List = emptyList(), - faceSamples: List = emptyList(), - flowType: FlowType, - subjectQuery: SubjectQuery, + fingerprintSamples: List = emptyList(), + faceSamples: List = emptyList(), + flowType: FlowType, + subjectQuery: SubjectQuery, + biometricDataSource: BiometricDataSource, ) = MatchFragmentArgs(MatchParams( faceSamples, fingerprintSamples, flowType, - subjectQuery + subjectQuery, + biometricDataSource, )).toBundle() } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt index d7d3a8bf7b..6327eb6005 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/MatchParams.kt @@ -3,18 +3,20 @@ package com.simprints.matcher import android.os.Parcelable import androidx.annotation.Keep import com.simprints.core.domain.common.FlowType +import com.simprints.core.domain.fingerprint.IFingerIdentifier +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports -import com.simprints.core.domain.fingerprint.IFingerIdentifier import kotlinx.parcelize.Parcelize @Keep @Parcelize data class MatchParams( - val probeFaceSamples: List = emptyList(), - val probeFingerprintSamples: List = emptyList(), - val flowType: FlowType, - val queryForCandidates: SubjectQuery, + val probeFaceSamples: List = emptyList(), + val probeFingerprintSamples: List = emptyList(), + val flowType: FlowType, + val queryForCandidates: SubjectQuery, + val biometricDataSource: BiometricDataSource, ) : Parcelable { fun isFaceMatch() = probeFaceSamples.isNotEmpty() diff --git a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt index f6fc47c9ac..e88a102d6e 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchFragment.kt @@ -32,14 +32,19 @@ internal class MatchFragment : Fragment(R.layout.fragment_matcher) { super.onViewCreated(view, savedInstanceState) observeViewModel() - if(!viewModel.isInitialized) { + if (!viewModel.isInitialized) { viewModel.setupMatch(args.params) } } private fun setIdentificationProgress(progress: Int) = requireActivity().runOnUiThread { ObjectAnimator - .ofInt(binding.faceMatchProgress, "progress", binding.faceMatchProgress.progress, progress) + .ofInt( + binding.faceMatchProgress, + "progress", + binding.faceMatchProgress.progress, + progress + ) .setDuration(progress * PROGRESS_DURATION_MULTIPLIER) .start() } @@ -70,14 +75,16 @@ internal class MatchFragment : Fragment(R.layout.fragment_matcher) { private fun renderLoadingCandidates() { binding.apply { faceMatchTvMatchingProgressStatus1.isVisible = true - faceMatchTvMatchingProgressStatus1.text = getString(IDR.string.matcher_loading_candidates) + faceMatchTvMatchingProgressStatus1.text = + getString(IDR.string.matcher_loading_candidates) faceMatchProgress.isVisible = true } setIdentificationProgress(LOADING_PROGRESS) } private fun renderMatching() { - binding.faceMatchTvMatchingProgressStatus1.text = getString(IDR.string.matcher_matching_candidates) + binding.faceMatchTvMatchingProgressStatus1.text = + getString(IDR.string.matcher_matching_candidates) setIdentificationProgress(MATCHING_PROGRESS) } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt index dbd0cb46e3..9350f665ac 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/screen/MatchViewModel.kt @@ -65,7 +65,7 @@ internal class MatchViewModel @Inject constructor( endTime, params, totalCandidates, - matcherUseCase.matcherName, + matcherUseCase.matcherName(), sortedResults ) @@ -112,9 +112,9 @@ internal class MatchViewModel @Inject constructor( // TODO This configuration should be provided by SDK or project configuration // https://simprints.atlassian.net/browse/CORE-2923 companion object { - const val veryGoodMatchThreshold = 50.0 - const val goodMatchThreshold = 35.0 - const val fairMatchThreshold = 20.0 - const val matchingEndWaitTimeInMillis = 1000L + private const val veryGoodMatchThreshold = 50.0 + private const val goodMatchThreshold = 35.0 + private const val fairMatchThreshold = 20.0 + private const val matchingEndWaitTimeInMillis = 1000L } } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt index 73f19edc19..1702a77d70 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FaceMatcherUseCase.kt @@ -1,7 +1,8 @@ package com.simprints.matcher.usecases -import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.core.DispatcherBG +import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.facebiosdk.matching.FaceIdentity import com.simprints.infra.facebiosdk.matching.FaceMatcher @@ -24,7 +25,7 @@ internal class FaceMatcherUseCase @Inject constructor( ) : MatcherUseCase { override val crashReportTag = LoggingConstants.CrashReportTag.FACE_MATCHING.name - override val matcherName = faceMatcher.matcherName + override suspend fun matcherName ()= faceMatcher.matcherName override suspend operator fun invoke( matchParams: MatchParams, @@ -34,7 +35,7 @@ internal class FaceMatcherUseCase @Inject constructor( return@coroutineScope Pair(emptyList(), 0) } val samples = mapSamples(matchParams.probeFaceSamples) - val totalCandidates = enrolmentRecordRepository.count(matchParams.queryForCandidates) + val totalCandidates = enrolmentRecordRepository.count(matchParams.queryForCandidates, dataSource = matchParams.biometricDataSource) if (totalCandidates == 0) { return@coroutineScope Pair(emptyList(), 0) } @@ -43,7 +44,7 @@ internal class FaceMatcherUseCase @Inject constructor( createRanges(totalCandidates) .map { range -> async(dispatcher) { - val batchCandidates = getCandidates(matchParams.queryForCandidates, range) + val batchCandidates = getCandidates(matchParams.queryForCandidates, range, dataSource = matchParams.biometricDataSource) match(batchCandidates, samples) } } @@ -55,11 +56,15 @@ internal class FaceMatcherUseCase @Inject constructor( private fun mapSamples(probes: List) = probes .map { FaceSample(it.faceId, it.template) } - private suspend fun getCandidates(query: SubjectQuery, range: IntRange) = enrolmentRecordRepository - .loadFaceIdentities(query, range) + private suspend fun getCandidates( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource = BiometricDataSource.SIMPRINTS, + ) = enrolmentRecordRepository + .loadFaceIdentities(query, range, dataSource) .map { FaceIdentity( - it.personId, + it.subjectId, it.faces.map { face -> FaceSample(face.id, face.template) } ) } @@ -70,7 +75,7 @@ internal class FaceMatcherUseCase @Inject constructor( ) = batchCandidates.fold(MatchResultSet()) { acc, item -> acc.add( FaceMatchResult.Item( - item.faceId, + item.subjectId, faceMatcher.getHighestComparisonScoreForCandidate(samples, item) ) ) diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt index ab26ff6198..248f5835d9 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/FingerprintMatcherUseCase.kt @@ -8,8 +8,9 @@ import com.simprints.fingerprint.infra.basebiosdk.matching.domain.Fingerprint import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerprintIdentity import com.simprints.fingerprint.infra.biosdk.ResolveBioSdkWrapperUseCase import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.config.store.models.FingerprintConfiguration.FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.logging.LoggingConstants import com.simprints.matcher.FingerprintMatchResult @@ -23,14 +24,14 @@ import javax.inject.Inject internal class FingerprintMatcherUseCase @Inject constructor( private val enrolmentRecordRepository: EnrolmentRecordRepository, - private val resolveBioSdkWrapperUseCase: ResolveBioSdkWrapperUseCase, + private val resolveBioSdkWrapper: ResolveBioSdkWrapperUseCase, private val configRepository: ConfigRepository, private val createRanges: CreateRangesUseCase, @DispatcherBG private val dispatcher: CoroutineDispatcher, ) : MatcherUseCase { override val crashReportTag = LoggingConstants.CrashReportTag.MATCHING.name - override val matcherName = MATCHER_NAME + override suspend fun matcherName() = resolveBioSdkWrapper().matcherName override suspend operator fun invoke( matchParams: MatchParams, @@ -40,7 +41,12 @@ internal class FingerprintMatcherUseCase @Inject constructor( return@coroutineScope Pair(emptyList(), 0) } val samples = mapSamples(matchParams.probeFingerprintSamples) - val totalCandidates = enrolmentRecordRepository.count(matchParams.queryForCandidates) + // Only candidates with supported template format are considered + val queryWithSupportedFormat = + matchParams.queryForCandidates.copy( + fingerprintSampleFormat = resolveBioSdkWrapper().supportedTemplateFormat + ) + val totalCandidates = enrolmentRecordRepository.count(queryWithSupportedFormat, dataSource = matchParams.biometricDataSource) if (totalCandidates == 0) { return@coroutineScope Pair(emptyList(), 0) } @@ -49,7 +55,7 @@ internal class FingerprintMatcherUseCase @Inject constructor( createRanges(totalCandidates) .map { range -> async(dispatcher) { - val batchCandidates = getCandidates(matchParams.queryForCandidates, range) + val batchCandidates = getCandidates(queryWithSupportedFormat, range, matchParams.biometricDataSource) match(samples, batchCandidates, matchParams.flowType) .fold(MatchResultSet()) { acc, item -> acc.add(FingerprintMatchResult.Item(item.id, item.score)) @@ -64,11 +70,15 @@ internal class FingerprintMatcherUseCase @Inject constructor( private fun mapSamples(probes: List) = probes .map { Fingerprint(it.fingerId.toMatcherDomain(), it.template, it.format) } - private suspend fun getCandidates(query: SubjectQuery, range: IntRange) = enrolmentRecordRepository - .loadFingerprintIdentities(query, range) + private suspend fun getCandidates( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource = BiometricDataSource.SIMPRINTS, + ) = enrolmentRecordRepository + .loadFingerprintIdentities(query, range, dataSource) .map { FingerprintIdentity( - it.patientId, + it.subjectId, it.fingerprints.map { finger -> Fingerprint( finger.fingerIdentifier.toMatcherDomain(), @@ -83,7 +93,7 @@ internal class FingerprintMatcherUseCase @Inject constructor( probes: List, candidates: List, flowType: FlowType, - ) = resolveBioSdkWrapperUseCase().match( + ) = resolveBioSdkWrapper().match( FingerprintIdentity("", probes), candidates, isCrossFingerMatchingEnabled(flowType), @@ -94,7 +104,7 @@ internal class FingerprintMatcherUseCase @Inject constructor( ?.getProjectConfiguration() ?.fingerprint ?.bioSdkConfiguration - ?.comparisonStrategyForVerification == FingerprintConfiguration.FingerComparisonStrategy.CROSS_FINGER_USING_MEAN_OF_MAX + ?.comparisonStrategyForVerification == CROSS_FINGER_USING_MEAN_OF_MAX private fun IFingerIdentifier.toMatcherDomain() = when (this) { IFingerIdentifier.RIGHT_5TH_FINGER -> FingerIdentifier.RIGHT_5TH_FINGER @@ -109,8 +119,5 @@ internal class FingerprintMatcherUseCase @Inject constructor( IFingerIdentifier.LEFT_5TH_FINGER -> FingerIdentifier.LEFT_5TH_FINGER } - companion object { - private const val MATCHER_NAME = "SIM_AFIS" - } } diff --git a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt index 599354712c..3a69e548d8 100644 --- a/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt +++ b/feature/matcher/src/main/java/com/simprints/matcher/usecases/MatcherUseCase.kt @@ -6,7 +6,7 @@ import com.simprints.matcher.MatchResultItem internal interface MatcherUseCase { val crashReportTag: String - val matcherName: String + suspend fun matcherName(): String /** * Returns a list of [MatchResultItem]s sorted by confidence score in descending order diff --git a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt index 09fd8fc393..c859820e2a 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/screen/MatchViewModelTest.kt @@ -7,6 +7,7 @@ import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.matcher.FaceMatchResult import com.simprints.matcher.FingerprintMatchResult import com.simprints.matcher.MatchParams @@ -63,8 +64,8 @@ internal class MatchViewModelTest { cb1 = slot() every { timeHelper.now() } returns Timestamp(0L) - every { faceMatcherUseCase.matcherName } returns MATCHER_NAME - every { fingerprintMatcherUseCase.matcherName } returns MATCHER_NAME + coEvery { faceMatcherUseCase.matcherName() } returns MATCHER_NAME + coEvery { fingerprintMatcherUseCase.matcherName() } returns MATCHER_NAME viewModel = MatchViewModel( faceMatcherUseCase, @@ -73,6 +74,7 @@ internal class MatchViewModelTest { timeHelper ) } + @Test fun `when setup is called, then view model becomes initialized`() = runTest { val responseItems = listOf( @@ -91,7 +93,8 @@ internal class MatchViewModelTest { viewModel.setupMatch(MatchParams( probeFaceSamples = listOf(getFaceSample()), flowType = FlowType.ENROL, - queryForCandidates = mockk {} + queryForCandidates = mockk {}, + biometricDataSource = BiometricDataSource.SIMPRINTS, )) assertThat(viewModel.isInitialized).isTrue() @@ -116,11 +119,14 @@ internal class MatchViewModelTest { coJustRun { saveMatchEvent.invoke(any(), any(), any(), any(), any(), any()) } val states = viewModel.matchState.test() - viewModel.setupMatch(MatchParams( - probeFaceSamples = listOf(getFaceSample()), - flowType = FlowType.ENROL, - queryForCandidates = mockk {} - )) + viewModel.setupMatch( + MatchParams( + probeFaceSamples = listOf(getFaceSample()), + flowType = FlowType.ENROL, + queryForCandidates = mockk {}, + biometricDataSource = BiometricDataSource.SIMPRINTS, + ) + ) // Waiting for the ::delay in viewModel::setupMatch advanceUntilIdle() @@ -158,11 +164,14 @@ internal class MatchViewModelTest { val states = viewModel.matchState.test() - viewModel.setupMatch(MatchParams( - probeFingerprintSamples = listOf(getFingerprintSample()), - flowType = FlowType.ENROL, - queryForCandidates = mockk {} - )) + viewModel.setupMatch( + MatchParams( + probeFingerprintSamples = listOf(getFingerprintSample()), + flowType = FlowType.ENROL, + queryForCandidates = mockk {}, + biometricDataSource = BiometricDataSource.SIMPRINTS, + ) + ) // Waiting for the ::delay in viewModel::setupMatch advanceUntilIdle() diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt index b3f2df7627..1a6b443b49 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FaceMatcherUseCaseTest.kt @@ -5,6 +5,7 @@ import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.face.FaceSample import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.facebiosdk.matching.FaceMatcher @@ -57,7 +58,8 @@ internal class FaceMatcherUseCaseTest { useCase.invoke( MatchParams( flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), ) @@ -74,7 +76,8 @@ internal class FaceMatcherUseCaseTest { MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)) ), flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), ) @@ -84,9 +87,9 @@ internal class FaceMatcherUseCaseTest { @Test fun `Correctly calls SDK matcher`() = runTest { - coEvery { enrolmentRecordRepository.count(any()) } returns 100 + coEvery { enrolmentRecordRepository.count(any(), any()) } returns 100 coEvery { createRangesUseCase(any()) } returns listOf(0..99) - coEvery { enrolmentRecordRepository.loadFaceIdentities(any(), any()) } returns listOf( + coEvery { enrolmentRecordRepository.loadFaceIdentities(any(), any(), any()) } returns listOf( FaceIdentity( "subjectId", listOf(FaceSample(byteArrayOf(1, 2, 3), "format", "faceTemplate")) @@ -102,7 +105,8 @@ internal class FaceMatcherUseCaseTest { MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3)) ), flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), onLoadingCandidates = { onLoadingCalled = true }, ) diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt index 58352f50f7..f6bbcdc1af 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/FingerprintMatcherUseCaseTest.kt @@ -5,10 +5,12 @@ import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.FlowType import com.simprints.core.domain.fingerprint.FingerprintSample import com.simprints.core.domain.fingerprint.IFingerIdentifier -import com.simprints.fingerprint.infra.biosdk.ResolveBioSdkWrapperUseCase import com.simprints.fingerprint.infra.biosdk.BioSdkWrapper +import com.simprints.fingerprint.infra.biosdk.ResolveBioSdkWrapperUseCase import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.matcher.MatchParams @@ -51,6 +53,10 @@ internal class FingerprintMatcherUseCaseTest { fun setUp() { MockKAnnotations.init(this, relaxed = true) coEvery { resolveBioSdkWrapperUseCase() } returns bioSdkWrapper + coEvery { + configRepository.getProjectConfiguration().fingerprint?.allowedSDKs + } returns listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER) + useCase = FingerprintMatcherUseCase( enrolmentRecordRepository, resolveBioSdkWrapperUseCase, @@ -60,12 +66,22 @@ internal class FingerprintMatcherUseCaseTest { ) } + @Test + fun `Correctly get the matcher name`() = runTest { + coEvery { bioSdkWrapper.matcherName } returns "SIM_AFIS" + coEvery { configRepository.getProjectConfiguration().fingerprint?.allowedSDKs } returns listOf( + FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER + ) + assertThat(useCase.matcherName()).isEqualTo("SIM_AFIS") + } + @Test fun `Skips matching if there are no probes`() = runTest { useCase.invoke( MatchParams( flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), ) @@ -88,7 +104,8 @@ internal class FingerprintMatcherUseCaseTest { ), ), flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), ) @@ -97,9 +114,15 @@ internal class FingerprintMatcherUseCaseTest { @Test fun `Correctly calls SDK matcher`() = runTest { - coEvery { enrolmentRecordRepository.count(any()) } returns 100 + coEvery { enrolmentRecordRepository.count(any(), any()) } returns 100 coEvery { createRangesUseCase(any()) } returns listOf(0..99) - coEvery { enrolmentRecordRepository.loadFingerprintIdentities(any(), any()) } returns listOf( + coEvery { + enrolmentRecordRepository.loadFingerprintIdentities( + any(), + any(), + any(), + ) + } returns listOf( FingerprintIdentity( "personId", listOf( @@ -130,7 +153,8 @@ internal class FingerprintMatcherUseCaseTest { ), ), flowType = FlowType.VERIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), onLoadingCandidates = { onLoadingCalled = true }, ) diff --git a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt index ec01300067..c6741090d1 100644 --- a/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt +++ b/feature/matcher/src/test/java/com/simprints/matcher/usecases/SaveMatchEventUseCaseTest.kt @@ -6,6 +6,7 @@ import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.core.tools.time.Timestamp import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.FingerprintConfiguration.FingerComparisonStrategy +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.events.SessionEventRepository import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent @@ -64,6 +65,7 @@ class SaveMatchEventUseCaseTest { flowType = FlowType.VERIFY, queryForCandidates = SubjectQuery(subjectId = "subjectId"), probeFaceSamples = listOf(MatchParams.FaceSample("faceId", byteArrayOf(1, 2, 3))), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 2, "faceMatcherName", @@ -102,6 +104,7 @@ class SaveMatchEventUseCaseTest { byteArrayOf(1, 2, 3) ) ), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 2, "faceMatcherName", @@ -135,6 +138,7 @@ class SaveMatchEventUseCaseTest { emptyList(), FlowType.IDENTIFY, SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 2, "faceMatcherName", @@ -165,6 +169,7 @@ class SaveMatchEventUseCaseTest { MatchParams( flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(attendantId = "userId"), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 0, "faceMatcherName", @@ -187,6 +192,7 @@ class SaveMatchEventUseCaseTest { MatchParams( flowType = FlowType.IDENTIFY, queryForCandidates = SubjectQuery(moduleId = "moduleId"), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 0, "faceMatcherName", @@ -209,7 +215,8 @@ class SaveMatchEventUseCaseTest { MatchParams( emptyList(), flowType = FlowType.IDENTIFY, - queryForCandidates = SubjectQuery() + queryForCandidates = SubjectQuery(), + biometricDataSource = BiometricDataSource.SIMPRINTS, ), 0, "faceMatcherName", diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/AlertConfigurationMapper.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/AlertConfigurationMapper.kt index d80f40e2de..14e4fe3eb6 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/AlertConfigurationMapper.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/AlertConfigurationMapper.kt @@ -1,8 +1,7 @@ package com.simprints.feature.orchestrator -import android.os.Bundle -import androidx.core.os.bundleOf import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.AlertConfigurationBuilder import com.simprints.feature.alert.alertConfiguration import com.simprints.feature.alert.config.AlertButtonConfig @@ -10,7 +9,6 @@ import com.simprints.feature.alert.config.AlertColor import com.simprints.feature.clientapi.models.ClientApiError import com.simprints.feature.logincheck.LoginCheckError import com.simprints.infra.events.event.domain.models.AlertScreenEvent -import com.simprints.core.domain.response.AppErrorReason import javax.inject.Inject import com.simprints.infra.resources.R as IDR @@ -23,12 +21,8 @@ internal class AlertConfigurationMapper @Inject constructor() { image = IDR.drawable.ic_alert_default messageRes = getMessage(clientApiError) eventType = getEventType(clientApiError) + appErrorReason = AppErrorReason.UNEXPECTED_ERROR leftButton = AlertButtonConfig.Close - - payload = bundleOf( - PAYLOAD_TYPE_KEY to ClientApiError::name, - PAYLOAD_KEY to clientApiError.name, - ) } private fun getMessage(clientApiError: ClientApiError) = when (clientApiError) { @@ -49,13 +43,9 @@ internal class AlertConfigurationMapper @Inject constructor() { image = IDR.drawable.ic_alert_default messageRes = getMessage(loginCheckError) messageIcon = getMessageIcon(loginCheckError) + appErrorReason = getAppReason(loginCheckError) eventType = getEventType(loginCheckError) leftButton = AlertButtonConfig.Close - - payload = bundleOf( - PAYLOAD_TYPE_KEY to LoginCheckError::name.name, - PAYLOAD_KEY to loginCheckError.name, - ) } private fun getBackgroundColor(loginCheckError: LoginCheckError) = when (loginCheckError) { @@ -110,7 +100,6 @@ internal class AlertConfigurationMapper @Inject constructor() { ClientApiError.INVALID_VERIFY_ID -> AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.INVALID_VERIFY_ID } - private fun getEventType(loginCheckError: LoginCheckError) = when (loginCheckError) { LoginCheckError.DIFFERENT_PROJECT_ID -> AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.DIFFERENT_PROJECT_ID LoginCheckError.PROJECT_PAUSED -> AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.PROJECT_PAUSED @@ -123,35 +112,18 @@ internal class AlertConfigurationMapper @Inject constructor() { LoginCheckError.ROOTED_DEVICE -> AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.UNEXPECTED_ERROR } + private fun getAppReason(loginCheckError: LoginCheckError) = when (loginCheckError) { + LoginCheckError.MISSING_GOOGLE_PLAY_SERVICES, + LoginCheckError.GOOGLE_PLAY_SERVICES_OUTDATED, + LoginCheckError.INTEGRITY_SERVICE_ERROR, + LoginCheckError.MISSING_OR_OUTDATED_GOOGLE_PLAY_STORE_APP, + LoginCheckError.UNEXPECTED_LOGIN_ERROR, + -> AppErrorReason.UNEXPECTED_ERROR - companion object { - - fun reasonFromPayload(extras: Bundle): AppErrorReason { - val type = extras.getString(PAYLOAD_TYPE_KEY) ?: return AppErrorReason.UNEXPECTED_ERROR - val payload = extras.getString(PAYLOAD_KEY) ?: return AppErrorReason.UNEXPECTED_ERROR - - return when (type) { - ClientApiError::name.name -> AppErrorReason.UNEXPECTED_ERROR - LoginCheckError::name.name -> when (LoginCheckError.valueOf(payload)) { - LoginCheckError.MISSING_GOOGLE_PLAY_SERVICES, - LoginCheckError.GOOGLE_PLAY_SERVICES_OUTDATED, - LoginCheckError.INTEGRITY_SERVICE_ERROR, - LoginCheckError.MISSING_OR_OUTDATED_GOOGLE_PLAY_STORE_APP, - LoginCheckError.UNEXPECTED_LOGIN_ERROR, - -> AppErrorReason.UNEXPECTED_ERROR - - LoginCheckError.DIFFERENT_PROJECT_ID -> AppErrorReason.DIFFERENT_PROJECT_ID_SIGNED_IN - LoginCheckError.PROJECT_PAUSED -> AppErrorReason.PROJECT_PAUSED - LoginCheckError.PROJECT_ENDING -> AppErrorReason.PROJECT_ENDING - LoginCheckError.ROOTED_DEVICE -> AppErrorReason.ROOTED_DEVICE - } - - else -> AppErrorReason.UNEXPECTED_ERROR - } - } - - private const val PAYLOAD_TYPE_KEY = "alert_payload_type" - private const val PAYLOAD_KEY = "alert_payload" + LoginCheckError.DIFFERENT_PROJECT_ID -> AppErrorReason.DIFFERENT_PROJECT_ID_SIGNED_IN + LoginCheckError.PROJECT_PAUSED -> AppErrorReason.PROJECT_PAUSED + LoginCheckError.PROJECT_ENDING -> AppErrorReason.PROJECT_ENDING + LoginCheckError.ROOTED_DEVICE -> AppErrorReason.ROOTED_DEVICE } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/ExecutionTracker.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/ExecutionTracker.kt new file mode 100644 index 0000000000..242ec37274 --- /dev/null +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/ExecutionTracker.kt @@ -0,0 +1,73 @@ +package com.simprints.feature.orchestrator + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.logging.Simber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class ExecutionTracker @Inject constructor( + private val timeHelper: TimeHelper, + @ExecutorLockTimeoutSec private val executionTimeLimitSec: Int, +) : LifecycleEventObserver { + + /** + * Timestamp representing when the [currentLifecycleOwnerId] was set. This is a safeguard + * measure for cases when [Lifecycle.Event.ON_DESTROY] is not called by the system. Should it + * ever happen, the [ExecutionTracker] has a check for whether enough time has passed, and if + * the [currentLifecycleOwnerId] can be released. + * + * Limit for execution is set by the [executionTimeLimitSec] (in seconds). + */ + private var timestamp: Long = 0 + + /** + * ID of the [LifecycleOwner] that is considered the main executor. + */ + private var currentLifecycleOwnerId: Int? = null + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + val ownerId = source.id() + if (event == Lifecycle.Event.ON_CREATE) { + if (currentLifecycleOwnerId == null || enoughTimePassedSinceLastLock()) { + currentLifecycleOwnerId = ownerId + timestamp = timeHelper.now().ms + Simber.i("Lifecycle owner [$ownerId] is set as main executor") + } else { + Simber.i("Main executor already set, ignoring Lifecycle owner [$ownerId]") + } + } else if (event == Lifecycle.Event.ON_DESTROY) { + if (currentLifecycleOwnerId == ownerId) { + currentLifecycleOwnerId = null + timestamp = 0 + Simber.i("Lifecycle owner [$ownerId] removed from main executor") + } + } + } + + /** + * Check for whether enough time has passed since the last set of the [currentLifecycleOwnerId]. + * + * @return true when the difference between current time and the [timestamp] is greater than + * amount of seconds specified in the [executionTimeLimitSec]. False otherwise. + */ + private fun enoughTimePassedSinceLastLock() = + timeHelper.msBetweenNowAndTime(timestamp) > 1000 * executionTimeLimitSec + + fun LifecycleOwner.id(): Int = hashCode() + + /** + * Checks whether the current activity (more technically, its [LifecycleOwner]) has rights to be + * executed at this moment. + * + * @param activity reference to the corresponding [LifecycleOwner] + * @return true if there are no other instances of this lifecycle owner running. False + * otherwise. + */ + fun isMain(activity: LifecycleOwner): Boolean { + return currentLifecycleOwnerId == activity.hashCode() + } +} diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorActivity.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorActivity.kt index dbe69c2748..d52811a6c8 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorActivity.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorActivity.kt @@ -7,16 +7,21 @@ import androidx.core.os.bundleOf import androidx.navigation.findNavController import com.simprints.core.tools.activity.BaseActivity import com.simprints.feature.orchestrator.databinding.ActivityOrchestratorBinding +import com.simprints.infra.logging.Simber import com.simprints.infra.orchestration.data.results.AppResult import com.simprints.infra.uibase.navigation.handleResult import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint internal class OrchestratorActivity : BaseActivity() { private val binding by viewBinding(ActivityOrchestratorBinding::inflate) + @Inject + lateinit var activityTracker: ExecutionTracker + /** * Flag for the navigation graph initialization state. The graph should only be initialized once * during the existence of this activity, and the flag tracks graph's state. @@ -25,9 +30,14 @@ internal class OrchestratorActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) isGraphInitialized = savedInstanceState?.getBoolean(KEY_IS_GRAPH_INITIALIZED) ?: false + lifecycle.addObserver(activityTracker) + setContentView(binding.root) - binding.orchestrationHost.handleResult(this, R.id.orchestratorRootFragment) { result -> + binding.orchestrationHost.handleResult( + this, + R.id.orchestratorRootFragment + ) { result -> setResult(result.resultCode, Intent().putExtras(result.extras)) finish() } @@ -35,15 +45,21 @@ internal class OrchestratorActivity : BaseActivity() { override fun onStart() { super.onStart() - if(!isGraphInitialized) { - val action = intent.action.orEmpty() - val extras = intent.extras ?: bundleOf() - - findNavController(R.id.orchestrationHost).setGraph( - R.navigation.graph_orchestration, - OrchestratorFragmentArgs(action, extras).toBundle() - ) - isGraphInitialized = true + + if (activityTracker.isMain(activity = this)) { + if (!isGraphInitialized) { + val action = intent.action.orEmpty() + val extras = intent.extras ?: bundleOf() + + findNavController(R.id.orchestrationHost).setGraph( + R.navigation.graph_orchestration, + OrchestratorFragmentArgs(action, extras).toBundle() + ) + isGraphInitialized = true + } + } else { + Simber.e("Orchestrator already executing, finishing with RESULT_CANCELED") + finish() } } 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 e6989a32fc..09a2b0ea5f 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 @@ -36,6 +36,7 @@ import com.simprints.infra.orchestration.data.responses.AppVerifyResponse import com.simprints.infra.orchestration.data.results.AppResult import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.matcher.MatchContract import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -73,6 +74,16 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { private val clientApiVm by viewModels() private val orchestratorVm by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState != null) { + orchestratorVm.requestProcessed = savedInstanceState.getBoolean(KEY_REQUEST_PROCESSED) + savedInstanceState.getString(KEY_ACTION_REQUEST) + ?.run(orchestratorVm::setActionRequestFromJson) + orchestratorVm.restoreStepsIfNeeded() + orchestratorVm.restoreModalitiesIfNeeded() + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -85,7 +96,7 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { handleResult(AlertContract.DESTINATION) { alertResult -> clientApiVm.handleErrorResponse( args.requestAction, - AppErrorResponse(AlertConfigurationMapper.reasonFromPayload(alertResult.payload)) + AppErrorResponse(alertResult.appErrorReason ?: AppErrorReason.UNEXPECTED_ERROR) ) } @@ -107,69 +118,123 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { } private fun handleResult(destination: Int, block: (T) -> Unit) { - findNavController().handleResult(viewLifecycleOwner, R.id.orchestratorRootFragment, destination, block) + findNavController().handleResult( + viewLifecycleOwner, + R.id.orchestratorRootFragment, + destination, + block + ) } private fun observeLoginCheckVm() { - loginCheckVm.showAlert.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { error -> - findNavController().navigate( - R.id.action_orchestratorFragment_to_alert, - alertConfigurationMapper.buildAlertConfig(error).toArgs() - ) - }) + loginCheckVm.showAlert.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { error -> + findNavController().navigateSafely( + currentFragment = this, + actionId = R.id.action_orchestratorFragment_to_alert, + args = alertConfigurationMapper.buildAlertConfig(error).toArgs() + ) + }) - loginCheckVm.showLoginFlow.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { request -> - findNavController().navigate( - R.id.action_orchestratorFragment_to_login, - LoginContract.toArgs(request.projectId, request.userId), - ) - }) + loginCheckVm.showLoginFlow.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { request -> + findNavController().navigateSafely( + currentFragment = this, + actionId = R.id.action_orchestratorFragment_to_login, + args = LoginContract.toArgs(request.projectId, request.userId), + ) + }) loginCheckVm.returnLoginNotComplete.observe(viewLifecycleOwner, LiveDataEventObserver { - clientApiVm.handleErrorResponse(args.requestAction, AppErrorResponse(AppErrorReason.LOGIN_NOT_COMPLETE)) + clientApiVm.handleErrorResponse( + args.requestAction, + AppErrorResponse(AppErrorReason.LOGIN_NOT_COMPLETE) + ) }) - loginCheckVm.proceedWithAction.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { action -> - orchestratorVm.handleAction(action) - }) + loginCheckVm.proceedWithAction.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { action -> + orchestratorVm.handleAction(action) + }) } private fun observeClientApiVm() { clientApiVm.newSessionCreated.observe(viewLifecycleOwner, LiveDataEventObserver { orchestratorCache.clearSteps() }) - clientApiVm.showAlert.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { error -> - findNavController().navigate( - R.id.action_orchestratorFragment_to_alert, - alertConfigurationMapper.buildAlertConfig(error).toArgs() - ) - }) - clientApiVm.returnResponse.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { responseExtras -> - val resultCode = responseExtras.getResultCodeFromExtras() - findNavController().finishWithResult(this, AppResult(resultCode, responseExtras)) - }) + clientApiVm.showAlert.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { error -> + findNavController().navigateSafely( + currentFragment = this, + actionId = R.id.action_orchestratorFragment_to_alert, + args = alertConfigurationMapper.buildAlertConfig(error).toArgs() + ) + }) + clientApiVm.returnResponse.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { responseExtras -> + val resultCode = responseExtras.getResultCodeFromExtras() + findNavController().finishWithResult(this, AppResult(resultCode, responseExtras)) + }) } private fun observeOrchestratorVm() { - orchestratorVm.currentStep.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { step -> - if (step != null) { - findNavController().navigate(step.navigationActionId, step.payload) - } - }) - orchestratorVm.appResponse.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { response -> - if (response.request == null) { - clientApiVm.handleErrorResponse(args.requestAction, AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR)) - } else { - when (response.response) { - is AppEnrolResponse -> clientApiVm.handleEnrolResponse(response.request, response.response) - is AppIdentifyResponse -> clientApiVm.handleIdentifyResponse(response.request, response.response) - is AppConfirmationResponse -> clientApiVm.handleConfirmResponse(response.request, response.response) - is AppVerifyResponse -> clientApiVm.handleVerifyResponse(response.request, response.response) - is AppRefusalResponse -> clientApiVm.handleExitFormResponse(response.request, response.response) - is AppErrorResponse -> clientApiVm.handleErrorResponse(args.requestAction, response.response) + orchestratorVm.currentStep.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { step -> + if (step != null) { + findNavController().navigateSafely(this, step.navigationActionId, step.payload) } - } - }) + }) + orchestratorVm.appResponse.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { response -> + if (response.request == null) { + clientApiVm.handleErrorResponse( + args.requestAction, + AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) + ) + } else { + when (response.response) { + is AppEnrolResponse -> { + clientApiVm.handleEnrolResponse(response.request, response.response) + } + + is AppIdentifyResponse -> { + clientApiVm.handleIdentifyResponse(response.request, response.response) + } + + is AppConfirmationResponse -> { + clientApiVm.handleConfirmResponse(response.request, response.response) + } + + is AppVerifyResponse -> { + clientApiVm.handleVerifyResponse(response.request, response.response) + } + + is AppRefusalResponse -> { + clientApiVm.handleExitFormResponse(response.request, response.response) + } + + is AppErrorResponse -> { + clientApiVm.handleErrorResponse(args.requestAction, response.response) + } + } + } + }) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(KEY_REQUEST_PROCESSED, orchestratorVm.requestProcessed) + // [MS-405] Saving the action request in the bundle, since ViewModels don't survive the + // process death. ActionRequest is important in mapping the correct SID response, hence it + // is important for it to be able to survive both configuration changes and process death. + outState.putString(KEY_ACTION_REQUEST, orchestratorVm.getActionRequestJson()) } override fun onResume() { @@ -179,7 +244,8 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { if (loginCheckVm.isDeviceSafe()) { orchestratorVm.requestProcessed = true lifecycleScope.launch { - val actionRequest = clientApiVm.handleIntent(args.requestAction, args.requestParams) + val actionRequest = + clientApiVm.handleIntent(args.requestAction, args.requestParams) if (actionRequest != null) { loginCheckVm.validateSignInAndProceed(actionRequest) } @@ -188,4 +254,8 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { } } + companion object { + private const val KEY_REQUEST_PROCESSED = "requestProcessed" + private const val KEY_ACTION_REQUEST = "actionRequest" + } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorModule.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorModule.kt new file mode 100644 index 0000000000..018af3a277 --- /dev/null +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorModule.kt @@ -0,0 +1,23 @@ +package com.simprints.feature.orchestrator + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton + + +@Qualifier +@Retention(AnnotationRetention.BINARY) +internal annotation class ExecutorLockTimeoutSec + +@Module +@InstallIn(SingletonComponent::class) +internal object OrchestratorModule { + + @Provides + @Singleton + @ExecutorLockTimeoutSec + fun provideExecutorLockTimeout(): Int = 60 +} \ No newline at end of file diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt index a2bba2e095..7c4b668998 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt @@ -4,8 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.module.SimpleModule +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameDeserializer +import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameSerializer import com.simprints.core.livedata.LiveDataEventWithContent import com.simprints.core.livedata.send +import com.simprints.core.tools.json.JsonHelper import com.simprints.face.capture.FaceCaptureResult import com.simprints.feature.orchestrator.cache.OrchestratorCache import com.simprints.feature.orchestrator.model.OrchestratorResult @@ -71,14 +77,14 @@ internal class OrchestratorViewModel @Inject constructor( doNextStep() } - fun handleResult(result: Serializable) { + fun handleResult(result: Serializable) = viewModelScope.launch { Simber.d(result.toString()) val errorResponse = mapRefusalOrErrorResult(result) if (errorResponse != null) { // Shortcut the flow execution if any refusal or error result is found addCallbackEvent(errorResponse) _appResponse.send(OrchestratorResult(actionRequest, errorResponse)) - return + return@launch } steps.firstOrNull { it.status == StepStatus.IN_PROGRESS }?.let { @@ -89,12 +95,28 @@ internal class OrchestratorViewModel @Inject constructor( } if (shouldCreatePerson(actionRequest, modalities, steps)) { - viewModelScope.launch { createPersonEvent(steps.mapNotNull { it.result }) } + createPersonEvent(steps.mapNotNull { it.result }) } doNextStep() } + fun restoreStepsIfNeeded() { + if (steps.isEmpty()) { + // Restore the steps from cache + steps = cache.steps + } + } + + fun restoreModalitiesIfNeeded() { + viewModelScope.launch { + if (modalities.isEmpty()) { + val projectConfiguration = configRepository.getProjectConfiguration() + modalities = projectConfiguration.general.modalities.toSet() + } + } + } + override fun onCleared() { cache.steps = steps super.onCleared() @@ -150,7 +172,13 @@ internal class OrchestratorViewModel @Inject constructor( if (matchingStep != null) { val fingerprintSamples = result.results.mapNotNull { it.sample } - .map { MatchParams.FingerprintSample(it.fingerIdentifier, it.format, it.template) } + .map { + MatchParams.FingerprintSample( + fingerId = it.fingerIdentifier, + format = it.format, + template = it.template + ) + } val newPayload = matchingStep.payload .getParcelable(MatchStepStubPayload.STUB_KEY) ?.toFingerprintStepArgs(fingerprintSamples) @@ -161,4 +189,33 @@ internal class OrchestratorViewModel @Inject constructor( } } } + + fun setActionRequestFromJson(json: String) { + try { + actionRequest = JsonHelper.fromJson( + json = json, + module = dbSerializationModule, + type = object : TypeReference() {}) + } catch (e: Exception) { + Simber.e(e) + } + } + + fun getActionRequestJson(): String? { + return try { + actionRequest?.let { + JsonHelper.toJson(it, dbSerializationModule) + } + } catch (e: Exception) { + Simber.e(e) + null + } + } + + companion object { + val dbSerializationModule = SimpleModule().apply { + addSerializer(TokenizableString::class.java, TokenizationClassNameSerializer()) + addDeserializer(TokenizableString::class.java, TokenizationClassNameDeserializer()) + } + } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt index 00f450c449..6d3e942c11 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/steps/MatchStepStubPayload.kt @@ -3,9 +3,10 @@ package com.simprints.feature.orchestrator.steps import android.os.Parcelable import androidx.core.os.bundleOf import com.simprints.core.domain.common.FlowType +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.matcher.MatchContract import com.simprints.matcher.MatchParams -import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import kotlinx.parcelize.Parcelize /** @@ -16,28 +17,32 @@ import kotlinx.parcelize.Parcelize */ @Parcelize internal data class MatchStepStubPayload( - val flowType: FlowType, - val subjectQuery: SubjectQuery, + val flowType: FlowType, + val subjectQuery: SubjectQuery, + val biometricDataSource: BiometricDataSource, ) : Parcelable { fun toFaceStepArgs(samples: List) = MatchContract.getArgs( faceSamples = samples, flowType = flowType, subjectQuery = subjectQuery, + biometricDataSource = biometricDataSource, ) fun toFingerprintStepArgs(samples: List) = MatchContract.getArgs( fingerprintSamples = samples, flowType = flowType, subjectQuery = subjectQuery, + biometricDataSource = biometricDataSource, ) companion object { const val STUB_KEY = "match_step_stub_payload" fun asBundle( - flowType: FlowType, - subjectQuery: SubjectQuery, - ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery)) + flowType: FlowType, + subjectQuery: SubjectQuery, + biometricDataSource: BiometricDataSource, + ) = bundleOf(STUB_KEY to MatchStepStubPayload(flowType, subjectQuery, biometricDataSource)) } } 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 1de081e92a..3b0fa75b3b 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 @@ -66,6 +66,7 @@ internal data class Step( var status: StepStatus = StepStatus.NOT_STARTED, var result: Serializable? = null, ) : Serializable { + // Do not remove. // Even though it may be marked as unused by IDE, it is referenced in the JsonTypeInfo annotation @Suppress("unused") @@ -75,6 +76,7 @@ internal data class Step( @Keep enum class StepStatus { + NOT_STARTED, IN_PROGRESS, COMPLETED, diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt index 2be4423f65..58bbdce3a0 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCase.kt @@ -1,12 +1,13 @@ package com.simprints.feature.orchestrator.usecases +import com.simprints.core.domain.response.AppErrorReason +import com.simprints.feature.alert.AlertResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.fetchsubject.FetchSubjectResult -import com.simprints.infra.orchestration.data.responses.AppErrorResponse -import com.simprints.infra.orchestration.data.responses.AppRefusalResponse import com.simprints.feature.setup.SetupResult import com.simprints.fingerprint.connect.FingerprintConnectResult -import com.simprints.core.domain.response.AppErrorReason +import com.simprints.infra.orchestration.data.responses.AppErrorResponse +import com.simprints.infra.orchestration.data.responses.AppRefusalResponse import com.simprints.infra.orchestration.data.responses.AppResponse import java.io.Serializable import javax.inject.Inject @@ -28,6 +29,8 @@ internal class MapRefusalOrErrorResultUseCase @Inject constructor() { is FingerprintConnectResult -> result.takeUnless { it.isSuccess } ?.let { AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) } + is AlertResult -> AppErrorResponse(result.appErrorReason ?: AppErrorReason.UNEXPECTED_ERROR) + else -> null } } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt index bb4d248a81..ad60f8ce28 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCase.kt @@ -5,6 +5,7 @@ import com.simprints.feature.orchestrator.steps.Step import com.simprints.feature.orchestrator.steps.StepId import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.logging.Simber import com.simprints.infra.orchestration.data.ActionRequest import javax.inject.Inject @@ -15,7 +16,12 @@ internal class ShouldCreatePersonUseCase @Inject constructor() { modalities: Set, results: List ): Boolean { - if (actionRequest !is ActionRequest.FlowAction || modalities.isEmpty()) { + if (actionRequest !is ActionRequest.FlowAction) { + return false + } + + if (modalities.isEmpty()) { + Simber.e("Modalities are empty") return false } diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt index e1b4e38ce6..e8731a9334 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/AppResponseBuilderUseCase.kt @@ -1,10 +1,9 @@ package com.simprints.feature.orchestrator.usecases.response -import android.os.Parcelable -import com.simprints.infra.orchestration.data.responses.AppErrorResponse +import com.simprints.core.domain.response.AppErrorReason import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.orchestration.data.ActionRequest -import com.simprints.core.domain.response.AppErrorReason +import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppResponse import java.io.Serializable import javax.inject.Inject diff --git a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt index 6c4c2feffd..2fe1ce3323 100644 --- a/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt +++ b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/usecases/response/CreateEnrolResponseUseCase.kt @@ -1,12 +1,13 @@ package com.simprints.feature.orchestrator.usecases.response +import com.simprints.core.domain.response.AppErrorReason import com.simprints.face.capture.FaceCaptureResult -import com.simprints.infra.orchestration.data.responses.AppEnrolResponse -import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.eventsync.sync.down.tasks.SubjectFactory +import com.simprints.infra.logging.Simber import com.simprints.infra.orchestration.data.ActionRequest -import com.simprints.core.domain.response.AppErrorReason +import com.simprints.infra.orchestration.data.responses.AppEnrolResponse +import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.orchestration.data.responses.AppResponse import java.io.Serializable import javax.inject.Inject @@ -32,7 +33,7 @@ internal class CreateEnrolResponseUseCase @Inject constructor( AppEnrolResponse(subject.subjectId) } catch (e: Exception) { - e.printStackTrace() + Simber.e(e) AppErrorResponse(AppErrorReason.UNEXPECTED_ERROR) } } 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 7587d2134d..447a1ca7e1 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 @@ -20,6 +20,7 @@ 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.fromDomainToModuleApi +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.matcher.MatchContract @@ -45,6 +46,7 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration, FlowType.ENROL, buildMatcherSubjectQuery(projectConfiguration, action), + BiometricDataSource.fromString(action.biometricDataSource), ) } else emptyList(), ) @@ -60,6 +62,7 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration, FlowType.IDENTIFY, buildMatcherSubjectQuery(projectConfiguration, action), + BiometricDataSource.fromString(action.biometricDataSource), ) ) @@ -75,6 +78,7 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration, FlowType.VERIFY, SubjectQuery(subjectId = action.verifyGuid), + BiometricDataSource.fromString(action.biometricDataSource), ) ) @@ -143,6 +147,7 @@ internal class BuildStepsUseCase @Inject constructor( projectConfiguration: ProjectConfiguration, flowType: FlowType, subjectQuery: SubjectQuery, + biometricDataSource: BiometricDataSource, ) = projectConfiguration.general.modalities.map { Step( id = when (it) { @@ -151,7 +156,7 @@ internal class BuildStepsUseCase @Inject constructor( }, navigationActionId = R.id.action_orchestratorFragment_to_matcher, destinationId = MatchContract.DESTINATION, - payload = MatchStepStubPayload.asBundle(flowType, subjectQuery), + payload = MatchStepStubPayload.asBundle(flowType, subjectQuery, biometricDataSource), ) } diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/ExecutionTrackerTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/ExecutionTrackerTest.kt new file mode 100644 index 0000000000..83abb1d6df --- /dev/null +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/ExecutionTrackerTest.kt @@ -0,0 +1,118 @@ +package com.simprints.feature.orchestrator + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.time.TimeHelper +import com.simprints.core.tools.time.Timestamp +import io.mockk.every +import io.mockk.mockk +import org.junit.Before +import org.junit.Test + +class ExecutionTrackerTest { + + private lateinit var executionTracker: ExecutionTracker + private lateinit var timeHelper: TimeHelper + private val executionTimeLimitSec: Int = 1 + + @Before + fun setUp() { + timeHelper = mockk() + every { timeHelper.now() } returns Timestamp(0L) + every { timeHelper.msBetweenNowAndTime(any()) } returns 0L + + executionTracker = ExecutionTracker( + timeHelper = timeHelper, + executionTimeLimitSec = executionTimeLimitSec + ) + } + + @Test + fun `when owner is created, then it becomes the main executor`() { + val ownerId = 123 + val owner = mockk() + every { owner.hashCode() } returns ownerId + + executionTracker.onStateChanged(owner, Lifecycle.Event.ON_CREATE) + + assertThat(executionTracker.isMain(owner)).isTrue() + } + + @Test + fun `when owner is destroyed, then it is no longer remains the main executor`() { + val ownerId = 123 + val owner = mockk() + every { owner.hashCode() } returns ownerId + + executionTracker.onStateChanged(owner, Lifecycle.Event.ON_CREATE) + executionTracker.onStateChanged(owner, Lifecycle.Event.ON_DESTROY) + + assertThat(executionTracker.isMain(owner)).isFalse() + } + + @Test + fun `given second owner, when first owner is destroyed, then the second owner becomes the main executor`() { + val ownerId1 = 123 + val ownerId2 = 456 + val firstOwner = mockk() + val secondOwner = mockk() + every { firstOwner.hashCode() } returns ownerId1 + every { secondOwner.hashCode() } returns ownerId2 + + executionTracker.onStateChanged(firstOwner, Lifecycle.Event.ON_CREATE) + executionTracker.onStateChanged(firstOwner, Lifecycle.Event.ON_DESTROY) + executionTracker.onStateChanged(secondOwner, Lifecycle.Event.ON_CREATE) + + assertThat(executionTracker.isMain(secondOwner)).isTrue() + } + + @Test + fun `given second owner, when first owner is not yet destroyed, then the first owner remains the main executor`() { + val ownerId1 = 123 + val ownerId2 = 456 + val firstOwner = mockk() + val secondOwner = mockk() + every { firstOwner.hashCode() } returns ownerId1 + every { secondOwner.hashCode() } returns ownerId2 + + executionTracker.onStateChanged(firstOwner, Lifecycle.Event.ON_CREATE) + executionTracker.onStateChanged(secondOwner, Lifecycle.Event.ON_CREATE) + + assertThat(executionTracker.isMain(firstOwner)).isTrue() + } + + @Test + fun `given second owner and not enough time passed to unlock the main executor, when first owner is not yet destroyed, then the first owner remains the main executor`() { + val ownerId1 = 123 + val ownerId2 = 456 + val firstOwner = mockk() + val secondOwner = mockk() + + every { firstOwner.hashCode() } returns ownerId1 + every { secondOwner.hashCode() } returns ownerId2 + every { timeHelper.msBetweenNowAndTime(any()) } returns 1000L * (executionTimeLimitSec) + + executionTracker.onStateChanged(firstOwner, Lifecycle.Event.ON_CREATE) + executionTracker.onStateChanged(secondOwner, Lifecycle.Event.ON_CREATE) + + assertThat(executionTracker.isMain(firstOwner)).isTrue() + } + + @Test + fun `given second owner and enough time passed to unlock the main executor, when first owner is not yet destroyed, then the second owner becomes the main executor`() { + val ownerId1 = 123 + val ownerId2 = 456 + val firstOwner = mockk() + val secondOwner = mockk() + + every { firstOwner.hashCode() } returns ownerId1 + every { secondOwner.hashCode() } returns ownerId2 + every { timeHelper.msBetweenNowAndTime(any()) } returns 1000L * (executionTimeLimitSec + 1) + + executionTracker.onStateChanged(firstOwner, Lifecycle.Event.ON_CREATE) + executionTracker.onStateChanged(secondOwner, Lifecycle.Event.ON_CREATE) + + assertThat(executionTracker.isMain(secondOwner)).isTrue() + } +} diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt index 8f375c04a8..4b50886ee8 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt @@ -25,6 +25,8 @@ import com.simprints.feature.setup.LocationStore import com.simprints.feature.setup.SetupResult import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.testtools.common.coroutines.TestCoroutineRule @@ -36,6 +38,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.justRun import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -183,7 +186,10 @@ internal class OrchestratorViewModelTest { fun `Updates face matcher step payload when receiving face capture`() = runTest { every { stepsBuilder.build(any(), any()) } returns listOf( createMockStep(StepId.FACE_CAPTURE), - createMockStep(StepId.FACE_MATCHER, MatchStepStubPayload.asBundle(FlowType.VERIFY, SubjectQuery())), + createMockStep(StepId.FACE_MATCHER, MatchStepStubPayload.asBundle( + FlowType.VERIFY, + SubjectQuery(), + BiometricDataSource.SIMPRINTS)), ) every { mapRefusalOrErrorResult(any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false @@ -200,7 +206,10 @@ internal class OrchestratorViewModelTest { fun `Updates fingerprint matcher step payload when receiving fingerprint capture`() = runTest { every { stepsBuilder.build(any(), any()) } returns listOf( createMockStep(StepId.FINGERPRINT_CAPTURE), - createMockStep(StepId.FINGERPRINT_MATCHER, MatchStepStubPayload.asBundle(FlowType.VERIFY, SubjectQuery())), + createMockStep(StepId.FINGERPRINT_MATCHER, MatchStepStubPayload.asBundle( + FlowType.VERIFY, + SubjectQuery(), + BiometricDataSource.SIMPRINTS)), ) every { mapRefusalOrErrorResult(any()) } returns null every { shouldCreatePerson(any(), any(), any()) } returns false @@ -213,6 +222,71 @@ internal class OrchestratorViewModelTest { } } + @Test + fun `Restores steps if empty`() = runTest { + every { stepsBuilder.build(any(), any()) } returns emptyList() + val savedSteps = listOf( + createMockStep(StepId.SETUP), + createMockStep(StepId.CONSENT), + ) + every { cache.steps } returns savedSteps + + viewModel.handleAction(mockk()) + viewModel.restoreStepsIfNeeded() + + verify { cache.steps } + } + + @Test + fun `Does not restore steps if not empty`() = runTest { + val originalSteps = listOf( + createMockStep(StepId.FINGERPRINT_CAPTURE), + ) + every { stepsBuilder.build(any(), any()) } returns originalSteps + val savedSteps = listOf( + createMockStep(StepId.SETUP), + createMockStep(StepId.CONSENT), + ) + every { cache.steps } returns savedSteps + + viewModel.handleAction(mockk()) + viewModel.restoreStepsIfNeeded() + + verify(exactly = 0) { cache.steps } + } + + @Test + fun `Restores modalities if empty`() = runTest { + val projectModalities = listOf( + mockk(), + mockk(), + ) + coEvery { configRepository.getProjectConfiguration() } returns mockk { + every { general.modalities } returns emptyList() andThen projectModalities + } + + viewModel.handleAction(mockk()) + viewModel.restoreModalitiesIfNeeded() + + coVerify(exactly = 3) { configRepository.getProjectConfiguration() } + } + + @Test + fun `Does not restore modalities if not empty`() = runTest { + val projectModalities = listOf( + mockk(), + mockk(), + ) + coEvery { configRepository.getProjectConfiguration() } returns mockk { + every { general.modalities } returns projectModalities + } + + viewModel.handleAction(mockk()) + viewModel.restoreModalitiesIfNeeded() + + coVerify(exactly = 2) { configRepository.getProjectConfiguration() } + } + private fun createMockStep(stepId: Int, payload: Bundle = Bundle()) = Step( id = stepId, navigationActionId = 0, diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt index 909dde46af..2ac48baf65 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/MapRefusalOrErrorResultUseCaseTest.kt @@ -2,6 +2,7 @@ package com.simprints.feature.orchestrator.usecases import com.google.common.truth.Truth.assertThat import com.simprints.face.capture.FaceCaptureResult +import com.simprints.feature.alert.AlertResult import com.simprints.feature.exitform.ExitFormResult import com.simprints.feature.fetchsubject.FetchSubjectResult import com.simprints.feature.setup.SetupResult @@ -26,9 +27,12 @@ class MapRefusalOrErrorResultUseCaseTest { mapOf( ExitFormResult(true) to AppRefusalResponse::class.java, FetchSubjectResult(found = false) to AppErrorResponse::class.java, - SetupResult(isSuccess = false) to AppErrorResponse::class.java, + SetupResult(isSuccess = false) to AppErrorResponse::class.java, FingerprintConnectResult(isSuccess = false) to AppErrorResponse::class.java, - ).forEach { (result, responseClass) -> assertThat(useCase(result)).isInstanceOf(responseClass) } + AlertResult(buttonKey = "buttonKey") to AppErrorResponse::class.java, + ).forEach { (result, responseClass) -> + assertThat(useCase(result)).isInstanceOf(responseClass) + } } @Test diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt index 21cda7b6e6..650f973cb5 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/ShouldCreatePersonUseCaseTest.kt @@ -164,6 +164,7 @@ class ShouldCreatePersonUseCaseTest { projectId = "", userId = "".asTokenizableRaw(), moduleId = "".asTokenizableRaw(), + biometricDataSource = "", metadata = "", unknownExtras = emptyMap(), ) diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/SetupResult.kt b/feature/setup/src/main/java/com/simprints/feature/setup/SetupResult.kt index 5c9fa82796..0905611319 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/SetupResult.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/SetupResult.kt @@ -1,11 +1,9 @@ package com.simprints.feature.setup import androidx.annotation.Keep -import com.simprints.core.domain.response.AppErrorReason import java.io.Serializable @Keep data class SetupResult( val isSuccess: Boolean, - val error: AppErrorReason? = null, ) : Serializable diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/data/ErrorType.kt b/feature/setup/src/main/java/com/simprints/feature/setup/data/ErrorType.kt index 715aebd0a4..24ce477ee7 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/data/ErrorType.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/data/ErrorType.kt @@ -1,9 +1,7 @@ package com.simprints.feature.setup.data -import android.os.Bundle import androidx.annotation.Keep import androidx.annotation.StringRes -import androidx.core.os.bundleOf import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.alertConfiguration import com.simprints.feature.alert.config.AlertButtonConfig @@ -32,8 +30,8 @@ internal enum class ErrorType( CONFIGURATION_ERROR( null, IDR.string.configuration_generic_error_message, - alertType = AlertScreenEventType.FACE_LICENSE_MISSING, - errorReason = AppErrorReason.FACE_CONFIGURATION_ERROR, + alertType = AlertScreenEventType.LICENSE_MISSING, + errorReason = AppErrorReason.LICENSE_MISSING, ), ; @@ -45,16 +43,7 @@ internal enum class ErrorType( messageRes = this@ErrorType.message image = IDR.drawable.ic_exclamation leftButton = AlertButtonConfig.Close - payload = bundleOf(PAYLOAD_TYPE_KEY to this@ErrorType.errorReason) + appErrorReason = errorReason eventType = this@ErrorType.alertType }.toArgs() - - @ExcludedFromGeneratedTestCoverageReports("Inner code of excluded file") - companion object { - private const val PAYLOAD_TYPE_KEY = "error_type" - - fun reasonFromPayload(extras: Bundle): AppErrorReason = extras.getString(PAYLOAD_TYPE_KEY) - ?.let { AppErrorReason.valueOf(it) } - ?: AppErrorReason.UNEXPECTED_ERROR - } } diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupFragment.kt b/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupFragment.kt index b7d557638e..90015ab569 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupFragment.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupFragment.kt @@ -25,6 +25,7 @@ import com.simprints.infra.logging.LoggingConstants.CrashReportTag.LICENSE import com.simprints.infra.logging.Simber import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @@ -61,12 +62,7 @@ internal class SetupFragment : Fragment(R.layout.fragment_setup) { viewLifecycleOwner, R.id.setupFragment, AlertContract.DESTINATION, - ) { result -> - findNavController().finishWithResult( - this, - SetupResult(false, ErrorType.reasonFromPayload(result.payload)) - ) - } + ) { result -> findNavController().finishWithResult(this, result) } // Request location permission viewModel.requestLocationPermission.observe(viewLifecycleOwner) { if (requireActivity().hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { @@ -128,7 +124,8 @@ internal class SetupFragment : Fragment(R.layout.fragment_setup) { getString(IDR.string.configuration_generic_error_title, errorCode) Simber.tag(LICENSE.name) .i("Error with configuration download. Error = $errorTitle") - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_global_errorFragment, ErrorType.CONFIGURATION_ERROR.apply { this.customTitle = errorTitle }.toAlertArgs() ) @@ -146,7 +143,8 @@ internal class SetupFragment : Fragment(R.layout.fragment_setup) { Simber.tag(LICENSE.name) .i("Error with configuration download. The backend is under maintenance") - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_global_errorFragment, ErrorType.BACKEND_MAINTENANCE_ERROR.apply { this.customMessage = errorMessage } .toAlertArgs() diff --git a/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupViewModel.kt b/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupViewModel.kt index b5d0df3009..ffaab17f45 100644 --- a/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupViewModel.kt +++ b/feature/setup/src/main/java/com/simprints/feature/setup/screen/SetupViewModel.kt @@ -13,6 +13,8 @@ import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.license.LicenseRepository import com.simprints.infra.license.LicenseState +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor import com.simprints.infra.license.Vendor.Companion.NEC import com.simprints.infra.license.Vendor.Companion.RANK_ONE @@ -28,6 +30,7 @@ internal class SetupViewModel @Inject constructor( private val licenseRepository: LicenseRepository, @DeviceID private val deviceID: String, private val authStore: AuthStore, + private val saveLicenseCheckEvent: SaveLicenseCheckEventUseCase ) : ViewModel() { val requestLocationPermission: LiveData @@ -79,6 +82,8 @@ internal class SetupViewModel @Inject constructor( if (licenceState is LicenseState.FinishedWithError || licenceState is LicenseState.FinishedWithBackendMaintenanceError ) { + // Save the license state event + saveLicenseCheckEvent(licenseVendor,LicenseStatus.MISSING) _overallSetupResult.postValue(false) } // if this is the last license to download, then update the overall setup result diff --git a/feature/setup/src/test/java/com/simprints/feature/setup/screen/SetupViewModelTest.kt b/feature/setup/src/test/java/com/simprints/feature/setup/screen/SetupViewModelTest.kt index e174de8620..bcb7c17694 100644 --- a/feature/setup/src/test/java/com/simprints/feature/setup/screen/SetupViewModelTest.kt +++ b/feature/setup/src/test/java/com/simprints/feature/setup/screen/SetupViewModelTest.kt @@ -9,10 +9,15 @@ import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.license.LicenseRepository import com.simprints.infra.license.LicenseState +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor +import com.simprints.infra.license.remote.License import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.justRun @@ -42,6 +47,9 @@ class SetupViewModelTest { @MockK private lateinit var locationStore: LocationStore + @MockK + private lateinit var saveLicenseCheckEvent: SaveLicenseCheckEventUseCase + private val configRepository = mockk() private lateinit var viewModel: SetupViewModel @@ -53,7 +61,14 @@ class SetupViewModelTest { MockKAnnotations.init(this) every { authStore.signedInProjectId } returns projectId viewModel = - SetupViewModel(locationStore, configRepository, licenseRepository, deviceID, authStore) + SetupViewModel( + locationStore, + configRepository, + licenseRepository, + deviceID, + authStore, + saveLicenseCheckEvent + ) } @Test @@ -148,7 +163,7 @@ class SetupViewModelTest { } every { licenseRepository.getLicenseStates(any(), any(), any()) - } returns listOf(LicenseState.FinishedWithSuccess("license")).asFlow() + } returns listOf(LicenseState.FinishedWithSuccess(License("expirationDate","license"))).asFlow() // When viewModel.downloadRequiredLicenses() @@ -159,7 +174,7 @@ class SetupViewModelTest { } -@Test + @Test fun `should not download required licenses if there are no required licenses`() = runTest { // Given coEvery { configRepository.getProjectConfiguration() } returns mockk { @@ -174,6 +189,7 @@ class SetupViewModelTest { // Then viewModel.overallSetupResult.test().assertValue(true) } + @Test fun `should fail if any license fails`() = runTest { // Given @@ -188,9 +204,10 @@ class SetupViewModelTest { every { allowedSDKs } returns listOf(FingerprintConfiguration.BioSdk.NEC) } } + coJustRun { saveLicenseCheckEvent(Vendor.RANK_ONE, LicenseStatus.MISSING)} every { licenseRepository.getLicenseStates(any(), any(), Vendor.NEC) - } returns listOf(LicenseState.FinishedWithSuccess("")).asFlow() + } returns listOf(LicenseState.FinishedWithSuccess(License("expirationDate", ""))).asFlow() every { licenseRepository.getLicenseStates(any(), any(), Vendor.RANK_ONE) } returns listOf(LicenseState.FinishedWithError("123")).asFlow() @@ -199,6 +216,7 @@ class SetupViewModelTest { viewModel.downloadRequiredLicenses() // Then + coVerify { saveLicenseCheckEvent(Vendor.RANK_ONE, LicenseStatus.MISSING)} viewModel.overallSetupResult.test().assertValue(false) } } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt index 8a738ae611..d0a9882b9d 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.View import androidx.activity.addCallback import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle @@ -34,15 +33,16 @@ import com.simprints.fingerprint.capture.state.CaptureState import com.simprints.fingerprint.capture.state.CollectFingerprintsState import com.simprints.fingerprint.capture.views.confirmfingerprints.ConfirmFingerprintsDialog import com.simprints.fingerprint.capture.views.fingerviewpager.FingerViewPagerManager -import com.simprints.fingerprint.capture.views.tryagainsplash.FullScreenSplashDialog +import com.simprints.fingerprint.capture.views.tryagainsplash.TryAnotherFingerSplashDialogFragment import com.simprints.fingerprint.connect.FingerprintConnectContract import com.simprints.fingerprint.connect.FingerprintConnectResult -import com.simprints.infra.events.event.domain.models.AlertScreenEvent +import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType import com.simprints.infra.logging.LoggingConstants.CrashReportTag.FINGER_CAPTURE import com.simprints.infra.logging.Simber import com.simprints.infra.uibase.extensions.showToast import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.system.Vibrate import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -114,7 +114,8 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri private fun observeBioSdkInit() { vm.invalidLicense.observe(viewLifecycleOwner) { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_fingerprintCaptureFragment_to_graphAlert, alertConfiguration { color = AlertColor.Gray @@ -122,15 +123,16 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri messageRes = IDR.string.configuration_licence_invalid_message image = IDR.drawable.ic_exclamation leftButton = AlertButtonConfig.Close - payload = bundleOf(PAYLOAD_TYPE_KEY to AppErrorReason.FACE_LICENSE_INVALID) - eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.FACE_LICENSE_INVALID + appErrorReason = AppErrorReason.LICENSE_INVALID + eventType = AlertScreenEventType.LICENSE_INVALID }.toArgs() ) } } private fun openRefusal() { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_fingerprintCaptureFragment_to_graphExitForm, exitFormConfiguration { titleRes = com.simprints.infra.resources.R.string.exit_form_title_fingerprinting @@ -200,14 +202,15 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri }) vm.launchAlert.observe(viewLifecycleOwner, LiveDataEventObserver { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_fingerprintCaptureFragment_to_graphAlert, alertConfiguration { titleRes = IDR.string.fingerprint_capture_error_title messageRes = IDR.string.fingerprint_capture_unexpected_error_message color = AlertColor.Red image = IDR.drawable.ic_alert_default - eventType = AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.UNEXPECTED_ERROR + eventType = AlertScreenEventType.UNEXPECTED_ERROR leftButton = AlertButtonConfig.Close }.toArgs() ) @@ -222,7 +225,8 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri //to a crash because a second navigation to ConnectScanner is attempted but by the time it's // executed we are already on the exit screen. try { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_fingerprintCaptureFragment_to_graphConnectScanner, FingerprintConnectContract.getArgs(true) ) @@ -271,7 +275,7 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri private fun updateSplashScreen(state: CollectFingerprintsState) { if (state.isShowingSplashScreen && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { if (!hasSplashScreenBeenTriggered) { - FullScreenSplashDialog().show(childFragmentManager, "splash") + TryAnotherFingerSplashDialogFragment().show(childFragmentManager, "splash") hasSplashScreenBeenTriggered = true } } else { @@ -283,7 +287,4 @@ internal class FingerprintCaptureFragment : Fragment(R.layout.fragment_fingerpri confirmDialog?.dismiss() super.onDestroyView() } - companion object { - private const val PAYLOAD_TYPE_KEY = "error_type" - } } diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt index b8fb28c7ae..0ecae9ec94 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/screen/FingerprintCaptureViewModel.kt @@ -257,7 +257,15 @@ internal class FingerprintCaptureViewModel @Inject constructor( _stateLiveData.value = initialState } - fun isImageTransferRequired(): Boolean = + /** + * Every bio sdk has a different timeout for scanning and image transfer + * This function returns the timeout for scanning plus the timeout for image transfer if it is required + * */ + fun progressBarTimeout() = + bioSdkWrapper.scanningTimeoutMs + + if (isImageTransferRequired()) bioSdkWrapper.imageTransferTimeoutMs else 0 + + private fun isImageTransferRequired(): Boolean = bioSdkConfiguration.vero2?.imageSavingStrategy?.isImageTransferRequired() ?: false && scannerManager.scanner.isImageTransferSupported() @@ -330,19 +338,16 @@ internal class FingerprintCaptureViewModel @Inject constructor( scannerManager.scanner.setUiIdle() val capturedFingerprint = bioSdkWrapper.acquireFingerprintTemplate( bioSdkConfiguration.vero2?.captureStrategy?.toInt(), - scanningTimeoutMs.toInt(), - qualityThreshold() + bioSdkWrapper.scanningTimeoutMs.toInt(), + qualityThreshold(), + // is this is the last bad scan, we allow low quality extraction + tooManyBadScans(state.currentCaptureState(), plusBadScan = true) ) handleCaptureSuccess(capturedFingerprint) } catch (ex: CancellationException) { // ignore cancellation exception, but log behaviour Simber.d("Fingerprint scanning was cancelled") - } catch (ex: BioSdkException.ImageQualityBelowThresholdException) { - // this exception is thrown when the image quality is below the threshold - // and it is thrown from NEC SDK it should be handled as a no finger detected exception not - // as a low quality scan issue because there is no template extracted from the image - handleNoFingerDetected() } catch (ex: Throwable) { handleScannerCommunicationsError(ex) } @@ -676,9 +681,6 @@ internal class FingerprintCaptureViewModel @Inject constructor( const val maximumTotalNumberOfFingersForAutoAdding = 4 const val numberOfBadScansRequiredToAutoAddNewFinger = 3 - const val scanningTimeoutMs = 3000L - const val imageTransferTimeoutMs = 3000L - const val AUTO_SWIPE_DELAY: Long = 500 const val TRY_DIFFERENT_FINGER_SPLASH_DELAY: Long = 2000 diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt index f47b21e294..3a2b1d602c 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/fingerviewpager/FingerFragment.kt @@ -7,7 +7,9 @@ import android.widget.ProgressBar import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.fingerprint.capture.R +import com.simprints.fingerprint.capture.databinding.FragmentFingerBinding import com.simprints.fingerprint.capture.resources.captureNumberTextId import com.simprints.fingerprint.capture.resources.directionTextColour import com.simprints.fingerprint.capture.resources.directionTextId @@ -20,12 +22,8 @@ import com.simprints.fingerprint.capture.screen.FingerprintCaptureViewModel import com.simprints.fingerprint.capture.state.CaptureState import com.simprints.fingerprint.capture.state.CollectFingerprintsState import com.simprints.fingerprint.capture.state.FingerState -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningOnlyTimeoutBar -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningWithImageTransferTimeoutBar -import com.simprints.fingerprint.capture.databinding.FragmentFingerBinding +import com.simprints.fingerprint.capture.views.timeoutbar.ScanCountdownBar import com.simprints.infra.uibase.viewbinding.viewBinding -import com.simprints.core.domain.fingerprint.IFingerIdentifier import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -36,12 +34,12 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { private lateinit var fingerId: IFingerIdentifier - private lateinit var timeoutBars: List + private lateinit var timeoutBars: List override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - fingerId = IFingerIdentifier.values()[arguments?.getInt(FINGER_ID_BUNDLE_KEY) + fingerId = IFingerIdentifier.entries.toTypedArray()[arguments?.getInt(FINGER_ID_BUNDLE_KEY) ?: throw IllegalArgumentException()] initTimeoutBars() @@ -49,10 +47,10 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { vm.stateLiveData.observe(viewLifecycleOwner) { updateOrHideFingerImageAccordingToSettings() updateFingerNameText() - updateFingerCaptureNumberText(it) - updateFingerResultText(it) + updateFingerCaptureNumberText() + updateFingerResultText() updateFingerDirectionText(it) - updateTimeoutBars(it) + updateTimeoutBars() } } @@ -70,18 +68,7 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { } }.map { progressBar -> binding.progressBarContainer.addView(progressBar) - if (vm.isImageTransferRequired()) { - ScanningWithImageTransferTimeoutBar( - progressBar, - FingerprintCaptureViewModel.scanningTimeoutMs, - FingerprintCaptureViewModel.imageTransferTimeoutMs - ) - } else { - ScanningOnlyTimeoutBar( - progressBar, - FingerprintCaptureViewModel.scanningTimeoutMs - ) - } + ScanCountdownBar(progressBar, vm.progressBarTimeout()) } } @@ -104,7 +91,7 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { binding.fingerNumberText.setTextColor(resources.getColor(fingerId.nameTextColour(), null)) } - private fun updateFingerCaptureNumberText(state: CollectFingerprintsState) = withFingerState { + private fun updateFingerCaptureNumberText() = withFingerState { if (isMultiCapture()) { binding.fingerCaptureNumberText.setTextColor( resources.getColor( @@ -121,7 +108,7 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { } - private fun updateFingerResultText(state: CollectFingerprintsState) = withFingerState { + private fun updateFingerResultText() = withFingerState { binding.fingerResultText.text = getString(currentCapture().resultTextId()) binding.fingerResultText.setTextColor( resources.getColor( @@ -141,7 +128,7 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { ) } - private fun updateTimeoutBars(state: CollectFingerprintsState) = withFingerState { + private fun updateTimeoutBars() = withFingerState { timeoutBars.forEachIndexed { captureIndex, timeoutBar -> with(timeoutBar) { when (val fingerState = captures[captureIndex]) { @@ -155,7 +142,10 @@ internal class FingerFragment : Fragment(R.layout.fragment_finger) { } is CaptureState.Scanning -> startTimeoutBar() - is CaptureState.TransferringImage -> handleScanningFinished() + is CaptureState.TransferringImage -> { + //Do nothing + } + is CaptureState.NotDetected -> { handleCancelled() progressBar.progressDrawable = ContextCompat.getDrawable( diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanCountdownBar.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanCountdownBar.kt new file mode 100644 index 0000000000..e1fbba4482 --- /dev/null +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanCountdownBar.kt @@ -0,0 +1,39 @@ +package com.simprints.fingerprint.capture.views.timeoutbar + +import android.os.CountDownTimer +import android.widget.ProgressBar +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports + +@ExcludedFromGeneratedTestCoverageReports("UI code") +internal class ScanCountdownBar( + val progressBar: ProgressBar, scanningTimeoutMs: Long +) : CountDownTimer(scanningTimeoutMs, scanningTimeoutMs / TOTAL_PROGRESS.toLong()) { + + private var scanningProgress = INITIAL_PROGRESS + + fun startTimeoutBar() { + progressBar.progress = INITIAL_PROGRESS + scanningProgress = INITIAL_PROGRESS + start() + } + + override fun onTick(millisUntilFinished: Long) { + scanningProgress += PROGRESS_INCREMENT + progressBar.progress = scanningProgress + } + + override fun onFinish() { + progressBar.progress = TOTAL_PROGRESS + } + + fun handleCancelled() { + cancel() + progressBar.progress = INITIAL_PROGRESS + } + + companion object { + const val INITIAL_PROGRESS = 0 + const val TOTAL_PROGRESS = 100 + const val PROGRESS_INCREMENT = 1 + } +} diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningOnlyTimeoutBar.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningOnlyTimeoutBar.kt deleted file mode 100644 index e051a975f3..0000000000 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningOnlyTimeoutBar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.simprints.fingerprint.capture.views.timeoutbar - -import android.os.CountDownTimer -import android.widget.ProgressBar -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.FINISHED_PROGRESS -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.INITIAL_PROGRESS -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.PROGRESS_INCREMENT -import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports - -@ExcludedFromGeneratedTestCoverageReports("UI code") -internal class ScanningOnlyTimeoutBar( - override val progressBar: ProgressBar, - private val scanningTimeoutMs: Long -) : ScanningTimeoutBar { - - private var countDownTimer: CountDownTimer? = null - - private var scanningProgress = INITIAL_PROGRESS - - override fun startTimeoutBar() { - progressBar.progress = INITIAL_PROGRESS - scanningProgress = INITIAL_PROGRESS - countDownTimer = createScanningTimer().also { it.start() } - } - - private fun createScanningTimer(): CountDownTimer = - object : CountDownTimer(scanningTimeoutMs, (scanningTimeoutMs / FINISHED_PROGRESS)) { - override fun onTick(millisUntilFinished: Long) { - scanningProgress += PROGRESS_INCREMENT - progressBar.progress = scanningProgress - } - - override fun onFinish() { - progressBar.progress = FINISHED_PROGRESS - } - } - - override fun handleScanningFinished() { - handleAllStepsFinished() - } - - override fun handleAllStepsFinished() { - countDownTimer?.let { - it.cancel() - it.onFinish() - } - } - - override fun handleCancelled() { - countDownTimer?.let { - it.cancel() - progressBar.progress = INITIAL_PROGRESS - } - } -} diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningTimeoutBar.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningTimeoutBar.kt deleted file mode 100644 index 6111fbe565..0000000000 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningTimeoutBar.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.simprints.fingerprint.capture.views.timeoutbar - -import android.widget.ProgressBar - -internal interface ScanningTimeoutBar { - - val progressBar: ProgressBar - - fun startTimeoutBar() - - fun handleAllStepsFinished() - - fun handleCancelled() - - fun handleScanningFinished() - - companion object { - const val INITIAL_PROGRESS = 0 - const val FINISHED_PROGRESS = 100 - const val PROGRESS_INCREMENT = 1 - } -} diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningWithImageTransferTimeoutBar.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningWithImageTransferTimeoutBar.kt deleted file mode 100644 index 2084aa954f..0000000000 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/timeoutbar/ScanningWithImageTransferTimeoutBar.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.simprints.fingerprint.capture.views.timeoutbar - -import android.os.CountDownTimer -import android.widget.ProgressBar -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.FINISHED_PROGRESS -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.INITIAL_PROGRESS -import com.simprints.fingerprint.capture.views.timeoutbar.ScanningTimeoutBar.Companion.PROGRESS_INCREMENT -import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports - -@ExcludedFromGeneratedTestCoverageReports("UI code") -internal class ScanningWithImageTransferTimeoutBar( - override val progressBar: ProgressBar, - private val scanningTimeoutMs: Long, - private val imageTransferTimeoutMs: Long -) : ScanningTimeoutBar { - - private var scanningCountDownTimer: CountDownTimer? = null - private var imageTransferCountDownTimer: CountDownTimer? = null - - private var scanningProgress = INITIAL_PROGRESS - private var imageTransferProgress = INITIAL_PROGRESS - - override fun startTimeoutBar() { - progressBar.progress = INITIAL_PROGRESS - scanningProgress = INITIAL_PROGRESS - imageTransferProgress = INITIAL_PROGRESS - scanningCountDownTimer = createScanningTimer().also { it.start() } - imageTransferCountDownTimer = createImageTransferTimer() - } - - private fun createScanningTimer(): CountDownTimer = - object : CountDownTimer(scanningTimeoutMs, (scanningTimeoutMs / SCANNING_STEP_FINISHED_PROGRESS)) { - override fun onTick(millisUntilFinished: Long) { - scanningProgress += PROGRESS_INCREMENT - progressBar.progress = scanningProgress - } - - override fun onFinish() { - progressBar.progress = SCANNING_STEP_FINISHED_PROGRESS - } - } - - private fun createImageTransferTimer(): CountDownTimer = - object : CountDownTimer(imageTransferTimeoutMs, (imageTransferTimeoutMs / SCANNING_STEP_FINISHED_PROGRESS)) { - override fun onTick(millisUntilFinished: Long) { - imageTransferProgress += PROGRESS_INCREMENT - progressBar.progress = SCANNING_STEP_FINISHED_PROGRESS + imageTransferProgress - } - - override fun onFinish() { - progressBar.progress = FINISHED_PROGRESS - } - } - - override fun handleScanningFinished() { - scanningCountDownTimer?.let { - it.cancel() - it.onFinish() - } - imageTransferCountDownTimer?.start() - } - - override fun handleAllStepsFinished() { - scanningCountDownTimer?.let { - it.cancel() - it.onFinish() - } - imageTransferCountDownTimer?.let { - it.cancel() - it.onFinish() - } - } - - override fun handleCancelled() { - scanningCountDownTimer?.let { - it.cancel() - progressBar.progress = INITIAL_PROGRESS - } - imageTransferCountDownTimer?.let { - it.cancel() - progressBar.progress = INITIAL_PROGRESS - } - } - - companion object { - const val SCANNING_STEP_FINISHED_PROGRESS = 50 // Half-way through the progress bar - } -} diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/FullScreenSplashDialog.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerSplashDialogFragment.kt similarity index 52% rename from fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/FullScreenSplashDialog.kt rename to fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerSplashDialogFragment.kt index 5df5a35c0f..b4a0a59b3b 100644 --- a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/FullScreenSplashDialog.kt +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerSplashDialogFragment.kt @@ -3,23 +3,25 @@ package com.simprints.fingerprint.capture.views.tryagainsplash import android.os.Bundle import android.view.View import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope +import androidx.fragment.app.viewModels import com.simprints.fingerprint.capture.R -import com.simprints.fingerprint.capture.screen.FingerprintCaptureViewModel.Companion.TRY_DIFFERENT_FINGER_SPLASH_DELAY -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR -internal class FullScreenSplashDialog : DialogFragment(R.layout.activity_splash_screen) { +@AndroidEntryPoint +internal class TryAnotherFingerSplashDialogFragment : DialogFragment(R.layout.activity_splash_screen) { + + private val vm: TryAnotherFingerViewModel by viewModels() override fun getTheme() = IDR.style.Theme_Simprints_Dialog_FullScreen override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - lifecycleScope.launch { - delay(TRY_DIFFERENT_FINGER_SPLASH_DELAY) - dismiss() + vm.dismiss.observe(viewLifecycleOwner) { + if (it) dismissAllowingStateLoss() } } + } + diff --git a/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerViewModel.kt b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerViewModel.kt new file mode 100644 index 0000000000..7d22d6db1b --- /dev/null +++ b/fingerprint/capture/src/main/java/com/simprints/fingerprint/capture/views/tryagainsplash/TryAnotherFingerViewModel.kt @@ -0,0 +1,25 @@ +package com.simprints.fingerprint.capture.views.tryagainsplash + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.simprints.fingerprint.capture.screen.FingerprintCaptureViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class TryAnotherFingerViewModel @Inject constructor() : ViewModel() { + + val dismiss: LiveData get() = _dismiss + private val _dismiss = MutableLiveData(false) + + init { + viewModelScope.launch { + delay(FingerprintCaptureViewModel.TRY_DIFFERENT_FINGER_SPLASH_DELAY) + _dismiss.value = true + } + } +} diff --git a/fingerprint/capture/src/main/res/layout/activity_splash_screen.xml b/fingerprint/capture/src/main/res/layout/activity_splash_screen.xml index 086c0c0144..b945ff1378 100644 --- a/fingerprint/capture/src/main/res/layout/activity_splash_screen.xml +++ b/fingerprint/capture/src/main/res/layout/activity_splash_screen.xml @@ -1,11 +1,9 @@ + android:background="#E600B3D1"> { /* Do nothing */ - } - + BackButtonBehaviour.DISABLED, null -> { /* Do nothing */ } BackButtonBehaviour.EXIT_WITH_ERROR -> _finish.send(false) BackButtonBehaviour.EXIT_FORM -> { _scannerConnected.send(false) diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelper.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelper.kt index 443be2138c..09317adb02 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelper.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelper.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.provider.Settings +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.AlertContract import com.simprints.feature.alert.AlertResult import java.util.concurrent.atomic.AtomicBoolean @@ -27,14 +28,9 @@ internal class AlertActivityHelper { retry: () -> Unit, finishWithError: () -> Unit, ) { - val alertError = result.payload - .getString(AlertError.PAYLOAD_KEY) - ?.let { AlertError.valueOf(it) } - ?: AlertError.UNEXPECTED_ERROR - when (result.buttonKey) { AlertContract.ALERT_BUTTON_PRESSED_BACK -> { - if (alertError == AlertError.UNEXPECTED_ERROR) { + if (AppErrorReason.UNEXPECTED_ERROR == result.appErrorReason) { finishWithError() } else { showRefusal() diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertError.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertError.kt index 377fb8cd75..66fcf70be9 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertError.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/alert/AlertError.kt @@ -2,7 +2,7 @@ package com.simprints.fingerprint.connect.screens.alert import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.core.os.bundleOf +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.alertButton import com.simprints.feature.alert.alertConfiguration import com.simprints.feature.alert.config.AlertButtonConfig @@ -16,6 +16,7 @@ import com.simprints.fingerprint.connect.screens.alert.AlertError.MULTIPLE_PAIRE import com.simprints.fingerprint.connect.screens.alert.AlertError.NOT_PAIRED import com.simprints.fingerprint.connect.screens.alert.AlertError.UNEXPECTED_ERROR import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType +import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.resources.R as IDR /** @@ -38,6 +39,7 @@ import com.simprints.infra.resources.R as IDR * @property leftButton the specific action to be triggered when the left button is clicked * @property rightButton the specific action to be triggered when the right button is clicked */ +@ExcludedFromGeneratedTestCoverageReports("Error config code") internal enum class AlertError( @StringRes val title: Int, @StringRes val message: Int, @@ -45,6 +47,7 @@ internal enum class AlertError( @DrawableRes val mainDrawable: Int, @DrawableRes val hintDrawable: Int? = null, val eventType: AlertScreenEventType, + val appErrorReason: AppErrorReason? = null, val leftButton: AlertButtonConfig, val rightButton: AlertButtonConfig? = null, ) { @@ -56,6 +59,7 @@ internal enum class AlertError( mainDrawable = IDR.drawable.ic_alert_bt, hintDrawable = IDR.drawable.ic_alert_hint_bt_disabled, eventType = AlertScreenEventType.BLUETOOTH_NOT_SUPPORTED, + appErrorReason = AppErrorReason.BLUETOOTH_NOT_SUPPORTED, leftButton = Buttons.closeButton(), ), @@ -65,6 +69,7 @@ internal enum class AlertError( mainDrawable = IDR.drawable.ic_alert_bt, hintDrawable = IDR.drawable.ic_alert_hint_bt_disabled, eventType = AlertScreenEventType.BLUETOOTH_NOT_SUPPORTED, + appErrorReason = AppErrorReason.BLUETOOTH_NOT_SUPPORTED, leftButton = Buttons.tryAgainButton(), rightButton = Buttons.bluetoothSettingsButton(), ), @@ -75,6 +80,7 @@ internal enum class AlertError( mainDrawable = IDR.drawable.ic_alert_bt, hintDrawable = IDR.drawable.ic_alert_hint_bt_disabled, eventType = AlertScreenEventType.BLUETOOTH_NO_PERMISSION, + appErrorReason = AppErrorReason.BLUETOOTH_NO_PERMISSION, leftButton = Buttons.appSettingsButton(), ), @@ -124,6 +130,7 @@ internal enum class AlertError( backgroundColor = AlertColor.Red, mainDrawable = IDR.drawable.ic_alert_default, eventType = AlertScreenEventType.UNEXPECTED_ERROR, + appErrorReason = AppErrorReason.UNEXPECTED_ERROR, leftButton = Buttons.closeButton(), ); @@ -173,17 +180,14 @@ internal enum class AlertError( messageRes = this@AlertError.message messageIcon = this@AlertError.hintDrawable eventType = this@AlertError.eventType + appErrorReason = this@AlertError.appErrorReason leftButton = this@AlertError.leftButton rightButton = this@AlertError.rightButton - - payload = bundleOf(PAYLOAD_KEY to this@AlertError.name) } companion object { - internal const val PAYLOAD_KEY = "alert_payload" - internal const val ACTION_CLOSE = "action_close" internal const val ACTION_RETRY = "action_retry" internal const val ACTION_REFUSAL = "action_refusal" diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt index 6e598f83e9..45756527c4 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt @@ -11,6 +11,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.navigation.NavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -39,12 +40,14 @@ import com.simprints.fingerprint.connect.screens.ota.OtaFragmentParams import com.simprints.infra.logging.Simber import com.simprints.infra.uibase.navigation.finishWithResult import com.simprints.infra.uibase.navigation.handleResult +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.system.Vibrate import dagger.hilt.android.AndroidEntryPoint import com.simprints.infra.resources.R as IDR @AndroidEntryPoint -internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_connect_scanner_controller) { +internal class ConnectScannerControllerFragment : + Fragment(R.layout.fragment_connect_scanner_controller) { private var shouldRequestPermissions = true @@ -68,7 +71,12 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con ) { permissions: Map -> val permission = permissions .filterKeys(bluetoothPermissions::contains) - .map { (permission, isGranted) -> requireActivity().permissionFromResult(permission, isGranted) } + .map { (permission, isGranted) -> + requireActivity().permissionFromResult( + permission, + isGranted + ) + } .worstPermissionStatus() Simber.i("Bluetooth permission: $permission") @@ -80,9 +88,14 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con } } - private fun internalNavController() = childFragmentManager - .findFragmentById(R.id.connect_scanner_host_fragment) - ?.findNavController() + private val hostFragment: Fragment? + get() = childFragmentManager.findFragmentById(R.id.connect_scanner_host_fragment) + + private val internalNavController: NavController? + get() = hostFragment?.findNavController() + + private val currentlyDisplayedInternalFragment: Fragment? + get() = hostFragment?.childFragmentManager?.fragments?.first() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -94,52 +107,89 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con fragmentViewModel.isInitialized = true } - findNavController().handleResult(this, R.id.connectScannerControllerFragment, ExitFormContract.DESTINATION, ::handleExitForm) - findNavController().handleResult(this, R.id.connectScannerControllerFragment, AlertContract.DESTINATION, ::handleResult) - - activityViewModel.showScannerIssueScreen.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { screen -> - when (screen) { - ConnectScannerIssueScreen.BluetoothNoPermission -> showAlert(AlertError.BLUETOOTH_NO_PERMISSION) - ConnectScannerIssueScreen.BluetoothNotSupported -> showAlert(AlertError.BLUETOOTH_NOT_SUPPORTED) - ConnectScannerIssueScreen.LowBattery -> showAlert(AlertError.LOW_BATTERY) - ConnectScannerIssueScreen.UnexpectedError -> showAlert(AlertError.UNEXPECTED_ERROR) - - ConnectScannerIssueScreen.ExitForm -> showExitForm() - - ConnectScannerIssueScreen.BluetoothOff -> internalNavController()?.navigate(R.id.issueBluetoothOffFragment) - ConnectScannerIssueScreen.NfcOff -> internalNavController()?.navigate(R.id.issueNfcOffFragment) - ConnectScannerIssueScreen.NfcPair -> internalNavController()?.navigate(R.id.issueNfcPairFragment) - ConnectScannerIssueScreen.SerialEntryPair -> internalNavController()?.navigate(R.id.issueSerialEntryPairFragment) - - is ConnectScannerIssueScreen.ScannerOff -> internalNavController()?.navigate( - R.id.issueScannerOffFragment, - ScannerOffFragmentArgs(screen.currentScannerId).toBundle() - ) - - is ConnectScannerIssueScreen.ScannerError -> screen.currentScannerId?.let { showKnownScannerDialog(it) } + findNavController().handleResult( + this, + R.id.connectScannerControllerFragment, + ExitFormContract.DESTINATION, + ::handleExitForm + ) + findNavController().handleResult( + this, + R.id.connectScannerControllerFragment, + AlertContract.DESTINATION, + ::handleResult + ) - is ConnectScannerIssueScreen.Ota -> internalNavController()?.navigate( - R.id.otaFragment, - OtaFragmentArgs(OtaFragmentParams(screen.availableOtas)).toBundle() - ) - } - }) - activityViewModel.scannerConnected.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { isSuccess -> - if (isSuccess) { - Vibrate.vibrate(requireContext()) - activityViewModel.finishConnectionFlow(true) - } - }) + activityViewModel.showScannerIssueScreen.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { screen -> + when (screen) { + ConnectScannerIssueScreen.BluetoothNoPermission -> showAlert(AlertError.BLUETOOTH_NO_PERMISSION) + ConnectScannerIssueScreen.BluetoothNotSupported -> showAlert(AlertError.BLUETOOTH_NOT_SUPPORTED) + ConnectScannerIssueScreen.LowBattery -> showAlert(AlertError.LOW_BATTERY) + ConnectScannerIssueScreen.UnexpectedError -> showAlert(AlertError.UNEXPECTED_ERROR) + + ConnectScannerIssueScreen.ExitForm -> showExitForm() + + ConnectScannerIssueScreen.BluetoothOff -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.issueBluetoothOffFragment + ) + + ConnectScannerIssueScreen.NfcOff -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.issueNfcOffFragment + ) + + ConnectScannerIssueScreen.NfcPair -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.issueNfcPairFragment + ) + + ConnectScannerIssueScreen.SerialEntryPair -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.issueSerialEntryPairFragment + ) + + is ConnectScannerIssueScreen.ScannerOff -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.issueScannerOffFragment, + ScannerOffFragmentArgs(screen.currentScannerId).toBundle() + ) + + is ConnectScannerIssueScreen.ScannerError -> screen.currentScannerId?.let { + showKnownScannerDialog( + it + ) + } + + is ConnectScannerIssueScreen.Ota -> internalNavController?.navigateSafely( + currentlyDisplayedInternalFragment, + R.id.otaFragment, + OtaFragmentArgs(OtaFragmentParams(screen.availableOtas)).toBundle() + ) + } + }) + activityViewModel.scannerConnected.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { isSuccess -> + if (isSuccess) { + Vibrate.vibrate(requireContext()) + activityViewModel.finishConnectionFlow(true) + } + }) - activityViewModel.finish.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { isSuccess -> - finishWithResult(isSuccess) - }) + activityViewModel.finish.observe( + viewLifecycleOwner, + LiveDataEventWithContentObserver { isSuccess -> + finishWithResult(isSuccess) + }) requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { activityViewModel.handleBackPress() } - internalNavController()?.setGraph(R.navigation.graph_connect_internal) + internalNavController?.setGraph(R.navigation.graph_connect_internal) if (shouldRequestPermissions) { shouldRequestPermissions = false @@ -162,8 +212,13 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con .create() } //Update scannerId in case it has changed - knownScannedDialog?.setTitle(getString(IDR.string.fingerprint_connect_scanner_id_confirmation_message, scannerId)) - if (internalNavController()?.currentDestination?.id == R.id.connectProgressFragment) { + knownScannedDialog?.setTitle( + getString( + IDR.string.fingerprint_connect_scanner_id_confirmation_message, + scannerId + ) + ) + if (internalNavController?.currentDestination?.id == R.id.connectProgressFragment) { knownScannedDialog?.takeUnless { it.isShowing }?.show() } } @@ -175,7 +230,13 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con override fun onResume() { super.onResume() - alertHelper.handleResume { shouldRequestPermissions = true } + + if (shouldRequestPermissions) { + shouldRequestPermissions = false + checkBluetoothPermissions() + } else { + alertHelper.handleResume { shouldRequestPermissions = true } + } } override fun onPause() { @@ -200,7 +261,11 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con } private fun showAlert(error: AlertError) { - findNavController().navigate(R.id.action_global_to_alertFragment, error.toAlertConfig().toArgs()) + findNavController().navigateSafely( + this, + R.id.action_global_to_alertFragment, + error.toAlertConfig().toArgs() + ) } private fun handleResult(result: AlertResult) { @@ -214,7 +279,8 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con } private fun showExitForm() { - findNavController().navigate( + findNavController().navigateSafely( + this, R.id.action_global_to_exitFormFragment, exitFormConfiguration { titleRes = IDR.string.exit_form_title_fingerprinting diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/bluetoothoff/BluetoothOffFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/bluetoothoff/BluetoothOffFragment.kt index 21562538c2..45bcf8de00 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/bluetoothoff/BluetoothOffFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/bluetoothoff/BluetoothOffFragment.kt @@ -20,6 +20,7 @@ import com.simprints.fingerprint.connect.screens.ConnectScannerViewModel import com.simprints.fingerprint.connect.usecase.ReportAlertScreenEventUseCase import com.simprints.fingerprint.infra.scanner.component.bluetooth.ComponentBluetoothAdapter import com.simprints.infra.uibase.extensions.showToast +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -104,18 +105,19 @@ internal class BluetoothOffFragment : Fragment(R.layout.fragment_bluetooth_off) lifecycleScope.launch { delay(FINISHED_TIME_DELAY_MS) - retryConnectAndFinishFragment() + finishFragmentAndRetryConnect() } } - private fun retryConnectAndFinishFragment() { - connectScannerViewModel.connect() - findNavController().navigate( + private fun finishFragmentAndRetryConnect() { + // Order of execution is important here. It's necessary to navigate first and attempt to + // reconnect afterwards. + findNavController().navigateSafely( + this, BluetoothOffFragmentDirections.actionIssueBluetoothOffFragmentToConnectProgressFragment(), navOptions { popUpTo(R.id.connectProgressFragment) } ) - - findNavController().popBackStack() + connectScannerViewModel.connect() } companion object { diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcoff/NfcOffFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcoff/NfcOffFragment.kt index fa35b7c258..0422062256 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcoff/NfcOffFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcoff/NfcOffFragment.kt @@ -12,6 +12,7 @@ import com.simprints.fingerprint.connect.R import com.simprints.fingerprint.connect.databinding.FragmentNfcOffBinding import com.simprints.fingerprint.connect.usecase.ReportAlertScreenEventUseCase import com.simprints.fingerprint.infra.scanner.NfcManager +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -75,11 +76,11 @@ internal class NfcOffFragment : Fragment(R.layout.fragment_nfc_off) { } private fun continueToNfcPair() { - findNavController().navigate(NfcOffFragmentDirections.actionNfcOffFragmentToNfcPairFragment()) + findNavController().navigateSafely(this, NfcOffFragmentDirections.actionNfcOffFragmentToNfcPairFragment()) } private fun continueToSerialEntryPair() { - findNavController().navigate(NfcOffFragmentDirections.actionNfcOffFragmentToSerialEntryPairFragment()) + findNavController().navigateSafely(this, NfcOffFragmentDirections.actionNfcOffFragmentToSerialEntryPairFragment()) } companion object { diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcpair/NfcPairFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcpair/NfcPairFragment.kt index 7b2b669ff7..7b7f7891cc 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcpair/NfcPairFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/nfcpair/NfcPairFragment.kt @@ -12,7 +12,6 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import androidx.navigation.navOptions import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.fingerprint.connect.R import com.simprints.fingerprint.connect.databinding.FragmentNfcPairBinding @@ -25,6 +24,7 @@ import com.simprints.fingerprint.infra.scanner.nfc.ComponentNfcTag import com.simprints.fingerprint.infra.scanner.tools.SerialNumberConverter import com.simprints.infra.recent.user.activity.RecentUserActivityManager import com.simprints.infra.uibase.extensions.showToast +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.system.Vibrate import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -192,17 +192,14 @@ internal class NfcPairFragment : Fragment(R.layout.fragment_nfc_pair) { private fun retryConnectAndFinishFragment() { determineWhetherPairingWasSuccessfulJob?.cancel() connectViewModel.connect() - findNavController().navigate( - NfcPairFragmentDirections.actionNfcPairFragmentToSerialEntryPairFragment(), - navOptions { popUpTo(R.id.connectProgressFragment) } - ) } private fun goToSerialEntryPair() { - findNavController().navigate(NfcPairFragmentDirections.actionNfcPairFragmentToSerialEntryPairFragment()) + findNavController().navigateSafely(this, NfcPairFragmentDirections.actionNfcPairFragmentToSerialEntryPairFragment()) } companion object { + private const val PAIRING_WAIT_TIMEOUT = 6000L } } diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/serialentrypair/SerialEntryPairFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/serialentrypair/SerialEntryPairFragment.kt index b42cc59c91..67cc6767e6 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/serialentrypair/SerialEntryPairFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/issues/serialentrypair/SerialEntryPairFragment.kt @@ -25,6 +25,7 @@ import com.simprints.fingerprint.infra.scanner.component.bluetooth.ComponentBlue import com.simprints.fingerprint.infra.scanner.tools.SerialNumberConverter import com.simprints.infra.recent.user.activity.RecentUserActivityManager import com.simprints.infra.uibase.extensions.showToast +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -152,7 +153,7 @@ internal class SerialEntryPairFragment : Fragment(R.layout.fragment_serial_entry recentUserActivityManager.updateRecentUserActivity { it.copy(lastScannerUsed = serialNumberConverter.convertMacAddressToSerialNumber(macAddress)) } - retryConnectAndFinishFragment() + finishFragmentAndRetryConnect() } else { handlePairingAttemptFailed(false) } @@ -176,13 +177,16 @@ internal class SerialEntryPairFragment : Fragment(R.layout.fragment_serial_entry } } - private fun retryConnectAndFinishFragment() { + private fun finishFragmentAndRetryConnect() { determineWhetherPairingWasSuccessfulJob?.cancel() - connectScannerViewModel.connect() - findNavController().navigate( + // Order of execution is important here. It's necessary to navigate first and attempt to + // reconnect afterwards. + findNavController().navigateSafely( + this, SerialEntryPairFragmentDirections.actionSerialEntryPairFragmentToConnectProgressFragment(), navOptions { popUpTo(R.id.connectProgressFragment) } ) + connectScannerViewModel.connect() } companion object { diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt index 50b5dc1247..78da698cb8 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/OtaFragment.kt @@ -15,6 +15,7 @@ import com.simprints.fingerprint.connect.R import com.simprints.fingerprint.connect.databinding.FragmentOtaBinding import com.simprints.fingerprint.connect.screens.ConnectScannerViewModel import com.simprints.fingerprint.connect.usecase.ReportAlertScreenEventUseCase +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -89,13 +90,13 @@ internal class OtaFragment : Fragment(R.layout.fragment_ota) { private fun listenForRecoveryEvent() { viewModel.otaRecovery.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { - findNavController().navigate(OtaFragmentDirections.actionOtaFragmentToOtaRecoveryFragment(it)) + findNavController().navigateSafely(this, OtaFragmentDirections.actionOtaFragmentToOtaRecoveryFragment(it)) }) } private fun listenForFailedEvent() { viewModel.otaFailed.observe(viewLifecycleOwner, LiveDataEventWithContentObserver { - findNavController().navigate(OtaFragmentDirections.actionOtaFragmentToOtaFailedFragment(it)) + findNavController().navigateSafely(this, OtaFragmentDirections.actionOtaFragmentToOtaFailedFragment(it)) }) } @@ -105,17 +106,20 @@ internal class OtaFragment : Fragment(R.layout.fragment_ota) { lifecycleScope.launch { delay(FINISHED_TIME_DELAY_MS) - retryConnectAndFinishFragment() + finishFragmentAndRetryConnect() } }) } - private fun retryConnectAndFinishFragment() { - connectScannerViewModel.connect() - findNavController().navigate( + private fun finishFragmentAndRetryConnect() { + // Order of execution is important here. It's necessary to navigate first and attempt to + // reconnect afterwards. + findNavController().navigateSafely( + this, OtaFragmentDirections.actionOtaFragmentToConnectProgressFragment(), navOptions { popUpTo(R.id.connectProgressFragment) } ) + connectScannerViewModel.connect() } companion object { diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt index 5442ec8f4e..bfeead2208 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ota/recovery/OtaRecoveryFragment.kt @@ -12,6 +12,7 @@ import com.simprints.fingerprint.connect.databinding.FragmentOtaRecoveryBinding import com.simprints.fingerprint.connect.screens.ota.OtaFragmentParams import com.simprints.fingerprint.connect.usecase.ReportAlertScreenEventUseCase import com.simprints.fingerprint.infra.scanner.domain.ota.OtaRecoveryStrategy +import com.simprints.infra.uibase.navigation.navigateSafely import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -64,7 +65,8 @@ internal class OtaRecoveryFragment : Fragment(R.layout.fragment_ota_recovery) { } private fun retryOta() { - findNavController().navigate( + findNavController().navigateSafely( + this, OtaRecoveryFragmentDirections.actionOtaRecoveryFragmentToOtaFragment(OtaFragmentParams( args.params.remainingOtas, args.params.currentRetryAttempt + 1 @@ -73,7 +75,8 @@ internal class OtaRecoveryFragment : Fragment(R.layout.fragment_ota_recovery) { } private fun goToOtaFailed() { - findNavController().navigate( + findNavController().navigateSafely( + this, OtaRecoveryFragmentDirections.actionOtaRecoveryFragmentToOtaFailedFragment(null) ) } diff --git a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelperTest.kt b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelperTest.kt index 6b47dda311..d356a00cab 100644 --- a/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelperTest.kt +++ b/fingerprint/connect/src/test/java/com/simprints/fingerprint/connect/screens/alert/AlertActivityHelperTest.kt @@ -1,16 +1,15 @@ package com.simprints.fingerprint.connect.screens.alert import android.app.Activity -import android.os.Bundle -import androidx.core.os.bundleOf import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.* +import com.google.common.truth.Truth.assertThat +import com.simprints.core.domain.response.AppErrorReason import com.simprints.feature.alert.AlertContract import com.simprints.feature.alert.AlertResult import io.mockk.clearMocks import io.mockk.mockk import io.mockk.verify -import org.junit.Assert.* +import org.junit.Assert.fail import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,10 +33,7 @@ class AlertActivityHelperTest { var refuseCalled = false helper.handleAlertResult( activity, - result = AlertResult( - AlertContract.ALERT_BUTTON_PRESSED_BACK, - bundleOf(AlertError.PAYLOAD_KEY to AlertError.LOW_BATTERY.name) - ), + result = AlertResult(AlertContract.ALERT_BUTTON_PRESSED_BACK), showRefusal = { refuseCalled = true }, retry = {}, finishWithError = {}, @@ -50,23 +46,7 @@ class AlertActivityHelperTest { var finished = false helper.handleAlertResult( activity, - result = AlertResult( - AlertContract.ALERT_BUTTON_PRESSED_BACK, - bundleOf(AlertError.PAYLOAD_KEY to AlertError.UNEXPECTED_ERROR.name) - ), - showRefusal = {}, - retry = {}, - finishWithError = { finished = true }, - ) - assertThat(finished).isTrue() - } - - @Test - fun finishes_whenHandlingBackFromMalformedError() { - var finished = false - helper.handleAlertResult( - activity, - result = AlertResult(AlertContract.ALERT_BUTTON_PRESSED_BACK, Bundle()), + result = AlertResult(AlertContract.ALERT_BUTTON_PRESSED_BACK, AppErrorReason.UNEXPECTED_ERROR), showRefusal = {}, retry = {}, finishWithError = { finished = true }, @@ -79,7 +59,7 @@ class AlertActivityHelperTest { helper.handleResume { fail("Should not be called") } helper.handleAlertResult( mockk(relaxed = true), - result = AlertResult(AlertError.ACTION_PAIR, Bundle()), + result = AlertResult(AlertError.ACTION_PAIR), showRefusal = {}, retry = {}, finishWithError = {}, @@ -94,7 +74,7 @@ class AlertActivityHelperTest { helper.handleResume { fail("Should not be called") } helper.handleAlertResult( mockk(relaxed = true), - result = AlertResult(AlertError.ACTION_CLOSE, Bundle()), + result = AlertResult(AlertError.ACTION_CLOSE), showRefusal = {}, retry = {}, finishWithError = {}, @@ -107,7 +87,7 @@ class AlertActivityHelperTest { var finished = false helper.handleAlertResult( activity, - result = AlertResult(AlertError.ACTION_CLOSE, Bundle()), + result = AlertResult(AlertError.ACTION_CLOSE), showRefusal = {}, retry = {}, finishWithError = { finished = true }, @@ -120,7 +100,7 @@ class AlertActivityHelperTest { var refuseCalled = false helper.handleAlertResult( activity, - result = AlertResult(AlertError.ACTION_REFUSAL, Bundle()), + result = AlertResult(AlertError.ACTION_REFUSAL), showRefusal = { refuseCalled = true }, retry = {}, finishWithError = {}, @@ -133,7 +113,7 @@ class AlertActivityHelperTest { var retryCalled = false helper.handleAlertResult( activity, - result = AlertResult(AlertError.ACTION_RETRY, Bundle()), + result = AlertResult(AlertError.ACTION_RETRY), showRefusal = {}, retry = { retryCalled = true }, finishWithError = {}, @@ -145,7 +125,7 @@ class AlertActivityHelperTest { fun opensSettings_whenHandlingSettingsAction() { helper.handleAlertResult( activity, - result = AlertResult(AlertError.ACTION_BT_SETTINGS, Bundle()), + result = AlertResult(AlertError.ACTION_BT_SETTINGS), showRefusal = {}, retry = {}, finishWithError = {}, @@ -157,7 +137,7 @@ class AlertActivityHelperTest { fun opensSettings_whenOpensAppSettings() { helper.handleAlertResult( activity, - result = AlertResult(AlertError.ACTION_APP_SETTINGS, Bundle()), + result = AlertResult(AlertError.ACTION_APP_SETTINGS), showRefusal = {}, retry = {}, finishWithError = {}, diff --git a/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/FingerprintBioSdk.kt b/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/FingerprintBioSdk.kt index 1bcb8aa8a0..40d0ae449c 100644 --- a/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/FingerprintBioSdk.kt +++ b/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/FingerprintBioSdk.kt @@ -31,4 +31,9 @@ class FingerprintBioSdk { +interface FingerprintMatcher { /** * Matches a [probe] against the given flow of [candidates] @@ -18,4 +18,6 @@ fun interface FingerprintMatcher { settings: MatcherSettings? ) : List + val supportedTemplateFormat: String + val matcherName: String } diff --git a/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/matching/domain/FingerprintIdentity.kt b/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/matching/domain/FingerprintIdentity.kt index 28b30b6b5b..a8c630fb77 100644 --- a/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/matching/domain/FingerprintIdentity.kt +++ b/fingerprint/infra/base-bio-sdk/src/main/java/com/simprints/fingerprint/infra/basebiosdk/matching/domain/FingerprintIdentity.kt @@ -1,3 +1,3 @@ package com.simprints.fingerprint.infra.basebiosdk.matching.domain -class FingerprintIdentity(val id: String, val fingerprints: List) +class FingerprintIdentity(val subjectId: String, val fingerprints: List) diff --git a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/BioSdkWrapper.kt b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/BioSdkWrapper.kt index c3bca50451..6051a84c56 100644 --- a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/BioSdkWrapper.kt +++ b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/BioSdkWrapper.kt @@ -6,6 +6,17 @@ import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerp import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintTemplateResponse interface BioSdkWrapper { + + // Maximum time to wait for the bio sdk to capture a fingerprint template + val scanningTimeoutMs: Long + + // Maximum time to wait for the bio sdk to transfer the fingerprint image + val imageTransferTimeoutMs: Long + + val matcherName: String + + val supportedTemplateFormat: String + suspend fun initialize() suspend fun match( @@ -16,7 +27,8 @@ interface BioSdkWrapper { suspend fun acquireFingerprintTemplate( capturingResolution: Int?, timeOutMs: Int, - qualityThreshold: Int + qualityThreshold: Int, + allowLowQualityExtraction: Boolean ): AcquireFingerprintTemplateResponse suspend fun acquireFingerprintImage(): AcquireFingerprintImageResponse diff --git a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapper.kt b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapper.kt index ecf75e3a8c..5c1e0e500c 100644 --- a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapper.kt +++ b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapper.kt @@ -15,6 +15,15 @@ import javax.inject.Inject class NECBioSdkWrapper @Inject constructor( private val bioSdk: FingerprintBioSdk ) : BioSdkWrapper { + override val scanningTimeoutMs: Long + get() = 8000 // 8 seconds = maximum duration for scanning + image transfer + image processing + NEC template extraction + override val imageTransferTimeoutMs: Long + get() = 0 // 0 seconds as the image is already captured and stored in the memory from the scanning step + + override val matcherName: String = bioSdk.matcherName + + override val supportedTemplateFormat: String = bioSdk.supportedTemplateFormat + override suspend fun initialize() = bioSdk.initialize() @@ -29,12 +38,14 @@ class NECBioSdkWrapper @Inject constructor( override suspend fun acquireFingerprintTemplate( capturingResolution: Int?, timeOutMs: Int, - qualityThreshold: Int + qualityThreshold: Int, + allowLowQualityExtraction: Boolean ): AcquireFingerprintTemplateResponse { val settings = FingerprintTemplateAcquisitionSettings( capturingResolution?.let { Dpi(it.toShort()) }, timeOutMs, - qualityThreshold + qualityThreshold, + allowLowQualityExtraction ) return bioSdk.acquireFingerprintTemplate(settings).toDomain() } @@ -52,4 +63,5 @@ fun TemplateResponse.toDomain(): AcquireFingerprint templateMetadata!!.templateFormat, templateMetadata!!.imageQualityScore ) + } diff --git a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapper.kt b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapper.kt index 8a831ca400..d388d37918 100644 --- a/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapper.kt +++ b/fingerprint/infra/bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapper.kt @@ -16,6 +16,15 @@ class SimprintsBioSdkWrapper @Inject constructor( private val bioSdk: FingerprintBioSdk ) : BioSdkWrapper { + override val scanningTimeoutMs + get() = 3000L + override val imageTransferTimeoutMs + get() = 3000L + + override val matcherName: String + get()= bioSdk.matcherName + override val supportedTemplateFormat: String + get() = bioSdk.supportedTemplateFormat override suspend fun initialize() { bioSdk.initialize() } @@ -28,12 +37,14 @@ class SimprintsBioSdkWrapper @Inject constructor( override suspend fun acquireFingerprintTemplate( capturingResolution: Int?, timeOutMs: Int, - qualityThreshold: Int + qualityThreshold: Int, + allowLowQualityExtraction: Boolean ): AcquireFingerprintTemplateResponse { val settings = FingerprintTemplateAcquisitionSettings( capturingResolution?.let { Dpi(it.toShort()) }, timeOutMs, - qualityThreshold + qualityThreshold, + allowLowQualityExtraction ) return bioSdk.acquireFingerprintTemplate(settings).toDomain() } diff --git a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapperTest.kt b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapperTest.kt index f9a3f1a413..18d63695c5 100644 --- a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapperTest.kt +++ b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/NECBioSdkWrapperTest.kt @@ -37,6 +37,21 @@ class NECBioSdkWrapperTest { necBioSdkWrapper = NECBioSdkWrapper(bioSdk) } + @Test + fun `test scanningTimeoutMs and imageTransferTimeoutMs`() { + // Given + val expectedScanningTimeoutMs = 8000L + val expectedImageTransferTimeoutMs = 0L + + // When + val actualScanningTimeoutMs = necBioSdkWrapper.scanningTimeoutMs + val actualImageTransferTimeoutMs = necBioSdkWrapper.imageTransferTimeoutMs + + // Then + Truth.assertThat(actualScanningTimeoutMs).isEqualTo(expectedScanningTimeoutMs) + Truth.assertThat(actualImageTransferTimeoutMs).isEqualTo(expectedImageTransferTimeoutMs) + } + @Test fun `initializes bio sdk`() = runTest { //When @@ -67,6 +82,7 @@ class NECBioSdkWrapperTest { val captureFingerprintStrategy = 1000 val captureTimeOutMs = 1000 val captureQualityThreshold = 100 + val captureAllowLowQualityExtraction = true val bioSdkResponse = TemplateResponse( byteArrayOf(1, 2, 3), FingerprintTemplateMetadata( @@ -78,7 +94,7 @@ class NECBioSdkWrapperTest { //When val response = necBioSdkWrapper.acquireFingerprintTemplate( - captureFingerprintStrategy, captureTimeOutMs, captureQualityThreshold + captureFingerprintStrategy, captureTimeOutMs, captureQualityThreshold, captureAllowLowQualityExtraction ) //Then @@ -88,6 +104,7 @@ class NECBioSdkWrapperTest { .isEqualTo(captureFingerprintStrategy.toShort()) Truth.assertThat(timeOutMs).isEqualTo(captureTimeOutMs) Truth.assertThat(qualityThreshold).isEqualTo(captureQualityThreshold) + Truth.assertThat(allowLowQualityExtraction).isEqualTo(captureAllowLowQualityExtraction) } Truth.assertThat(bioSdkResponse.template).isEqualTo(response.template) Truth.assertThat(bioSdkResponse.templateMetadata?.templateFormat) @@ -107,7 +124,7 @@ class NECBioSdkWrapperTest { ) assertThrows { - necBioSdkWrapper.acquireFingerprintTemplate(1, 1, 1) + necBioSdkWrapper.acquireFingerprintTemplate(1, 1, 1, true) } } diff --git a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapperTest.kt b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapperTest.kt index 3ed4d99555..c7aaf8d6ce 100644 --- a/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapperTest.kt +++ b/fingerprint/infra/bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdk/SimprintsBioSdkWrapperTest.kt @@ -32,6 +32,21 @@ class SimprintsBioSdkWrapperTest { simprintsBioSdkWrapper = SimprintsBioSdkWrapper(bioSdk) } + @Test + fun `test scanningTimeoutMs and imageTransferTimeoutMs`() { + // Given + val expectedScanningTimeoutMs = 3000L + val expectedImageTransferTimeoutMs = 3000L + + // When + val actualScanningTimeoutMs = simprintsBioSdkWrapper.scanningTimeoutMs + val actualImageTransferTimeoutMs = simprintsBioSdkWrapper.imageTransferTimeoutMs + + // Then + assertThat(actualScanningTimeoutMs).isEqualTo(expectedScanningTimeoutMs) + assertThat(actualImageTransferTimeoutMs).isEqualTo(expectedImageTransferTimeoutMs) + } + @Test fun `Initializes bio sdk`() = runTest { //When @@ -62,18 +77,19 @@ class SimprintsBioSdkWrapperTest { val captureFingerprintStrategy = 1000 val captureTimeOutMs = 1000 val captureQualityThreshold = 100 + val captureAllowLowQualityExtraction = true val bioSdkResponse = TemplateResponse( byteArrayOf(1, 2, 3), FingerprintTemplateMetadata( - "TemplateFormat", 100 - ) + "TemplateFormat", 100 + ) ) val settingsSlot = slot() coEvery { bioSdk.acquireFingerprintTemplate(capture(settingsSlot)) } returns bioSdkResponse //When val response = simprintsBioSdkWrapper.acquireFingerprintTemplate( - captureFingerprintStrategy, captureTimeOutMs, captureQualityThreshold + captureFingerprintStrategy, captureTimeOutMs, captureQualityThreshold, captureAllowLowQualityExtraction ) //Then @@ -82,6 +98,7 @@ class SimprintsBioSdkWrapperTest { assertThat(captureFingerprintDpi?.value).isEqualTo(captureFingerprintStrategy.toShort()) assertThat(timeOutMs).isEqualTo(captureTimeOutMs) assertThat(qualityThreshold).isEqualTo(captureQualityThreshold) + assertThat(allowLowQualityExtraction).isEqualTo(captureAllowLowQualityExtraction) } assertThat(bioSdkResponse.template).isEqualTo(response.template) assertThat(bioSdkResponse.templateMetadata?.templateFormat).isEqualTo(response.templateFormat) @@ -90,10 +107,16 @@ class SimprintsBioSdkWrapperTest { @Test fun `Fails if template does not have meta data`() = runTest { - coEvery { bioSdk.acquireFingerprintTemplate(any()) } returns TemplateResponse(byteArrayOf(1, 2, 3), null) + coEvery { bioSdk.acquireFingerprintTemplate(any()) } returns TemplateResponse( + byteArrayOf( + 1, + 2, + 3 + ), null + ) assertThrows { - simprintsBioSdkWrapper.acquireFingerprintTemplate(1, 1, 1) + simprintsBioSdkWrapper.acquireFingerprintTemplate(1, 1, 1, true) } } diff --git a/fingerprint/infra/nec-bio-sdk/build.gradle.kts b/fingerprint/infra/nec-bio-sdk/build.gradle.kts index 152ed3a973..7321fba1bd 100644 --- a/fingerprint/infra/nec-bio-sdk/build.gradle.kts +++ b/fingerprint/infra/nec-bio-sdk/build.gradle.kts @@ -9,6 +9,8 @@ android { dependencies { implementation(project(":fingerprint:infra:scanner")) implementation(project(":infra:license")) + implementation(project(":infra:security")) + implementation(project(":infra:recent-user-activity")) api(project(":fingerprint:infra:base-bio-sdk")) //NEC SDK lib and wrapper diff --git a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCase.kt b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCase.kt new file mode 100644 index 0000000000..bb0cb58bcb --- /dev/null +++ b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCase.kt @@ -0,0 +1,44 @@ +package com.simprints.fingerprint.infra.necsdkimpl.acquisition.template + +import androidx.core.content.edit +import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapperFactory +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.security.SecurityManager +import javax.inject.Inject + +@OptIn(ExperimentalStdlibApi::class) +internal class AcquireImageDistortionConfigurationUseCase @Inject constructor( + private val fingerprintCaptureWrapperFactory: FingerprintCaptureWrapperFactory, + securityManager: SecurityManager, + private val recentUserActivityManager: RecentUserActivityManager +) { + companion object { + private const val SECURE_PREFS_FILENAME = "1bbc46c9-e911-4c5b-859f-594e5b145ec7" + private const val DISTORTION_CONFIGURATION_KEY = "distortion_configuration" + } + + private var sharedPreferences = + securityManager.buildEncryptedSharedPreferences(SECURE_PREFS_FILENAME) + + // try to read the image distortion configuration from the shared preferences + // if it's not available, acquire it from the scanner and save it in the shared preferences + suspend operator fun invoke(): ByteArray = + sharedPreferences.getString(getDistortionConfigurationKey(), null)?.hexToByteArray() + ?: acquireImageDistortionConfigurationFromScanner().also { + sharedPreferences.edit { + putString(getDistortionConfigurationKey(), it.toHexString()) + } + } + + // Each scanner has a unique distortion configuration file + // store the distortion configuration for each scanner separately using the scanner's ID as the key + private suspend fun getDistortionConfigurationKey() = + "$DISTORTION_CONFIGURATION_KEY-${recentUserActivityManager.getRecentUserActivity().lastScannerUsed}" + + private suspend fun acquireImageDistortionConfigurationFromScanner() = + fingerprintCaptureWrapperFactory + .captureWrapper + .acquireImageDistortionMatrixConfiguration() + +} + diff --git a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt index e231d5ecb1..b4941d9bd9 100644 --- a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt +++ b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt @@ -6,5 +6,6 @@ import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.model data class FingerprintTemplateAcquisitionSettings( val processingResolution: Dpi?, val timeOutMs: Int, - val qualityThreshold: Int + val qualityThreshold: Int, + val allowLowQualityExtraction: Boolean ) diff --git a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt index 62ede90219..ce79571342 100644 --- a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt +++ b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt @@ -1,82 +1,82 @@ package com.simprints.fingerprint.infra.necsdkimpl.acquisition.template +import com.simprints.core.DispatcherIO import com.simprints.fingerprint.infra.basebiosdk.acquisition.FingerprintTemplateProvider import com.simprints.fingerprint.infra.basebiosdk.acquisition.domain.TemplateResponse -import com.simprints.fingerprint.infra.basebiosdk.exceptions.BioSdkException import com.simprints.fingerprint.infra.necsdkimpl.acquisition.image.ProcessedImageCache import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapperFactory import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.models.Dpi import com.simprints.infra.logging.Simber import com.simprints.sgimagecorrection.SecugenImageCorrection +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import javax.inject.Inject internal class FingerprintTemplateProviderImpl @Inject constructor( private val fingerprintCaptureWrapperFactory: FingerprintCaptureWrapperFactory, private val decodeWSQImageUseCase: DecodeWSQImageUseCase, private val secugenImageCorrection: SecugenImageCorrection, + private val acquireImageDistortionConfigurationUseCase: AcquireImageDistortionConfigurationUseCase, private val calculateNecImageQualityUseCase: CalculateNecImageQualityUseCase, private val captureProcessedImageCache: ProcessedImageCache, - private val extractNecTemplateUseCase: ExtractNecTemplateUseCase + private val extractNecTemplateUseCase: ExtractNecTemplateUseCase, + @DispatcherIO private val ioDispatcher: CoroutineDispatcher, ) : FingerprintTemplateProvider { - private lateinit var imageDistortionConfiguration: ByteArray - override suspend fun acquireFingerprintTemplate(settings: FingerprintTemplateAcquisitionSettings?): TemplateResponse { - require(settings != null) { "Settings cannot be null" } - readImageDistortionConfiguration() + override suspend fun acquireFingerprintTemplate(settings: FingerprintTemplateAcquisitionSettings?) = + withContext(ioDispatcher) { + require(settings != null) { "Settings cannot be null" } - // 1- Acquire unprocessed image from the scanner - // 2- Use secugen image processing to convert it to wsq format - // 3- Use wsq sdk to convert it to bitmap - // 4- Use nec sdk to check image quality - // 5- Use nec sdk to convert it to template - // 6- Return the template and cache the image for later use + // 1- Acquire unprocessed image from the scanner + // 2- Use secugen image processing to convert it to wsq format + // 3- Use wsq sdk to convert it to bitmap + // 4- Use nec sdk to check image quality + // 5- Use nec sdk to convert it to template + // 6- Return the template and cache the image for later use + val captureWrapper = fingerprintCaptureWrapperFactory.captureWrapper - val captureWrapper = fingerprintCaptureWrapperFactory.captureWrapper - - // Always require a new image from the scanner using the minimum resolution as we will - // process it using secugen image correction - log("Acquiring unprocessed image") - val unprocessedImage = captureWrapper.acquireUnprocessedImage( - Dpi(MIN_CAPTURE_DPI) - ).rawUnprocessedImage - captureProcessedImageCache.recentlyCapturedImage = unprocessedImage.imageData - log("Unprocessed image acquired, processing it") - val decodedImage = decodeWSQImageUseCase(unprocessedImage) - log("Image decoded successfully ${decodedImage.resolution}") - log("processing image using secugen image correction") - val secugenProcessedImage = processImage(settings, decodedImage) - log("quality checking image using nec sdk") - val qualityScore = calculateNecImageQualityUseCase(secugenProcessedImage) - log("quality score is $qualityScore the threshold is ${settings.qualityThreshold}") - if (qualityScore < settings.qualityThreshold) - throw BioSdkException.ImageQualityBelowThresholdException(qualityScore) - log("extracting template using nec sdk") - return extractNecTemplateUseCase(secugenProcessedImage, qualityScore) - } + // Always require a new image from the scanner using the minimum resolution as we will + // process it using secugen image correction + log("Acquiring unprocessed image") + val unprocessedImage = captureWrapper.acquireUnprocessedImage( + Dpi(MIN_CAPTURE_DPI) + ).rawUnprocessedImage + captureProcessedImageCache.recentlyCapturedImage = unprocessedImage.imageData + log("Unprocessed image acquired, processing it") + val decodedImage = decodeWSQImageUseCase(unprocessedImage) + log("Image decoded successfully ${decodedImage.resolution}") + log("processing image using secugen image correction") + val secugenProcessedImage = processImage(settings, decodedImage) + log("quality checking image using nec sdk") + val qualityScore = calculateNecImageQualityUseCase(secugenProcessedImage) + log("quality score is $qualityScore the threshold is ${settings.qualityThreshold}") + if (qualityScore < settings.qualityThreshold && !settings.allowLowQualityExtraction) + // if the quality score is less than the threshold return an empty template + TemplateResponse( + byteArrayOf(), + FingerprintTemplateMetadata( + templateFormat = NEC_TEMPLATE_FORMAT, + imageQualityScore = qualityScore + ) + ) else { + log("extracting template using nec sdk") + extractNecTemplateUseCase(secugenProcessedImage, qualityScore) + } + } private fun log(message: String) { Simber.tag("NEC_SDK").d(message) } - private suspend fun readImageDistortionConfiguration() { - // if imageDistortionConfiguration not initialized read it from the scanner - if (!::imageDistortionConfiguration.isInitialized) { - log("Reading image distortion configuration from the scanner") - imageDistortionConfiguration = - fingerprintCaptureWrapperFactory.captureWrapper.acquireImageDistortionMatrixConfiguration() - .configurationBytes - //Todo save the configuration in the datastore for later use to avoid reading it from the scanner every time - } - } - private fun processImage( + private suspend fun processImage( settings: FingerprintTemplateAcquisitionSettings, rawImage: FingerprintRawImage ): FingerprintImage { val scannerConfig = SecugenImageCorrection.ScannerConfig( - imageDistortionConfiguration, + acquireImageDistortionConfigurationUseCase(), settings.processingResolution?.value ?: DEFAULT_RESOLUTION, rawImage.un20SerialNumber, rawImage.brightness diff --git a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImpl.kt b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImpl.kt index 5bec7abdd0..ba9ebd7b2c 100644 --- a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImpl.kt +++ b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImpl.kt @@ -5,7 +5,10 @@ import com.simprints.core.tools.utils.EncodingUtilsImpl import com.simprints.fingerprint.infra.basebiosdk.exceptions.BioSdkException import com.simprints.fingerprint.infra.basebiosdk.initialization.SdkInitializer import com.simprints.infra.license.LicenseRepository +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor +import com.simprints.infra.license.determineLicenseStatus import com.simprints.necwrapper.nec.NEC import com.simprints.necwrapper.nec.tools.toByteBuffer import dagger.hilt.android.qualifiers.ApplicationContext @@ -15,16 +18,23 @@ internal class SdkInitializerImpl @Inject constructor( @ApplicationContext private val context: Context, private val necInstance: NEC, private val licenseRepository: LicenseRepository, + private val saveLicenseCheck: SaveLicenseCheckEventUseCase ) : SdkInitializer { override suspend fun initialize(initializationParams: Unit?) { - val licence = licenseRepository.getCachedLicense(Vendor.NEC) + var licenseStatus: LicenseStatus? = null try { - necInstance.init(licence.encodeAndConvertToByteBuffer(), context) + val licence = licenseRepository.getCachedLicense(Vendor.NEC) + licenseStatus = licence.determineLicenseStatus() + if (licenseStatus != LicenseStatus.VALID) { + throw BioSdkException.BioSdkInitializationException(message = "License is $licenseStatus") + } + necInstance.init(licence!!.data.encodeAndConvertToByteBuffer(), context) } catch (e: Exception) { - // if we fail to init NEC we should delete the license from the local storage - // because it is most likely corrupted or expired license licenseRepository.deleteCachedLicense(Vendor.NEC) + licenseStatus = LicenseStatus.ERROR throw BioSdkException.BioSdkInitializationException(e) + } finally { + licenseStatus?.let { saveLicenseCheck(Vendor.NEC, it) } } } } diff --git a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImpl.kt b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImpl.kt index 30cdf004f5..ec4101afe5 100644 --- a/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImpl.kt +++ b/fingerprint/infra/nec-bio-sdk/src/main/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImpl.kt @@ -14,11 +14,19 @@ import javax.inject.Inject internal class FingerprintMatcherImpl @Inject constructor( private val nec: NEC ) : FingerprintMatcher { + + override val supportedTemplateFormat: String = NEC_TEMPLATE_FORMAT + override val matcherName: String = "NEC" + override suspend fun match( probe: FingerprintIdentity, candidates: List, settings: NecMatchingSettings? ): List { + // if probe template format is not supported by NEC matcher, return empty list + if (probe.templateFormatNotSupportedByNecMatcher()) { + return emptyList() + } return if (settings?.crossFingerComparison == true) { crossFingerMatching(probe, candidates) } else { @@ -49,16 +57,12 @@ internal class FingerprintMatcherImpl @Inject constructor( ): MatchResult { var fingers = 0 // the number of fingers used in matching val total = probe.fingerprints.sumOf { fingerprint -> - // we should ignore probe fingers that doesn't have matching candidate fingers - - require(fingerprint.format == NEC_TEMPLATE_FORMAT) candidate.templateForFinger(fingerprint.fingerId)?.let { candidateTemplate -> - require(candidateTemplate.format == NEC_TEMPLATE_FORMAT) fingers++ verify(fingerprint, candidateTemplate) } ?: 0.toDouble() } - return MatchResult(candidate.id, getOverallScore(total, fingers)) + return MatchResult(candidate.subjectId, getOverallScore(total, fingers)) } private fun verify(probe: Fingerprint, candidate: Fingerprint) = try { @@ -88,7 +92,7 @@ internal class FingerprintMatcherImpl @Inject constructor( } } // Matching score = total/number of fingers - return MatchResult(candidate.id, getOverallScore(total, fingers)) + return MatchResult(candidate.subjectId, getOverallScore(total, fingers)) } private fun FingerprintIdentity.templateForFinger(fingerId: FingerIdentifier) = @@ -102,3 +106,6 @@ internal class FingerprintMatcherImpl @Inject constructor( (total / fingers).toFloat() } } + +private fun FingerprintIdentity.templateFormatNotSupportedByNecMatcher(): Boolean = + fingerprints.any { it.format != NEC_TEMPLATE_FORMAT } diff --git a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCaseTest.kt b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCaseTest.kt new file mode 100644 index 0000000000..4d51ff4031 --- /dev/null +++ b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/AcquireImageDistortionConfigurationUseCaseTest.kt @@ -0,0 +1,92 @@ +package com.simprints.fingerprint.infra.necsdkimpl.acquisition.template + +import android.content.SharedPreferences +import com.google.common.truth.Truth +import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapper +import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapperFactory +import com.simprints.infra.recent.user.activity.RecentUserActivityManager +import com.simprints.infra.security.SecurityManager +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalStdlibApi::class) +class AcquireImageDistortionConfigurationUseCaseTest { + + @MockK + private lateinit var fingerprintCaptureWrapperFactory: FingerprintCaptureWrapperFactory + + @MockK + private lateinit var captureWrapper: FingerprintCaptureWrapper + + @MockK + private lateinit var recentUserActivityManager: RecentUserActivityManager + + @MockK + private lateinit var securityManager: SecurityManager + + @RelaxedMockK + private lateinit var sharedPreferences: SharedPreferences + + private lateinit var acquireImageDistortionConfigurationUseCase: AcquireImageDistortionConfigurationUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this) + + every { fingerprintCaptureWrapperFactory.captureWrapper } returns captureWrapper + every { securityManager.buildEncryptedSharedPreferences(any()) } returns sharedPreferences + coEvery { recentUserActivityManager.getRecentUserActivity().lastScannerUsed } returns "scannerId" + + acquireImageDistortionConfigurationUseCase = AcquireImageDistortionConfigurationUseCase( + fingerprintCaptureWrapperFactory, securityManager, recentUserActivityManager + ) + + } + + @Test + fun `should acquire image distortion configuration from scanner if not available in shared preferences`() = + runTest { + + // given + val distortionConfiguration = byteArrayOf(1, 2, 3).toHexString() + every { sharedPreferences.getString(any(),null) } returns null + coEvery { + captureWrapper.acquireImageDistortionMatrixConfiguration() + } returns distortionConfiguration.hexToByteArray() + + + // when + val result = acquireImageDistortionConfigurationUseCase() + + // then + Truth.assertThat(result.toHexString()) + .isEqualTo(distortionConfiguration) + coVerify { captureWrapper.acquireImageDistortionMatrixConfiguration() } + coVerify { sharedPreferences.edit().putString(any(), any())} + } + + @Test + fun `should acquire image distortion configuration from shared preferences if available`() = + runTest { + + // given + val distortionConfiguration = byteArrayOf(1, 2, 3).toHexString() + every { sharedPreferences.getString(any(),null) } returns distortionConfiguration + + + // when + val result = acquireImageDistortionConfigurationUseCase() + + // then + Truth.assertThat(result.toHexString()) + .isEqualTo(distortionConfiguration) + coVerify(exactly = 0) { captureWrapper.acquireImageDistortionMatrixConfiguration() } + } +} diff --git a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImplTest.kt b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImplTest.kt index 87b5175f18..c3e3130f01 100644 --- a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImplTest.kt +++ b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/acquisition/template/FingerprintTemplateProviderImplTest.kt @@ -1,17 +1,23 @@ package com.simprints.fingerprint.infra.necsdkimpl.acquisition.template -import com.simprints.fingerprint.infra.basebiosdk.exceptions.BioSdkException +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.google.common.truth.Truth +import com.simprints.fingerprint.infra.basebiosdk.acquisition.domain.TemplateResponse import com.simprints.fingerprint.infra.necsdkimpl.acquisition.image.ProcessedImageCache import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapper import com.simprints.fingerprint.infra.scanner.capture.FingerprintCaptureWrapperFactory import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.models.Dpi import com.simprints.sgimagecorrection.SecugenImageCorrection +import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test class FingerprintTemplateProviderImplTest { @@ -24,10 +30,13 @@ class FingerprintTemplateProviderImplTest { @RelaxedMockK private lateinit var secugenImageCorrection: SecugenImageCorrection - @RelaxedMockK + @MockK + private lateinit var acquireImageDistortionConfigurationUseCase: AcquireImageDistortionConfigurationUseCase + + @MockK private lateinit var fingerprintCaptureWrapperFactory: FingerprintCaptureWrapperFactory - @RelaxedMockK + @MockK private lateinit var extractNecTemplateUseCase: ExtractNecTemplateUseCase @RelaxedMockK @@ -39,19 +48,33 @@ class FingerprintTemplateProviderImplTest { @RelaxedMockK private lateinit var captureWrapper: FingerprintCaptureWrapper + @get:Rule + val rule = InstantTaskExecutorRule() + + @get:Rule + val testCoroutineRule = TestCoroutineRule() + @Before fun setUp() { MockKAnnotations.init(this) every { fingerprintCaptureWrapperFactory.captureWrapper } returns captureWrapper - + coEvery { acquireImageDistortionConfigurationUseCase.invoke() } returns byteArrayOf(1, 2, 3) + coEvery{ + extractNecTemplateUseCase.invoke(any(), any()) + } returns TemplateResponse(byteArrayOf(1, 2, 3),FingerprintTemplateMetadata( + templateFormat = NEC_TEMPLATE_FORMAT, + imageQualityScore = 10 + )) fingerprintTemplateProviderImpl = FingerprintTemplateProviderImpl( fingerprintCaptureWrapperFactory = fingerprintCaptureWrapperFactory, decodeWSQImageUseCase = decodeWsqImageUseCase, secugenImageCorrection = secugenImageCorrection, + acquireImageDistortionConfigurationUseCase = acquireImageDistortionConfigurationUseCase, calculateNecImageQualityUseCase = calculateNecImageQualityUseCase, + captureProcessedImageCache = processedImageCache, extractNecTemplateUseCase = extractNecTemplateUseCase, - captureProcessedImageCache = processedImageCache + ioDispatcher = testCoroutineRule.testCoroutineDispatcher ) } @@ -72,7 +95,8 @@ class FingerprintTemplateProviderImplTest { val settings = FingerprintTemplateAcquisitionSettings( processingResolution = Dpi(500), qualityThreshold = 0, - timeOutMs = 0 + timeOutMs = 0, + allowLowQualityExtraction = false ) // When fingerprintTemplateProviderImpl.acquireFingerprintTemplate(settings) @@ -89,18 +113,19 @@ class FingerprintTemplateProviderImplTest { } } - @Test(expected = BioSdkException.ImageQualityBelowThresholdException::class) - fun `test acquireFingerprintTemplate fails if quality score is less than threshold`() = + @Test + fun `test acquireFingerprintTemplate fails if quality score is less than threshold and allowLowQualityExtraction is false`() = runTest { // Given every { calculateNecImageQualityUseCase.invoke(any()) } returns 10 val settings = FingerprintTemplateAcquisitionSettings( processingResolution = Dpi(500), qualityThreshold = 20, - timeOutMs = 0 + timeOutMs = 0, + allowLowQualityExtraction = false ) // When - fingerprintTemplateProviderImpl.acquireFingerprintTemplate(settings) + val result=fingerprintTemplateProviderImpl.acquireFingerprintTemplate(settings) // Then coVerify { captureWrapper @@ -113,6 +138,31 @@ class FingerprintTemplateProviderImplTest { coVerify(exactly = 0) { extractNecTemplateUseCase.invoke(any(), any()) } - + Truth.assertThat(result.template).isEmpty() + } + @Test + fun `test acquireFingerprintTemplate extracts template if quality score is less than threshold and allowLowQualityExtraction is true`() = + runTest { + // Given + every { calculateNecImageQualityUseCase.invoke(any()) } returns 10 + val settings = FingerprintTemplateAcquisitionSettings( + processingResolution = Dpi(500), + qualityThreshold = 20, + timeOutMs = 0, + allowLowQualityExtraction = true + ) + // When + val result=fingerprintTemplateProviderImpl.acquireFingerprintTemplate(settings) + // Then + coVerify { + captureWrapper + .acquireUnprocessedImage(any()) + decodeWsqImageUseCase.invoke(any()) + processedImageCache.recentlyCapturedImage = any() + secugenImageCorrection.processRawImage(any(), any()) + calculateNecImageQualityUseCase.invoke(any()) + extractNecTemplateUseCase.invoke(any(), any()) + } + Truth.assertThat(result.template).isNotEmpty() } } diff --git a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImplTest.kt b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImplTest.kt index f9c85966d7..2de95064b1 100644 --- a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImplTest.kt +++ b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/initialization/SdkInitializerImplTest.kt @@ -2,10 +2,14 @@ package com.simprints.fingerprint.infra.necsdkimpl.initialization import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import com.simprints.fingerprint.infra.basebiosdk.exceptions.BioSdkException import com.simprints.fingerprint.infra.basebiosdk.initialization.SdkInitializer import com.simprints.infra.license.LicenseRepository +import com.simprints.infra.license.LicenseStatus +import com.simprints.infra.license.SaveLicenseCheckEventUseCase import com.simprints.infra.license.Vendor +import com.simprints.infra.license.remote.License import com.simprints.necwrapper.nec.NEC import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -14,6 +18,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.justRun +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Before @@ -33,30 +38,37 @@ class SdkInitializerImplTest { @MockK lateinit var licenseRepository: LicenseRepository + @MockK + lateinit var saveLicenseCheck: SaveLicenseCheckEventUseCase private lateinit var sdkInitializer: SdkInitializer + + @Before fun setUp() { MockKAnnotations.init(this) justRun { nec.init(any(), context) } - coJustRun { licenseRepository.deleteCachedLicense(Vendor.NEC) } + coEvery { + licenseRepository.getCachedLicense(Vendor.NEC) + } returns License("2133-12-30T17:32:28Z", "license") + coJustRun { licenseRepository.deleteCachedLicense(Vendor.NEC) } sdkInitializer = - SdkInitializerImpl(context, nec, licenseRepository) + SdkInitializerImpl(context, nec, licenseRepository, saveLicenseCheck) } @Test fun `test initialize success`() = runTest { //Given - coEvery { - licenseRepository.getCachedLicense(Vendor.NEC) - } returns "license" + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheck(Vendor.NEC, capture(licenseStatusSlot)) } // When sdkInitializer.initialize(null) // Then verify { nec.init(any(), context) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.VALID) } @Test(expected = BioSdkException.BioSdkInitializationException::class) @@ -64,14 +76,28 @@ class SdkInitializerImplTest { //Given coEvery { licenseRepository.getCachedLicense(Vendor.NEC) - } returns "license" + } returns License("2011-12-30T17:32:28Z", "license") + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheck(Vendor.NEC, capture(licenseStatusSlot)) } + // When + sdkInitializer.initialize(null) + // Then + coVerify { licenseRepository.deleteCachedLicense(Vendor.NEC) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.EXPIRED) + } + + @Test(expected = BioSdkException.BioSdkInitializationException::class) + fun `test error during initialization`() = runTest { + //Given every { nec.init(any(),context) } throws Exception() + val licenseStatusSlot = slot() + coJustRun { saveLicenseCheck(Vendor.NEC, capture(licenseStatusSlot)) } // When sdkInitializer.initialize(null) // Then coVerify { licenseRepository.deleteCachedLicense(Vendor.NEC) } + assertThat(licenseStatusSlot.captured).isEqualTo(LicenseStatus.ERROR) } - } diff --git a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImplTest.kt b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImplTest.kt index 26528268f5..4c96255ae2 100644 --- a/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImplTest.kt +++ b/fingerprint/infra/nec-bio-sdk/src/test/java/com/simprints/fingerprint/infra/necsdkimpl/matching/FingerprintMatcherImplTest.kt @@ -114,14 +114,33 @@ class FingerprintMatcherImplTest { val candidate = generatePersonIdentity(FingerIdentifier.LEFT_INDEX_FINGER, FingerIdentifier.RIGHT_THUMB) // When - matcher.match(probe, listOf(candidate), NecMatchingSettings(false)) + matcher.match(probe, listOf(candidate), NecMatchingSettings(false)) } - private fun generatePersonIdentity(vararg fingers: FingerIdentifier) = FingerprintIdentity( + @Test + fun `test match FingerprintIdentities probe with different template format`() = runTest { + // Given + val probe = + generatePersonIdentity( + FingerIdentifier.LEFT_INDEX_FINGER, + FingerIdentifier.RIGHT_INDEX_FINGER, format = "Unsupported" + ) + val candidate = + generatePersonIdentity(FingerIdentifier.LEFT_THUMB, FingerIdentifier.RIGHT_THUMB) + // When + val result = matcher.match(probe, listOf(candidate), NecMatchingSettings(false)) + // Then + Truth.assertThat(result.size).isEqualTo(0) + } + + private fun generatePersonIdentity( + vararg fingers: FingerIdentifier, + format: String = "NEC_1" + ) = FingerprintIdentity( "ID", fingers.map { - Fingerprint(it, ByteArray(0), "NEC_1") + Fingerprint(it, ByteArray(0), format) } ) diff --git a/fingerprint/infra/scanner/build.gradle.kts b/fingerprint/infra/scanner/build.gradle.kts index 6a0b8898ab..87ad85bff0 100644 --- a/fingerprint/infra/scanner/build.gradle.kts +++ b/fingerprint/infra/scanner/build.gradle.kts @@ -8,20 +8,6 @@ android { defaultConfig { consumerProguardFiles("consumer-rules.pro") } - - buildTypes { - getByName("release") { - buildConfigField("long", "FIRMWARE_UPDATE_WORKER_INTERVAL_MINUTES", "1440L") - } - - getByName("staging") { - buildConfigField("long", "FIRMWARE_UPDATE_WORKER_INTERVAL_MINUTES", "15L") - } - - getByName("debug") { - buildConfigField("long", "FIRMWARE_UPDATE_WORKER_INTERVAL_MINUTES", "15L") - } - } } dependencies { @@ -29,7 +15,6 @@ dependencies { implementation(libs.kotlin.reflect) implementation(libs.kotlin.coroutine.rx2.adapter) - implementation(libs.workManager.work) implementation(libs.retrofit.core) runtimeOnly(libs.jackson.core) diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapper.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapper.kt index e1a9bd4593..fb5624353b 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapper.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapper.kt @@ -2,7 +2,6 @@ package com.simprints.fingerprint.infra.scanner.capture import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintImageResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintTemplateResponse -import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireImageDistortionMatrixConfigurationResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireUnprocessedImageResponse import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.models.Dpi @@ -14,11 +13,12 @@ interface FingerprintCaptureWrapper { suspend fun acquireFingerprintTemplate( captureDpi: Dpi?, timeOutMs: Int, - qualityThreshold: Int + qualityThreshold: Int, + allowLowQualityExtraction: Boolean ): AcquireFingerprintTemplateResponse val templateFormat: String get() = "ISO_19794_2" // This is the only template format Veros support - suspend fun acquireImageDistortionMatrixConfiguration(): AcquireImageDistortionMatrixConfigurationResponse + suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt index 4508979f5f..d5bf051dba 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1.kt @@ -2,7 +2,6 @@ package com.simprints.fingerprint.infra.scanner.capture import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintImageResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintTemplateResponse -import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireImageDistortionMatrixConfigurationResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireUnprocessedImageResponse import com.simprints.fingerprint.infra.scanner.exceptions.safe.NoFingerDetectedException import com.simprints.fingerprint.infra.scanner.exceptions.safe.ScannerDisconnectedException @@ -33,7 +32,7 @@ internal class FingerprintCaptureWrapperV1( throw UnavailableVero2FeatureException(UnavailableVero2Feature.IMAGE_ACQUISITION) } - override suspend fun acquireImageDistortionMatrixConfiguration(): AcquireImageDistortionMatrixConfigurationResponse { + override suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray { throw UnavailableVero2FeatureException(UnavailableVero2Feature.IMAGE_ACQUISITION) } @@ -41,7 +40,10 @@ internal class FingerprintCaptureWrapperV1( captureDpi: Dpi?, timeOutMs: Int, qualityThreshold: Int, + allowLowQualityExtraction: Boolean, ): AcquireFingerprintTemplateResponse = withContext(ioDispatcher) { + // V1 scanner does not have a separate method to extract fingerprint template so we should + // ignore the allowLowQualityExtraction parameter suspendCancellableCoroutine { cont -> scannerV1.startContinuousCapture( qualityThreshold, diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt index 3f1428845d..4fe099d743 100644 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt +++ b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2.kt @@ -2,7 +2,6 @@ package com.simprints.fingerprint.infra.scanner.capture import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintImageResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintTemplateResponse -import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireImageDistortionMatrixConfigurationResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireUnprocessedImageResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.RawUnprocessedImage import com.simprints.fingerprint.infra.scanner.exceptions.safe.NoFingerDetectedException @@ -27,10 +26,9 @@ internal class FingerprintCaptureWrapperV2( private val ioDispatcher: CoroutineDispatcher, ) : FingerprintCaptureWrapper { - override suspend fun acquireImageDistortionMatrixConfiguration(): AcquireImageDistortionMatrixConfigurationResponse = + override suspend fun acquireImageDistortionMatrixConfiguration(): ByteArray = withContext(ioDispatcher) { scannerV2.acquireImageDistortionConfigurationMatrix() - .map { AcquireImageDistortionMatrixConfigurationResponse(it) } .switchIfEmpty(Single.error(NoImageDistortionConfigurationMatrixException())) .wrapErrorsFromScanner().await() } @@ -76,7 +74,8 @@ internal class FingerprintCaptureWrapperV2( override suspend fun acquireFingerprintTemplate( captureDpi: Dpi?, timeOutMs: Int, - qualityThreshold: Int + qualityThreshold: Int, + allowLowQualityExtraction: Boolean ): AcquireFingerprintTemplateResponse = withContext(ioDispatcher) { require(captureDpi != null && (captureDpi.value in MIN_CAPTURE_DPI..MAX_CAPTURE_DPI)) { "Capture DPI must be between $MIN_CAPTURE_DPI and $MAX_CAPTURE_DPI" @@ -87,10 +86,18 @@ internal class FingerprintCaptureWrapperV2( .andThen(scannerV2.getImageQualityScore()) .switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire image quality score"))) .setLedStateBasedOnQualityScoreOrInterpretAsNoFingerDetected(qualityThreshold) - .acquireTemplateAndAssembleResponse() - .switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire template"))) - .ifNoFingerDetectedThenSetBadScanLedState() - .wrapErrorsFromScanner() + .flatMap { qualityScore -> + // If the quality score is below the threshold and we don't allow low quality extraction, return an empty template + if (qualityScore < qualityThreshold && !allowLowQualityExtraction) { + Single.just(AcquireFingerprintTemplateResponse(ByteArray(0), templateFormat, qualityScore)) + } else { + Single.just(qualityScore) + .acquireTemplateAndAssembleResponse() + .switchIfEmpty(Single.error(NoFingerDetectedException("Failed to acquire template"))) + .ifNoFingerDetectedThenSetBadScanLedState() + .wrapErrorsFromScanner() + } + } .await() } diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateScheduler.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateScheduler.kt deleted file mode 100644 index 6f68a8da8b..0000000000 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateScheduler.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.simprints.fingerprint.infra.scanner.data.worker - -import android.content.Context -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import com.simprints.core.ExternalScope -import com.simprints.fingerprint.infra.scanner.BuildConfig -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.FingerprintConfiguration -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** - * This class is responsible for scheduling the worker [FirmwareFileUpdateWorker] that updates the - * firmware version, if any updates available, on the device. - * - * @property context the application context used for scheduling the worker - * @property configRepository the configuration manager for checking the version of the connected Vero scanner - */ -class FirmwareFileUpdateScheduler @Inject constructor( - @ApplicationContext private val context: Context, - private val configRepository: ConfigRepository, - @ExternalScope private val externalScope: CoroutineScope, -) { - - fun scheduleOrCancelWorkIfNecessary() { - externalScope.launch { - if (configRepository.getProjectConfiguration().fingerprint?.allowedScanners?.contains( - FingerprintConfiguration.VeroGeneration.VERO_2 - ) == true - ) { - scheduleWork() - } else { - cancelWork() - } - } - } - - private fun scheduleWork() { - WorkManager - .getInstance(context) - .enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, buildWork()) - } - - private fun cancelWork() { - WorkManager - .getInstance(context) - .cancelUniqueWork(WORK_NAME) - } - - private fun buildWork(): PeriodicWorkRequest = - PeriodicWorkRequestBuilder(REPEAT_INTERVAL, REPEAT_INTERVAL_UNIT) - .setConstraints(getConstraints()) - .build() - - private fun getConstraints() = - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - companion object { - - const val WORK_NAME = "firmware-file-update-work" - const val REPEAT_INTERVAL = BuildConfig.FIRMWARE_UPDATE_WORKER_INTERVAL_MINUTES - val REPEAT_INTERVAL_UNIT = TimeUnit.MINUTES - } -} diff --git a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/fingerprint/AcquireImageDistortionMatrixConfigurationResponse.kt b/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/fingerprint/AcquireImageDistortionMatrixConfigurationResponse.kt deleted file mode 100644 index 12c01dfaf3..0000000000 --- a/fingerprint/infra/scanner/src/main/java/com/simprints/fingerprint/infra/scanner/domain/fingerprint/AcquireImageDistortionMatrixConfigurationResponse.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.simprints.fingerprint.infra.scanner.domain.fingerprint - -class AcquireImageDistortionMatrixConfigurationResponse( - val configurationBytes: ByteArray -) diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1Test.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1Test.kt index 46cad57f76..d935dbad0a 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1Test.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV1Test.kt @@ -91,7 +91,8 @@ class FingerprintCaptureWrapperV1Test { private suspend fun startCapturing() = scannerWrapper.acquireFingerprintTemplate( Dpi(1000), 3000, - 60 + 60, + false ) } diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2Test.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2Test.kt index bc88d5f566..4aa2d51214 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2Test.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/capture/FingerprintCaptureWrapperV2Test.kt @@ -3,7 +3,6 @@ package com.simprints.fingerprint.infra.scanner.capture import com.google.common.truth.Truth.assertThat import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintImageResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireFingerprintTemplateResponse -import com.simprints.fingerprint.infra.scanner.domain.fingerprint.AcquireImageDistortionMatrixConfigurationResponse import com.simprints.fingerprint.infra.scanner.domain.fingerprint.RawUnprocessedImage import com.simprints.fingerprint.infra.scanner.exceptions.safe.NoFingerDetectedException import com.simprints.fingerprint.infra.scanner.exceptions.unexpected.UnexpectedScannerException @@ -45,12 +44,12 @@ class FingerprintCaptureWrapperV2Test { @Test fun `test acquireImageDistortionMatrixConfiguration success`() = runTest { - val expectedResp = AcquireImageDistortionMatrixConfigurationResponse(byteArrayOf(1, 2, 3)) + val expectedResp = byteArrayOf(1, 2, 3) every { scannerV2.acquireImageDistortionConfigurationMatrix() } returns Maybe.just( - expectedResp.configurationBytes + expectedResp ) val actualResponse = scannerWrapper.acquireImageDistortionMatrixConfiguration() - assertThat(actualResponse.configurationBytes).isEqualTo(expectedResp.configurationBytes) + assertThat(actualResponse).isEqualTo(expectedResp) } @Test @@ -95,7 +94,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( null, 1000, - 50 + 50, + false ) } } @@ -106,7 +106,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(499), 1000, - 50 + 50, + false ) } } @@ -118,7 +119,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1701), 1000, - 50 + 50, + false ) } } @@ -141,7 +143,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - 50 + 50, + false ) } // and then throws UnexpectedScannerException @@ -149,7 +152,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - 50 + 50, + false ) } // and then throws UnknownScannerIssueException @@ -157,7 +161,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - 50 + 50, + false ) } } @@ -196,7 +201,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), timeOutMs = 30000, - qualityThreshold = 7 + qualityThreshold = 7, + false ) } @@ -224,7 +230,8 @@ class FingerprintCaptureWrapperV2Test { val actualResponse = scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - qualityThreshold + qualityThreshold, + false ) assertThat(expectedCaptureResponse.template).isEqualTo(actualResponse.template) @@ -246,7 +253,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - qualityThreshold + qualityThreshold, + false ) } } @@ -265,12 +273,33 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - qualityThreshold + qualityThreshold, + false ) verify(exactly = 1) { scannerUiHelper.badScanLedState() } + verify(exactly = 0) {scannerV2.acquireTemplate(any()) } } + @Test + fun `should extract template when captured fingerprint's image quality score is less than specified image quality_threshold and allowLowQualityExtraction is true`() = + runTest { + val qualityThreshold = 50 + every { scannerV2.getImageQualityScore() } returns Maybe.just(qualityThreshold - 10) + every { scannerV2.setSmileLedState(any()) } returns Completable.complete() + every { scannerV2.captureFingerprint(any()) } answers { + (Single.just(CaptureFingerprintResult.OK)) + } + every { scannerV2.acquireTemplate(any()) } returns Maybe.just(TemplateData(byteArrayOf())) + scannerWrapper.acquireFingerprintTemplate( + Dpi(1300), + 1000, + qualityThreshold, + true + ) + + verify(exactly = 1) {scannerV2.acquireTemplate(any()) } + } @Test fun `should trigger good_scan LED when captured fingerprint's image quality score is greater or equal to specified image quality_threshold`() = runTest { @@ -285,7 +314,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - qualityThreshold + qualityThreshold, + false ) verify(exactly = 1) { scannerUiHelper.goodScanLedState() } @@ -304,7 +334,8 @@ class FingerprintCaptureWrapperV2Test { scannerWrapper.acquireFingerprintTemplate( Dpi(1300), 1000, - 50 + 50, + false ) } } diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateSchedulerTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateSchedulerTest.kt deleted file mode 100644 index c08df94500..0000000000 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/data/worker/FirmwareFileUpdateSchedulerTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.simprints.fingerprint.infra.scanner.data.worker - -import androidx.work.WorkManager -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.FingerprintConfiguration -import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import io.mockk.verifySequence -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class FirmwareFileUpdateSchedulerTest { - - @get:Rule - val testCoroutineRule = TestCoroutineRule() - - private val workManagerMock = mockk() - private val fingerprintConfiguration = mockk() - private val configRepository = mockk { - coEvery { getProjectConfiguration() } returns mockk { - every { fingerprint } returns fingerprintConfiguration - } - } - - private val firmwareFileUpdateScheduler = - FirmwareFileUpdateScheduler( - mockk(), - configRepository, - CoroutineScope(testCoroutineRule.testCoroutineDispatcher), - ) - - @Before - fun setUp() { - mockkStatic(WorkManager::class) - every { WorkManager.getInstance(any()) } returns workManagerMock - } - - @Test - fun projectIsOnVero2Only_schedulesWork() = runTest { - every { fingerprintConfiguration.allowedScanners } returns listOf( - FingerprintConfiguration.VeroGeneration.VERO_2 - ) - every { workManagerMock.enqueueUniquePeriodicWork(any(), any(), any()) } returns mockk() - - firmwareFileUpdateScheduler.scheduleOrCancelWorkIfNecessary() - - verifySequence { workManagerMock.enqueueUniquePeriodicWork(any(), any(), any()) } - } - - @Test - fun projectIsBothVero1AndVero2_schedulesWork() = runTest { - every { fingerprintConfiguration.allowedScanners } returns listOf( - FingerprintConfiguration.VeroGeneration.VERO_1, - FingerprintConfiguration.VeroGeneration.VERO_2 - ) - every { workManagerMock.enqueueUniquePeriodicWork(any(), any(), any()) } returns mockk() - - firmwareFileUpdateScheduler.scheduleOrCancelWorkIfNecessary() - - verifySequence { workManagerMock.enqueueUniquePeriodicWork(any(), any(), any()) } - } - - @Test - fun projectIsOnVero1Only_cancelsScheduledWork() = runTest { - every { fingerprintConfiguration.allowedScanners } returns listOf( - FingerprintConfiguration.VeroGeneration.VERO_1 - ) - every { workManagerMock.cancelUniqueWork(any()) } returns mockk() - - firmwareFileUpdateScheduler.scheduleOrCancelWorkIfNecessary() - - verifySequence { workManagerMock.cancelUniqueWork(any()) } - } - - @After - fun tearDown() { - unmockkAll() - } -} diff --git a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerFactoryTest.kt b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerFactoryTest.kt index 1223ca1014..979e9e853a 100644 --- a/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerFactoryTest.kt +++ b/fingerprint/infra/scanner/src/test/java/com/simprints/fingerprint/infra/scanner/wrapper/ScannerFactoryTest.kt @@ -86,7 +86,6 @@ class ScannerFactoryTest { // Given val macAddress = "F0:AC:D7:C0:01:00" val serialNumber = "serialNumber" - val scannerGeneration = "scannerGeneration" every { serialNumberConverter.convertMacAddressToSerialNumber(macAddress) } returns serialNumber every { scannerGenerationDeterminer.determineScannerGenerationFromSerialNumber(serialNumber) diff --git a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt index 35fe9f2985..eb2b555afb 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateAcquisitionSettings.kt @@ -6,5 +6,6 @@ import com.simprints.fingerprint.infra.scanner.v2.domain.main.message.un20.model data class FingerprintTemplateAcquisitionSettings( val captureFingerprintDpi: Dpi?, val timeOutMs: Int, - val qualityThreshold: Int + val qualityThreshold: Int, + val allowLowQualityExtraction: Boolean=false ) diff --git a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt index d2b9dd2154..23939c5f64 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerprintTemplateProviderImpl.kt @@ -13,7 +13,8 @@ internal class FingerprintTemplateProviderImpl @Inject constructor( val response = fingerprintCaptureWrapperFactory.captureWrapper.acquireFingerprintTemplate( settings.captureFingerprintDpi, settings.timeOutMs, - settings.qualityThreshold + settings.qualityThreshold, + settings.allowLowQualityExtraction ) return TemplateResponse( response.template, diff --git a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/FingerprintMatcherImpl.kt b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/FingerprintMatcherImpl.kt index 696eb0b0b0..7c7b8234cf 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/FingerprintMatcherImpl.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/FingerprintMatcherImpl.kt @@ -3,12 +3,16 @@ package com.simprints.fingerprint.infra.biosdkimpl.matching import com.simprints.fingerprint.infra.basebiosdk.matching.FingerprintMatcher import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerprintIdentity import com.simprints.fingerprint.infra.basebiosdk.matching.domain.MatchResult +import com.simprints.fingerprint.infra.biosdkimpl.matching.SimAfisMatcher.Companion.SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT import javax.inject.Inject internal class FingerprintMatcherImpl @Inject constructor( private val simAfisMatcher: SimAfisMatcher ) : FingerprintMatcher { + override val supportedTemplateFormat = SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT + override val matcherName: String = "SIM_AFIS" + override suspend fun match( probe: FingerprintIdentity, candidates: List, diff --git a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcher.kt b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcher.kt index 442b8bc4da..6ba85309f4 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcher.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/main/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcher.kt @@ -15,7 +15,6 @@ import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerIdentifi import com.simprints.fingerprint.infra.basebiosdk.matching.domain.Fingerprint import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerprintIdentity import com.simprints.fingerprint.infra.basebiosdk.matching.domain.MatchResult -import com.simprints.fingerprint.infra.simafiswrapper.JNILibAfis import com.simprints.fingerprint.infra.simafiswrapper.JNILibAfisInterface import com.simprints.fingerprint.infra.simafiswrapper.models.SimAfisFingerIdentifier import com.simprints.fingerprint.infra.simafiswrapper.models.SimAfisFingerprint @@ -32,16 +31,16 @@ import javax.inject.Inject * list. It does not currently support progress indication and matching results are only available * when all matching is completed. */ -internal class SimAfisMatcher(private val jniLibAfis: JNILibAfisInterface) { - - @Inject - constructor() : this(JNILibAfis) - +internal class SimAfisMatcher @Inject constructor(private val jniLibAfis: JNILibAfisInterface) { fun match( probe: FingerprintIdentity, candidates: List, crossFingerComparison: Boolean ): List { + // if probe template format is not supported by SimAfisMatcher, return empty list + if (probe.templateFormatNotSupportedBySimAfisMatcher()) { + return emptyList() + } return if (crossFingerComparison) { crossFingerMatch(probe, candidates) } else { @@ -69,13 +68,10 @@ internal class SimAfisMatcher(private val jniLibAfis: JNILibAfisInterface) { } private fun FingerprintIdentity.toSimAfisPerson(): SimAfisPerson = - SimAfisPerson(id, fingerprints.map { it.toSimAfisFingerprint() }) + SimAfisPerson(subjectId, fingerprints.map { it.toSimAfisFingerprint() }) private fun Fingerprint.toSimAfisFingerprint(): SimAfisFingerprint { - require(format == SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT) { - "Attempting to use $format template format for SimAfisMatcher which only accepts $SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT" - } - return SimAfisFingerprint(fingerId.toSimAfisFingerIdentifier(), template) + return SimAfisFingerprint(fingerId.toSimAfisFingerIdentifier(), template) } @ExcludedFromGeneratedTestCoverageReports(reason = "This is just a mapping function") @@ -99,8 +95,6 @@ internal class SimAfisMatcher(private val jniLibAfis: JNILibAfisInterface) { ) = candidates.map { crossFingerMatching(probe, it, jniLibAfis) } - - /** * This method gets the matching score by: * - Getting the maximum matching score for each probe finger template with all candidate finger templates @@ -129,10 +123,9 @@ internal class SimAfisMatcher(private val jniLibAfis: JNILibAfisInterface) { }.toDouble() } // Matching score = total/number of fingers - return MatchResult(candidate.id, getOverallScore(total, fingers)) + return MatchResult(candidate.subjectId, getOverallScore(total, fingers)) } - private fun getOverallScore(total: Double, fingers: Int) = if (fingers == 0) { 0.toFloat() @@ -140,13 +133,15 @@ internal class SimAfisMatcher(private val jniLibAfis: JNILibAfisInterface) { (total / fingers).toFloat() } - - companion object { const val SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT = "ISO_19794_2" } } + val FingerprintIdentity.fingerprintsTemplates get() = fingerprints.map { it.template.toByteBuffer() } private fun ByteArray.toByteBuffer(): ByteBuffer = ByteBuffer.allocateDirect(size).put(this) + +fun FingerprintIdentity.templateFormatNotSupportedBySimAfisMatcher(): Boolean = + fingerprints.any { it.format != SimAfisMatcher.SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT } diff --git a/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerPrintTemplateProviderImplTest.kt b/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerPrintTemplateProviderImplTest.kt index 6f135926ae..b7509d46ff 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerPrintTemplateProviderImplTest.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/acquisition/template/FingerPrintTemplateProviderImplTest.kt @@ -48,7 +48,8 @@ class FingerPrintTemplateProviderImplTest { fingerprintCaptureWrapperFactory.captureWrapper.acquireFingerprintTemplate( settings.captureFingerprintDpi, settings.timeOutMs, - settings.qualityThreshold + settings.qualityThreshold, + settings.allowLowQualityExtraction ) } returns templateResponse //When diff --git a/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcherTest.kt b/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcherTest.kt index d1e3621f7b..5758b7ea0e 100644 --- a/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcherTest.kt +++ b/fingerprint/infra/simprints-bio-sdk/src/test/java/com/simprints/fingerprint/infra/biosdkimpl/matching/SimAfisMatcherTest.kt @@ -1,6 +1,6 @@ package com.simprints.fingerprint.infra.biosdkimpl.matching -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerIdentifier import com.simprints.fingerprint.infra.basebiosdk.matching.domain.Fingerprint import com.simprints.fingerprint.infra.basebiosdk.matching.domain.FingerprintIdentity @@ -35,13 +35,11 @@ class SimAfisMatcherTest { fun `test same finger match`() = runTest { every { jniLibAfis.identify(any(), any(), 1) } returns floatArrayOf(1F) val candidate = FingerprintIdentity( - "candidate", - listOf( - + "candidate", listOf( Fingerprint( FingerIdentifier.RIGHT_THUMB, IsoFingerprintTemplateGenerator.generate(1), - "ISO_19794_2" + SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT ) ) ) @@ -49,16 +47,14 @@ class SimAfisMatcherTest { val result = simAfisMatcher.match(candidate, listOf(candidate), false).last() //Then verify { jniLibAfis.identify(any(), any(), any()) } - Truth.assertThat(result.score).isEqualTo(1) + assertThat(result.score).isEqualTo(1) } - @Test(expected = IllegalArgumentException::class) - fun `test matching other template format fails`() = runTest { + @Test + fun `test matching probe with other template format ignore candidate`() = runTest { every { jniLibAfis.identify(any(), any(), 1) } returns floatArrayOf(1F) val candidate = FingerprintIdentity( - "candidate", - listOf( - + "candidate", listOf( Fingerprint( FingerIdentifier.RIGHT_3RD_FINGER, IsoFingerprintTemplateGenerator.generate(1), @@ -67,46 +63,46 @@ class SimAfisMatcherTest { ) ) //When - simAfisMatcher.match(candidate, listOf(candidate), false).last() - //Then throws IllegalArgumentException + val result = simAfisMatcher.match(candidate, listOf(candidate), false) + //Then + assertThat(result).isEmpty() } @Test fun `test cross finger match`() = runTest { mockkStatic("com.simprints.fingerprint.infra.biosdkimpl.matching.SimAfisMatcherKt") - - val probe = mockk() - val candidate1 = mockk() - val candidate2 = mockk() val template1 = mockk() val template2 = mockk() val template3 = mockk() - every { probe.fingerprintsTemplates } returns listOf(template1, template2) - - every { candidate1.fingerprintsTemplates } returns listOf(template2, template1) - every { candidate2.fingerprintsTemplates } returns listOf(template3, template1) - - every { candidate1.id } returns "candidate1" - every { candidate2.id } returns "candidate2" - - every { jniLibAfis.verify(template1, template1) } returns 1F - every { jniLibAfis.verify(template2, template2) } returns 1F - every { jniLibAfis.verify(template1, template2) } returns 0F - every { jniLibAfis.verify(template2, template1) } returns 0F - every { jniLibAfis.verify(template1, template3) } returns 0F - every { jniLibAfis.verify(template2, template3) } returns 0F + val probe = mockk { + every { templateFormatNotSupportedBySimAfisMatcher() } returns false + every { fingerprintsTemplates } returns listOf(template1, template2) + } + val candidate1 = mockk { + every { subjectId } returns "candidate1" + every { templateFormatNotSupportedBySimAfisMatcher() } returns false + every { fingerprintsTemplates } returns listOf(template2, template1) + } + val candidate2 = mockk { + every { subjectId } returns "candidate2" + every { templateFormatNotSupportedBySimAfisMatcher() } returns false + every { fingerprintsTemplates } returns listOf(template3, template1) + } + + every { jniLibAfis.verify(any(), any()) } answers { + if (firstArg() === secondArg()) 1F else 0F + } //When val matchingResult = simAfisMatcher.match( - probe, listOf(candidate1, candidate2), - true + probe, listOf(candidate1, candidate2), true ) val maxScore = matchingResult.maxOf { it.score } val minScore = matchingResult.minOf { it.score } //Then verify(exactly = 8) { jniLibAfis.verify(any(), any()) } - Truth.assertThat(maxScore).isEqualTo(1) - Truth.assertThat(minScore).isEqualTo(0.5f) + assertThat(maxScore).isEqualTo(1) + assertThat(minScore).isEqualTo(0.5f) } @@ -116,13 +112,15 @@ class SimAfisMatcherTest { every { jniLibAfis.verify(any(), any()) } returns 1F val probe = FingerprintIdentity("probe", listOf()) val candidate = FingerprintIdentity( - "candidate", - listOf( - Fingerprint( - FingerIdentifier.LEFT_THUMB, IsoFingerprintTemplateGenerator.generate(1), "ISO_19794_2" - ), + "candidate", listOf( Fingerprint( - FingerIdentifier.LEFT_3RD_FINGER, IsoFingerprintTemplateGenerator.generate(1), "ISO_19794_2" + FingerIdentifier.LEFT_THUMB, + IsoFingerprintTemplateGenerator.generate(1), + SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT + ), Fingerprint( + FingerIdentifier.LEFT_3RD_FINGER, + IsoFingerprintTemplateGenerator.generate(1), + SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT ) ) ) @@ -130,6 +128,10 @@ class SimAfisMatcherTest { val result = simAfisMatcher.match(probe, listOf(candidate), true) //Then verify(exactly = 0) { jniLibAfis.verify(any(), any()) } - Truth.assertThat(result[0].score).isEqualTo(0) + assertThat(result[0].score).isEqualTo(0) + } + + companion object { + const val SIMAFIS_MATCHER_SUPPORTED_TEMPLATE_FORMAT = "ISO_19794_2" } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1eef2f443..fbda443993 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] -kotlin_version = "1.9.22" -kotlin_coroutine_version = "1.8.0" +kotlin_version = "1.9.23" +kotlin_coroutine_version = "1.8.1" -android_gradlePlugin_version = "8.2.2" +android_gradlePlugin_version = "8.3.2" androidx_version = "1.5.2" -androidx_core_version = "1.12.0" +androidx_core_version = "1.13.1" androidx_app_compat_version = "1.6.1" androidx_fragment_version = "1.6.2" androidx_navigation_version = "2.7.7" androidx_lifecycle_version = "2.7.0" androidx_work_version = "2.9.0" androidx_room_version = "2.6.1" -androidx_datastore_version = "1.0.0" -androidx_camerax_version = "1.3.1" +androidx_datastore_version = "1.1.0" +androidx_camerax_version = "1.3.3" androidx_test_orchestrator_version = "1.4.2" androidx_dex_version = "2.0.1" androidx_constraint_version = "2.1.4" @@ -23,29 +23,28 @@ androidx_viewpager_version = "1.0.0" androidx_security_version = "1.0.0" androidx_annotation_version = "1.7.1" androidx_arch_core_version = "2.2.0" -matertial_version = "1.11.0" +matertial_version = "1.12.0" -hilt_version = "2.50" -hilt_androidx_version = "1.1.0" +hilt_version = "2.51.1" +hilt_androidx_version = "1.2.0" -play_base_services_version = "18.3.0" -play_location_services_version = "21.1.0" +play_base_services_version = "18.4.0" +play_location_services_version = "21.2.0" play_integrity_version = "1.3.0" gsm_plugin_version = "4.4.1" -play_publisher_version = "3.9.0" +play_publisher_version = "3.9.1" play_barcode_version = "18.3.0" -firebase_auth_version = "22.3.1" -firebase_storage_version = "20.3.0" -firebase_crashlytics_version = "18.6.1" -# Update analytics with caution as it might cause `NoClassDefFoundError: Failed resolution of: ImmutableSet;` -firebase_analytics_version = "21.4.0" -firebase_perf_version = "20.5.2" -firebase_crashlyticsPlugin_version = "2.9.9" +firebase_auth_version = "23.0.0" +firebase_storage_version = "21.0.0" +firebase_crashlytics_version = "19.0.0" +firebase_analytics_version = "22.0.0" +firebase_perf_version = "21.0.0" +firebase_crashlyticsPlugin_version = "3.0.1" firebase_perfPlugin_version = "1.4.2" -firebase_distrtibutionPlugin_version = "4.1.0" +firebase_distrtibutionPlugin_version = "5.0.0" -retrofit_version = "2.9.0" +retrofit_version = "2.11.0" okttp_version = "4.12.0" # Newer versions of Jackson don't support android apis <=25.Jackson shouldn't be updated as long as SID MIN supported APIs <=25 jackson_version = "2.13.4" @@ -59,13 +58,13 @@ rx_adapter_version = "1.8.0" sqlCipher_version = "4.5.4" fuzzywuzzy_version = "1.4.0" rootbeer_version = "0.1.0" -commons_io_version = "2.15.1" +commons_io_version = "2.16.1" kronos_version = "0.0.1-alpha11" -protobuf_version = "3.25.3" +protobuf_version = "4.26.1" circleImageView_version = "3.1.0" timber_version = "5.0.1" -libsimprints_version = "2023.4.1" +libsimprints_version = "2024.1.1" simmatcher_version = "1.2.0" roc_wrapper_version = "1.23" nec_version = "1.5.0" @@ -75,22 +74,22 @@ bitmapConverter_version = "0.2.0" junit_version = "4.13.2" junit_ext_version = "1.1.5" -truth_version = "1.4.1" +truth_version = "1.4.2" livedata_testing_version = "1.3.0" -mockk_version = "1.13.9" -robolectric_version = "4.11.1" +mockk_version = "1.13.10" +robolectric_version = "4.12.1" espresso_version = "3.5.1" -espresso_accessibility_version = "4.1.0" +espresso_accessibility_version = "4.1.1" barista_version = "4.3.0" -uiAutomator_version = "2.2.0" -jacoco_version = "0.8.11" +uiAutomator_version = "2.3.0" +jacoco_version = "0.8.12" -sonar_plugin_version = "4.4.1.3373" -retry_version = "1.5.8" -realm_version = "1.13.0" +sonar_plugin_version = "5.0.0.4638" +retry_version = "1.5.9" +realm_version = "1.15.0" protobuf_plugin_version = "0.9.4" deps_graph_version = "0.8.0" -tink_version = "1.12.0" +tink_version = "1.13.0" [libraries] diff --git a/id/build.gradle.kts b/id/build.gradle.kts index d4dffaed8a..3a5552fae6 100644 --- a/id/build.gradle.kts +++ b/id/build.gradle.kts @@ -28,22 +28,12 @@ dependencies { implementation(project(":infra:core")) implementation(project(":infra:event-sync")) - implementation(project(":infra:config-sync")) - implementation(project(":infra:images")) - implementation(project(":infra:auth-logic")) - implementation(project(":infra:auth-store")) + implementation(project(":infra:sync")) implementation(project(":feature:orchestrator")) implementation(project(":feature:dashboard")) - implementation(project(":fingerprint:infra:scanner")) - implementation(libs.androidX.core) implementation(libs.androidX.appcompat) implementation(libs.rxJava2.core) - - testImplementation(libs.testing.junit) - testImplementation(libs.testing.mockk.core) - testImplementation(libs.testing.coroutines) - testImplementation(project(":infra:test-tools")) } diff --git a/id/proguard-rules.pro b/id/proguard-rules.pro index 933ddc1ae1..dfadc4ca30 100644 --- a/id/proguard-rules.pro +++ b/id/proguard-rules.pro @@ -21,58 +21,6 @@ static void checkParameterIsNotNull(java.lang.Object, java.lang.String); } --keep @com.simprints.core.annotations.Unobfuscate public class * {*;} - -# Platform calls Class.forName on types which do not exist on Android to determine platform. --dontnote retrofit2.Platform -# Platform used when running on Java 8 VMs. Will not be used at runtime. --dontwarn retrofit2.Platform$Java8 -# Retain generic type information for use by reflection by converters and adapters. --keepattributes Signature -# Retain declared checked exceptions for use by a Proxy instance. --keepattributes Exceptions - -#okhttp3 --dontwarn okhttp3.** --dontwarn okio.** --dontwarn javax.annotation.** --dontwarn org.conscrypt.** -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Gson uses generic type information stored in a class file when working with fields. Proguard -# removes such information by default, so configure it to keep all of it. --keepattributes Signature - --keepclassmembers enum * { *; } - -# These contain serialised models - TODO: should be more selective? --keep class com.simprints.libsimprints.** { *; } --keep class com.simprints.infra.eventsync.event.remote.models.** { *; } --keep class com.simprints.infra.events.event.domain.models.** { *; } --keep class com.simprints.infra.events.event.local.** { *; } - -# Deobfuscations for Crashlytics: -# https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports --keepattributes SourceFile,LineNumberTable,*Annotation* --keep public class * extends java.lang.Exception --keep public class * extends java.lang.RuntimeException --keep public class * extends java.lang.Throwable --keep class com.crashlytics.** { *; } --dontwarn com.crashlytics.** - -# Keep firebase classes. more details at https://github.com/firebase/firebase-android-sdk/issues/2124#issuecomment-1091962221 --keep public class com.google.firebase.** {*;} --keep class com.google.android.gms.internal.** {*;} --keepclasseswithmembers class com.google.firebase.FirebaseException - - -# Dagger --dontwarn com.google.errorprone.annotations.** - -# RxJava https://github.com/ReactiveX/RxJava --dontwarn java.util.concurrent.Flow* - # Kotlin Coroutines # ServiceLoader support -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} @@ -89,20 +37,6 @@ -keepclassmembernames class kotlin.coroutines.SafeContinuation { volatile ; } -# https://github.com/Kotlin/kotlinx.coroutines/issues/1270 --dontwarn kotlinx.coroutines.flow.** --dontwarn com.simprints.id.services.scheduledSync.people.up.workers.** --dontwarn com.simprints.id.tools.extensions.** --dontwarn com.simprints.id.activities.settings.syncinformation.** --dontwarn com.simprints.id.activities.consent.** --dontwarn com.simprints.infra.events.session.** - -# https://github.com/Kotlin/kotlinx.coroutines/issues/2046 --dontwarn java.lang.instrument.ClassFileTransformer --dontwarn sun.misc.SignalHandler --dontwarn java.lang.instrument.Instrumentation --dontwarn sun.misc.Signal - # Jackson # Proguard configuration for Jackson 2.x (fasterxml package instead of codehaus package) @@ -124,3 +58,8 @@ #net.zetetic:android-database-sqlcipher -keep class net.sqlcipher.** { *; } +# Do not obfuscate names in out packages` +-keepnames class com.simprints.** { *; } +# Keep all marshallable classes as-is +-keep class com.simprints.** extends java.io.Serializable { *; } +-keep class com.simprints.** extends android.os.Parcelable { *; } diff --git a/id/src/main/AndroidManifest.xml b/id/src/main/AndroidManifest.xml index 08b9ae3513..9e039a6b96 100644 --- a/id/src/main/AndroidManifest.xml +++ b/id/src/main/AndroidManifest.xml @@ -4,9 +4,12 @@ + - + + + + = Q then use FOREGROUND_SERVICE_TYPE_DATA_SYNC + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + startForeground(1, notification) + } } } diff --git a/id/src/test/java/com/simprints/id/ScheduleBackgroundSyncUseCaseTest.kt b/id/src/test/java/com/simprints/id/ScheduleBackgroundSyncUseCaseTest.kt deleted file mode 100644 index 3db0720db5..0000000000 --- a/id/src/test/java/com/simprints/id/ScheduleBackgroundSyncUseCaseTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.simprints.id - -import com.simprints.fingerprint.infra.scanner.data.worker.FirmwareFileUpdateScheduler -import com.simprints.infra.authstore.AuthStore -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler -import io.mockk.MockKAnnotations -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -class ScheduleBackgroundSyncUseCaseTest { - - @MockK - lateinit var eventSyncManager: EventSyncManager - - @MockK - lateinit var imageUpSyncScheduler: ImageUpSyncScheduler - - @MockK - lateinit var configScheduler: ProjectConfigurationScheduler - - @MockK - lateinit var authStore: AuthStore - - @MockK - lateinit var firmwareFileUpdateScheduler: FirmwareFileUpdateScheduler - - private lateinit var useCase: ScheduleBackgroundSyncUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - useCase = ScheduleBackgroundSyncUseCase( - eventSyncManager, - imageUpSyncScheduler, - configScheduler, - authStore, - firmwareFileUpdateScheduler, - ) - } - - @Test - fun `If user is signed in - schedules all syncs when called`() = runTest { - every { authStore.signedInProjectId } returns "projectId" - - useCase.invoke() - - verify { - eventSyncManager.scheduleSync() - configScheduler.scheduleProjectSync() - configScheduler.scheduleDeviceSync() - firmwareFileUpdateScheduler.scheduleOrCancelWorkIfNecessary() - } - coVerify { - imageUpSyncScheduler.scheduleImageUpSync() - } - } - - @Test - fun `If user is not signed in - does nothing`() = runTest { - every { authStore.signedInProjectId } returns "" - - useCase.invoke() - - verify(exactly = 0) { - eventSyncManager.scheduleSync() - configScheduler.scheduleProjectSync() - configScheduler.scheduleDeviceSync() - firmwareFileUpdateScheduler.scheduleOrCancelWorkIfNecessary() - } - coVerify(exactly = 0) { - imageUpSyncScheduler.scheduleImageUpSync() - } - } -} diff --git a/infra/auth-logic/build.gradle.kts b/infra/auth-logic/build.gradle.kts index 6582a220a7..df3f7c3650 100644 --- a/infra/auth-logic/build.gradle.kts +++ b/infra/auth-logic/build.gradle.kts @@ -5,19 +5,6 @@ plugins { android { namespace = "com.simprints.infra.authlogic" - - - buildTypes { - getByName("release") { - buildConfigField("long", "SECURITY_STATE_PERIODIC_WORKER_INTERVAL_MINUTES", "30L") - } - getByName("staging") { - buildConfigField("long", "SECURITY_STATE_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - getByName("debug") { - buildConfigField("long", "SECURITY_STATE_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - } } dependencies { @@ -29,6 +16,7 @@ dependencies { implementation(project(":infra:enrolment-records-store")) implementation(project(":infra:images")) implementation(project(":infra:recent-user-activity")) + implementation(project(":infra:license")) implementation(project(":fingerprint:infra:scanner")) diff --git a/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt b/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt index cd1e1c6bb2..2553543b86 100644 --- a/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt +++ b/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt @@ -8,6 +8,7 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.events.EventRepository import com.simprints.infra.images.ImageRepository +import com.simprints.infra.license.LicenseRepository import com.simprints.infra.logging.LoggingConstants import com.simprints.infra.logging.Simber import com.simprints.infra.network.SimNetwork @@ -24,6 +25,7 @@ internal class SignerManager @Inject constructor( private val imageRepository: ImageRepository, private val eventRepository: EventRepository, private val enrolmentRecordRepository: EnrolmentRecordRepository, + private val licenseRepository: LicenseRepository, private val scannerManager: ScannerManager, @DispatcherIO private val dispatcher: CoroutineDispatcher, ) { @@ -50,8 +52,6 @@ internal class SignerManager @Inject constructor( } suspend fun signOut() = withContext(dispatcher) { - authStore.cleanCredentials() - authStore.clearFirebaseToken() simNetwork.resetApiBaseUrl() configRepository.clearData() @@ -61,6 +61,10 @@ internal class SignerManager @Inject constructor( eventRepository.deleteAll() enrolmentRecordRepository.deleteAll() scannerManager.deleteFirmwareFiles() + licenseRepository.deleteCachedLicenses() + + authStore.cleanCredentials() + authStore.clearFirebaseToken() Simber.tag(LoggingConstants.CrashReportTag.LOGOUT.name).i("Signed out") } diff --git a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt index 5a20e26f9a..0e00389546 100644 --- a/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt +++ b/infra/auth-logic/src/test/java/com/simprints/infra/authlogic/authenticator/SignerManagerTest.kt @@ -12,6 +12,7 @@ import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.events.EventRepository import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.images.ImageRepository +import com.simprints.infra.license.LicenseRepository import com.simprints.infra.network.SimNetwork import com.simprints.infra.recent.user.activity.RecentUserActivityManager import com.simprints.testtools.common.syntax.assertThrows @@ -47,6 +48,9 @@ internal class SignerManagerTest { @MockK lateinit var mockImageRepository: ImageRepository + @MockK + lateinit var mockLicenseRepository: LicenseRepository + @MockK lateinit var mockEnrolmentRecordRepository: EnrolmentRecordRepository @@ -74,6 +78,7 @@ internal class SignerManagerTest { mockImageRepository, mockEventRepository, mockEnrolmentRecordRepository, + mockLicenseRepository, scannerManager, UnconfinedTestDispatcher(), ) @@ -186,6 +191,7 @@ internal class SignerManagerTest { coVerify { mockEventRepository.deleteAll() } coVerify { mockEnrolmentRecordRepository.deleteAll() } coVerify { scannerManager.deleteFirmwareFiles() } + coVerify { mockLicenseRepository.deleteCachedLicenses() } } @Test diff --git a/infra/auth-store/src/main/java/com/simprints/infra/authstore/db/FirebaseAuthManager.kt b/infra/auth-store/src/main/java/com/simprints/infra/authstore/db/FirebaseAuthManager.kt index 11c535ffc9..5d1ddc086b 100644 --- a/infra/auth-store/src/main/java/com/simprints/infra/authstore/db/FirebaseAuthManager.kt +++ b/infra/auth-store/src/main/java/com/simprints/infra/authstore/db/FirebaseAuthManager.kt @@ -153,13 +153,13 @@ internal class FirebaseAuthManager @Inject constructor( // We try to return the core app right away in case there are follow on synchronized requests getCoreFirebaseApp() } catch (ex: IllegalStateException) { + val token = Token( "", loginInfoStore.coreFirebaseProjectId, loginInfoStore.coreFirebaseApiKey, loginInfoStore.coreFirebaseApplicationId ) - check(!(token.projectId.isEmpty() || token.apiKey.isEmpty() || token.applicationId.isEmpty())) { "Core Firebase App options are not stored" } diff --git a/infra/auth-store/src/main/java/com/simprints/infra/authstore/domain/LoginInfoStore.kt b/infra/auth-store/src/main/java/com/simprints/infra/authstore/domain/LoginInfoStore.kt index 34208f3d9a..d2304f3cb4 100644 --- a/infra/auth-store/src/main/java/com/simprints/infra/authstore/domain/LoginInfoStore.kt +++ b/infra/auth-store/src/main/java/com/simprints/infra/authstore/domain/LoginInfoStore.kt @@ -50,7 +50,7 @@ internal class LoginInfoStore @Inject constructor( putString(CORE_FIREBASE_APPLICATION_ID, prefs.getString(CORE_FIREBASE_APPLICATION_ID, "")) putString(CORE_FIREBASE_API_KEY, prefs.getString(CORE_FIREBASE_API_KEY, "")) } - prefs.edit(commit = true) { clear() } + prefs.clearValues() } return securePrefs } @@ -119,8 +119,8 @@ internal class LoginInfoStore @Inject constructor( signedInProjectId.isNotEmpty() && signedInProjectId == possibleProjectId fun cleanCredentials() { - securePrefs.edit { clear() } - prefs.edit { clear() } + securePrefs.clearValues() + prefs.clearValues() } fun clearCachedTokenClaims() { @@ -131,4 +131,14 @@ internal class LoginInfoStore @Inject constructor( remove(CORE_FIREBASE_API_KEY) } } + + private fun SharedPreferences.clearValues() = edit(commit = true) { + remove(USER_ID_VALUE) + remove(USER_ID_TOKENIZED) + remove(PROJECT_ID) + remove(PROJECT_ID_CLAIM) + remove(CORE_FIREBASE_PROJECT_ID) + remove(CORE_FIREBASE_APPLICATION_ID) + remove(CORE_FIREBASE_API_KEY) + } } diff --git a/infra/auth-store/src/test/java/com/simprints/infra/authstore/domain/LoginInfoStoreTest.kt b/infra/auth-store/src/test/java/com/simprints/infra/authstore/domain/LoginInfoStoreTest.kt index bc89f6198e..37fddea737 100644 --- a/infra/auth-store/src/test/java/com/simprints/infra/authstore/domain/LoginInfoStoreTest.kt +++ b/infra/auth-store/src/test/java/com/simprints/infra/authstore/domain/LoginInfoStoreTest.kt @@ -65,11 +65,11 @@ class LoginInfoStoreTest { secureEditor.putString(any(), any()) secureEditor.putBoolean(any(), any()) } + // Check that legacy prefs cleared + verify(exactly = 7) { legacyEditor.remove(any()) } verify(exactly = 1) { - secureEditor.commit() - // Check that legacy prefs cleared - legacyEditor.clear() legacyEditor.commit() + secureEditor.commit() } } @@ -261,10 +261,8 @@ class LoginInfoStoreTest { fun `cleanCredentials should reset all the credentials`() { loginInfoStoreImpl.cleanCredentials() - verify(exactly = 1) { - secureEditor.clear() - secureEditor.apply() - } + verify(exactly = 7) { secureEditor.remove(any()) } + verify(exactly = 1) { secureEditor.commit() } } @Test diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt index c4a06907d2..b944504582 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/ConfigLocalDataSourceImpl.kt @@ -179,12 +179,13 @@ internal class ConfigLocalDataSourceImpl @Inject constructor( frequency = SynchronizationConfiguration.Frequency.PERIODICALLY, up = UpSynchronizationConfiguration( simprints = UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( - kind = UpSynchronizationConfiguration.UpSynchronizationKind.NONE + kind = UpSynchronizationConfiguration.UpSynchronizationKind.NONE, + batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes.default(), + imagesRequireUnmeteredConnection = false, ), coSync = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration( kind = UpSynchronizationConfiguration.UpSynchronizationKind.NONE ), - imagesRequireUnmeteredConnection = false, ), down = DownSynchronizationConfiguration( partitionType = DownSynchronizationConfiguration.PartitionType.USER, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt index fb320592b3..ebcbb04084 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/OldProjectConfig.kt @@ -4,7 +4,20 @@ import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonProperty import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.json.JsonHelper -import com.simprints.infra.config.store.models.* +import com.simprints.infra.config.store.models.ConsentConfiguration +import com.simprints.infra.config.store.models.DecisionPolicy +import com.simprints.infra.config.store.models.DownSynchronizationConfiguration +import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.Finger +import com.simprints.infra.config.store.models.FingerprintConfiguration +import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.config.store.models.IdentificationConfiguration +import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.SettingsPasswordConfig +import com.simprints.infra.config.store.models.SynchronizationConfiguration +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration +import com.simprints.infra.config.store.models.Vero1Configuration +import com.simprints.infra.config.store.models.Vero2Configuration import org.json.JSONObject @@ -197,7 +210,13 @@ internal data class OldProjectConfig( UpSynchronizationConfiguration.UpSynchronizationKind.valueOf( simprintsSync ) - } + }, + batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes( + sessions = 1, + upSyncs = 1, + downSyncs = 1, + ), + imagesRequireUnmeteredConnection = false, ), coSync = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration( kind = if (coSync == null) { @@ -212,7 +231,6 @@ internal data class OldProjectConfig( ) } ), - imagesRequireUnmeteredConnection = false, ), down = DownSynchronizationConfiguration( partitionType = DownSynchronizationConfiguration.PartitionType.valueOf( diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt index f0a3ae2b76..2b444f0f53 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/UpSynchronizationConfiguration.kt @@ -1,18 +1,19 @@ package com.simprints.infra.config.store.local.models -import com.simprints.infra.config.store.models.UpSynchronizationConfiguration import com.simprints.infra.config.store.exceptions.InvalidProtobufEnumException +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration internal fun UpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration = ProtoUpSynchronizationConfiguration.newBuilder() .setSimprints(simprints.toProto()) .setCoSync(coSync.toProto()) - .setImagesRequireUnmeteredConnection(imagesRequireUnmeteredConnection) .build() internal fun UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration = ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(kind.toProto()) + .setBatchSizes(batchSizes.toProto()) + .setImagesRequireUnmeteredConnection(imagesRequireUnmeteredConnection) .build() internal fun UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toProto(): ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration = @@ -28,11 +29,17 @@ internal fun UpSynchronizationConfiguration.UpSynchronizationKind.toProto(): Pro UpSynchronizationConfiguration.UpSynchronizationKind.ALL -> ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL } +internal fun UpSynchronizationConfiguration.UpSyncBatchSizes.toProto(): ProtoUpSyncBatchSizes = ProtoUpSyncBatchSizes.newBuilder() + .setSessions(sessions) + .setUpSyncs(upSyncs) + .setDownSyncs(downSyncs) + .build() + internal fun ProtoUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration = - UpSynchronizationConfiguration(simprints.toDomain(), coSync.toDomain(), imagesRequireUnmeteredConnection) + UpSynchronizationConfiguration(simprints.toDomain(), coSync.toDomain()) internal fun ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration = - UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration(kind.toDomain()) + UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration(kind.toDomain(), batchSizes.toDomain(), imagesRequireUnmeteredConnection) internal fun ProtoUpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration.toDomain(): UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration(kind.toDomain()) @@ -47,3 +54,6 @@ internal fun ProtoUpSynchronizationConfiguration.UpSynchronizationKind.toDomain( "invalid UpSynchronizationKind $name" ) } + +internal fun ProtoUpSyncBatchSizes.toDomain(): UpSynchronizationConfiguration.UpSyncBatchSizes = + UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, upSyncs, downSyncs) 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 1c37c071d3..9d30491d8a 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 @@ -40,4 +40,4 @@ fun ProjectConfiguration.isEventDownSyncAllowed(): Boolean = synchronization.frequency != SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC fun ProjectConfiguration.imagesUploadRequiresUnmeteredConnection(): Boolean = - synchronization.up.imagesRequireUnmeteredConnection + synchronization.up.simprints.imagesRequireUnmeteredConnection diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt index 7c949560b4..fbaa495ea7 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/UpSynchronizationConfiguration.kt @@ -3,13 +3,28 @@ package com.simprints.infra.config.store.models data class UpSynchronizationConfiguration( val simprints: SimprintsUpSynchronizationConfiguration, val coSync: CoSyncUpSynchronizationConfiguration, - val imagesRequireUnmeteredConnection: Boolean, ) { - data class SimprintsUpSynchronizationConfiguration(val kind: UpSynchronizationKind) + data class SimprintsUpSynchronizationConfiguration( + val kind: UpSynchronizationKind, + val batchSizes: UpSyncBatchSizes, + val imagesRequireUnmeteredConnection: Boolean, + ) data class CoSyncUpSynchronizationConfiguration(val kind: UpSynchronizationKind) + data class UpSyncBatchSizes( + val sessions: Int, + val upSyncs: Int, + val downSyncs: Int, + ) { + + companion object { + + fun default() = UpSyncBatchSizes(1, 1, 1) + } + } + enum class UpSynchronizationKind { NONE, ONLY_ANALYTICS, 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 b20170fb5e..bcaba4b5ea 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 @@ -6,7 +6,6 @@ import com.simprints.infra.config.store.remote.models.ApiProject import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration import com.simprints.infra.network.SimRemoteInterface import retrofit2.http.GET -import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.Query @@ -15,13 +14,9 @@ internal interface ConfigRemoteInterface : SimRemoteInterface { @GET("projects/{projectId}/configuration") suspend fun getConfiguration(@Path("projectId") projectId: String): ApiProjectConfiguration - // TODO Remove once all sync-revamp API changes have been implemented - @Headers("X-Force-Version: 2024.1.1") @GET("projects/{projectId}") suspend fun getProject(@Path("projectId") projectId: String): ApiProject - // TODO Remove once all sync-revamp API changes have been implemented - @Headers("X-Force-Version: 2024.1.1") @GET("projects/{projectId}/devices/{deviceId}") suspend fun getDeviceState( @Path("projectId") projectId: String, diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt index 23ef057178..709c814be6 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiSynchronizationConfiguration.kt @@ -22,6 +22,7 @@ internal data class ApiSynchronizationConfiguration( @Keep enum class Frequency { + ONLY_PERIODICALLY_UP_SYNC, PERIODICALLY, PERIODICALLY_AND_ON_SESSION_START; @@ -38,30 +39,40 @@ internal data class ApiSynchronizationConfiguration( data class ApiUpSynchronizationConfiguration( val simprints: ApiSimprintsUpSynchronizationConfiguration, val coSync: ApiCoSyncUpSynchronizationConfiguration, - val imagesRequireUnmeteredConnection: Boolean, ) { fun toDomain(): UpSynchronizationConfiguration = UpSynchronizationConfiguration( simprints.toDomain(), coSync.toDomain(), - imagesRequireUnmeteredConnection ) @Keep - data class ApiSimprintsUpSynchronizationConfiguration(val kind: UpSynchronizationKind) { + data class ApiSimprintsUpSynchronizationConfiguration( + val kind: UpSynchronizationKind, + val batchSizes: ApiUpSyncBatchSizes?, + val imagesRequireUnmeteredConnection: Boolean?, + ) { + fun toDomain(): UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration = - UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration(kind.toDomain()) + UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( + kind = kind.toDomain(), + batchSizes = batchSizes?.toDomain() + ?: UpSynchronizationConfiguration.UpSyncBatchSizes.default(), + imagesRequireUnmeteredConnection = imagesRequireUnmeteredConnection ?: false, + ) } @Keep data class ApiCoSyncUpSynchronizationConfiguration(val kind: UpSynchronizationKind) { + fun toDomain(): UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration = UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration(kind.toDomain()) } @Keep enum class UpSynchronizationKind { + NONE, ONLY_ANALYTICS, ONLY_BIOMETRICS, @@ -75,13 +86,24 @@ internal data class ApiSynchronizationConfiguration( ALL -> UpSynchronizationConfiguration.UpSynchronizationKind.ALL } } + + @Keep + data class ApiUpSyncBatchSizes( + val sessions: Int, + val upSyncs: Int, + val downSyncs: Int, + ) { + + fun toDomain(): UpSynchronizationConfiguration.UpSyncBatchSizes = + UpSynchronizationConfiguration.UpSyncBatchSizes(sessions, upSyncs, downSyncs) + } } @Keep data class ApiDownSynchronizationConfiguration( val partitionType: PartitionType, val maxNbOfModules: Int, - val moduleOptions: List? + val moduleOptions: List?, ) { fun toDomain(): DownSynchronizationConfiguration = @@ -93,6 +115,7 @@ internal data class ApiSynchronizationConfiguration( @Keep enum class PartitionType { + PROJECT, MODULE, USER; diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/tokenization/TokenizationProcessor.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/tokenization/TokenizationProcessor.kt index 6d63d8a70a..10501938c8 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/tokenization/TokenizationProcessor.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/tokenization/TokenizationProcessor.kt @@ -20,7 +20,7 @@ class TokenizationProcessor @Inject constructor( * @param tokenKeyType corresponding key type of the provided string * @param project current project configuration containing tokenization keys * - * @return [TokenizableString.Tokenized] is case of successful tokenization, [TokenizableString.Raw] + * @return [TokenizableString.Tokenized] in case of successful tokenization, [TokenizableString.Raw] * with the [decrypted] value otherwise */ fun encrypt( @@ -40,12 +40,12 @@ class TokenizationProcessor @Inject constructor( /** * Tries to decrypt [encrypted] value in safely manner. * - * @param encrypted raw string value for encryption + * @param encrypted tokenized string value for decryption * @param tokenKeyType corresponding key type of the provided string * @param project current project configuration containing tokenization keys * - * @return [TokenizableString.Tokenized] is case of successful tokenization, [TokenizableString.Raw] - * with the [encrypted] value otherwise + * @return [TokenizableString.Raw] in case of successful decryption, [TokenizableString.Tokenized] + * with the original [encrypted] value otherwise */ fun decrypt( encrypted: TokenizableString.Tokenized, diff --git a/infra/config-store/src/main/proto/project_config.proto b/infra/config-store/src/main/proto/project_config.proto index 158d6f46b3..b9c685a439 100644 --- a/infra/config-store/src/main/proto/project_config.proto +++ b/infra/config-store/src/main/proto/project_config.proto @@ -171,10 +171,11 @@ message ProtoDownSynchronizationConfiguration { message ProtoUpSynchronizationConfiguration { SimprintsUpSynchronizationConfiguration simprints = 1; CoSyncUpSynchronizationConfiguration co_sync = 2; - bool imagesRequireUnmeteredConnection = 3; message SimprintsUpSynchronizationConfiguration { UpSynchronizationKind kind = 1; + ProtoUpSyncBatchSizes batchSizes = 2; + bool imagesRequireUnmeteredConnection = 3; } message CoSyncUpSynchronizationConfiguration { @@ -189,6 +190,11 @@ message ProtoUpSynchronizationConfiguration { } } +message ProtoUpSyncBatchSizes { + int32 sessions = 1; + int32 upSyncs = 2; + int32 downSyncs = 3; +} message ProtoDecisionPolicy { int32 low = 1; diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt index f1c381940e..39db8c4641 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigrationTest.kt @@ -9,10 +9,27 @@ import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.local.migrations.ProjectConfigSharedPrefsMigration.Companion.ALL_KEYS import com.simprints.infra.config.store.local.migrations.ProjectConfigSharedPrefsMigration.Companion.PROJECT_SETTINGS_JSON_STRING_KEY import com.simprints.infra.config.store.local.migrations.models.OldProjectConfig -import com.simprints.infra.config.store.local.models.* +import com.simprints.infra.config.store.local.models.ProtoConsentConfiguration +import com.simprints.infra.config.store.local.models.ProtoDecisionPolicy +import com.simprints.infra.config.store.local.models.ProtoDownSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoFaceConfiguration +import com.simprints.infra.config.store.local.models.ProtoFinger +import com.simprints.infra.config.store.local.models.ProtoFingerprintConfiguration +import com.simprints.infra.config.store.local.models.ProtoGeneralConfiguration +import com.simprints.infra.config.store.local.models.ProtoIdentificationConfiguration +import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration +import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes +import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoVero1Configuration +import com.simprints.infra.config.store.local.models.ProtoVero2Configuration import com.simprints.infra.config.store.testtools.protoProjectConfiguration import com.simprints.testtools.common.syntax.assertThrows -import io.mockk.* +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -501,6 +518,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setSimprints( ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL) + .setBatchSizes(ProtoUpSyncBatchSizes.newBuilder().setSessions(1).setUpSyncs(1).setDownSyncs(1).build()) .build() ) .setCoSync( @@ -526,6 +544,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setSimprints( ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL) + .setBatchSizes(ProtoUpSyncBatchSizes.newBuilder().setSessions(1).setUpSyncs(1).setDownSyncs(1).build()) .build() ) .setCoSync( @@ -551,6 +570,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setSimprints( ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE) + .setBatchSizes(ProtoUpSyncBatchSizes.newBuilder().setSessions(1).setUpSyncs(1).setDownSyncs(1).build()) .build() ) .setCoSync( @@ -577,6 +597,7 @@ class ProjectConfigSharedPrefsMigrationTest { .setSimprints( ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(ProtoUpSynchronizationConfiguration.UpSynchronizationKind.NONE) + .setBatchSizes(ProtoUpSyncBatchSizes.newBuilder().setSessions(1).setUpSyncs(1).setDownSyncs(1).build()) .build() ) .setCoSync( diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt index 36104a673a..b3a96ca901 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ProjectConfigurationTest.kt @@ -1,11 +1,17 @@ package com.simprints.infra.config.store.models import com.google.common.truth.Truth.assertThat -import com.simprints.infra.config.store.models.SynchronizationConfiguration.Frequency.* +import com.simprints.infra.config.store.models.SynchronizationConfiguration.Frequency.ONLY_PERIODICALLY_UP_SYNC +import com.simprints.infra.config.store.models.SynchronizationConfiguration.Frequency.PERIODICALLY +import com.simprints.infra.config.store.models.SynchronizationConfiguration.Frequency.PERIODICALLY_AND_ON_SESSION_START import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration -import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.* +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ALL +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.NONE +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_ANALYTICS +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration.UpSynchronizationKind.ONLY_BIOMETRICS import com.simprints.infra.config.store.testtools.projectConfiguration +import com.simprints.infra.config.store.testtools.simprintsUpSyncConfigurationConfiguration import com.simprints.infra.config.store.testtools.synchronizationConfiguration import org.junit.Test @@ -116,8 +122,8 @@ class ProjectConfigurationTest { val config = projectConfiguration.copy( synchronization = synchronizationConfiguration.copy( up = synchronizationConfiguration.up.copy( - simprints = SimprintsUpSynchronizationConfiguration( - kind = it.key + simprints = simprintsUpSyncConfigurationConfiguration.copy( + kind = it.key, ) ) ) @@ -139,8 +145,8 @@ class ProjectConfigurationTest { val config = projectConfiguration.copy( synchronization = synchronizationConfiguration.copy( up = synchronizationConfiguration.up.copy( - simprints = SimprintsUpSynchronizationConfiguration( - kind = it.key + simprints = simprintsUpSyncConfigurationConfiguration.copy( + kind = it.key, ) ) ) @@ -163,7 +169,9 @@ class ProjectConfigurationTest { synchronization = synchronizationConfiguration.copy( up = synchronizationConfiguration.up.copy( simprints = SimprintsUpSynchronizationConfiguration( - kind = it.key + kind = it.key, + batchSizes = UpSynchronizationConfiguration.UpSyncBatchSizes.default(), + imagesRequireUnmeteredConnection = false ) ) ) @@ -185,8 +193,8 @@ class ProjectConfigurationTest { val config = projectConfiguration.copy( synchronization = synchronizationConfiguration.copy( up = synchronizationConfiguration.up.copy( - simprints = SimprintsUpSynchronizationConfiguration( - kind = it.key + simprints = simprintsUpSyncConfigurationConfiguration.copy( + kind = it.key, ) ) ) @@ -220,7 +228,11 @@ class ProjectConfigurationTest { values.forEach { val config = projectConfiguration.copy( synchronization = synchronizationConfiguration.copy( - up = synchronizationConfiguration.up.copy(imagesRequireUnmeteredConnection = it) + up = synchronizationConfiguration.up.copy( + simprints = simprintsUpSyncConfigurationConfiguration.copy( + imagesRequireUnmeteredConnection = it + ), + ) ) ) assertThat(config.imagesUploadRequiresUnmeteredConnection()).isEqualTo(it) 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 1a4b7c631e..9742934d2b 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 @@ -11,19 +11,36 @@ import com.simprints.infra.config.store.local.models.ProtoIdentificationConfigur import com.simprints.infra.config.store.local.models.ProtoProject import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration import com.simprints.infra.config.store.local.models.ProtoSynchronizationConfiguration +import com.simprints.infra.config.store.local.models.ProtoUpSyncBatchSizes import com.simprints.infra.config.store.local.models.ProtoUpSynchronizationConfiguration import com.simprints.infra.config.store.local.models.ProtoVero2Configuration import com.simprints.infra.config.store.local.models.toProto -import com.simprints.infra.config.store.models.* -import com.simprints.infra.config.store.remote.models.* +import com.simprints.infra.config.store.models.ConsentConfiguration +import com.simprints.infra.config.store.models.DecisionPolicy +import com.simprints.infra.config.store.models.DeviceConfiguration +import com.simprints.infra.config.store.models.DeviceState +import com.simprints.infra.config.store.models.DownSynchronizationConfiguration +import com.simprints.infra.config.store.models.FaceConfiguration +import com.simprints.infra.config.store.models.GeneralConfiguration +import com.simprints.infra.config.store.models.IdentificationConfiguration +import com.simprints.infra.config.store.models.Project +import com.simprints.infra.config.store.models.ProjectConfiguration +import com.simprints.infra.config.store.models.ProjectState +import com.simprints.infra.config.store.models.SettingsPasswordConfig +import com.simprints.infra.config.store.models.SynchronizationConfiguration +import com.simprints.infra.config.store.models.TokenKeyType +import com.simprints.infra.config.store.models.UpSynchronizationConfiguration +import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.store.remote.models.ApiConsentConfiguration import com.simprints.infra.config.store.remote.models.ApiDecisionPolicy +import com.simprints.infra.config.store.remote.models.ApiDeviceState import com.simprints.infra.config.store.remote.models.ApiFaceConfiguration import com.simprints.infra.config.store.remote.models.ApiFingerprintConfiguration import com.simprints.infra.config.store.remote.models.ApiGeneralConfiguration import com.simprints.infra.config.store.remote.models.ApiIdentificationConfiguration import com.simprints.infra.config.store.remote.models.ApiProject import com.simprints.infra.config.store.remote.models.ApiProjectConfiguration +import com.simprints.infra.config.store.remote.models.ApiProjectState import com.simprints.infra.config.store.remote.models.ApiSynchronizationConfiguration import com.simprints.infra.config.store.remote.models.ApiVero1Configuration import com.simprints.infra.config.store.remote.models.ApiVero2Configuration @@ -200,12 +217,13 @@ internal val apiSynchronizationConfiguration = ApiSynchronizationConfiguration( ApiSynchronizationConfiguration.Frequency.PERIODICALLY, ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration( ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiSimprintsUpSynchronizationConfiguration( - ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.ALL + ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.ALL, + ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiUpSyncBatchSizes(1, 2, 3), + false, ), ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.ApiCoSyncUpSynchronizationConfiguration( ApiSynchronizationConfiguration.ApiUpSynchronizationConfiguration.UpSynchronizationKind.NONE ), - false, ), ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration( ApiSynchronizationConfiguration.ApiDownSynchronizationConfiguration.PartitionType.PROJECT, @@ -214,16 +232,19 @@ internal val apiSynchronizationConfiguration = ApiSynchronizationConfiguration( ) ) +internal val simprintsUpSyncConfigurationConfiguration = UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( + UpSynchronizationConfiguration.UpSynchronizationKind.ALL, + UpSynchronizationConfiguration.UpSyncBatchSizes(1, 2, 3), + false +) + internal val synchronizationConfiguration = SynchronizationConfiguration( SynchronizationConfiguration.Frequency.PERIODICALLY, UpSynchronizationConfiguration( - UpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration( - UpSynchronizationConfiguration.UpSynchronizationKind.ALL - ), + simprintsUpSyncConfigurationConfiguration, UpSynchronizationConfiguration.CoSyncUpSynchronizationConfiguration( UpSynchronizationConfiguration.UpSynchronizationKind.NONE ), - false, ), DownSynchronizationConfiguration( DownSynchronizationConfiguration.PartitionType.PROJECT, @@ -239,6 +260,13 @@ internal val protoSynchronizationConfiguration = ProtoSynchronizationConfigurati .setSimprints( ProtoUpSynchronizationConfiguration.SimprintsUpSynchronizationConfiguration.newBuilder() .setKind(ProtoUpSynchronizationConfiguration.UpSynchronizationKind.ALL) + .setBatchSizes( + ProtoUpSyncBatchSizes.newBuilder() + .setSessions(1) + .setUpSyncs(2) + .setDownSyncs(3) + .build() + ) .build() ) .setCoSync( diff --git a/infra/config-sync/build.gradle.kts b/infra/config-sync/build.gradle.kts deleted file mode 100644 index 46d112bd95..0000000000 --- a/infra/config-sync/build.gradle.kts +++ /dev/null @@ -1,36 +0,0 @@ -plugins { - id("simprints.infra") -} - -android { - namespace = "com.simprints.infra.config.sync" - - - buildTypes { - getByName("release") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "60L") - buildConfigField("long", "DEVICE_PERIODIC_WORKER_INTERVAL_MINUTES", "30L") - } - getByName("staging") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - buildConfigField("long", "DEVICE_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - getByName("debug") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - buildConfigField("long", "DEVICE_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - } -} - -dependencies { - implementation(project(":infra:auth-store")) - implementation(project(":infra:auth-logic")) - implementation(project(":infra:config-store")) - implementation(project(":infra:enrolment-records-store")) - implementation(project(":infra:enrolment-records-sync")) - implementation(project(":infra:events")) - implementation(project(":infra:event-sync")) - implementation(project(":infra:images")) - - implementation(libs.workManager.work) -} diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManagerModule.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManagerModule.kt deleted file mode 100644 index 56fbcd26e2..0000000000 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManagerModule.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.simprints.infra.config.sync - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class ConfigManagerModule { - - @Binds - internal abstract fun provideConfigurationScheduler(configurationScheduler: ProjectConfigurationSchedulerImpl): ProjectConfigurationScheduler - -} diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationScheduler.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationScheduler.kt deleted file mode 100644 index 50c5c85ea5..0000000000 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationScheduler.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.simprints.infra.config.sync - -interface ProjectConfigurationScheduler { - fun scheduleProjectSync() - fun cancelProjectSync() - - fun startDeviceSync() - fun scheduleDeviceSync() - fun cancelDeviceSync() -} diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImpl.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImpl.kt deleted file mode 100644 index 83d49d16c9..0000000000 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImpl.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.simprints.infra.config.sync - -import android.content.Context -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import com.simprints.infra.config.sync.worker.DeviceConfigDownSyncWorker -import com.simprints.infra.config.sync.worker.ProjectConfigDownSyncWorker -import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -internal class ProjectConfigurationSchedulerImpl @Inject constructor(@ApplicationContext context: Context) : - ProjectConfigurationScheduler { - - companion object { - - internal const val PROJECT_SYNC_WORK_NAME = "project-sync-work" - private const val PROJECT_SYNC_REPEAT_INTERVAL = - BuildConfig.SYNC_PERIODIC_WORKER_INTERVAL_MINUTES - - internal const val DEVICE_SYNC_WORK_NAME = "device-sync-work" - internal const val DEVICE_SYNC_WORK_NAME_ONE_TIME = "device-sync-work-one-time" - private const val DEVICE_SYNC_REPEAT_INTERVAL = - BuildConfig.DEVICE_PERIODIC_WORKER_INTERVAL_MINUTES - - private val SYNC_REPEAT_UNIT = TimeUnit.MINUTES - } - - private val workManager = WorkManager.getInstance(context) - - override fun scheduleProjectSync() { - workManager.enqueueUniquePeriodicWork( - PROJECT_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - PeriodicWorkRequestBuilder( - PROJECT_SYNC_REPEAT_INTERVAL, - SYNC_REPEAT_UNIT - ).setConstraints(workerConstraints()).build() - ) - } - - override fun cancelProjectSync() { - workManager.cancelUniqueWork(PROJECT_SYNC_WORK_NAME) - } - - override fun startDeviceSync() { - workManager.enqueueUniqueWork( - DEVICE_SYNC_WORK_NAME_ONE_TIME, - ExistingWorkPolicy.KEEP, - OneTimeWorkRequestBuilder() - .setConstraints(workerConstraints()) - .build() - ) - } - - override fun scheduleDeviceSync() { - workManager.enqueueUniquePeriodicWork( - DEVICE_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - PeriodicWorkRequestBuilder( - DEVICE_SYNC_REPEAT_INTERVAL, - SYNC_REPEAT_UNIT - ).setConstraints(workerConstraints()).build() - ) - } - - override fun cancelDeviceSync() { - workManager.cancelUniqueWork(DEVICE_SYNC_WORK_NAME) - } - - private fun workerConstraints() = - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - -} diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/usecase/LogoutUseCase.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/usecase/LogoutUseCase.kt deleted file mode 100644 index 4c2e7e365d..0000000000 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/usecase/LogoutUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.simprints.infra.config.sync.usecase - -import com.simprints.infra.authlogic.AuthManager -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler -import javax.inject.Inject - -internal class LogoutUseCase @Inject constructor( - private val configScheduler: ProjectConfigurationScheduler, - private val imageUpSyncScheduler: ImageUpSyncScheduler, - private val eventSyncManager: EventSyncManager, - private val authManager: AuthManager, -) { - - suspend operator fun invoke() { - imageUpSyncScheduler.cancelImageUpSync() - configScheduler.cancelProjectSync() - configScheduler.cancelDeviceSync() - eventSyncManager.cancelScheduledSync() - eventSyncManager.deleteSyncInfo() - authManager.signOut() - } -} diff --git a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImplTest.kt b/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImplTest.kt deleted file mode 100644 index 10f1125dfc..0000000000 --- a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/ProjectConfigurationSchedulerImplTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.simprints.infra.config.sync - -import android.content.Context -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.verify -import org.junit.Before -import org.junit.Test - -class ProjectConfigurationSchedulerImplTest { - - private val ctx = mockk() - private val workManager = mockk(relaxed = true) - private lateinit var configurationSchedulerImpl: ProjectConfigurationSchedulerImpl - - @Before - fun setup() { - mockkStatic(WorkManager::class) - every { WorkManager.getInstance(ctx) } returns workManager - - configurationSchedulerImpl = - ProjectConfigurationSchedulerImpl(ctx) - } - - @Test - fun `scheduleProjectSync should schedule the worker`() { - configurationSchedulerImpl.scheduleProjectSync() - - verify { - workManager.enqueueUniquePeriodicWork( - ProjectConfigurationSchedulerImpl.PROJECT_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - any(), - ) - } - } - - @Test - fun `cancelProjectSync should cancel the worker`() { - configurationSchedulerImpl.cancelProjectSync() - - verify { workManager.cancelUniqueWork(ProjectConfigurationSchedulerImpl.PROJECT_SYNC_WORK_NAME) } - } - - @Test - fun `startDeviceSync should schedule the worker`() { - configurationSchedulerImpl.startDeviceSync() - - verify { - workManager.enqueueUniqueWork( - ProjectConfigurationSchedulerImpl.DEVICE_SYNC_WORK_NAME_ONE_TIME, - ExistingWorkPolicy.KEEP, - any(), - ) - } - } - - @Test - fun `scheduleDeviceSync should schedule the worker`() { - configurationSchedulerImpl.scheduleDeviceSync() - - verify { - workManager.enqueueUniquePeriodicWork( - ProjectConfigurationSchedulerImpl.DEVICE_SYNC_WORK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - any(), - ) - } - } - - @Test - fun `cancelDeviceSync should cancel the worker`() { - configurationSchedulerImpl.cancelDeviceSync() - - verify { workManager.cancelUniqueWork(ProjectConfigurationSchedulerImpl.DEVICE_SYNC_WORK_NAME) } - } -} diff --git a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/usecase/LogoutUseCaseTest.kt b/infra/config-sync/src/test/java/com/simprints/infra/config/sync/usecase/LogoutUseCaseTest.kt deleted file mode 100644 index 86f854b66d..0000000000 --- a/infra/config-sync/src/test/java/com/simprints/infra/config/sync/usecase/LogoutUseCaseTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.simprints.infra.config.sync.usecase - -import com.simprints.infra.authlogic.AuthManager -import com.simprints.infra.config.sync.ProjectConfigurationScheduler -import com.simprints.infra.eventsync.EventSyncManager -import com.simprints.infra.images.ImageUpSyncScheduler -import io.mockk.MockKAnnotations -import io.mockk.coVerify -import io.mockk.impl.annotations.MockK -import io.mockk.verify -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -class LogoutUseCaseTest { - - @MockK - private lateinit var configScheduler: ProjectConfigurationScheduler - - @MockK - private lateinit var imageUpSyncScheduler: ImageUpSyncScheduler - - @MockK - private lateinit var eventSyncManager: EventSyncManager - - @MockK - private lateinit var authManager: AuthManager - - private lateinit var useCase: LogoutUseCase - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - useCase = LogoutUseCase( - configScheduler = configScheduler, - imageUpSyncScheduler = imageUpSyncScheduler, - eventSyncManager = eventSyncManager, - authManager = authManager, - ) - } - - @Test - fun `Fully logs out when called`() = runTest { - useCase.invoke() - - verify { - imageUpSyncScheduler.cancelImageUpSync() - configScheduler.cancelProjectSync() - configScheduler.cancelDeviceSync() - eventSyncManager.cancelScheduledSync() - } - coVerify { - authManager.signOut() - eventSyncManager.deleteSyncInfo() - } - } -} diff --git a/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt b/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt index 963a3340d6..5cbc2d8acc 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/response/AppErrorReason.kt @@ -15,8 +15,8 @@ enum class AppErrorReason { UNEXPECTED_ERROR, ROOTED_DEVICE, ENROLMENT_LAST_BIOMETRICS_FAILED, - FACE_LICENSE_MISSING, - FACE_LICENSE_INVALID, + LICENSE_MISSING, + LICENSE_INVALID, FINGERPRINT_CONFIGURATION_ERROR, FACE_CONFIGURATION_ERROR, BACKEND_MAINTENANCE_ERROR, diff --git a/infra/core/src/main/java/com/simprints/core/domain/tokenization/serialization/EventDatabaseSerialization.kt b/infra/core/src/main/java/com/simprints/core/domain/tokenization/serialization/EventDatabaseSerialization.kt index 0ec327688a..6e4c970991 100644 --- a/infra/core/src/main/java/com/simprints/core/domain/tokenization/serialization/EventDatabaseSerialization.kt +++ b/infra/core/src/main/java/com/simprints/core/domain/tokenization/serialization/EventDatabaseSerialization.kt @@ -68,7 +68,7 @@ class TokenizationClassNameDeserializer : StdDeserializer(Tok override fun deserialize(p: JsonParser, ctxt: DeserializationContext): TokenizableString { val node: JsonNode = p.codec.readTree(p) - val className = node[FIELD_CLASS_NAME].asText() + val className = node[FIELD_CLASS_NAME]?.asText() ?: "" val value = node[FIELD_VALUE].asText() return when (className) { diff --git a/infra/core/src/main/java/com/simprints/core/tools/time/Timestamp.kt b/infra/core/src/main/java/com/simprints/core/tools/time/Timestamp.kt index 58b769c952..4d05a9095d 100644 --- a/infra/core/src/main/java/com/simprints/core/tools/time/Timestamp.kt +++ b/infra/core/src/main/java/com/simprints/core/tools/time/Timestamp.kt @@ -1,5 +1,8 @@ package com.simprints.core.tools.time +import androidx.annotation.Keep + +@Keep data class Timestamp( val ms: Long, val isTrustworthy: Boolean = false, diff --git a/infra/core/src/main/java/com/simprints/core/tools/utils/BatteryOptimizationUtils.kt b/infra/core/src/main/java/com/simprints/core/tools/utils/BatteryOptimizationUtils.kt new file mode 100644 index 0000000000..61e5b4842f --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/tools/utils/BatteryOptimizationUtils.kt @@ -0,0 +1,15 @@ +package com.simprints.core.tools.utils + +import android.content.Context +import android.os.PowerManager +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports + +@ExcludedFromGeneratedTestCoverageReports("Platform glue code") +object BatteryOptimizationUtils { + + fun isFollowingBatteryOptimizations(context: Context): Boolean = + (context.getSystemService(Context.POWER_SERVICE) as PowerManager) + .isIgnoringBatteryOptimizations(context.packageName) + .not() + +} diff --git a/infra/core/src/main/java/com/simprints/core/workers/SimCoroutineWorker.kt b/infra/core/src/main/java/com/simprints/core/workers/SimCoroutineWorker.kt index 43a1dbdfa3..ad06909f05 100644 --- a/infra/core/src/main/java/com/simprints/core/workers/SimCoroutineWorker.kt +++ b/infra/core/src/main/java/com/simprints/core/workers/SimCoroutineWorker.kt @@ -5,6 +5,7 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import android.content.pm.ServiceInfo import android.os.Build import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker @@ -12,6 +13,7 @@ import androidx.work.Data import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.simprints.core.ExcludedFromGeneratedTestCoverageReports +import com.simprints.core.tools.utils.BatteryOptimizationUtils import com.simprints.infra.logging.LoggingConstants.CrashReportTag import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.NetworkConnectionException @@ -57,6 +59,9 @@ abstract class SimCoroutineWorker( protected suspend fun showProgressNotification() { try { + if (BatteryOptimizationUtils.isFollowingBatteryOptimizations(context)) { + return + } setForeground(getForegroundInfo()) } catch (setForegroundException: Throwable) { // Setting foreground (showing the notification) may be restricted by the system @@ -94,15 +99,19 @@ abstract class SimCoroutineWorker( .setOnlyAlertOnce(true) .apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_DEFERRED + foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE } } .build() - return ForegroundInfo(WORKER_FOREGROUND_NOTIFICATION_ID, notification) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ForegroundInfo(WORKER_FOREGROUND_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + ForegroundInfo(WORKER_FOREGROUND_NOTIFICATION_ID, notification) + } } protected fun crashlyticsLog(message: String) { - Simber.tag(CrashReportTag.SYNC.name).i("$tag - $message") + Simber.tag(CrashReportTag.SYNC.name).i("$tag - $message".take(99)) } private fun logExceptionIfRequired(t: Throwable?) { diff --git a/infra/enrolment-records-store/build.gradle.kts b/infra/enrolment-records-store/build.gradle.kts index 305478e4f4..bb95959674 100644 --- a/infra/enrolment-records-store/build.gradle.kts +++ b/infra/enrolment-records-store/build.gradle.kts @@ -11,7 +11,9 @@ dependencies { implementation(project(":infra:config-store")) implementation(project(":infra:auth-store")) implementation(project(":infra:realm")) + implementation(project(":infra:events")) + implementation(libs.libsimprints) implementation(libs.retrofit.core) implementation(libs.jackson.core) } diff --git a/infra/enrolment-records-store/src/main/AndroidManifest.xml b/infra/enrolment-records-store/src/main/AndroidManifest.xml index 5c3d3655b5..ae65c8860f 100644 --- a/infra/enrolment-records-store/src/main/AndroidManifest.xml +++ b/infra/enrolment-records-store/src/main/AndroidManifest.xml @@ -1,2 +1,6 @@ - + + + + + \ No newline at end of file diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepository.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepository.kt index f773b329f5..42e5eaeb08 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepository.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepository.kt @@ -1,9 +1,32 @@ package com.simprints.infra.enrolment.records.store +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.infra.config.store.models.Project +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity +import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSource +@ExcludedFromGeneratedTestCoverageReports("This is an interface with no logic") interface EnrolmentRecordRepository : EnrolmentRecordLocalDataSource { suspend fun uploadRecords(subjectIds: List) suspend fun tokenizeExistingRecords(project: Project) + + suspend fun count( + query: SubjectQuery = SubjectQuery(), + dataSource: BiometricDataSource = BiometricDataSource.SIMPRINTS + ): Int + + suspend fun loadFingerprintIdentities( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource = BiometricDataSource.SIMPRINTS + ): List + + suspend fun loadFaceIdentities( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource = BiometricDataSource.SIMPRINTS + ): List } diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImpl.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImpl.kt index 53db975306..8eb5cbf905 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImpl.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImpl.kt @@ -6,6 +6,9 @@ import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSource @@ -21,22 +24,25 @@ internal class EnrolmentRecordRepositoryImpl( context: Context, private val remoteDataSource: EnrolmentRecordRemoteDataSource, private val localDataSource: EnrolmentRecordLocalDataSource, + private val commCareDataSource: IdentityDataSource, private val tokenizationProcessor: TokenizationProcessor, private val dispatcher: CoroutineDispatcher, private val batchSize: Int, -) : EnrolmentRecordRepository, EnrolmentRecordLocalDataSource by localDataSource { +) : EnrolmentRecordRepository, EnrolmentRecordLocalDataSource by localDataSource { @Inject constructor( @ApplicationContext context: Context, remoteDataSource: EnrolmentRecordRemoteDataSource, localDataSource: EnrolmentRecordLocalDataSource, + @CommCareDataSource commCareDataSource: IdentityDataSource, tokenizationProcessor: TokenizationProcessor, @DispatcherIO dispatcher: CoroutineDispatcher, ) : this( context = context, remoteDataSource = remoteDataSource, localDataSource = localDataSource, + commCareDataSource = commCareDataSource, tokenizationProcessor = tokenizationProcessor, dispatcher = dispatcher, batchSize = BATCH_SIZE @@ -109,4 +115,26 @@ internal class EnrolmentRecordRepositoryImpl( project = project ) } + + override suspend fun count(query: SubjectQuery, dataSource: BiometricDataSource): Int = + fromIdentityDataSource(dataSource).count(query) + + override suspend fun loadFingerprintIdentities( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource + ): List = + fromIdentityDataSource(dataSource).loadFingerprintIdentities(query, range) + + override suspend fun loadFaceIdentities( + query: SubjectQuery, + range: IntRange, + dataSource: BiometricDataSource + ): List = + fromIdentityDataSource(dataSource).loadFaceIdentities(query, range) + + private fun fromIdentityDataSource(dataSource: BiometricDataSource) = when (dataSource) { + BiometricDataSource.SIMPRINTS -> localDataSource + BiometricDataSource.COMMCARE -> commCareDataSource + } } diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordsStoreModule.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordsStoreModule.kt index 9e70bba407..8b1922f4d6 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordsStoreModule.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordsStoreModule.kt @@ -1,15 +1,26 @@ package com.simprints.infra.enrolment.records.store +import android.content.Context +import com.simprints.core.tools.json.JsonHelper +import com.simprints.core.tools.utils.EncodingUtils +import com.simprints.infra.enrolment.records.store.commcare.CommCareIdentityDataSource import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSource import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSourceImpl import com.simprints.infra.enrolment.records.store.remote.EnrolmentRecordRemoteDataSource import com.simprints.infra.enrolment.records.store.remote.EnrolmentRecordRemoteDataSourceImpl import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier -@Module +@Module( + includes = [ + IdentityDataSourceModule::class, + ] +) @InstallIn(SingletonComponent::class) abstract class EnrolmentRecordsStoreModule { @@ -22,3 +33,23 @@ abstract class EnrolmentRecordsStoreModule { @Binds internal abstract fun bindEnrolmentRecordRemoteDataSource(impl: EnrolmentRecordRemoteDataSourceImpl): EnrolmentRecordRemoteDataSource } + +@Module +@InstallIn(SingletonComponent::class) +class IdentityDataSourceModule { + @CommCareDataSource + @Provides + fun provideCommCareIdentityDataSource( + encoder: EncodingUtils, + jsonHelper: JsonHelper, + @ApplicationContext context: Context, + ): IdentityDataSource = CommCareIdentityDataSource( + encoder = encoder, + jsonHelper = jsonHelper, + context = context, + ) +} + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommCareDataSource diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/IdentityDataSource.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/IdentityDataSource.kt new file mode 100644 index 0000000000..cb82832030 --- /dev/null +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/IdentityDataSource.kt @@ -0,0 +1,12 @@ +package com.simprints.infra.enrolment.records.store + +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity +import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery + +interface IdentityDataSource { + + suspend fun count(query: SubjectQuery = SubjectQuery()): Int + suspend fun loadFingerprintIdentities(query: SubjectQuery, range: IntRange): List + suspend fun loadFaceIdentities(query: SubjectQuery, range: IntRange): List +} \ No newline at end of file diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSource.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSource.kt new file mode 100644 index 0000000000..f7eb5353d4 --- /dev/null +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSource.kt @@ -0,0 +1,159 @@ +package com.simprints.infra.enrolment.records.store.commcare + +import android.content.Context +import android.net.Uri +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.module.SimpleModule +import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.fingerprint.FingerprintSample +import com.simprints.core.domain.tokenization.TokenizableString +import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameDeserializer +import com.simprints.core.domain.tokenization.serialization.TokenizationClassNameSerializer +import com.simprints.core.tools.json.JsonHelper +import com.simprints.core.tools.utils.EncodingUtils +import com.simprints.infra.enrolment.records.store.IdentityDataSource +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity +import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery +import com.simprints.infra.events.event.cosync.CoSyncEnrolmentRecordEvents +import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent +import com.simprints.infra.events.event.domain.models.subject.FaceReference +import com.simprints.infra.events.event.domain.models.subject.FingerprintReference +import com.simprints.infra.logging.Simber +import com.simprints.libsimprints.Constants.SIMPRINTS_COSYNC_SUBJECT_ACTIONS +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal class CommCareIdentityDataSource @Inject constructor( + private val encoder: EncodingUtils, + private val jsonHelper: JsonHelper, + @ApplicationContext private val context: Context, +) : IdentityDataSource { + + companion object { + val CASE_METADATA_URI: Uri = Uri.parse("content://org.commcare.dalvik.case/casedb/case") + val CASE_DATA_URI: Uri = Uri.parse("content://org.commcare.dalvik.case/casedb/data") + const val COLUMN_CASE_ID = "case_id" + const val COLUMN_DATUM_ID = "datum_id" + const val COLUMN_VALUE = "value" + } + + override suspend fun loadFingerprintIdentities( + query: SubjectQuery, + range: IntRange, + ): List = loadEnrolmentRecordCreationEvents(range) + .filter { erce -> erce.payload.biometricReferences.any { it is FingerprintReference } } + .map { + FingerprintIdentity( + it.payload.subjectId, + it.payload.biometricReferences.filterIsInstance() + .flatMap { fingerprintReference -> + fingerprintReference.templates.map { fingerprintTemplate -> + FingerprintSample( + fingerIdentifier = fingerprintTemplate.finger, + templateQualityScore = fingerprintTemplate.quality, + template = encoder.base64ToBytes(fingerprintTemplate.template), + format = fingerprintReference.format, + ) + } + } + ) + } + + override suspend fun loadFaceIdentities( + query: SubjectQuery, + range: IntRange, + ): List = loadEnrolmentRecordCreationEvents(range) + .filter { erce -> erce.payload.biometricReferences.any { it is FaceReference } } + .map { + FaceIdentity( + it.payload.subjectId, + it.payload.biometricReferences.filterIsInstance() + .flatMap { faceReference -> + faceReference.templates.map { faceTemplate -> + FaceSample( + template = encoder.base64ToBytes(faceTemplate.template), + format = faceReference.format, + ) + } + } + ) + } + + private fun loadEnrolmentRecordCreationEvents(range: IntRange): List { + val enrolmentRecordCreationEvents: MutableList = mutableListOf() + + try { + context.contentResolver.query(CASE_METADATA_URI, null, null, null, null)?.use { caseMetadataCursor -> + if (caseMetadataCursor.moveToPosition(range.first)) { + do { + caseMetadataCursor.getString(caseMetadataCursor.getColumnIndexOrThrow(COLUMN_CASE_ID))?.let { caseId -> + enrolmentRecordCreationEvents.addAll(loadEnrolmentRecordCreationEvents(caseId)) + } + } while (caseMetadataCursor.moveToNext() && caseMetadataCursor.position < range.last) + } + } + } catch (e: Exception) { + Simber.e("Error while querying CommCare", e) + } + + return enrolmentRecordCreationEvents + } + + private fun loadEnrolmentRecordCreationEvents(caseId: String): List { + val caseEnrolmentRecordCreationEvents: MutableList = mutableListOf() + + //Access Case Data Listing for the caseId + val caseDataUri = CASE_DATA_URI.buildUpon().appendPath(caseId).build() + context.contentResolver.query(caseDataUri, null, null, null, null)?.use { caseDataCursor -> + var subjectActions = "" + while (caseDataCursor.moveToNext()) { + val key = caseDataCursor.getString(caseDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID)) + if (key == SIMPRINTS_COSYNC_SUBJECT_ACTIONS) { + subjectActions = caseDataCursor.getString(caseDataCursor.getColumnIndexOrThrow(COLUMN_VALUE)) + break + } + } + + val coSyncEnrolmentRecordEvents = subjectActions.takeIf(String::isNotEmpty)?.let { + try { + val coSyncSerializationModule = SimpleModule().apply { + addSerializer( + TokenizableString::class.java, + TokenizationClassNameSerializer() + ) + addDeserializer( + TokenizableString::class.java, + TokenizationClassNameDeserializer() + ) + } + jsonHelper.fromJson( + json = it, + module = coSyncSerializationModule, + type = object : TypeReference() {} + ) + } catch (e: Exception) { + Simber.e("Error while parsing subjectActions", e) + null + } + } + coSyncEnrolmentRecordEvents?.events?.filterIsInstance() + ?.let { events -> + caseEnrolmentRecordCreationEvents.addAll(events) + } + + Simber.d(subjectActions) + } + + return caseEnrolmentRecordCreationEvents + } + + override suspend fun count(query: SubjectQuery): Int { + var count = 0 + context.contentResolver.query(CASE_METADATA_URI, null, null, null, null)?.use { caseMetadataCursor -> + count = caseMetadataCursor.count + } + + return count + } +} diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSource.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSource.kt new file mode 100644 index 0000000000..975245deff --- /dev/null +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSource.kt @@ -0,0 +1,13 @@ +package com.simprints.infra.enrolment.records.store.domain.models + +enum class BiometricDataSource { + SIMPRINTS, + COMMCARE; + + companion object { + fun fromString(value: String) = when (value.uppercase()) { + "COMMCARE" -> COMMCARE + else -> SIMPRINTS + } + } +} \ No newline at end of file diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FaceIdentity.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FaceIdentity.kt index e1e8c0cf77..ffffc5aaca 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FaceIdentity.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FaceIdentity.kt @@ -5,5 +5,5 @@ import com.simprints.core.domain.face.FaceSample import kotlinx.parcelize.Parcelize @Parcelize -class FaceIdentity(val personId: String, val faces: List) : Parcelable +class FaceIdentity(val subjectId: String, val faces: List) : Parcelable diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FingerprintIdentity.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FingerprintIdentity.kt index fe692c2939..a5abb37bf1 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FingerprintIdentity.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/FingerprintIdentity.kt @@ -5,5 +5,5 @@ import com.simprints.core.domain.fingerprint.FingerprintSample import kotlinx.parcelize.Parcelize @Parcelize -class FingerprintIdentity(val patientId: String, +class FingerprintIdentity(val subjectId: String, val fingerprints: List): Parcelable diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/SubjectQuery.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/SubjectQuery.kt index 4c0bd5c077..a02dc7be8d 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/SubjectQuery.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/domain/models/SubjectQuery.kt @@ -9,6 +9,7 @@ data class SubjectQuery( val subjectId: String? = null, val subjectIds: List? = null, val attendantId: String? = null, + val fingerprintSampleFormat: String? = null, val hasUntokenizedFields: Boolean? = null, val moduleId: String? = null, val sort: Boolean = false, diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSource.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSource.kt index d8d2e08e81..5b57819299 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSource.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSource.kt @@ -1,20 +1,16 @@ package com.simprints.infra.enrolment.records.store.local -import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity -import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity +import com.simprints.infra.enrolment.records.store.IdentityDataSource import com.simprints.infra.enrolment.records.store.domain.models.Subject import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery -interface EnrolmentRecordLocalDataSource { +interface EnrolmentRecordLocalDataSource : IdentityDataSource { suspend fun load(query: SubjectQuery): List - suspend fun loadFingerprintIdentities(query: SubjectQuery, range: IntRange): List - suspend fun loadFaceIdentities(query: SubjectQuery, range: IntRange): List suspend fun delete(queries: List) suspend fun deleteAll() - suspend fun count(query: SubjectQuery = SubjectQuery()): Int suspend fun performActions(actions: List) } diff --git a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImpl.kt b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImpl.kt index 2076fc544b..ec7495e3f8 100644 --- a/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImpl.kt +++ b/infra/enrolment-records-store/src/main/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImpl.kt @@ -32,6 +32,8 @@ internal class EnrolmentRecordLocalDataSourceImpl @Inject constructor( const val MODULE_ID_FIELD = "moduleId" const val IS_ATTENDANT_ID_TOKENIZED_FIELD = "isAttendantIdTokenized" const val IS_MODULE_ID_TOKENIZED_FIELD = "isModuleIdTokenized" + const val FINGERPRINT_SAMPLES_FIELD = "fingerprintSamples" + const val FORMAT_FIELD = "format" } override suspend fun load(query: SubjectQuery): List = realmWrapper.readRealm { @@ -148,6 +150,12 @@ internal class EnrolmentRecordLocalDataSourceImpl @Inject constructor( if (query.moduleId != null) { realmQuery = realmQuery.query("$MODULE_ID_FIELD == $0", query.moduleId) } + if (query.fingerprintSampleFormat != null) { + realmQuery = realmQuery.query( + "ANY $FINGERPRINT_SAMPLES_FIELD.$FORMAT_FIELD == $0", + query.fingerprintSampleFormat + ) + } if (query.afterSubjectId != null) { realmQuery = realmQuery.query( "$SUBJECT_ID_FIELD >= $0", diff --git a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImplTest.kt b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImplTest.kt index 0efcedfbfe..b9ff2989fc 100644 --- a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImplTest.kt +++ b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/EnrolmentRecordRepositoryImplTest.kt @@ -7,6 +7,9 @@ import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.infra.config.store.models.Project import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity import com.simprints.infra.enrolment.records.store.domain.models.Subject import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery @@ -50,6 +53,7 @@ class EnrolmentRecordRepositoryImplTest { private val tokenizationProcessor = mockk() private val localDataSource = mockk(relaxed = true) + private val commCareDataSource = mockk(relaxed = true) private val remoteDataSource = mockk(relaxed = true) private val prefsEditor = mockk(relaxed = true) private val prefs = mockk { @@ -69,6 +73,7 @@ class EnrolmentRecordRepositoryImplTest { context = ctx, remoteDataSource = remoteDataSource, localDataSource = localDataSource, + commCareDataSource = commCareDataSource, tokenizationProcessor = tokenizationProcessor, dispatcher = UnconfinedTestDispatcher(), batchSize = BATCH_SIZE, @@ -235,6 +240,7 @@ class EnrolmentRecordRepositoryImplTest { repository.tokenizeExistingRecords(project) coVerify { localDataSource.performActions(emptyList()) } } + @Test fun `when tokenizing existing subjects throws exception, then it is captured and not thrown up the calling chain`() = runTest { @@ -245,4 +251,94 @@ class EnrolmentRecordRepositoryImplTest { repository.tokenizeExistingRecords(project) } + + @Test + fun `should return the correct count of subjects when dataSource is Simprints`() = runTest { + val expectedSubjectQuery = SubjectQuery() + coEvery { localDataSource.count(expectedSubjectQuery) } returns 5 + + val count = repository.count(query = expectedSubjectQuery, dataSource = BiometricDataSource.SIMPRINTS) + + assert(count == 5) + coVerify(exactly = 1) { localDataSource.count(expectedSubjectQuery) } + } + + @Test + fun `should return the correct count of subjects when dataSource is CommCare`() = runTest { + val expectedSubjectQuery = SubjectQuery() + coEvery { commCareDataSource.count(expectedSubjectQuery) } returns 5 + + val count = repository.count(query = expectedSubjectQuery, dataSource = BiometricDataSource.COMMCARE) + + assert(count == 5) + coVerify(exactly = 1) { commCareDataSource.count(expectedSubjectQuery) } + } + + @Test + fun `should forward the call to the local data source when loading fingerprint identities and dataSource is Simprints`() = runTest { + val expectedSubjectQuery = SubjectQuery() + val expectedRange = 0..10 + val expectedFingerprintIdentities = listOf() + coEvery { localDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange) } returns expectedFingerprintIdentities + + val fingerprintIdentities = repository.loadFingerprintIdentities( + query = expectedSubjectQuery, + range = expectedRange, + dataSource = BiometricDataSource.SIMPRINTS + ) + + assert(fingerprintIdentities == expectedFingerprintIdentities) + coVerify(exactly = 1) { localDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange) } + } + + @Test + fun `should forward the call to the commcare data source when loading fingerprint identities and dataSource is CommCare`() = runTest { + val expectedSubjectQuery = SubjectQuery() + val expectedRange = 0..10 + val expectedFingerprintIdentities = listOf() + coEvery { commCareDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange) } returns expectedFingerprintIdentities + + val fingerprintIdentities = repository.loadFingerprintIdentities( + query = expectedSubjectQuery, + range = expectedRange, + dataSource = BiometricDataSource.COMMCARE + ) + + assert(fingerprintIdentities == expectedFingerprintIdentities) + coVerify(exactly = 1) { commCareDataSource.loadFingerprintIdentities(expectedSubjectQuery, expectedRange) } + } + + @Test + fun `should forward the call to the local data source when loading face identities and dataSource is Simprints`() = runTest { + val expectedSubjectQuery = SubjectQuery() + val expectedRange = 0..10 + val expectedFaceIdentities = listOf() + coEvery { localDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange) } returns expectedFaceIdentities + + val faceIdentities = repository.loadFaceIdentities( + query = expectedSubjectQuery, + range = expectedRange, + dataSource = BiometricDataSource.SIMPRINTS + ) + + assert(faceIdentities == expectedFaceIdentities) + coVerify(exactly = 1) { localDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange) } + } + + @Test + fun `should forward the call to the commcare data source when loading face identities and dataSource is CommCare`() = runTest { + val expectedSubjectQuery = SubjectQuery() + val expectedRange = 0..10 + val expectedFaceIdentities = listOf() + coEvery { commCareDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange) } returns expectedFaceIdentities + + val faceIdentities = repository.loadFaceIdentities( + query = expectedSubjectQuery, + range = expectedRange, + dataSource = BiometricDataSource.COMMCARE + ) + + assert(faceIdentities == expectedFaceIdentities) + coVerify(exactly = 1) { commCareDataSource.loadFaceIdentities(expectedSubjectQuery, expectedRange) } + } } diff --git a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSourceTest.kt b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSourceTest.kt new file mode 100644 index 0000000000..20009d5250 --- /dev/null +++ b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/commcare/CommCareIdentityDataSourceTest.kt @@ -0,0 +1,505 @@ +package com.simprints.infra.enrolment.records.store.commcare + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import com.simprints.core.domain.face.FaceSample +import com.simprints.core.domain.fingerprint.FingerprintSample +import com.simprints.core.domain.fingerprint.IFingerIdentifier.LEFT_INDEX_FINGER +import com.simprints.core.domain.fingerprint.IFingerIdentifier.LEFT_THUMB +import com.simprints.core.tools.json.JsonHelper +import com.simprints.core.tools.utils.EncodingUtils +import com.simprints.infra.enrolment.records.store.commcare.CommCareIdentityDataSource.Companion.COLUMN_DATUM_ID +import com.simprints.infra.enrolment.records.store.commcare.CommCareIdentityDataSource.Companion.COLUMN_VALUE +import com.simprints.infra.enrolment.records.store.domain.models.FaceIdentity +import com.simprints.infra.enrolment.records.store.domain.models.FingerprintIdentity +import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery +import com.simprints.infra.logging.Simber +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.unmockkStatic +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test + +class CommCareIdentityDataSourceTest { + + companion object { + private const val SUBJECT_ACTIONS_FINGERPRINT_1 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"b26c91bc-b307-4131-80c3-55090ba5dbf2","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"quality":99,"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA","finger":"LEFT_THUMB"},{"quality":88,"template":"Rk1SACAyMAAAAAEUAAABLAGQAMUAxQEAABBYKUBqACiHAIBKAD0RAECyAEjuAEAfAF6sAEAnAG4sAIB4AHT2AIByAIuiAEAGAIs1AEC9AJDUAEB3AJS9AICaAJXRAEDzAJ/bAECHAKi9AIBzALJDAEAXALfDAEB6ALszAIDGAL3JAED2AL/QAEBHAMrRAEENAMrWAECiAMy9AIAkANfNAEDwANnKAEB3AOr4AICjAPKhAEANAPXGAIEEAPjJAEBCAPzaAICNAQUDAEEZAQdpAEDOARsfAIDpAR0TAEDZASUUAEDnASeNAIDsAS0NAED+ATONAEDxAT8KAIB9AUB5AEDAAVUNAEBdAVZzAECCAWt9AAAA","finger":"LEFT_INDEX_FINGER"}],"format":"ISO_19794_2","type":"FINGERPRINT_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + private const val SUBJECT_ACTIONS_FINGERPRINT_2 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"a961fcb4-8573-4270-a1b2-088e88275b00","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"quality":77,"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA","finger":"LEFT_THUMB"},{"quality":66,"template":"Rk1SACAyMAAAAAEUAAABLAGQAMUAxQEAABBYKUBqACiHAIBKAD0RAECyAEjuAEAfAF6sAEAnAG4sAIB4AHT2AIByAIuiAEAGAIs1AEC9AJDUAEB3AJS9AICaAJXRAEDzAJ/bAECHAKi9AIBzALJDAEAXALfDAEB6ALszAIDGAL3JAED2AL/QAEBHAMrRAEENAMrWAECiAMy9AIAkANfNAEDwANnKAEB3AOr4AICjAPKhAEANAPXGAIEEAPjJAEBCAPzaAICNAQUDAEEZAQdpAEDOARsfAIDpAR0TAEDZASUUAEDnASeNAIDsAS0NAED+ATONAEDxAT8KAIB9AUB5AEDAAVUNAEBdAVZzAECCAWt9AAAA","finger":"LEFT_INDEX_FINGER"}],"format":"ISO_19794_2","type":"FINGERPRINT_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + private const val SUBJECT_ACTIONS_FACE_1 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"b26c91bc-b307-4131-80c3-55090ba5dbf2","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA"}],"format":"ROC_1_23","type":"FACE_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + private const val SUBJECT_ACTIONS_FACE_2 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"a961fcb4-8573-4270-a1b2-088e88275b00","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA"}],"format":"ROC_3","type":"FACE_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + private const val SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_1 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"b26c91bc-b307-4131-80c3-55090ba5dbf2","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"quality":99,"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA","finger":"LEFT_THUMB"},{"quality":88,"template":"Rk1SACAyMAAAAAEUAAABLAGQAMUAxQEAABBYKUBqACiHAIBKAD0RAECyAEjuAEAfAF6sAEAnAG4sAIB4AHT2AIByAIuiAEAGAIs1AEC9AJDUAEB3AJS9AICaAJXRAEDzAJ/bAECHAKi9AIBzALJDAEAXALfDAEB6ALszAIDGAL3JAED2AL/QAEBHAMrRAEENAMrWAECiAMy9AIAkANfNAEDwANnKAEB3AOr4AICjAPKhAEANAPXGAIEEAPjJAEBCAPzaAICNAQUDAEEZAQdpAEDOARsfAIDpAR0TAEDZASUUAEDnASeNAIDsAS0NAED+ATONAEDxAT8KAIB9AUB5AEDAAVUNAEBdAVZzAECCAWt9AAAA","finger":"LEFT_INDEX_FINGER"}],"format":"ISO_19794_2","type":"FINGERPRINT_REFERENCE"},{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA"}],"format":"ROC_1_23","type":"FACE_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + private const val SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_2 = """{"events":[{"id":"0dafcd03-96c4-4ca5-b802-292da6d4f799","payload":{"subjectId":"a961fcb4-8573-4270-a1b2-088e88275b00","projectId":"nXcj9neYhXP9rFp56uWk","moduleId":{"value":"AWuA3H0WGtHI2uod+ePZ3yiWTt9etQ=="},"attendantId":{"value":"AdySMrjuy7uq0Dcxov3rUFIw66uXTFrKd0BnzSr9MYXl5maWEpyKQT8AUdcPuVHUWpOkO88="},"biometricReferences":[{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"quality":77,"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA","finger":"LEFT_THUMB"},{"quality":66,"template":"Rk1SACAyMAAAAAEUAAABLAGQAMUAxQEAABBYKUBqACiHAIBKAD0RAECyAEjuAEAfAF6sAEAnAG4sAIB4AHT2AIByAIuiAEAGAIs1AEC9AJDUAEB3AJS9AICaAJXRAEDzAJ/bAECHAKi9AIBzALJDAEAXALfDAEB6ALszAIDGAL3JAED2AL/QAEBHAMrRAEENAMrWAECiAMy9AIAkANfNAEDwANnKAEB3AOr4AICjAPKhAEANAPXGAIEEAPjJAEBCAPzaAICNAQUDAEEZAQdpAEDOARsfAIDpAR0TAEDZASUUAEDnASeNAIDsAS0NAED+ATONAEDxAT8KAIB9AUB5AEDAAVUNAEBdAVZzAECCAWt9AAAA","finger":"LEFT_INDEX_FINGER"}],"format":"ISO_19794_2","type":"FINGERPRINT_REFERENCE"},{"id":"2b9b4991-29d7-3eee-ac02-191afaa0c1a2","templates":[{"template":"Rk1SACAyMAAAAADkAAABLAGQAMUAxQEAABBjIYCzAAgQAEAhABoeAIB3ACQNAEAoACsYAEDvADYDAECrAFgDAEBQAHIaAEDUAH2AAEEEAIX4AEDhAJX4AEDpALZ7AIB1AL0RAEEJAMPuAECSAM0NAED2ANPxAEDmAOXsAEDdAPrnAEBhAQYaAIDcARDaAICwARn8AEDJASlnAECMATcVAEB7ATgbAECxAT2qAECnAT6hAEB3AT6eAECIAT6eAEEWAUDGAIDHAUizAED9AU2zAEDsAVU9AED1AWYzAIC2AWciAAAA"}],"format":"ROC_3","type":"FACE_REFERENCE"}]},"type":"EnrolmentRecordCreation"}]}""" + + private val expectedFingerprintIdentities = listOf( + FingerprintIdentity( + subjectId = "b26c91bc-b307-4131-80c3-55090ba5dbf2", + fingerprints = + listOf( + FingerprintSample( + fingerIdentifier = LEFT_THUMB, + templateQualityScore = 99, + template = byteArrayOf(), + format = "ISO_19794_2"), + FingerprintSample( + fingerIdentifier = LEFT_INDEX_FINGER, + templateQualityScore = 88, + template = byteArrayOf(), + format = "ISO_19794_2"))), + FingerprintIdentity( + subjectId = "a961fcb4-8573-4270-a1b2-088e88275b00", + fingerprints = + listOf( + FingerprintSample( + fingerIdentifier = LEFT_THUMB, + templateQualityScore = 77, + template = byteArrayOf(), + format = "ISO_19794_2"), + FingerprintSample( + fingerIdentifier = LEFT_INDEX_FINGER, + templateQualityScore = 66, + template = byteArrayOf(), + format = "ISO_19794_2"))), + ) + val expectedFaceIdentities = listOf( + FaceIdentity( + subjectId = "b26c91bc-b307-4131-80c3-55090ba5dbf2", + faces = listOf(FaceSample(template = byteArrayOf(), format = "ROC_1_23"))), + FaceIdentity( + subjectId = "a961fcb4-8573-4270-a1b2-088e88275b00", + faces = listOf(FaceSample(template = byteArrayOf(), format = "ROC_3"))), + ) + + @JvmStatic lateinit var mockMetadataUri: Uri + @JvmStatic lateinit var mockDataUri: Uri + @JvmStatic lateinit var mockDataCaseIdUri: Uri + + @JvmStatic + @BeforeClass + fun setupClass() { + mockkObject(Simber) + mockMetadataUri = mockk(relaxed = true) + mockDataUri = mockk(relaxed = true) + mockDataCaseIdUri = mockk(relaxed = true) + mockkStatic(Uri::class) + every { Uri.parse("content://org.commcare.dalvik.case/casedb/case") } returns mockMetadataUri + every { Uri.parse("content://org.commcare.dalvik.case/casedb/data") } returns mockDataUri + every { mockDataUri.buildUpon().appendPath(any()).build() } returns mockDataCaseIdUri + } + + @JvmStatic + @AfterClass + fun cleanupClass() { + clearAllMocks() + unmockkAll() + unmockkStatic(Uri::class) + } + } + + @MockK private lateinit var encoder: EncodingUtils + + @MockK private lateinit var context: Context + + @MockK private lateinit var mockContentResolver: ContentResolver + + private lateinit var mockMetadataCursor: Cursor + + private lateinit var mockDataCursor: Cursor + + private lateinit var dataSource: CommCareIdentityDataSource + + @Before + fun setUp() { + MockKAnnotations.init(this) + + every { context.contentResolver } returns mockContentResolver + + mockMetadataCursor = mockk(relaxed = true) + mockDataCursor = mockk(relaxed = true) + + every { mockMetadataCursor.close() } just Runs + every { mockDataCursor.close() } just Runs + + every { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } returns mockMetadataCursor + every { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } returns mockDataCursor + + every { encoder.base64ToBytes(any()) } returns byteArrayOf() + + dataSource = CommCareIdentityDataSource(encoder, JsonHelper, context) + } + + @Test + fun testLoadFingerprintIdentities() = runTest { + every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FINGERPRINT_1, SUBJECT_ACTIONS_FINGERPRINT_2) + + val query = SubjectQuery() + val range = 0..expectedFingerprintIdentities.size + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(expectedFingerprintIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFingerprintIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.fingerprints.zip(actual.fingerprints) { expectedFingerprint, actualFingerprint -> + expectedFingerprint.fingerIdentifier == actualFingerprint.fingerIdentifier && + expectedFingerprint.templateQualityScore == actualFingerprint.templateQualityScore && + expectedFingerprint.template.contentEquals(actualFingerprint.template) && + expectedFingerprint.format == actualFingerprint.format + } .all { it } + } .all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun testLoadFaceIdentities() = runTest { + every { mockMetadataCursor.count } returns expectedFaceIdentities.size + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FACE_1, SUBJECT_ACTIONS_FACE_2) + + val query = SubjectQuery() + val range = 0..expectedFaceIdentities.size + val actualIdentities = dataSource.loadFaceIdentities(query, range) + + assertEquals(expectedFaceIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFaceIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.faces.zip(actual.faces) { expectedFace, actualFace -> + expectedFace.template.contentEquals(actualFace.template) && + expectedFace.format == actualFace.format + }.all { it} + }.all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test loadFingerprintIdentities returns only identities with fingerprint references`() = runTest { + every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size + 1 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FINGERPRINT_1, SUBJECT_ACTIONS_FINGERPRINT_2, SUBJECT_ACTIONS_FACE_1) + + val query = SubjectQuery() + val range = 0..expectedFingerprintIdentities.size + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(expectedFingerprintIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFingerprintIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.fingerprints.zip(actual.fingerprints) { expectedFingerprint, actualFingerprint -> + expectedFingerprint.fingerIdentifier == actualFingerprint.fingerIdentifier && + expectedFingerprint.templateQualityScore == actualFingerprint.templateQualityScore && + expectedFingerprint.template.contentEquals(actualFingerprint.template) && + expectedFingerprint.format == actualFingerprint.format + } .all { it } + } .all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test loadFaceIdentities returns only identities with face references`() = runTest { + every { mockMetadataCursor.count } returns expectedFaceIdentities.size + 1 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FACE_1, SUBJECT_ACTIONS_FACE_2, SUBJECT_ACTIONS_FINGERPRINT_1) + + val query = SubjectQuery() + val range = 0..expectedFaceIdentities.size + val actualIdentities = dataSource.loadFaceIdentities(query, range) + + assertEquals(expectedFaceIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFaceIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.faces.zip(actual.faces) { expectedFace, actualFace -> + expectedFace.template.contentEquals(actualFace.template) && + expectedFace.format == actualFace.format + }.all { it} + }.all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test loadFingerprintIdentities returns only fingerprint references for dual modality identities`() = runTest { + every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_1, SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_2) + + val query = SubjectQuery() + val range = 0..expectedFingerprintIdentities.size + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(expectedFingerprintIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFingerprintIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.fingerprints.zip(actual.fingerprints) { expectedFingerprint, actualFingerprint -> + expectedFingerprint.fingerIdentifier == actualFingerprint.fingerIdentifier && + expectedFingerprint.templateQualityScore == actualFingerprint.templateQualityScore && + expectedFingerprint.template.contentEquals(actualFingerprint.template) && + expectedFingerprint.format == actualFingerprint.format + } .all { it } + } .all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test loadFaceIdentities returns only face references for dual modality identities`() = runTest { + every { mockMetadataCursor.count } returns expectedFaceIdentities.size + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_1, SUBJECT_ACTIONS_FINGERPRINT_AND_FACE_2) + + val query = SubjectQuery() + val range = 0..expectedFaceIdentities.size + val actualIdentities = dataSource.loadFaceIdentities(query, range) + + assertEquals(expectedFaceIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFaceIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.faces.zip(actual.faces) { expectedFace, actualFace -> + expectedFace.template.contentEquals(actualFace.template) && + expectedFace.format == actualFace.format + }.all { it} + }.all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun testCount() = runTest { + val expectedCount = 5 + every { mockMetadataCursor.count } returns expectedCount + + val query = SubjectQuery() + val actualCount = dataSource.count(query) + + assertEquals(expectedCount, actualCount) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockMetadataCursor.count } + } + + @Test + fun `test handling of null metadata cursor`() = runTest { + every { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } returns null + + val query = SubjectQuery() + val range = 0..0 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertTrue(actualIdentities.isEmpty()) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify(exactly = 0) { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test metadata cursor size below range's first`() = runTest { + every { mockMetadataCursor.count } returns 1 + + val query = SubjectQuery() + val range = 2..3 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertTrue(actualIdentities.isEmpty()) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify(exactly = 0) { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `test metadata cursor size bigger than range`() = runTest { + every { mockMetadataCursor.count } returns expectedFingerprintIdentities.size + 1 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, true) + every { mockMetadataCursor.position } returnsMany listOf(1, 2) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf(SUBJECT_ACTIONS_FINGERPRINT_1, SUBJECT_ACTIONS_FINGERPRINT_2) + + val query = SubjectQuery() + val range = 0..expectedFingerprintIdentities.size + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(expectedFingerprintIdentities.size, actualIdentities.size) + val areContentsEqual = expectedFingerprintIdentities.zip(actualIdentities) { expected, actual -> + expected.subjectId == actual.subjectId && + expected.fingerprints.zip(actual.fingerprints) { expectedFingerprint, actualFingerprint -> + expectedFingerprint.fingerIdentifier == actualFingerprint.fingerIdentifier && + expectedFingerprint.templateQualityScore == actualFingerprint.templateQualityScore && + expectedFingerprint.template.contentEquals(actualFingerprint.template) && + expectedFingerprint.format == actualFingerprint.format + }.all { it } + }.all { it } + assertTrue(areContentsEqual) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `empty caseId results in empty result`() = runTest { + every { mockMetadataCursor.count } returns 2 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockMetadataCursor.getString(any()) } returns null + + val query = SubjectQuery() + val range = 0..2 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(0, actualIdentities.size) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify(exactly = 0) { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `exception during metadata cursor access is reported`() = runTest { + every { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } throws + RuntimeException("Some exception") + + val query = SubjectQuery() + val range = 0..2 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(0, actualIdentities.size) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify(exactly = 0) { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + coVerify { Simber.e(any(), ofType()) } + } + + @Test + fun `data cursor is null`() = runTest { + every { mockMetadataCursor.count } returns 2 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } returns null + + val query = SubjectQuery() + val range = 0..2 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(0, actualIdentities.size) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `subjectActions not found in cursor data`() = runTest { + every { mockMetadataCursor.count } returns 2 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returnsMany listOf(true, true, true, false) + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returns "someKey" + every { mockDataCursor.getString(1) } returns "someValue" + + val query = SubjectQuery() + val range = 0..2 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(0, actualIdentities.size) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + } + + @Test + fun `subjectActions contains invalid JSON`() = runTest { + every { mockMetadataCursor.count } returns 2 + every { mockMetadataCursor.moveToPosition(0) } returns true + every { mockMetadataCursor.moveToNext() } returnsMany listOf(true, false) + every { mockDataCursor.moveToNext() } returns true + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_DATUM_ID) } returns 0 + every { mockDataCursor.getColumnIndexOrThrow(COLUMN_VALUE) } returns 1 + every { mockDataCursor.getString(0) } returnsMany + listOf("someOtherDatumId", "subjectActions", "someOtherDatumId", "subjectActions") + every { mockDataCursor.getString(1) } returnsMany + listOf("invalid JSON 1", "invalid JSON 2") + + val query = SubjectQuery() + val range = 0..2 + val actualIdentities = dataSource.loadFingerprintIdentities(query, range) + + assertEquals(0, actualIdentities.size) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + coVerify { mockContentResolver.query(mockDataCaseIdUri, any(), any(), any(), any()) } + coVerify { Simber.e(any(), ofType()) } + } + + @Test + fun `null metadata cursor during count`() = runTest { + every { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } returns null + + val query = SubjectQuery() + val actualCount = dataSource.count(query) + + assertEquals(0, actualCount) + coVerify { mockContentResolver.query(mockMetadataUri, any(), any(), any(), any()) } + } +} diff --git a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSourceTest.kt b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSourceTest.kt new file mode 100644 index 0000000000..dbee8db8a6 --- /dev/null +++ b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/domain/models/BiometricDataSourceTest.kt @@ -0,0 +1,24 @@ +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.enrolment.records.store.domain.models.BiometricDataSource +import org.junit.Test + +class BiometricDataSourceTest { + + @Test + fun `should return SIMPRINTS when value is not SIMPRINTS`() { + val result = BiometricDataSource.fromString("SIMPRINTS") + assertThat(result).isEqualTo(BiometricDataSource.SIMPRINTS) + } + + @Test + fun `should return COMMCARE when value is COMMCARE`() { + val result = BiometricDataSource.fromString("COMMCARE") + assertThat(result).isEqualTo(BiometricDataSource.COMMCARE) + } + + @Test + fun `should return SIMPRINTS when value is unknown`() { + val result = BiometricDataSource.fromString("UNKNOWN") + assertThat(result).isEqualTo(BiometricDataSource.SIMPRINTS) + } +} \ No newline at end of file diff --git a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImplTest.kt b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImplTest.kt index 0a68b07958..975c6a32f0 100644 --- a/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImplTest.kt +++ b/infra/enrolment-records-store/src/test/java/com/simprints/infra/enrolment/records/store/local/EnrolmentRecordLocalDataSourceImplTest.kt @@ -6,6 +6,8 @@ import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.infra.enrolment.records.store.domain.models.Subject import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction import com.simprints.infra.enrolment.records.store.domain.models.SubjectQuery +import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSourceImpl.Companion.FINGERPRINT_SAMPLES_FIELD +import com.simprints.infra.enrolment.records.store.local.EnrolmentRecordLocalDataSourceImpl.Companion.FORMAT_FIELD import com.simprints.infra.enrolment.records.store.local.models.fromDbToDomain import com.simprints.infra.enrolment.records.store.local.models.fromDomainToDb import com.simprints.infra.realm.RealmWrapper @@ -17,6 +19,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.slot +import io.mockk.verify import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm import io.realm.kotlin.query.RealmQuery @@ -112,10 +115,22 @@ class EnrolmentRecordLocalDataSourceImplTest { .toList() listOf(fakePerson).zip(people).forEach { (subject, identity) -> - assertThat(subject.subjectId).isEqualTo(identity.patientId) + assertThat(subject.subjectId).isEqualTo(identity.subjectId) } } + @Test + fun `correctly query supported fingerprint format`() = runTest { + val format = "SupportedFormat" + + enrolmentRecordLocalDataSource + .loadFingerprintIdentities(SubjectQuery(fingerprintSampleFormat = format), IntRange(0, 20)) + .toList() + + verify { realmQuery.query( + "ANY ${FINGERPRINT_SAMPLES_FIELD}.${FORMAT_FIELD} == $0",format) } + } + @Test fun givenValidSerializableQueryForFace_loadIsCalled() = runTest { val savedPersons = saveFakePeople(getRandomPeople(20)) @@ -126,7 +141,7 @@ class EnrolmentRecordLocalDataSourceImplTest { .toList() listOf(fakePerson).zip(people).forEach { (subject, identity) -> - assertThat(subject.subjectId).isEqualTo(identity.personId) + assertThat(subject.subjectId).isEqualTo(identity.subjectId) } } diff --git a/infra/enrolment-records-sync/.gitignore b/infra/enrolment-records-sync/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/infra/enrolment-records-sync/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/infra/enrolment-records-sync/build.gradle.kts b/infra/enrolment-records-sync/build.gradle.kts deleted file mode 100644 index 80456036a9..0000000000 --- a/infra/enrolment-records-sync/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id("simprints.infra") - id("kotlin-parcelize") -} - -android { - namespace = "com.simprints.infra.enrolment.records.sync" -} - -dependencies { - implementation(project(":infra:config-store")) - implementation(project(":infra:enrolment-records-store")) - implementation(project(":infra:auth-store")) - implementation(project(":infra:realm")) - - implementation(libs.retrofit.core) - implementation(libs.jackson.core) - - implementation(libs.workManager.work) -} diff --git a/infra/enrolment-records-sync/src/main/AndroidManifest.xml b/infra/enrolment-records-sync/src/main/AndroidManifest.xml deleted file mode 100644 index e100076157..0000000000 --- a/infra/enrolment-records-sync/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/EnrolmentRecordsSyncModule.kt b/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/EnrolmentRecordsSyncModule.kt deleted file mode 100644 index 2898434bbd..0000000000 --- a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/EnrolmentRecordsSyncModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.simprints.infra.enrolment.records.sync - -import com.simprints.infra.enrolment.records.sync.worker.EnrolmentRecordScheduler -import com.simprints.infra.enrolment.records.sync.worker.EnrolmentRecordSchedulerImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class EnrolmentRecordsSyncModule { - - @Binds - internal abstract fun bindEnrolmentRecordScheduler(impl: EnrolmentRecordSchedulerImpl): EnrolmentRecordScheduler -} diff --git a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordScheduler.kt b/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordScheduler.kt deleted file mode 100644 index 6a263a17ef..0000000000 --- a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordScheduler.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.simprints.infra.enrolment.records.sync.worker - -interface EnrolmentRecordScheduler { - fun upload(id: String, subjectIds: List) -} - diff --git a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordSchedulerImpl.kt b/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordSchedulerImpl.kt deleted file mode 100644 index 2694d0459a..0000000000 --- a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordSchedulerImpl.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.simprints.infra.enrolment.records.sync.worker - -import android.content.Context -import androidx.work.* -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -internal class EnrolmentRecordSchedulerImpl @Inject constructor(@ApplicationContext context: Context) : - EnrolmentRecordScheduler { - - companion object { - private const val WORK_NAME = "upload-enrolment-record-work-one-time" - const val INPUT_ID_NAME = "INPUT_ID_NAME" - const val INPUT_SUBJECT_IDS_NAME = "INPUT_SUBJECT_IDS_NAME" - } - - private val workManager = WorkManager.getInstance(context) - - override fun upload(id: String, subjectIds: List) { - workManager.enqueueUniqueWork( - WORK_NAME, - ExistingWorkPolicy.KEEP, - buildOneTimeRequest(id, subjectIds) - ) - } - - private fun buildOneTimeRequest(id: String, subjectIds: List): OneTimeWorkRequest = - OneTimeWorkRequestBuilder() - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ) - .setInputData( - workDataOf( - INPUT_SUBJECT_IDS_NAME to subjectIds.toTypedArray(), - INPUT_ID_NAME to id - ) - ) - .build() -} - diff --git a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordWorker.kt b/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordWorker.kt deleted file mode 100644 index 9de62520bb..0000000000 --- a/infra/enrolment-records-sync/src/main/java/com/simprints/infra/enrolment/records/sync/worker/EnrolmentRecordWorker.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.simprints.infra.enrolment.records.sync.worker - -import android.content.Context -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.simprints.core.DispatcherIO -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext - -@HiltWorker -class EnrolmentRecordWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val enrolmentRecordRepository: EnrolmentRecordRepository, - private val configRepository: ConfigRepository, - @DispatcherIO private val dispatcher: CoroutineDispatcher, -) : CoroutineWorker(context, params) { - - override suspend fun doWork(): Result = - withContext(dispatcher) { - try { - val instructionId = - inputData.getString(EnrolmentRecordSchedulerImpl.INPUT_ID_NAME) - ?: throw IllegalArgumentException("input required") - val subjectIds = - inputData.getStringArray(EnrolmentRecordSchedulerImpl.INPUT_SUBJECT_IDS_NAME) - ?: throw IllegalArgumentException("input required") - - enrolmentRecordRepository.uploadRecords(subjectIds.toList()) - - configRepository.updateDeviceConfiguration { - it.apply { it.lastInstructionId = instructionId } - } - - Result.success() - } catch (e: Exception) { - Result.retry() - } - } -} diff --git a/infra/enrolment-records-sync/src/test/java/com/simprints/infra/enrolment/records/EnrolmentRecordManagerImplTest.kt b/infra/enrolment-records-sync/src/test/java/com/simprints/infra/enrolment/records/EnrolmentRecordManagerImplTest.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/infra/enrolment-records-sync/src/test/java/com/simprints/infra/enrolment/records/worker/EnrolmentRecordSchedulerImplTest.kt b/infra/enrolment-records-sync/src/test/java/com/simprints/infra/enrolment/records/worker/EnrolmentRecordSchedulerImplTest.kt deleted file mode 100644 index ed1433a4b1..0000000000 --- a/infra/enrolment-records-sync/src/test/java/com/simprints/infra/enrolment/records/worker/EnrolmentRecordSchedulerImplTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.simprints.infra.enrolment.records.worker - -import android.content.Context -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import com.simprints.infra.enrolment.records.sync.worker.EnrolmentRecordSchedulerImpl -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -class EnrolmentRecordSchedulerImplTest { - - companion object { - private const val INSTRUCTION_ID = "id" - private const val SUBJECT_ID = "subjectId" - } - - private val ctx = mockk() - private val workManager = mockk(relaxed = true) - private lateinit var enrolmentRecordSchedulerImpl: EnrolmentRecordSchedulerImpl - - @Before - fun setup() { - mockkStatic(WorkManager::class) - every { WorkManager.getInstance(ctx) } returns workManager - - enrolmentRecordSchedulerImpl = EnrolmentRecordSchedulerImpl(ctx) - } - - @Test - fun `upload should schedule the worker with the correct data`() = runTest { - enrolmentRecordSchedulerImpl.upload(INSTRUCTION_ID, listOf(SUBJECT_ID)) - - coVerify(exactly = 1) { - workManager.enqueueUniqueWork( - any(), - ExistingWorkPolicy.KEEP, - match { oneTimeWorkRequest -> - val subjectIdsInput = oneTimeWorkRequest.workSpec.input.getStringArray( - EnrolmentRecordSchedulerImpl.INPUT_SUBJECT_IDS_NAME - ) - val instructionIdInput = - oneTimeWorkRequest.workSpec.input.getString(EnrolmentRecordSchedulerImpl.INPUT_ID_NAME) - instructionIdInput == INSTRUCTION_ID && subjectIdsInput.contentEquals( - arrayOf( - SUBJECT_ID - ) - ) - } - ) - } - } -} diff --git a/infra/event-sync/build.gradle.kts b/infra/event-sync/build.gradle.kts index 3051507407..b0fb5e96d2 100644 --- a/infra/event-sync/build.gradle.kts +++ b/infra/event-sync/build.gradle.kts @@ -7,18 +7,6 @@ plugins { android { namespace = "com.simprints.infra.eventsync" - buildTypes { - getByName("release") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "60L") - } - getByName("staging") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - getByName("debug") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - } - sourceSets { // Adds exported room schema location as test app assets. getByName("debug") { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt index 88b9cedbf9..9d87b63c1a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt @@ -9,15 +9,13 @@ import java.util.Date interface EventSyncManager { + fun getPeriodicWorkTags(): List + fun getOneTimeWorkTags(): List + fun getAllWorkerTag(): String + suspend fun getLastSyncTime(): Date? fun getLastSyncState(): LiveData - fun sync() - fun stop() - - fun scheduleSync() - fun cancelScheduledSync() - suspend fun countEventsToUpload(type: EventType?): Flow suspend fun countEventsToDownload(): DownSyncCounts diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt index ef17377f3f..246b049b6b 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt @@ -1,22 +1,14 @@ package com.simprints.infra.eventsync -import android.content.Context import androidx.lifecycle.LiveData -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager import com.simprints.core.DispatcherIO import com.simprints.core.domain.tokenization.values +import com.simprints.core.tools.time.TimeHelper import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.events.event.domain.models.scope.EventScopeType -import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEventType import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation @@ -29,26 +21,18 @@ import com.simprints.infra.eventsync.sync.common.EventSyncCache import com.simprints.infra.eventsync.sync.common.MASTER_SYNC_SCHEDULERS import com.simprints.infra.eventsync.sync.common.MASTER_SYNC_SCHEDULER_ONE_TIME import com.simprints.infra.eventsync.sync.common.MASTER_SYNC_SCHEDULER_PERIODIC_TIME -import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG -import com.simprints.infra.eventsync.sync.common.addTagForBackgroundSyncMasterWorker -import com.simprints.infra.eventsync.sync.common.addTagForOneTimeSyncMasterWorker -import com.simprints.infra.eventsync.sync.common.addTagForScheduledAtNow -import com.simprints.infra.eventsync.sync.common.addTagForSyncMasterWorkers -import com.simprints.infra.eventsync.sync.common.cancelAllSubjectsSyncWorkers +import com.simprints.infra.eventsync.sync.common.TAG_SCHEDULED_AT +import com.simprints.infra.eventsync.sync.common.TAG_SUBJECTS_SYNC_ALL_WORKERS import com.simprints.infra.eventsync.sync.down.tasks.EventDownSyncTask -import com.simprints.infra.eventsync.sync.master.EventSyncMasterWorker -import com.simprints.infra.logging.Simber -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext import java.util.Date -import java.util.concurrent.TimeUnit import javax.inject.Inject internal class EventSyncManagerImpl @Inject constructor( - @ApplicationContext private val ctx: Context, + private val timeHelper: TimeHelper, private val eventSyncStateProcessor: EventSyncStateProcessor, private val downSyncScopeRepository: EventDownSyncScopeRepository, private val eventRepository: EventRepository, @@ -60,75 +44,24 @@ internal class EventSyncManagerImpl @Inject constructor( @DispatcherIO private val dispatcher: CoroutineDispatcher, ) : EventSyncManager { - companion object { - private const val SYNC_REPEAT_INTERVAL = BuildConfig.SYNC_PERIODIC_WORKER_INTERVAL_MINUTES - val SYNC_REPEAT_UNIT = TimeUnit.MINUTES - } - - private val wm = WorkManager.getInstance(ctx) - override suspend fun getLastSyncTime(): Date? = eventSyncCache.readLastSuccessfulSyncTime() override fun getLastSyncState(): LiveData = eventSyncStateProcessor.getLastSyncState() - override fun sync() { - Simber.tag(SYNC_LOG_TAG).d("[SCHEDULER] One time events master worker") - - wm.beginUniqueWork( - MASTER_SYNC_SCHEDULER_ONE_TIME, - ExistingWorkPolicy.KEEP, - buildOneTimeRequest() - ).enqueue() - } - - override fun scheduleSync() { - Simber.tag(SYNC_LOG_TAG).d("[SCHEDULER] Periodic events master worker") + override fun getPeriodicWorkTags(): List = listOf( + MASTER_SYNC_SCHEDULERS, + MASTER_SYNC_SCHEDULER_PERIODIC_TIME, + "$TAG_SCHEDULED_AT${timeHelper.now().ms}", + ) - wm.enqueueUniquePeriodicWork( - MASTER_SYNC_SCHEDULER_PERIODIC_TIME, - ExistingPeriodicWorkPolicy.UPDATE, - buildPeriodicRequest() - ) - } + override fun getOneTimeWorkTags(): List = listOf( + MASTER_SYNC_SCHEDULERS, + MASTER_SYNC_SCHEDULER_ONE_TIME, + "$TAG_SCHEDULED_AT${timeHelper.now().ms}", + ) - private fun buildOneTimeRequest(): OneTimeWorkRequest = - OneTimeWorkRequest.Builder(EventSyncMasterWorker::class.java) - .setConstraints(getDownSyncMasterWorkerConstraints()) - .addTagForSyncMasterWorkers() - .addTagForOneTimeSyncMasterWorker() - .addTagForScheduledAtNow() - .build() as OneTimeWorkRequest - - private fun buildPeriodicRequest(): PeriodicWorkRequest = - PeriodicWorkRequest.Builder( - EventSyncMasterWorker::class.java, - SYNC_REPEAT_INTERVAL, - SYNC_REPEAT_UNIT - ) - .setConstraints(getDownSyncMasterWorkerConstraints()) - .addTagForSyncMasterWorkers() - .addTagForBackgroundSyncMasterWorker() - .addTagForScheduledAtNow() - .build() as PeriodicWorkRequest - - private fun getDownSyncMasterWorkerConstraints() = - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - override fun cancelScheduledSync() { - wm.cancelAllWorkByTag(MASTER_SYNC_SCHEDULERS) - stop() - } - - override fun stop() { - wm.cancelAllSubjectsSyncWorkers() - } - - private fun cleanScheduledHistory() { - wm.pruneWork() - } + override fun getAllWorkerTag(): String = TAG_SUBJECTS_SYNC_ALL_WORKERS override suspend fun countEventsToUpload(type: EventType?): Flow = eventRepository.observeEventCount(type) @@ -143,21 +76,13 @@ internal class EventSyncManagerImpl @Inject constructor( syncPartitioning = projectConfig.synchronization.down.partitionType.toDomain() ) - var creationsToDownload = 0 - var deletionsToDownload = 0 - - downSyncScope.operations.forEach { syncOperation -> - val counts = eventRemoteDataSource.count(syncOperation.queryEvent.fromDomainToApi()) + val counts = downSyncScope.operations + .map { eventRemoteDataSource.count(it.queryEvent.fromDomainToApi()) } - creationsToDownload += counts - .firstOrNull { it.type == EnrolmentRecordEventType.EnrolmentRecordCreation } - ?.count ?: 0 - deletionsToDownload += counts - .firstOrNull { it.type == EnrolmentRecordEventType.EnrolmentRecordDeletion } - ?.count ?: 0 - } - - return DownSyncCounts(creationsToDownload, deletionsToDownload) + return DownSyncCounts( + count = counts.sumOf { it.count }, + isLowerBound = counts.any { it.isLowerBound } + ) } override suspend fun downSyncSubject( @@ -165,11 +90,13 @@ internal class EventSyncManagerImpl @Inject constructor( subjectId: String, ): Unit = withContext(dispatcher) { val eventScope = eventRepository.createEventScope(EventScopeType.DOWN_SYNC) - val op = EventDownSyncOperation(RemoteEventQuery( - projectId = projectId, - subjectId = subjectId, - modes = getProjectModes(configRepository.getProjectConfiguration()), - )) + val op = EventDownSyncOperation( + RemoteEventQuery( + projectId = projectId, + subjectId = subjectId, + modes = getProjectModes(configRepository.getProjectConfiguration()), + ) + ) downSyncTask.downSync(this, op, eventScope).toList() } @@ -188,7 +115,6 @@ internal class EventSyncManagerImpl @Inject constructor( upSyncScopeRepo.deleteAll() eventSyncCache.clearProgresses() eventSyncCache.storeLastSuccessfulSyncTime(null) - cleanScheduledHistory() } override suspend fun resetDownSyncInfo() { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSource.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSource.kt index acc9f2efc3..a497519a71 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSource.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSource.kt @@ -10,7 +10,6 @@ import com.simprints.infra.authstore.AuthStore import com.simprints.infra.events.event.domain.EventCount import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEvent import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException -import com.simprints.infra.eventsync.event.remote.models.fromApiToDomain import com.simprints.infra.eventsync.event.remote.models.subject.ApiEnrolmentRecordEvent import com.simprints.infra.eventsync.event.remote.models.subject.fromApiToDomain import com.simprints.infra.eventsync.status.down.domain.EventDownSyncResult @@ -21,7 +20,6 @@ import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.produce -import okhttp3.ResponseBody import retrofit2.Response import java.io.ByteArrayInputStream import java.io.InputStream @@ -32,8 +30,8 @@ internal class EventRemoteDataSource @Inject constructor( private val jsonHelper: JsonHelper, ) { - suspend fun count(query: ApiRemoteEventQuery): List = - executeCall { eventsRemoteInterface -> + suspend fun count(query: ApiRemoteEventQuery) = try { + val response = executeCall { eventsRemoteInterface -> eventsRemoteInterface.countEvents( projectId = query.projectId, moduleId = query.moduleId, @@ -41,9 +39,23 @@ internal class EventRemoteDataSource @Inject constructor( subjectId = query.subjectId, modes = query.modes, lastEventId = query.lastEventId - ).map { it.fromApiToDomain() } + ) } + getEventCountFromHeader(response) + } catch (t: Throwable) { + if (t is SyncCloudIntegrationException && t.httpStatusCode() == TOO_MANY_REQUEST_STATUS) + throw TooManyRequestsException() + else + throw t + } + private fun getEventCountFromHeader(response: Response<*>): EventCount { + val totalCount = response.headers()[COUNT_HEADER]?.toIntOrNull() ?: 0 + val isTotalLowerBound = response + .headers()[IS_COUNT_HEADER_LOWER_BOUND] + ?.toBoolean() ?: false // If not present, assume it's not lower bound + return EventCount(totalCount, isTotalLowerBound) + } suspend fun dumpInvalidEvents(projectId: String, events: List) { executeCall { remoteInterface -> @@ -52,23 +64,19 @@ internal class EventRemoteDataSource @Inject constructor( } suspend fun getEvents( + requestId: String, query: ApiRemoteEventQuery, scope: CoroutineScope, ): EventDownSyncResult { return try { - val response = takeStreaming(query) - - val totalCount = response.headers()[COUNT_HEADER]?.toIntOrNull() ?: 0 - val isTotalLowerBound = response - .headers()[IS_COUNT_HEADER_LOWER_BOUND] - ?.toBoolean() ?: false // If not present, assume it's not lower bound + val response = takeStreaming(requestId, query) + val eventCount = getEventCountFromHeader(response) val streaming = response.body()?.byteStream() ?: ByteArrayInputStream(byteArrayOf()) Simber.tag("SYNC").d("[EVENT_REMOTE_SOURCE] Stream taken") EventDownSyncResult( - totalCount = totalCount.takeUnless { isTotalLowerBound }, - requestId = getRequestId(response), + totalCount = eventCount.exactCount, status = response.code(), eventStream = scope.produce(capacity = CHANNEL_CAPACITY_FOR_PROPAGATION) { parseStreamAndEmitEvents(streaming, this) @@ -109,9 +117,10 @@ internal class EventRemoteDataSource @Inject constructor( } } - private suspend fun takeStreaming(query: ApiRemoteEventQuery) = + private suspend fun takeStreaming(requestId: String, query: ApiRemoteEventQuery) = executeCall { eventsRemoteInterface -> eventsRemoteInterface.downloadEvents( + requestId = requestId, projectId = query.projectId, moduleId = query.moduleId, attendantId = query.userId, @@ -122,22 +131,20 @@ internal class EventRemoteDataSource @Inject constructor( } suspend fun post( + requestId: String, projectId: String, body: ApiUploadEventsBody, acceptInvalidEvents: Boolean = true, ): EventUpSyncResult { val response = executeCall { remoteInterface -> - remoteInterface.uploadEvents(projectId, acceptInvalidEvents, body) + remoteInterface.uploadEvents(requestId, projectId, acceptInvalidEvents, body) } return EventUpSyncResult( - requestId = getRequestId(response), status = response.code(), ) } - fun getRequestId(response: Response<*>) = response.headers()[REQUEST_ID_HEADER].orEmpty() - private suspend fun executeCall(block: suspend (EventRemoteInterface) -> T): T = getEventsApiClient().executeCall { block(it) } @@ -150,7 +157,6 @@ internal class EventRemoteDataSource @Inject constructor( private const val TOO_MANY_REQUEST_STATUS = 429 private const val COUNT_HEADER = "x-event-count" - internal const val REQUEST_ID_HEADER = "x-request-id" private const val IS_COUNT_HEADER_LOWER_BOUND = "x-event-count-is-lower-bound" } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteInterface.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteInterface.kt index 23a8695943..b5b3f90393 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteInterface.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/EventRemoteInterface.kt @@ -1,46 +1,51 @@ package com.simprints.infra.eventsync.event.remote -import com.simprints.infra.eventsync.event.remote.models.ApiEventCount import com.simprints.infra.network.SimRemoteInterface import okhttp3.ResponseBody import retrofit2.Response -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.HEAD +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Streaming @JvmSuppressWildcards internal interface EventRemoteInterface : SimRemoteInterface { - @GET("projects/{projectId}/events/count") + @HEAD("projects/{projectId}/events") suspend fun countEvents( @Path("projectId") projectId: String, @Query("l_moduleId") moduleId: String?, @Query("l_attendantId") attendantId: String?, @Query("l_subjectId") subjectId: String?, @Query("l_mode") modes: List, - @Query("lastEventId") lastEventId: String? - ): List + @Query("lastEventId") lastEventId: String?, + ): Response - @Headers( - "Content-Encoding: gzip", - "X-Force-Version: 2024.1.1" - ) + @Headers("Content-Encoding: gzip") @POST("projects/{projectId}/events") suspend fun uploadEvents( + @Header("X-Request-ID") requestId: String, @Path("projectId") projectId: String, @Query("acceptInvalidEvents") acceptInvalidEvents: Boolean = true, - @Body body: ApiUploadEventsBody + @Body body: ApiUploadEventsBody, ): Response @Streaming - @Headers("X-Force-Version: 2024.1.1") @GET("projects/{projectId}/events") suspend fun downloadEvents( + @Header("X-Request-ID") requestId: String, @Path("projectId") projectId: String, @Query("l_moduleId") moduleId: String?, @Query("l_attendantId") attendantId: String?, @Query("l_subjectId") subjectId: String?, @Query("l_mode") modes: List, - @Query("lastEventId") lastEventId: String? + @Query("lastEventId") lastEventId: String?, ): Response @Headers("Content-Encoding: gzip") diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAlertScreenPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAlertScreenPayload.kt index 985fed35fe..2d1fd3f655 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAlertScreenPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAlertScreenPayload.kt @@ -5,19 +5,53 @@ import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType -import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.* +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BACKEND_MAINTENANCE_ERROR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NOT_ENABLED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NOT_SUPPORTED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NO_PERMISSION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DIFFERENT_PROJECT_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DIFFERENT_USER_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DISCONNECTED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GOOGLE_PLAY_SERVICES_OUTDATED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GUID_NOT_FOUND_OFFLINE +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GUID_NOT_FOUND_ONLINE +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INTEGRITY_SERVICE_ERROR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_INTENT_ACTION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_METADATA +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_MODULE_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_PROJECT_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_SELECTED_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_SESSION_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_STATE_FOR_INTENT_ACTION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_USER_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_VERIFY_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LICENSE_INVALID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LICENSE_MISSING +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LOW_BATTERY +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MISSING_GOOGLE_PLAY_SERVICES +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MISSING_OR_OUTDATED_GOOGLE_PLAY_STORE_APP +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MULTIPLE_PAIRED_SCANNERS +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NFC_NOT_ENABLED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NFC_PAIR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NOT_PAIRED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA_FAILED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA_RECOVERY +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.PROJECT_ENDING +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.PROJECT_PAUSED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.SERIAL_ENTRY_PAIR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.UNEXPECTED_ERROR @Keep internal data class ApiAlertScreenPayload( override val startTime: ApiTimestamp, - override val version: Int, val alertType: ApiAlertScreenEventType, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: AlertScreenPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.alertType.fromDomainToApi(), ) @@ -58,9 +92,9 @@ internal data class ApiAlertScreenPayload( INVALID_USER_ID, INVALID_VERIFY_ID, INTEGRITY_SERVICE_ERROR, - FACE_LICENSE_INVALID, + LICENSE_INVALID, BACKEND_MAINTENANCE_ERROR, - FACE_LICENSE_MISSING, + LICENSE_MISSING, GOOGLE_PLAY_SERVICES_OUTDATED, MISSING_GOOGLE_PLAY_SERVICES, PROJECT_PAUSED, @@ -104,8 +138,8 @@ internal fun AlertScreenEventType.fromDomainToApi(): ApiAlertScreenEventType = AlertScreenEventType.INTEGRITY_SERVICE_ERROR -> INTEGRITY_SERVICE_ERROR AlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED -> ENROLMENT_LAST_BIOMETRICS_FAILED AlertScreenEventType.INVALID_STATE_FOR_INTENT_ACTION -> INVALID_STATE_FOR_INTENT_ACTION - AlertScreenEventType.FACE_LICENSE_INVALID -> FACE_LICENSE_INVALID - AlertScreenEventType.FACE_LICENSE_MISSING -> FACE_LICENSE_MISSING + AlertScreenEventType.LICENSE_INVALID -> LICENSE_INVALID + AlertScreenEventType.LICENSE_MISSING -> LICENSE_MISSING AlertScreenEventType.BACKEND_MAINTENANCE_ERROR -> BACKEND_MAINTENANCE_ERROR AlertScreenEventType.GOOGLE_PLAY_SERVICES_OUTDATED -> GOOGLE_PLAY_SERVICES_OUTDATED AlertScreenEventType.MISSING_GOOGLE_PLAY_SERVICES -> MISSING_GOOGLE_PLAY_SERVICES diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthenticationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthenticationPayload.kt index f7b29eb868..a2eeb2825b 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthenticationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthenticationPayload.kt @@ -17,11 +17,10 @@ import com.simprints.infra.eventsync.event.remote.models.ApiAuthenticationPayloa @Keep internal data class ApiAuthenticationPayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val userInfo: ApiUserInfo, val result: ApiResult, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep data class ApiUserInfo(val projectId: String, val userId: String) { @@ -45,7 +44,6 @@ internal data class ApiAuthenticationPayload( constructor(domainPayload: AuthenticationPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), ApiUserInfo(domainPayload.userInfo), domainPayload.result.fromDomainToApi() diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthorizationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthorizationPayload.kt index 8fc2782702..384eb77963 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthorizationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiAuthorizationPayload.kt @@ -15,10 +15,9 @@ import com.simprints.infra.eventsync.event.remote.models.ApiAuthorizationPayload @JsonInclude(Include.NON_NULL) internal data class ApiAuthorizationPayload( override val startTime: ApiTimestamp, - override val version: Int, val result: ApiResult, val userInfo: ApiUserInfo?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep data class ApiUserInfo(val projectId: String, val userId: String) { @@ -35,7 +34,6 @@ internal data class ApiAuthorizationPayload( constructor(domainPayload: AuthorizationPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.result.fromDomainToApi(), domainPayload.userInfo?.let { ApiUserInfo(it) } ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCandidateReadPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCandidateReadPayload.kt index c40c502b0a..31fe8529b5 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCandidateReadPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCandidateReadPayload.kt @@ -14,12 +14,11 @@ import com.simprints.infra.eventsync.event.remote.models.ApiCandidateReadPayload @JsonInclude(Include.NON_NULL) internal data class ApiCandidateReadPayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val candidateId: String, val localResult: ApiLocalResult, val remoteResult: ApiRemoteResult?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep enum class ApiLocalResult { @@ -35,7 +34,6 @@ internal data class ApiCandidateReadPayload( constructor(domainPayload: CandidateReadPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), domainPayload.candidateId, domainPayload.localResult.fromDomainToApi(), diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCompletionCheckPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCompletionCheckPayload.kt index 09803881f9..49fbe52791 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCompletionCheckPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiCompletionCheckPayload.kt @@ -7,13 +7,11 @@ import com.simprints.infra.events.event.domain.models.CompletionCheckEvent.Compl @Keep internal data class ApiCompletionCheckPayload( override val startTime: ApiTimestamp, - override val version: Int, val completed: Boolean, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: CompletionCheckPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.completed, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConnectivitySnapshotPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConnectivitySnapshotPayload.kt index ea4c7a8e3a..20512f30f6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConnectivitySnapshotPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConnectivitySnapshotPayload.kt @@ -9,9 +9,8 @@ import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent. @Keep internal data class ApiConnectivitySnapshotPayload( override val startTime: ApiTimestamp, - override val version: Int, val connections: List, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep class ApiConnection(val type: String, val state: String) { @@ -22,7 +21,6 @@ internal data class ApiConnectivitySnapshotPayload( constructor(domainPayload: ConnectivitySnapshotPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.connections.map { ApiConnection(it) }, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConsentPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConsentPayload.kt index 9304a8cc43..9f37be48f9 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConsentPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiConsentPayload.kt @@ -14,11 +14,10 @@ import com.simprints.infra.eventsync.event.remote.models.ApiConsentPayload.ApiTy @Keep internal data class ApiConsentPayload( override val startTime: ApiTimestamp, - override val version: Int, var endTime: ApiTimestamp?, val consentType: ApiType, var result: ApiResult, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep enum class ApiType { @@ -34,7 +33,6 @@ internal data class ApiConsentPayload( constructor(domainPayload: ConsentPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), domainPayload.consentType.fromDomainToApi(), domainPayload.result.fromDomainToApi() diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt index 4e46a9daf0..c881852729 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV1.kt @@ -7,13 +7,11 @@ import com.simprints.infra.events.event.domain.models.EnrolmentEventV1 @Keep internal data class ApiEnrolmentPayloadV1( override val startTime: ApiTimestamp, - override val version: Int, val personId: String, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EnrolmentEventV1.EnrolmentPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.personId ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt index 7e73de633e..754d448518 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEnrolmentPayloadV2.kt @@ -7,17 +7,15 @@ import com.simprints.infra.events.event.domain.models.EnrolmentEventV2 @Keep internal data class ApiEnrolmentPayloadV2( override val startTime: ApiTimestamp, - override val version: Int, val subjectId: String, val projectId: String, val moduleId: String, val attendantId: String, val personCreationEventId: String, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EnrolmentEventV2.EnrolmentPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.subjectId, domainPayload.projectId, domainPayload.moduleId.value, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEvent.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEvent.kt index 3272b61ba5..5fb4ef9cec 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEvent.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEvent.kt @@ -2,13 +2,13 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep import com.simprints.core.domain.tokenization.TokenizableString -import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.Event @Keep internal data class ApiEvent( val id: String, val type: ApiEventPayloadType, + val version: Int, val payload: ApiEventPayload, val tokenizedFields: List, ) @@ -16,13 +16,14 @@ internal data class ApiEvent( internal fun Event.fromDomainToApi(): ApiEvent { val tokenizedKeyTypes = getTokenizedFields().filter { it.value is TokenizableString.Tokenized }.keys.toList() - val payload = payload.fromDomainToApi() - val tokenizedFields = tokenizedKeyTypes.mapNotNull(payload::getTokenizedFieldJsonPath) + val apiPayload = payload.fromDomainToApi() + val tokenizedFields = tokenizedKeyTypes.mapNotNull(apiPayload::getTokenizedFieldJsonPath) return ApiEvent( id = id, type = type.fromDomainToApi(), - payload = payload, + version = payload.eventVersion, + payload = apiPayload, tokenizedFields = tokenizedFields ) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventCount.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventCount.kt deleted file mode 100644 index 1ec8816822..0000000000 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiEventCount.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.simprints.infra.eventsync.event.remote.models - -import androidx.annotation.Keep -import com.simprints.infra.events.event.domain.EventCount -import com.simprints.infra.eventsync.event.remote.models.subject.ApiEnrolmentRecordPayloadType -import com.simprints.infra.eventsync.event.remote.models.subject.fromApiToDomain - -@Keep -internal data class ApiEventCount(val type: ApiEnrolmentRecordPayloadType, val count: Int) - -internal fun ApiEventCount.fromApiToDomain() = EventCount(type.fromApiToDomain(), count) 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 61cb1c1e30..58d7b873fe 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 @@ -32,9 +32,9 @@ import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT import com.simprints.infra.events.event.domain.models.EventType.CONSENT -import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 +import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_BIOMETRICS @@ -46,6 +46,7 @@ import com.simprints.infra.events.event.domain.models.EventType.FINGERPRINT_CAPT import com.simprints.infra.events.event.domain.models.EventType.GUID_SELECTION import com.simprints.infra.events.event.domain.models.EventType.INTENT_PARSING import com.simprints.infra.events.event.domain.models.EventType.INVALID_INTENT +import com.simprints.infra.events.event.domain.models.EventType.LICENSE_CHECK import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_MANY_MATCH import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_ONE_MATCH import com.simprints.infra.events.event.domain.models.EventType.PERSON_CREATION @@ -57,6 +58,7 @@ import com.simprints.infra.events.event.domain.models.EventType.VERO_2_INFO_SNAP import com.simprints.infra.events.event.domain.models.GuidSelectionEvent.GuidSelectionPayload import com.simprints.infra.events.event.domain.models.IntentParsingEvent.IntentParsingPayload import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.InvalidIntentPayload +import com.simprints.infra.events.event.domain.models.LicenseCheckEvent import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOneMatchPayload import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload @@ -131,7 +133,6 @@ import com.simprints.infra.eventsync.event.remote.models.upsync.ApiEventUpSyncRe ) @Keep internal abstract class ApiEventPayload( - open val version: Int, open val startTime: ApiTimestamp, ) { @@ -181,4 +182,5 @@ internal fun EventPayload.fromDomainToApi(): ApiEventPayload = FACE_CAPTURE_BIOMETRICS -> ApiFaceCaptureBiometricsPayload(this as FaceCaptureBiometricsEvent.FaceCaptureBiometricsPayload) 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) } 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 1e4c25a9e8..73c44940d8 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 @@ -2,7 +2,47 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.EventType -import com.simprints.infra.events.event.domain.models.EventType.* +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 +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_CONFIRMATION +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ENROLMENT +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_ERROR +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_IDENTIFICATION +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_REFUSAL +import com.simprints.infra.events.event.domain.models.EventType.CALLBACK_VERIFICATION +import com.simprints.infra.events.event.domain.models.EventType.CALLOUT_CONFIRMATION +import com.simprints.infra.events.event.domain.models.EventType.CALLOUT_ENROLMENT +import com.simprints.infra.events.event.domain.models.EventType.CALLOUT_IDENTIFICATION +import com.simprints.infra.events.event.domain.models.EventType.CALLOUT_LAST_BIOMETRICS +import com.simprints.infra.events.event.domain.models.EventType.CALLOUT_VERIFICATION +import com.simprints.infra.events.event.domain.models.EventType.CANDIDATE_READ +import com.simprints.infra.events.event.domain.models.EventType.COMPLETION_CHECK +import com.simprints.infra.events.event.domain.models.EventType.CONNECTIVITY_SNAPSHOT +import com.simprints.infra.events.event.domain.models.EventType.CONSENT +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V1 +import com.simprints.infra.events.event.domain.models.EventType.ENROLMENT_V2 +import com.simprints.infra.events.event.domain.models.EventType.EVENT_DOWN_SYNC_REQUEST +import com.simprints.infra.events.event.domain.models.EventType.EVENT_UP_SYNC_REQUEST +import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE +import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_BIOMETRICS +import com.simprints.infra.events.event.domain.models.EventType.FACE_CAPTURE_CONFIRMATION +import com.simprints.infra.events.event.domain.models.EventType.FACE_FALLBACK_CAPTURE +import com.simprints.infra.events.event.domain.models.EventType.FACE_ONBOARDING_COMPLETE +import com.simprints.infra.events.event.domain.models.EventType.FINGERPRINT_CAPTURE +import com.simprints.infra.events.event.domain.models.EventType.FINGERPRINT_CAPTURE_BIOMETRICS +import com.simprints.infra.events.event.domain.models.EventType.GUID_SELECTION +import com.simprints.infra.events.event.domain.models.EventType.INTENT_PARSING +import com.simprints.infra.events.event.domain.models.EventType.INVALID_INTENT +import com.simprints.infra.events.event.domain.models.EventType.LICENSE_CHECK +import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_MANY_MATCH +import com.simprints.infra.events.event.domain.models.EventType.ONE_TO_ONE_MATCH +import com.simprints.infra.events.event.domain.models.EventType.PERSON_CREATION +import com.simprints.infra.events.event.domain.models.EventType.REFUSAL +import com.simprints.infra.events.event.domain.models.EventType.SCANNER_CONNECTION +import com.simprints.infra.events.event.domain.models.EventType.SCANNER_FIRMWARE_UPDATE +import com.simprints.infra.events.event.domain.models.EventType.SUSPICIOUS_INTENT +import com.simprints.infra.events.event.domain.models.EventType.VERO_2_INFO_SNAPSHOT @Keep internal enum class ApiEventPayloadType { @@ -100,6 +140,9 @@ internal enum class ApiEventPayloadType { /* key added: EVENT_UP_SYNC_REQUEST_KEY */ EventUpSyncRequest, + + /* key added: LICENSE_CHECK_KEY */ + LicenseCheck, ; companion object { @@ -181,6 +224,7 @@ internal fun EventType.fromDomainToApi(): ApiEventPayloadType = when (this) { FACE_CAPTURE_BIOMETRICS -> ApiEventPayloadType.FaceCaptureBiometrics EVENT_DOWN_SYNC_REQUEST -> ApiEventPayloadType.EventDownSyncRequest EVENT_UP_SYNC_REQUEST -> ApiEventPayloadType.EventUpSyncRequest + LICENSE_CHECK -> ApiEventPayloadType.LicenseCheck } @@ -213,6 +257,7 @@ internal fun ApiEventPayloadType.fromApiToDomain(): EventType = when (this) { ApiEventPayloadType.FaceCaptureBiometrics -> FACE_CAPTURE_BIOMETRICS ApiEventPayloadType.EventDownSyncRequest -> EVENT_DOWN_SYNC_REQUEST ApiEventPayloadType.EventUpSyncRequest -> EVENT_UP_SYNC_REQUEST + ApiEventPayloadType.LicenseCheck -> LICENSE_CHECK ApiEventPayloadType.Callout -> throw UnsupportedOperationException("") ApiEventPayloadType.Callback -> throw UnsupportedOperationException("") } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCaptureBiometricsPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCaptureBiometricsPayload.kt index 8a19851bfc..39e75975c7 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCaptureBiometricsPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCaptureBiometricsPayload.kt @@ -1,17 +1,16 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureBiometricsEvent -import com.simprints.core.domain.fingerprint.IFingerIdentifier @Keep internal data class ApiFingerprintCaptureBiometricsPayload( - override val version: Int, override val startTime: ApiTimestamp, val fingerprint: Fingerprint, val id: String, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep data class Fingerprint( @@ -30,7 +29,6 @@ internal data class ApiFingerprintCaptureBiometricsPayload( } constructor(domainPayload: FingerprintCaptureBiometricsEvent.FingerprintCaptureBiometricsPayload) : this( - domainPayload.eventVersion, domainPayload.createdAt.fromDomainToApi(), Fingerprint(domainPayload.fingerprint), domainPayload.id diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayload.kt index c3bb21434d..5a1e0c9447 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayload.kt @@ -3,24 +3,27 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include +import com.simprints.core.domain.fingerprint.IFingerIdentifier import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload -import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.* +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.BAD_QUALITY +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.FAILURE_TO_ACQUIRE +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.GOOD_SCAN +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.NO_FINGER_DETECTED +import com.simprints.infra.events.event.domain.models.fingerprint.FingerprintCaptureEvent.FingerprintCapturePayload.Result.SKIPPED import com.simprints.infra.eventsync.event.remote.models.ApiFingerprintCapturePayload.ApiResult -import com.simprints.core.domain.fingerprint.IFingerIdentifier @Keep @JsonInclude(Include.NON_NULL) internal data class ApiFingerprintCapturePayload( val id: String, override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val qualityThreshold: Int, val finger: IFingerIdentifier, val result: ApiResult, val fingerprint: ApiFingerprint?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep data class ApiFingerprint( @@ -36,15 +39,15 @@ internal data class ApiFingerprintCapturePayload( ) } - constructor(domainPayload: FingerprintCapturePayload) : - this(domainPayload.id, - domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, - domainPayload.endedAt?.fromDomainToApi(), - domainPayload.qualityThreshold, - domainPayload.finger, - domainPayload.result.fromDomainToApi(), - domainPayload.fingerprint?.let { ApiFingerprint(it) }) + constructor(domainPayload: FingerprintCapturePayload) : this( + domainPayload.id, + domainPayload.createdAt.fromDomainToApi(), + domainPayload.endedAt?.fromDomainToApi(), + domainPayload.qualityThreshold, + domainPayload.finger, + domainPayload.result.fromDomainToApi(), + domainPayload.fingerprint?.let { ApiFingerprint(it) }, + ) @Keep enum class ApiResult { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayload.kt index ac4b46d28e..a63152bf1f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiGuidSelectionPayload.kt @@ -7,13 +7,11 @@ import com.simprints.infra.events.event.domain.models.GuidSelectionEvent.GuidSel @Keep internal data class ApiGuidSelectionPayload( override val startTime: ApiTimestamp, - override val version: Int, val selectedId: String, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: GuidSelectionPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.selectedId, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiIntentParsingPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiIntentParsingPayload.kt index b9c2438602..92c3eb9f63 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiIntentParsingPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiIntentParsingPayload.kt @@ -12,13 +12,11 @@ import com.simprints.infra.eventsync.event.remote.models.ApiIntentParsingPayload @Keep internal data class ApiIntentParsingPayload( override val startTime: ApiTimestamp, - override val version: Int, val integration: ApiIntegrationInfo, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: IntentParsingPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.integration.fromDomainToApi(), ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiInvalidIntentPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiInvalidIntentPayload.kt index 5767266c6d..4a2424b8d5 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiInvalidIntentPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiInvalidIntentPayload.kt @@ -10,14 +10,12 @@ import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.Invalid @JsonInclude(Include.NON_NULL) internal data class ApiInvalidIntentPayload( override val startTime: ApiTimestamp, - override val version: Int, val action: String, val extras: Map, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: InvalidIntentPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.action, domainPayload.extras, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiLicenseCheckingEventPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiLicenseCheckingEventPayload.kt new file mode 100644 index 0000000000..3fae3fc3c5 --- /dev/null +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiLicenseCheckingEventPayload.kt @@ -0,0 +1,22 @@ +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.LicenseCheckEvent + +@Keep +internal data class ApiLicenseCheckEventPayload( + override val startTime: ApiTimestamp, + val status: LicenseCheckEvent.LicenseStatus, + val vendor: String, +) : ApiEventPayload(startTime) { + + constructor(domainPayload: LicenseCheckEvent.LicenseCheckEventPayload) : this( + domainPayload.createdAt.fromDomainToApi(), + domainPayload.status, + domainPayload.vendor, + ) + + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = + null // this payload doesn't have tokenizable fields +} diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt index 9e854e303c..620beeb5df 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayload.kt @@ -8,12 +8,11 @@ import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToM @Keep internal data class ApiOneToManyMatchPayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val pool: ApiMatchPool, val matcher: String, val result: List?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload( startTime) { @Keep data class ApiMatchPool(val type: ApiMatchPoolType, val count: Int) { @@ -32,7 +31,6 @@ internal data class ApiOneToManyMatchPayload( constructor(domainPayload: OneToManyMatchPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), ApiMatchPool(domainPayload.pool), domainPayload.matcher, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt index 12226ab3a6..8f9a1949b3 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToOneMatchPayload.kt @@ -10,17 +10,15 @@ import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOn @JsonInclude(Include.NON_NULL) internal data class ApiOneToOneMatchPayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val candidateId: String, val matcher: String, val result: ApiMatchEntry?, val fingerComparisonStrategy: ApiFingerComparisonStrategy?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: OneToOneMatchPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), domainPayload.candidateId, domainPayload.matcher, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt index a7142a197e..81004972c9 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiPersonCreationPayload.kt @@ -8,16 +8,14 @@ import com.simprints.infra.events.event.domain.models.PersonCreationEvent.Person @Keep internal data class ApiPersonCreationPayload( override val startTime: ApiTimestamp, - override val version: Int, val fingerprintCaptureIds: List?, val fingerprintReferenceId: String?, val faceCaptureIds: List?, val faceReferenceId: String?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: PersonCreationPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.fingerprintCaptureIds, domainPayload.fingerprintReferenceId, domainPayload.faceCaptureIds, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiRefusalPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiRefusalPayload.kt index b81f53fae2..e8a866a302 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiRefusalPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiRefusalPayload.kt @@ -16,11 +16,10 @@ import com.simprints.infra.eventsync.event.remote.models.ApiRefusalPayload.ApiAn @Keep internal data class ApiRefusalPayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val reason: ApiAnswer, val otherText: String, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep enum class ApiAnswer { @@ -37,7 +36,6 @@ internal data class ApiRefusalPayload( constructor(domainPayload: RefusalPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), domainPayload.reason.toApiRefusalEventAnswer(), domainPayload.otherText, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerConnectionPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerConnectionPayload.kt index f66bab3736..41e9193d92 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerConnectionPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerConnectionPayload.kt @@ -14,9 +14,8 @@ import com.simprints.infra.eventsync.event.remote.models.ApiScannerConnectionPay @JsonInclude(Include.NON_NULL) internal data class ApiScannerConnectionPayload( override val startTime: ApiTimestamp, - override val version: Int, val scannerInfo: ApiScannerInfo, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep @JsonInclude(Include.NON_NULL) @@ -29,7 +28,6 @@ internal data class ApiScannerConnectionPayload( constructor(domainPayload: ScannerConnectionPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiScannerInfo( domainPayload.scannerInfo.scannerId, domainPayload.scannerInfo.macAddress, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerFirmwareUpdatePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerFirmwareUpdatePayload.kt index 7bb4fb4a2e..7108182957 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerFirmwareUpdatePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiScannerFirmwareUpdatePayload.kt @@ -11,16 +11,14 @@ import com.simprints.infra.events.event.domain.models.ScannerFirmwareUpdateEvent @JsonInclude(Include.NON_NULL) internal data class ApiScannerFirmwareUpdatePayload( override val startTime: ApiTimestamp, - override val version: Int, val endTime: ApiTimestamp?, val chip: String, val targetAppVersion: String, val failureReason: String?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: ScannerFirmwareUpdatePayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.endedAt?.fromDomainToApi(), domainPayload.chip, domainPayload.targetAppVersion, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiSuspiciousIntentPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiSuspiciousIntentPayload.kt index dd390a862d..4843da2c80 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiSuspiciousIntentPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiSuspiciousIntentPayload.kt @@ -10,13 +10,11 @@ import com.simprints.infra.events.event.domain.models.SuspiciousIntentEvent.Susp @JsonInclude(Include.NON_NULL) internal data class ApiSuspiciousIntentPayload( override val startTime: ApiTimestamp, - override val version: Int, val unexpectedExtras: Map, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: SuspiciousIntentPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.unexpectedExtras, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiVero2InfoSnapshotPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiVero2InfoSnapshotPayload.kt index 183b8b9847..975ff90a75 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiVero2InfoSnapshotPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/ApiVero2InfoSnapshotPayload.kt @@ -28,19 +28,16 @@ import com.simprints.infra.events.event.domain.models.Vero2InfoSnapshotEvent.Ver @Keep internal sealed class ApiVero2InfoSnapshotPayload( override val startTime: ApiTimestamp, - override val version: Int, open val scannerVersion: ApiVero2Version, open val battery: ApiBatteryInfo -): ApiEventPayload(version, startTime) { +): ApiEventPayload(startTime) { data class ApiVero2InfoSnapshotPayloadForNewApi( override val startTime: ApiTimestamp, - override val version: Int, override val scannerVersion: ApiVero2Version, override val battery: ApiBatteryInfo ): ApiVero2InfoSnapshotPayload( startTime, - version, scannerVersion, battery ) { @@ -51,12 +48,10 @@ internal sealed class ApiVero2InfoSnapshotPayload( @Deprecated(message = "used only for backwards compatibility") data class ApiVero2InfoSnapshotPayloadForOldApi( override val startTime: ApiTimestamp, - override val version: Int, override val scannerVersion: ApiVero2Version, override val battery: ApiBatteryInfo ): ApiVero2InfoSnapshotPayload( startTime, - version, scannerVersion, battery ) { @@ -140,7 +135,6 @@ internal fun toApiVero2InfoSnapshotPayload(domainPayload: Vero2InfoSnapshotPaylo is Vero2InfoSnapshotPayload.Vero2InfoSnapshotPayloadForNewApi -> { ApiVero2InfoSnapshotPayload.ApiVero2InfoSnapshotPayloadForNewApi( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.version.toApiVero2Version(), ApiVero2InfoSnapshotPayload.ApiBatteryInfo(domainPayload.battery) ) @@ -148,7 +142,6 @@ internal fun toApiVero2InfoSnapshotPayload(domainPayload: Vero2InfoSnapshotPaylo is Vero2InfoSnapshotPayload.Vero2InfoSnapshotPayloadForOldApi -> { ApiVero2InfoSnapshotPayload.ApiVero2InfoSnapshotPayloadForOldApi( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.version.toApiVero2Version(), ApiVero2InfoSnapshotPayload.ApiBatteryInfo(domainPayload.battery) ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiCallbackPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiCallbackPayload.kt index 8d75a3c514..7d0ffb5158 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiCallbackPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiCallbackPayload.kt @@ -12,27 +12,22 @@ import com.simprints.infra.events.event.domain.models.callback.RefusalCallbackEv import com.simprints.infra.events.event.domain.models.callback.VerificationCallbackEvent.VerificationCallbackPayload import com.simprints.infra.eventsync.event.remote.models.ApiEventPayload import com.simprints.infra.eventsync.event.remote.models.ApiTimestamp -import com.simprints.infra.eventsync.event.remote.models.callback.* -import com.simprints.infra.eventsync.event.remote.models.callback.ApiCallbackType.* import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @JsonInclude(Include.NON_NULL) internal data class ApiCallbackPayload( override val startTime: ApiTimestamp, - override val version: Int, val callback: ApiCallback, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EnrolmentCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiEnrolmentCallback(domainPayload.guid) ) constructor(domainPayload: IdentificationCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiIdentificationCallback( domainPayload.sessionId, domainPayload.scores.map { it.fromDomainToApi(domainPayload.eventVersion) }) @@ -40,25 +35,21 @@ internal data class ApiCallbackPayload( constructor(domainPayload: VerificationCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiVerificationCallback(domainPayload.score.fromDomainToApi(domainPayload.eventVersion)) ) constructor(domainPayload: ConfirmationCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiConfirmationCallback(domainPayload.identificationOutcome) ) constructor(domainPayload: ErrorCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiErrorCallback(domainPayload.reason.fromDomainToApi()) ) constructor(domainPayload: RefusalCallbackPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiRefusalCallback(domainPayload.reason, domainPayload.extra) ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt index c3f8a34dcd..c086a3a09f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callback/ApiErrorCallback.kt @@ -2,7 +2,22 @@ package com.simprints.infra.eventsync.event.remote.models.callback import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason -import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.* +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.BACKEND_MAINTENANCE_ERROR +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.BLUETOOTH_NOT_SUPPORTED +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.BLUETOOTH_NO_PERMISSION +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.DIFFERENT_PROJECT_ID_SIGNED_IN +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.DIFFERENT_USER_ID_SIGNED_IN +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.ENROLMENT_LAST_BIOMETRICS_FAILED +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.FACE_CONFIGURATION_ERROR +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.FINGERPRINT_CONFIGURATION_ERROR +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.GUID_NOT_FOUND_OFFLINE +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.GUID_NOT_FOUND_ONLINE +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.LICENSE_INVALID +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.LICENSE_MISSING +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.LOGIN_NOT_COMPLETE +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.PROJECT_ENDING +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.PROJECT_PAUSED +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason.UNEXPECTED_ERROR import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason.SCANNER_LOW_BATTERY @@ -25,8 +40,8 @@ internal data class ApiErrorCallback(val reason: ApiReason) : ApiCallback(ApiCal LOGIN_NOT_COMPLETE, ENROLMENT_LAST_BIOMETRICS_FAILED, - FACE_LICENSE_MISSING, - FACE_LICENSE_INVALID, + LICENSE_MISSING, + LICENSE_INVALID, PROJECT_ENDING, PROJECT_PAUSED, BLUETOOTH_NO_PERMISSION @@ -43,8 +58,8 @@ internal fun Reason.fromDomainToApi() = BLUETOOTH_NOT_SUPPORTED -> ApiReason.BLUETOOTH_NOT_SUPPORTED LOGIN_NOT_COMPLETE -> ApiReason.LOGIN_NOT_COMPLETE ENROLMENT_LAST_BIOMETRICS_FAILED -> ApiReason.ENROLMENT_LAST_BIOMETRICS_FAILED - FACE_LICENSE_MISSING -> ApiReason.FACE_LICENSE_MISSING - FACE_LICENSE_INVALID -> ApiReason.FACE_LICENSE_INVALID + LICENSE_MISSING -> ApiReason.LICENSE_MISSING + LICENSE_INVALID -> ApiReason.LICENSE_INVALID FINGERPRINT_CONFIGURATION_ERROR -> ApiReason.UNEXPECTED_ERROR FACE_CONFIGURATION_ERROR -> ApiReason.UNEXPECTED_ERROR BACKEND_MAINTENANCE_ERROR -> ApiReason.BACKEND_MAINTENANCE_ERROR @@ -65,8 +80,8 @@ internal fun ApiReason.fromApiToDomain(): Reason = SCANNER_LOW_BATTERY -> UNEXPECTED_ERROR ApiReason.LOGIN_NOT_COMPLETE -> LOGIN_NOT_COMPLETE ApiReason.ENROLMENT_LAST_BIOMETRICS_FAILED -> ENROLMENT_LAST_BIOMETRICS_FAILED - ApiReason.FACE_LICENSE_MISSING -> FACE_LICENSE_MISSING - ApiReason.FACE_LICENSE_INVALID -> FACE_LICENSE_INVALID + ApiReason.LICENSE_MISSING -> LICENSE_MISSING + ApiReason.LICENSE_INVALID -> LICENSE_INVALID ApiReason.BACKEND_MAINTENANCE_ERROR -> BACKEND_MAINTENANCE_ERROR ApiReason.PROJECT_ENDING -> PROJECT_ENDING ApiReason.PROJECT_PAUSED -> PROJECT_PAUSED diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutEvent.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutEvent.kt index 2f48aa5266..192d1b5a2b 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutEvent.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/callout/ApiCalloutEvent.kt @@ -17,13 +17,11 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @JsonInclude(Include.NON_NULL) internal data class ApiCalloutPayload( override val startTime: ApiTimestamp, - override val version: Int, val callout: ApiCallout, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EnrolmentCalloutPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiEnrolmentCallout( domainPayload.projectId, domainPayload.userId.value, @@ -32,7 +30,6 @@ internal data class ApiCalloutPayload( constructor(domainPayload: IdentificationCalloutPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiIdentificationCallout( domainPayload.projectId, domainPayload.userId.value, @@ -41,7 +38,6 @@ internal data class ApiCalloutPayload( constructor(domainPayload: VerificationCalloutPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiVerificationCallout( domainPayload.projectId, domainPayload.userId.value, @@ -51,14 +47,12 @@ internal data class ApiCalloutPayload( constructor(domainPayload: ConfirmationCalloutPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiConfirmationCallout( domainPayload.selectedGuid, domainPayload.sessionId)) constructor(domainPayload: EnrolmentLastBiometricsCalloutPayload) : this( domainPayload.createdAt.fromDomainToApi(), - domainPayload.eventVersion, ApiEnrolmentLastBiometricsCallout( domainPayload.projectId, domainPayload.userId.value, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayload.kt index a4e4fe3d78..936f9bf7fc 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayload.kt @@ -11,19 +11,17 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi internal data class ApiEventDownSyncRequestPayload( override val startTime: ApiTimestamp, val endTime: ApiTimestamp?, - override val version: Int, val requestId: String, val queryParameters: ApiQueryParameters, val responseStatus: Int?, val errorType: String?, val msToFirstResponseByte: Long?, val eventsRead: Int?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EventDownSyncRequestEvent.EventDownSyncRequestPayload) : this( domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.requestId, ApiQueryParameters( domainPayload.queryParameters.moduleId, @@ -47,5 +45,10 @@ internal data class ApiEventDownSyncRequestPayload( val lastEventId: String?, ) - override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = null + override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = + when (tokenKeyType) { + TokenKeyType.AttendantId -> "queryParameters.attendantId" + TokenKeyType.ModuleId -> "queryParameters.moduleId" + else -> null + } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureBiometricsPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureBiometricsPayload.kt index 758204b28d..bfba79647a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureBiometricsPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureBiometricsPayload.kt @@ -9,11 +9,10 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep internal class ApiFaceCaptureBiometricsPayload( - override val version: Int, override val startTime: ApiTimestamp, val id: String, val face: Face?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { @Keep data class Face( @@ -34,7 +33,6 @@ internal class ApiFaceCaptureBiometricsPayload( } constructor(domainPayload: FaceCaptureBiometricsEvent.FaceCaptureBiometricsPayload) : this( - domainPayload.eventVersion, domainPayload.createdAt.fromDomainToApi(), domainPayload.id, Face(domainPayload.face) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureConfirmationPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureConfirmationPayload.kt index bbcd7e1251..9d247f15ea 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureConfirmationPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCaptureConfirmationPayload.kt @@ -17,14 +17,12 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi internal data class ApiFaceCaptureConfirmationPayload( override val startTime: ApiTimestamp, //Not added on API yet val endTime: ApiTimestamp?, - override val version: Int, val result: ApiResult, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: FaceCaptureConfirmationPayload) : this( domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.result.fromDomainToApi() ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayload.kt index 512ff6edf1..8d288ca601 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayload.kt @@ -8,7 +8,12 @@ import com.simprints.infra.events.event.domain.models.face.FaceCaptureEvent.Face import com.simprints.infra.eventsync.event.remote.models.ApiEventPayload import com.simprints.infra.eventsync.event.remote.models.ApiTimestamp import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiFace -import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.* +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.INVALID +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.OFF_ROLL +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.OFF_YAW +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.TOO_CLOSE +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.TOO_FAR +import com.simprints.infra.eventsync.event.remote.models.face.ApiFaceCapturePayload.ApiResult.VALID import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi @Keep @@ -17,19 +22,17 @@ internal data class ApiFaceCapturePayload( val id: String, override val startTime: ApiTimestamp, val endTime: ApiTimestamp?, - override val version: Int, val attemptNb: Int, val qualityThreshold: Float, val result: ApiResult, val isFallback: Boolean, val face: ApiFace?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: FaceCapturePayload) : this( domainPayload.id, domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.attemptNb, domainPayload.qualityThreshold, domainPayload.result.fromDomainToApi(), diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceFallbackCapturePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceFallbackCapturePayload.kt index 3c69a7a2cb..3a8398cdab 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceFallbackCapturePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceFallbackCapturePayload.kt @@ -12,13 +12,11 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi internal data class ApiFaceFallbackCapturePayload( override val startTime: ApiTimestamp, //Not added on API yet val endTime: ApiTimestamp?, - override val version: Int, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: FaceFallbackCapturePayload) : this( domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceOnboardingCompletePayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceOnboardingCompletePayload.kt index 61aa1d0079..10fab139ea 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceOnboardingCompletePayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceOnboardingCompletePayload.kt @@ -11,13 +11,11 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi internal data class ApiFaceOnboardingCompletePayload( override val startTime: ApiTimestamp, //Not added on API yet val endTime: ApiTimestamp?, - override val version: Int, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: FaceOnboardingCompletePayload) : this( domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion ) override fun getTokenizedFieldJsonPath(tokenKeyType: TokenKeyType): String? = diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDatabaseInfo.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDatabaseInfo.kt index 544a591a58..3f24c4b6f7 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDatabaseInfo.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDatabaseInfo.kt @@ -10,10 +10,6 @@ import com.simprints.infra.events.event.domain.models.scope.DatabaseInfo internal data class ApiDatabaseInfo( var recordCount: Int?, var sessionCount: Int = 0, -) { - - constructor(databaseInfo: DatabaseInfo) : - this(databaseInfo.recordCount, databaseInfo.sessionCount) -} +) internal fun DatabaseInfo.fromDomainToApi() = ApiDatabaseInfo(recordCount, sessionCount) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDevice.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDevice.kt index 063e4a16a6..1bcba7fc8d 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDevice.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiDevice.kt @@ -7,13 +7,9 @@ import com.simprints.infra.events.event.domain.models.scope.Device @Keep internal data class ApiDevice( var androidSdkVersion: String = Build.VERSION.SDK_INT.toString(), - var deviceModel: String = Build.MANUFACTURER + "_" + Build.MODEL, - var deviceId: String = "", -) { - - constructor(deviceId: Device) : - this(deviceId.androidSdkVersion, deviceId.deviceModel, deviceId.deviceId) -} + var model: String = Build.MANUFACTURER + "_" + Build.MODEL, + var id: String = "", +) internal fun Device.fromDomainToApi() = ApiDevice(androidSdkVersion, deviceModel, deviceId) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScope.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScope.kt index f5d1948c92..b60ae7b2a6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScope.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScope.kt @@ -1,6 +1,8 @@ package com.simprints.infra.eventsync.event.remote.models.session import androidx.annotation.Keep +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include import com.simprints.infra.events.event.domain.models.Event import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.eventsync.event.remote.models.ApiEvent @@ -22,6 +24,7 @@ internal data class ApiEventScope( val device: ApiDevice, val databaseInfo: ApiDatabaseInfo, val location: ApiLocation?, + @JsonInclude(Include.NON_EMPTY) val projectConfigurationUpdatedAt: String, val events: List, ) { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt index 90e8eaf975..911ac17f1a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/upsync/ApiEventUpSyncRequestPayload.kt @@ -11,17 +11,15 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi internal data class ApiEventUpSyncRequestPayload( override val startTime: ApiTimestamp, val endTime: ApiTimestamp?, - override val version: Int, val requestId: String, val content: ApiUpSyncContent, val responseStatus: Int?, val errorType: String?, -) : ApiEventPayload(version, startTime) { +) : ApiEventPayload(startTime) { constructor(domainPayload: EventUpSyncRequestEvent.EventUpSyncRequestPayload) : this( domainPayload.createdAt.fromDomainToApi(), domainPayload.endedAt?.fromDomainToApi(), - domainPayload.eventVersion, domainPayload.requestId, ApiUpSyncContent( domainPayload.content.sessionCount, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepository.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepository.kt index 9c3bca5d5b..5d8b5e7d5b 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepository.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepository.kt @@ -2,7 +2,9 @@ package com.simprints.infra.eventsync.status.down import com.simprints.core.domain.common.Partitioning import com.simprints.core.domain.modality.Modes +import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.eventsync.exceptions.MissingArgumentForDownSyncScopeException @@ -20,6 +22,8 @@ internal class EventDownSyncScopeRepository @Inject constructor( private val authStore: AuthStore, private val recentUserActivityManager: RecentUserActivityManager, private val downSyncOperationOperationDao: DbEventDownSyncOperationStateDao, + private val configRepository: ConfigRepository, + private val tokenizationProcessor: TokenizationProcessor, ) { suspend fun getDownSyncScope( @@ -31,7 +35,7 @@ internal class EventDownSyncScopeRepository @Inject constructor( val syncScope = when (syncPartitioning) { Partitioning.GLOBAL -> SubjectProjectScope(projectId, modes) - Partitioning.USER -> SubjectUserScope(projectId, getUserId(), modes) + Partitioning.USER -> SubjectUserScope(projectId, getUserId(projectId), modes) Partitioning.MODULE -> SubjectModuleScope(projectId, selectedModuleIDs, modes) } @@ -47,15 +51,22 @@ internal class EventDownSyncScopeRepository @Inject constructor( return projectId } - private suspend fun getUserId(): String { + private suspend fun getUserId(projectId: String): String { // After we are certain that all users have user IDs cached (no-one uses 2023.3 for a while), the fallback can be removed - val possibleUserId: String = authStore.signedInUserId?.value - ?: recentUserActivityManager.getRecentUserActivity().lastUserUsed.value + val possibleUserId: TokenizableString = authStore.signedInUserId + ?: recentUserActivityManager.getRecentUserActivity().lastUserUsed - if (possibleUserId.isBlank()) { + if (possibleUserId.value.isBlank()) { throw MissingArgumentForDownSyncScopeException("UserId required") } - return possibleUserId + return when (possibleUserId) { + is TokenizableString.Raw -> tokenizationProcessor.encrypt( + decrypted = possibleUserId, + tokenKeyType = TokenKeyType.AttendantId, + project = configRepository.getProject(projectId) + ).value + is TokenizableString.Tokenized -> possibleUserId.value + } } suspend fun insertOrUpdate(syncScopeOperation: EventDownSyncOperation) { diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncResult.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncResult.kt index 0a4af5b045..02b43aa03d 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncResult.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/down/domain/EventDownSyncResult.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.channels.ReceiveChannel data class EventDownSyncResult( val totalCount: Int?, - val requestId: String, val status: Int, val eventStream: ReceiveChannel ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/DownSyncCounts.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/DownSyncCounts.kt index b4d7aa69b3..ec836890c2 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/DownSyncCounts.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/DownSyncCounts.kt @@ -1,6 +1,6 @@ package com.simprints.infra.eventsync.status.models data class DownSyncCounts( - val toCreate: Int, - val toDelete: Int + val count: Int, + val isLowerBound: Boolean, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncState.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncState.kt index e93caec679..0db747158e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncState.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncState.kt @@ -16,6 +16,42 @@ data class EventSyncState( val state: EventSyncWorkerState, ) - fun isSyncRunning(): Boolean = (upSyncWorkersInfo + downSyncWorkersInfo) + private val syncWorkersInfo: List + get() = upSyncWorkersInfo + downSyncWorkersInfo + + fun isThereNotSyncHistory() = syncWorkersInfo + .isEmpty() + + fun isSyncRunning() = syncWorkersInfo .any { it.state is EventSyncWorkerState.Running || it.state is EventSyncWorkerState.Enqueued } + + fun isSyncCompleted() = syncWorkersInfo + .all { it.state is EventSyncWorkerState.Succeeded } + + fun isSyncInProgress() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Running } + + fun isSyncConnecting() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Enqueued } + + fun isSyncFailedBecauseReloginRequired() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Failed && it.state.failedBecauseReloginRequired } + + fun isSyncFailedBecauseTooManyRequests() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Failed && it.state.failedBecauseTooManyRequest } + + fun isSyncFailedBecauseCloudIntegration() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Failed && it.state.failedBecauseCloudIntegration } + + fun isSyncFailedBecauseBackendMaintenance() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Failed && it.state.failedBecauseBackendMaintenance } + + fun getEstimatedBackendMaintenanceOutage() = syncWorkersInfo + .find { it.state is EventSyncWorkerState.Failed && it.state.estimatedOutage != 0L } + ?.let { it.state as? EventSyncWorkerState.Failed } + ?.estimatedOutage + + fun isSyncFailed() = syncWorkersInfo + .any { it.state is EventSyncWorkerState.Failed || it.state is EventSyncWorkerState.Blocked || it.state is EventSyncWorkerState.Cancelled } + } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncWorkerState.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncWorkerState.kt index 51c9fa6035..d61eeea62f 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncWorkerState.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/models/EventSyncWorkerState.kt @@ -5,10 +5,11 @@ import androidx.work.WorkInfo // val state: String is used for logs purpose only - otherwise any state.toString() would print the same output. sealed class EventSyncWorkerState(val state: String) { - object Enqueued : EventSyncWorkerState("Enqueued") - object Running : EventSyncWorkerState("Running") - object Succeeded : EventSyncWorkerState("Succeeded") + data object Enqueued : EventSyncWorkerState("Enqueued") + data object Running : EventSyncWorkerState("Running") + data object Succeeded : EventSyncWorkerState("Succeeded") class Failed( + val failedBecauseReloginRequired: Boolean = false, val failedBecauseCloudIntegration: Boolean = false, val failedBecauseBackendMaintenance: Boolean = false, val failedBecauseTooManyRequest: Boolean = false, @@ -16,12 +17,13 @@ sealed class EventSyncWorkerState(val state: String) { ) : EventSyncWorkerState("Failed") - object Blocked : EventSyncWorkerState("Blocked") - object Cancelled : EventSyncWorkerState("Cancelled") + data object Blocked : EventSyncWorkerState("Blocked") + data object Cancelled : EventSyncWorkerState("Cancelled") companion object { fun fromWorkInfo( state: WorkInfo.State, + failedBecauseReloginRequired: Boolean = false, failedBecauseCloudIntegration: Boolean = false, failedBecauseBackendMaintenance: Boolean = false, failedBecauseTooManyRequest: Boolean = false, @@ -32,6 +34,7 @@ sealed class EventSyncWorkerState(val state: String) { WorkInfo.State.RUNNING -> Running WorkInfo.State.SUCCEEDED -> Succeeded WorkInfo.State.FAILED -> Failed( + failedBecauseReloginRequired, failedBecauseCloudIntegration, failedBecauseBackendMaintenance, failedBecauseTooManyRequest, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/up/domain/EventUpSyncResult.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/up/domain/EventUpSyncResult.kt index 6ebb2a87bc..26d32c75c2 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/up/domain/EventUpSyncResult.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/status/up/domain/EventUpSyncResult.kt @@ -1,6 +1,5 @@ package com.simprints.infra.eventsync.status.up.domain data class EventUpSyncResult( - val requestId: String, val status: Int, ) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt index e7939bfc61..ae66393c0d 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt @@ -12,7 +12,16 @@ import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Companio import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.Companion.tagForType import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.DOWNLOADER import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.UPLOADER -import com.simprints.infra.eventsync.sync.common.* +import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG +import com.simprints.infra.eventsync.sync.common.SyncWorkersLiveDataProvider +import com.simprints.infra.eventsync.sync.common.didFailBecauseBackendMaintenance +import com.simprints.infra.eventsync.sync.common.didFailBecauseCloudIntegration +import com.simprints.infra.eventsync.sync.common.didFailBecauseReloginRequired +import com.simprints.infra.eventsync.sync.common.didFailBecauseTooManyRequests +import com.simprints.infra.eventsync.sync.common.filterByTags +import com.simprints.infra.eventsync.sync.common.getEstimatedOutageTime +import com.simprints.infra.eventsync.sync.common.sortByScheduledTime import com.simprints.infra.eventsync.sync.down.workers.extractDownSyncMaxCount import com.simprints.infra.eventsync.sync.down.workers.extractDownSyncProgress import com.simprints.infra.eventsync.sync.master.EventStartSyncReporterWorker.Companion.SYNC_ID_STARTED @@ -117,6 +126,7 @@ internal class EventSyncStateProcessor @Inject constructor( private fun WorkInfo.toEventSyncWorkerState(): EventSyncWorkerState = fromWorkInfo( state, + didFailBecauseReloginRequired(), didFailBecauseCloudIntegration(), didFailBecauseBackendMaintenance(), didFailBecauseTooManyRequests(), diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt index 266c0329f4..9333401154 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt @@ -2,11 +2,15 @@ package com.simprints.infra.eventsync.sync.common import androidx.work.WorkInfo +internal const val OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED = "OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED" internal const val OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS = "OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS" internal const val OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION = "OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION" internal const val OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE = "OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE" internal const val OUTPUT_ESTIMATED_MAINTENANCE_TIME = "OUTPUT_ESTIMATED_MAINTENANCE_TIME" +internal fun WorkInfo.didFailBecauseReloginRequired(): Boolean = + this.outputData.getBoolean(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED, false) + internal fun WorkInfo.didFailBecauseCloudIntegration(): Boolean = this.outputData.getBoolean(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION, false) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkRequestBuilder.ext.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkRequestBuilder.ext.kt index eb9aa093c8..2033395383 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkRequestBuilder.ext.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkRequestBuilder.ext.kt @@ -65,12 +65,6 @@ internal fun WorkRequest.Builder<*, *>.addTagForEndSyncReporter(): WorkRequest.B internal fun WorkRequest.Builder<*, *>.addTagForStartSyncReporter(): WorkRequest.Builder<*, *> = this.addTag(tagForType(START_SYNC_REPORTER)) -// Master Worker tags -internal fun WorkRequest.Builder<*, *>.addTagForSyncMasterWorkers(): WorkRequest.Builder<*, *> = this.addTag(MASTER_SYNC_SCHEDULERS) - -internal fun WorkRequest.Builder<*, *>.addTagForOneTimeSyncMasterWorker(): WorkRequest.Builder<*, *> = this.addTag(MASTER_SYNC_SCHEDULER_ONE_TIME) -internal fun WorkRequest.Builder<*, *>.addTagForBackgroundSyncMasterWorker(): WorkRequest.Builder<*, *> = this.addTag(MASTER_SYNC_SCHEDULER_PERIODIC_TIME) - /** * Use tags */ @@ -87,6 +81,5 @@ internal fun List.filterByTags(vararg tagsToFilter: String) = } internal fun WorkManager.getAllSubjectsSyncWorkersInfo() = getWorkInfosByTag(TAG_SUBJECTS_SYNC_ALL_WORKERS) -internal fun WorkManager.cancelAllSubjectsSyncWorkers() = cancelAllWorkByTag(TAG_SUBJECTS_SYNC_ALL_WORKERS) internal fun MutableList.sortByScheduledTime() = sortBy { it -> it.tags.first { it.contains(TAG_SCHEDULED_AT) } } internal fun List.sortByScheduledTime() = sortedBy { it -> it.tags.first { it.contains(TAG_SCHEDULED_AT) } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt index 0bd33950f1..d0e768b129 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt @@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting import com.simprints.core.domain.tokenization.values import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction @@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flow +import java.util.UUID import javax.inject.Inject internal class EventDownSyncTask @Inject constructor( @@ -57,11 +59,13 @@ internal class EventDownSyncTask @Inject constructor( val requestStartTime = timeHelper.now() var firstEventTimestamp: Timestamp? = null + val requestId = UUID.randomUUID().toString() var result: EventDownSyncResult? = null var errorType: String? = null try { result = eventRemoteDataSource.getEvents( + requestId, operation.queryEvent.fromDomainToApi(), scope ) @@ -96,6 +100,10 @@ internal class EventDownSyncTask @Inject constructor( lastOperation = lastOperation.copy(state = COMPLETE, lastSyncTime = timeHelper.now().ms) emitProgress(lastOperation, count, result.totalCount) } catch (t: Throwable) { + if (t is RemoteDbNotSignedInException) { + throw t + } + Simber.d(t) errorType = t.toString() @@ -113,7 +121,7 @@ internal class EventDownSyncTask @Inject constructor( EventDownSyncRequestEvent( createdAt = requestStartTime, endedAt = timeHelper.now(), - requestId = result?.requestId.orEmpty(), + requestId = requestId, query = operation.queryEvent.let { query -> EventDownSyncRequestEvent.QueryParameters( query.moduleId, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorker.kt index 1e6d721816..4fc678e921 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorker.kt @@ -8,15 +8,23 @@ import androidx.work.workDataOf import com.simprints.core.DispatcherBG import com.simprints.core.tools.json.JsonHelper import com.simprints.core.workers.SimCoroutineWorker +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation -import com.simprints.infra.eventsync.sync.common.* +import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TIME +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS +import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG +import com.simprints.infra.eventsync.sync.common.WorkerProgressCountReporter import com.simprints.infra.eventsync.sync.down.tasks.EventDownSyncTask -import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.PROGRESS_DOWN_MAX_SYNC -import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_SYNC import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_MAX_SYNC +import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_SYNC +import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.PROGRESS_DOWN_MAX_SYNC import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.PROGRESS_DOWN_SYNC import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.BackendMaintenanceException @@ -68,8 +76,8 @@ internal class EventDownSyncDownloaderWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { try { - Simber.tag(SYNC_LOG_TAG).d("[DOWNLOADER] Started") showProgressNotification() + Simber.tag(SYNC_LOG_TAG).d("[DOWNLOADER] Started") val workerId = this@EventDownSyncDownloaderWorker.id.toString() var count = syncCache.readProgress(workerId) @@ -94,7 +102,8 @@ internal class EventDownSyncDownloaderWorker @AssistedInject constructor( "Total downloaded: $count / $max" ) } catch (t: Throwable) { - Simber.tag(SYNC_LOG_TAG).d("[DOWNLOADER] Failed") + Simber.d(t) + Simber.tag(SYNC_LOG_TAG).d("[DOWNLOADER] Failed ${t.message}") handleSyncException(t) } } @@ -118,7 +127,9 @@ internal class EventDownSyncDownloaderWorker @AssistedInject constructor( is TooManyRequestsException -> fail( t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true) ) - + is RemoteDbNotSignedInException -> { + fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true)) + } else -> retry(t) } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt index f80f3c44df..fdab54820d 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt @@ -12,7 +12,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import java.util.* +import java.util.Date /** * It's executed at the end of the sync, when all workers succeed (downloaders and uploaders). @@ -32,9 +32,9 @@ internal class EventEndSyncReporterWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { try { + showProgressNotification() val syncId = inputData.getString(SYNC_ID_TO_MARK_AS_COMPLETED) crashlyticsLog("Start - Params: $syncId") - showProgressNotification() inputData.getString(EVENT_DOWN_SYNC_SCOPE_TO_CLOSE)?.let { scopeId -> eventRepository.closeEventScope(scopeId, EventScopeEndCause.WORKFLOW_ENDED) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventStartSyncReporterWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventStartSyncReporterWorker.kt index 417250e36d..fd06c6f0a9 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventStartSyncReporterWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventStartSyncReporterWorker.kt @@ -30,9 +30,9 @@ internal class EventStartSyncReporterWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { try { + showProgressNotification() val syncId = inputData.getString(SYNC_ID_STARTED) crashlyticsLog("Start - Params: $syncId") - showProgressNotification() success(inputData) } catch (t: Throwable) { fail(t) diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventSyncMasterWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventSyncMasterWorker.kt index 1571e079c3..4bc779ba46 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventSyncMasterWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventSyncMasterWorker.kt @@ -2,7 +2,11 @@ package com.simprints.infra.eventsync.sync.master import android.content.Context import androidx.hilt.work.HiltWorker -import androidx.work.* +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf import com.simprints.core.DispatcherBG import com.simprints.core.tools.time.TimeHelper import com.simprints.core.workers.SimCoroutineWorker @@ -14,7 +18,11 @@ import com.simprints.infra.config.store.models.canSyncDataToSimprints import com.simprints.infra.config.store.models.isEventDownSyncAllowed import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScopeType -import com.simprints.infra.eventsync.sync.common.* +import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG +import com.simprints.infra.eventsync.sync.common.getAllSubjectsSyncWorkersInfo +import com.simprints.infra.eventsync.sync.common.getUniqueSyncId +import com.simprints.infra.eventsync.sync.common.sortByScheduledTime import com.simprints.infra.eventsync.sync.down.EventDownSyncWorkersBuilder import com.simprints.infra.eventsync.sync.up.EventUpSyncWorkersBuilder import com.simprints.infra.logging.Simber @@ -23,10 +31,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import java.util.* +import java.util.UUID @HiltWorker -internal class EventSyncMasterWorker @AssistedInject constructor( +class EventSyncMasterWorker @AssistedInject internal constructor( @Assisted private val appContext: Context, @Assisted params: WorkerParameters, private val downSyncWorkerBuilder: EventDownSyncWorkersBuilder, @@ -63,10 +71,10 @@ internal class EventSyncMasterWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { try { + showProgressNotification() // check if device is rooted before starting the sync securityManager.checkIfDeviceIsRooted() crashlyticsLog("Start") - showProgressNotification() val configuration = configRepository.getProjectConfiguration() if (!configuration.canSyncDataToSimprints() && !isEventDownSyncAllowed(configuration)) return@withContext success( diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt index 140dcfac02..5d20ddf15a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt @@ -6,6 +6,7 @@ import com.simprints.core.tools.json.JsonHelper import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.canSyncAllDataToSimprints @@ -30,6 +31,7 @@ import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation.UpSyn import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation.UpSyncState.FAILED import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation.UpSyncState.RUNNING import com.simprints.infra.eventsync.status.up.domain.EventUpSyncResult +import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG import com.simprints.infra.eventsync.sync.up.EventUpSyncProgress import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.NetworkConnectionException @@ -37,6 +39,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import retrofit2.HttpException +import java.util.UUID import javax.inject.Inject internal class EventUpSyncTask @Inject constructor( @@ -62,32 +65,88 @@ internal class EventUpSyncTask @Inject constructor( val config = configRepository.getProjectConfiguration() var lastOperation = operation.copy() var count = 0 + var isUsefulUpload = false try { - lastOperation = - lastOperation.copy(lastState = RUNNING, lastSyncTime = timeHelper.now().ms) + lastOperation = lastOperation.copy( + lastState = RUNNING, + lastSyncTime = timeHelper.now().ms + ) - uploadSessionEvents(projectId = operation.projectId, config, eventScope).collect { + uploadEventScopeType( + eventScope = eventScope, + projectId = operation.projectId, + eventScopeTypeToUpload = EventScopeType.SESSION, + batchSize = config.synchronization.up.simprints.batchSizes.sessions, + eventFilter = { scopes -> + scopes.mapValues { (_, events) -> + events?.let { filterEventsToUpSync(it, config) } + } + }, + createUpSyncContentContent = { + isUsefulUpload = it > 0 + EventUpSyncRequestEvent.UpSyncContent(sessionCount = it) + }, + ).collect { count = it - lastOperation = - lastOperation.copy(lastState = RUNNING, lastSyncTime = timeHelper.now().ms) + lastOperation = lastOperation.copy( + lastState = RUNNING, + lastSyncTime = timeHelper.now().ms + ) emitProgress(lastOperation, count) } - uploadOutOfSessionEvents(operation.projectId, eventScope).collect { + uploadEventScopeType( + eventScope = eventScope, + projectId = operation.projectId, + eventScopeTypeToUpload = EventScopeType.DOWN_SYNC, + batchSize = config.synchronization.up.simprints.batchSizes.downSyncs, + createUpSyncContentContent = { + isUsefulUpload = it > 0 + EventUpSyncRequestEvent.UpSyncContent(eventDownSyncCount = it) + }, + ).collect { + count = it + lastOperation = lastOperation.copy( + lastState = RUNNING, + lastSyncTime = timeHelper.now().ms + ) + emitProgress(lastOperation, count) + } + uploadEventScopeType( + eventScope = eventScope, + projectId = operation.projectId, + eventScopeTypeToUpload = EventScopeType.UP_SYNC, + batchSize = config.synchronization.up.simprints.batchSizes.upSyncs, + createUpSyncContentContent = { + // Only tracking up-sync if there have been ay events in other scopes. + EventUpSyncRequestEvent.UpSyncContent( + eventUpSyncCount = if (isUsefulUpload) it else 0 + ) + }, + ).collect { count = it - lastOperation = - lastOperation.copy(lastState = RUNNING, lastSyncTime = timeHelper.now().ms) + lastOperation = lastOperation.copy( + lastState = RUNNING, + lastSyncTime = timeHelper.now().ms + ) emitProgress(lastOperation, count) } + lastOperation = lastOperation.copy( + lastState = COMPLETE, + lastSyncTime = timeHelper.now().ms + ) - lastOperation = - lastOperation.copy(lastState = COMPLETE, lastSyncTime = timeHelper.now().ms) emitProgress(lastOperation, count) } catch (t: Throwable) { - Simber.e(t) + if (t is RemoteDbNotSignedInException) { + throw t + } - lastOperation = - lastOperation.copy(lastState = FAILED, lastSyncTime = timeHelper.now().ms) + Simber.e(t) + lastOperation = lastOperation.copy( + lastState = FAILED, + lastSyncTime = timeHelper.now().ms + ) emitProgress(lastOperation, count) } @@ -101,13 +160,17 @@ internal class EventUpSyncTask @Inject constructor( this.emit(EventUpSyncProgress(lastOperation, count)) } - private fun uploadSessionEvents( - projectId: String, - config: ProjectConfiguration, + private fun uploadEventScopeType( eventScope: EventScope, + projectId: String, + eventScopeTypeToUpload: EventScopeType, + batchSize: Int, + eventFilter: (Map?>) -> Map?> = { it }, + createUpSyncContentContent: (Int) -> EventUpSyncRequestEvent.UpSyncContent, ) = flow { - Simber.d("[EVENT_REPO] Uploading session scopes") - val sessionScopes = getClosedScopesForType(EventScopeType.SESSION) + Simber.tag(SYNC_LOG_TAG) + .d("Uploading event scope - $eventScopeTypeToUpload in batches of $batchSize") + val sessionScopes = getClosedScopesForType(eventScopeTypeToUpload) // Re-emitting the number of uploaded corrupted events attemptInvalidEventUpload( @@ -117,84 +180,45 @@ internal class EventUpSyncTask @Inject constructor( val scopesToUpload = sessionScopes .filterValues { it != null } - .mapValues { (_, events) -> - events?.let { filterEventsToUpSync(events, config) }.orEmpty() - } - .map { (scope, events) -> ApiEventScope.fromDomain(scope, events) } + .let(eventFilter) + .map { (scope, events) -> ApiEventScope.fromDomain(scope, events.orEmpty()) } val uploadedScopes = mutableListOf() - scopesToUpload.chunked(UPLOAD_BATCH_SIZE).forEach { scope -> + scopesToUpload.chunked(batchSize.coerceAtLeast(1)).forEach { scopes -> + val requestId = UUID.randomUUID().toString() + val requestStartTime = timeHelper.now() try { val result = eventRemoteDataSource.post( + requestId, projectId, - ApiUploadEventsBody(sessions = scope) + scopes.asApiUploadEventsBody(eventScopeTypeToUpload) ) addRequestEvent( + requestId = requestId, eventScope = eventScope, startTime = requestStartTime, result = result, - uploadedSessionScopes = scope.size, + content = createUpSyncContentContent(scopes.size), ) - uploadedScopes.addAll(scope.map { it.id }) + uploadedScopes.addAll(scopes.map { it.id }) } catch (ex: Exception) { - handleFailedRequest(ex, eventScope, requestStartTime) + handleFailedRequest(requestId, ex, eventScope, requestStartTime) } } - Simber.d("[EVENT_REPO] Deleting ${uploadedScopes.size} session scopes") + Simber.tag(SYNC_LOG_TAG).d("Deleting ${uploadedScopes.size} session scopes") eventRepository.deleteEventScopes(uploadedScopes) } - private fun uploadOutOfSessionEvents(projectId: String, eventScope: EventScope) = flow { - Simber.d("[EVENT_REPO] Uploading event scopes") - - val upSyncScopes = getClosedScopesForType(EventScopeType.UP_SYNC) - val downSyncScopes = getClosedScopesForType(EventScopeType.DOWN_SYNC) - - // Re-emitting the number of uploaded corrupted events - attemptInvalidEventUpload( - projectId, - upSyncScopes.getCorruptedScopes() + downSyncScopes.getCorruptedScopes() - ).collect { emit(it) } - - val upSyncScopesToUpload = upSyncScopes - .filterValues { it != null } - .map { (scope, events) -> ApiEventScope.fromDomain(scope, events.orEmpty()) } - - val downSyncScopesToUpload = downSyncScopes - .filterValues { it != null } - .map { (scope, events) -> ApiEventScope.fromDomain(scope, events.orEmpty()) } - - if (upSyncScopesToUpload.isNotEmpty() || downSyncScopesToUpload.isNotEmpty()) { - val requestStartTime = timeHelper.now() - try { - val result = eventRemoteDataSource.post( - projectId, - ApiUploadEventsBody( - eventUpSyncs = upSyncScopesToUpload, - eventDownSyncs = downSyncScopesToUpload, - ) - ) - addRequestEvent( - eventScope = eventScope, - startTime = requestStartTime, - result = result, - uploadedUpSyncScopes = upSyncScopesToUpload.size, - uploadedDownSyncScopes = downSyncScopesToUpload.size, - ) - } catch (ex: Exception) { - handleFailedRequest(ex, eventScope, requestStartTime) - } - } - - val uploadedScopes = - upSyncScopesToUpload.map { it.id } + downSyncScopesToUpload.map { it.id } - Simber.d("[EVENT_REPO] Deleting ${uploadedScopes.size} event scopes") - uploadedScopes.forEach { eventRepository.deleteEventScope(it) } + private fun List.asApiUploadEventsBody( + eventScopeTypeToUpload: EventScopeType, + ) = when (eventScopeTypeToUpload) { + EventScopeType.SESSION -> ApiUploadEventsBody(sessions = this) + EventScopeType.DOWN_SYNC -> ApiUploadEventsBody(eventDownSyncs = this) + EventScopeType.UP_SYNC -> ApiUploadEventsBody(eventUpSyncs = this) } - private fun Map?>.getCorruptedScopes() = filterValues { it == null }.keys @@ -222,26 +246,20 @@ internal class EventUpSyncTask @Inject constructor( } private suspend fun addRequestEvent( + requestId: String, eventScope: EventScope, startTime: Timestamp, result: EventUpSyncResult, - uploadedSessionScopes: Int = 0, - uploadedUpSyncScopes: Int = 0, - uploadedDownSyncScopes: Int = 0, + content: EventUpSyncRequestEvent.UpSyncContent, ) { - if (uploadedSessionScopes > 0 || uploadedDownSyncScopes > 0) { - // Not tracking cases when only up sync scopes are uploaded as it is likely - // to cause a feedback loop of up-syncing the previous up-sync event. - + if (content.sessionCount > 0 || content.eventDownSyncCount > 0 || content.eventUpSyncCount > 0) { eventRepository.addOrUpdateEvent( eventScope, EventUpSyncRequestEvent( createdAt = startTime, endedAt = timeHelper.now(), - requestId = result.requestId, - sessionCount = uploadedSessionScopes, - eventUpSyncCount = uploadedUpSyncScopes, - eventDownSyncCount = uploadedDownSyncScopes, + requestId = requestId, + content = content, responseStatus = result.status, ) ) @@ -249,6 +267,7 @@ internal class EventUpSyncTask @Inject constructor( } private suspend fun handleFailedRequest( + requestId: String, ex: Exception, eventScope: EventScope, requestStartTime: Timestamp, @@ -258,14 +277,11 @@ internal class EventUpSyncTask @Inject constructor( is NetworkConnectionException -> Simber.i(ex) is HttpException -> { Simber.i(ex) - result = ex.response()?.let { - EventUpSyncResult( - eventRemoteDataSource.getRequestId(it), - it.code() - ) - } + result = ex.response()?.let { EventUpSyncResult(it.code()) } } + is RemoteDbNotSignedInException -> throw ex + else -> { Simber.e(ex) // Propagate other exceptions to report failure to the caller. @@ -277,7 +293,7 @@ internal class EventUpSyncTask @Inject constructor( EventUpSyncRequestEvent( createdAt = requestStartTime, endedAt = timeHelper.now(), - requestId = result?.requestId.orEmpty(), + requestId = requestId, responseStatus = result?.status, errorType = ex.toString(), ) @@ -324,16 +340,11 @@ internal class EventUpSyncTask @Inject constructor( when (t) { // We don't need to report http exceptions as cloud logs all of them. is NetworkConnectionException, is HttpException -> Simber.i(t) + is RemoteDbNotSignedInException -> throw t else -> Simber.e(t) } } } } - companion object { - - // TODO: This should be configurable via project configuration - private const val UPLOAD_BATCH_SIZE = 100 - } - } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt index 6258d24b27..e2bcdfc30e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt @@ -11,6 +11,7 @@ import com.simprints.core.DispatcherBG import com.simprints.core.tools.json.JsonHelper import com.simprints.core.workers.SimCoroutineWorker import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.exceptions.MalformedSyncOperationException import com.simprints.infra.eventsync.status.up.domain.EventUpSyncScope @@ -18,6 +19,7 @@ import com.simprints.infra.eventsync.sync.common.EventSyncCache import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TIME import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED import com.simprints.infra.eventsync.sync.common.SYNC_LOG_TAG import com.simprints.infra.eventsync.sync.common.WorkerProgressCountReporter import com.simprints.infra.eventsync.sync.up.tasks.EventUpSyncTask @@ -72,8 +74,8 @@ internal class EventUpSyncUploaderWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { try { - Simber.tag(SYNC_LOG_TAG).d("[UPLOADER] Started") showProgressNotification() + Simber.tag(SYNC_LOG_TAG).d("[UPLOADER] Started") val workerId = this@EventUpSyncUploaderWorker.id.toString() var count = eventSyncCache.readProgress(workerId) @@ -122,6 +124,10 @@ internal class EventUpSyncUploaderWorker @AssistedInject constructor( fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true)) } + is RemoteDbNotSignedInException -> { + fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true)) + } + else -> { retry(t) } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt index 55f94f130d..211702d668 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt @@ -1,30 +1,32 @@ package com.simprints.infra.eventsync -import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.Partitioning +import com.simprints.core.tools.time.TimeHelper +import com.simprints.core.tools.time.Timestamp import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.EventCount import com.simprints.infra.events.event.domain.models.scope.EventScope -import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEventType import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID_2 import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository +import com.simprints.infra.eventsync.status.models.DownSyncCounts import com.simprints.infra.eventsync.status.up.EventUpSyncScopeRepository import com.simprints.infra.eventsync.sync.EventSyncStateProcessor -import com.simprints.infra.eventsync.sync.common.* +import com.simprints.infra.eventsync.sync.common.EventSyncCache import com.simprints.infra.eventsync.sync.down.tasks.EventDownSyncTask import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest @@ -32,7 +34,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock @RunWith(AndroidJUnit4::class) internal class EventSyncManagerTest { @@ -41,10 +42,7 @@ internal class EventSyncManagerTest { val testCoroutineRule = TestCoroutineRule() @MockK - lateinit var ctx: Context - - @MockK - lateinit var workManager: WorkManager + lateinit var timeHelper: TimeHelper @MockK lateinit var eventSyncStateProcessor: EventSyncStateProcessor @@ -79,16 +77,14 @@ internal class EventSyncManagerTest { fun setup() { MockKAnnotations.init(this, relaxed = true) - mockkStatic(WorkManager::class) - every { WorkManager.getInstance(ctx) } returns workManager - + every { timeHelper.now() } returns Timestamp(1) coEvery { configRepository.getProjectConfiguration() } returns mockk { every { general.modalities } returns listOf() every { synchronization.down.partitionType.toDomain() } returns Partitioning.MODULE } eventSyncManagerImpl = EventSyncManagerImpl( - ctx = ctx, + timeHelper = timeHelper, eventSyncStateProcessor = eventSyncStateProcessor, downSyncScopeRepository = eventDownSyncScopeRepository, eventRepository = eventRepository, @@ -113,57 +109,6 @@ internal class EventSyncManagerTest { verify { eventSyncStateProcessor.getLastSyncState() } } - @Test - fun `sync should enqueue a one time sync master worker`() = runTest { - eventSyncManagerImpl.sync() - - verify(exactly = 1) { - workManager.beginUniqueWork( - MASTER_SYNC_SCHEDULER_ONE_TIME, - ExistingWorkPolicy.KEEP, - match { req -> - assertThat(req.tags.firstOrNull { it.contains(TAG_SCHEDULED_AT) }).isNotNull() - assertThat(req.tags).contains(MASTER_SYNC_SCHEDULER_ONE_TIME) - assertThat(req.tags).contains(MASTER_SYNC_SCHEDULERS) - true - } - ) - } - } - - @Test - fun `sync should enqueue periodic sync master worker`() = runTest { - eventSyncManagerImpl.scheduleSync() - - verify(exactly = 1) { - workManager.enqueueUniquePeriodicWork( - MASTER_SYNC_SCHEDULER_PERIODIC_TIME, - ExistingPeriodicWorkPolicy.UPDATE, - match { req -> - assertThat(req.tags.firstOrNull { it.contains(TAG_SCHEDULED_AT) }).isNotNull() - assertThat(req.tags).contains(MASTER_SYNC_SCHEDULER_PERIODIC_TIME) - assertThat(req.tags).contains(MASTER_SYNC_SCHEDULERS) - true - } - ) - } - } - - @Test - fun `cancelScheduledSync should clear periodic master workers`() = runTest { - eventSyncManagerImpl.cancelScheduledSync() - - verify(exactly = 1) { workManager.cancelAllWorkByTag(MASTER_SYNC_SCHEDULERS) } - verify(exactly = 1) { workManager.cancelAllWorkByTag(TAG_SUBJECTS_SYNC_ALL_WORKERS) } - } - - @Test - fun `stop should stop all workers`() = runTest { - eventSyncManagerImpl.stop() - - verify(exactly = 1) { workManager.cancelAllWorkByTag(TAG_SUBJECTS_SYNC_ALL_WORKERS) } - } - @Test fun `countEventsToUpload should call event repo`() = runTest { eventSyncManagerImpl.countEventsToUpload(null).toList() @@ -178,39 +123,8 @@ internal class EventSyncManagerTest { } returns SampleSyncScopes.modulesDownSyncScope coEvery { eventRemoteDataSource.count(any()) } returnsMany listOf( - listOf( - EventCount(EnrolmentRecordEventType.EnrolmentRecordCreation, 3), - EventCount(EnrolmentRecordEventType.EnrolmentRecordDeletion, 5), - ), - listOf( - EventCount(EnrolmentRecordEventType.EnrolmentRecordCreation, 7), - EventCount(EnrolmentRecordEventType.EnrolmentRecordDeletion, 11), - ) - ) - coEvery { configRepository.getDeviceConfiguration() } returns mockk { - every { selectedModules } returns listOf(DEFAULT_MODULE_ID, DEFAULT_MODULE_ID_2) - } - - val result = eventSyncManagerImpl.countEventsToDownload() - - assertThat(result.toCreate).isEqualTo(10) - assertThat(result.toDelete).isEqualTo(16) - } - - @Test - fun `getDownSyncCounts does not count record move`() = runTest { - coEvery { - eventDownSyncScopeRepository.getDownSyncScope(any(), any(), any()) - } returns SampleSyncScopes.modulesDownSyncScope - - coEvery { eventRemoteDataSource.count(any()) } returnsMany listOf( - listOf( - EventCount(EnrolmentRecordEventType.EnrolmentRecordCreation, 3), - ), - listOf( - EventCount(EnrolmentRecordEventType.EnrolmentRecordMove, 7), - EventCount(EnrolmentRecordEventType.EnrolmentRecordDeletion, 5), - ) + EventCount(8, false), + EventCount(18, true), ) coEvery { configRepository.getDeviceConfiguration() } returns mockk { every { selectedModules } returns listOf(DEFAULT_MODULE_ID, DEFAULT_MODULE_ID_2) @@ -218,8 +132,7 @@ internal class EventSyncManagerTest { val result = eventSyncManagerImpl.countEventsToDownload() - assertThat(result.toCreate).isEqualTo(3) - assertThat(result.toDelete).isEqualTo(5) + assertThat(result).isEqualTo(DownSyncCounts(26, isLowerBound = true)) } @Test @@ -247,7 +160,6 @@ internal class EventSyncManagerTest { coVerify(exactly = 1) { eventDownSyncScopeRepository.deleteAll() } coVerify(exactly = 1) { eventSyncCache.clearProgresses() } coVerify(exactly = 1) { eventSyncCache.storeLastSuccessfulSyncTime(null) } - verify(exactly = 1) { workManager.pruneWork() } } @Test 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 5b2c6c67ab..6474e1d20e 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 @@ -22,10 +22,11 @@ private val fingerIdentifiers = listOf( "RIGHT_5TH_FINGER" ) -fun validateCommonParams(json: JSONObject, type: String) { +fun validateCommonParams(json: JSONObject, type: String, version: Int) { assertThat(json.getString("id")).isNotNull() assertThat(json.getString("type")).isEqualTo(type) - assertThat(json.length()).isEqualTo(4) + assertThat(json.getInt("version")).isEqualTo(version) + assertThat(json.length()).isEqualTo(5) } fun validateTimestamp(jsonObject: JSONObject) { @@ -33,20 +34,17 @@ fun validateTimestamp(jsonObject: JSONObject) { assertThat(jsonObject.getBoolean("isUnixMsTrustworthy")).isNotNull() } -fun validateCallbackV1EventApiModel(json: JSONObject, expectedVersion: Int) { - validateCommonParams(json, "Callback") +fun validateCallbackV1EventApiModel(json: JSONObject) { + validateCommonParams(json, "Callback", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(expectedVersion) validateTimestamp(getJSONObject("startTime")) - verifyCallbackPayloadContent(expectedVersion) - assertThat(length()).isEqualTo(3) + verifyCallbackPayloadContent(3) } } fun validateCallbackV2EventApiModel(json: JSONObject) { - validateCommonParams(json, "Callback") + validateCommonParams(json, "Callback", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(3) validateTimestamp(getJSONObject("startTime")) verifyCallbackPayloadContent(3) } @@ -127,9 +125,8 @@ fun verifyCallbackErrorApiModel(json: JSONObject) { } fun validateCalloutEventApiModel(json: JSONObject) { - validateCommonParams(json, "Callout") + validateCommonParams(json, "Callout", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) with(getJSONObject("callout")) { when (ApiCalloutType.valueOf(getString("type"))) { @@ -142,7 +139,7 @@ fun validateCalloutEventApiModel(json: JSONObject) { ) } } - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } @@ -192,20 +189,18 @@ fun verifyCalloutConfirmationApiModel(json: JSONObject) { } fun validateAlertScreenEventApiModel(json: JSONObject) { - validateCommonParams(json, "AlertScreen") + validateCommonParams(json, "AlertScreen", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getString("alertType")).isIn(ApiAlertScreenEventType.values().valuesAsStrings()) - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateAuthenticationEventApiModel(json: JSONObject) { - validateCommonParams(json, "Authentication") + validateCommonParams(json, "Authentication", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) with(getJSONObject("userInfo")) { @@ -216,15 +211,14 @@ fun validateAuthenticationEventApiModel(json: JSONObject) { assertThat(getString("result")).isIn( ApiAuthenticationPayload.ApiResult.values().valuesAsStrings() ) - assertThat(length()).isEqualTo(5) + assertThat(length()).isEqualTo(4) } } fun validateAuthorizationEventApiModel(json: JSONObject) { - validateCommonParams(json, "Authorization") + validateCommonParams(json, "Authorization", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) with(getJSONObject("userInfo")) { assertThat(getString("projectId")).isNotEmpty() @@ -232,44 +226,41 @@ fun validateAuthorizationEventApiModel(json: JSONObject) { assertThat(length()).isEqualTo(2) } assertThat(getString("result")).isAnyOf("AUTHORIZED", "NOT_AUTHORIZED") - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateCandidateReadEventApiModel(json: JSONObject) { - validateCommonParams(json, "CandidateRead") + validateCommonParams(json, "CandidateRead", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("candidateId").isValidGuid()).isTrue() assertThat(getString("localResult")).isAnyOf("FOUND", "NOT_FOUND") if (has("remoteResult")) { assertThat(getString("remoteResult")).isAnyOf("FOUND", "NOT_FOUND") - assertThat(length()).isEqualTo(6) - } else { assertThat(length()).isEqualTo(5) + } else { + assertThat(length()).isEqualTo(4) } } } fun validateCompletionCheckEventApiModel(json: JSONObject) { - validateCommonParams(json, "CompletionCheck") + validateCommonParams(json, "CompletionCheck", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getBoolean("completed")).isNotNull() - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateConnectivitySnapshotEventApiModel(json: JSONObject) { - validateCommonParams(json, "ConnectivitySnapshot") + validateCommonParams(json, "ConnectivitySnapshot", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(3) validateTimestamp(getJSONObject("startTime")) val connections = getJSONArray("connections") for (i in 0 until connections.length()) { @@ -278,64 +269,59 @@ fun validateConnectivitySnapshotEventApiModel(json: JSONObject) { assertThat(connJson.getString("state")).isNotEmpty() assertThat(connJson.length()).isEqualTo(2) } - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateConsentEventApiModel(json: JSONObject) { - validateCommonParams(json, "Consent") + validateCommonParams(json, "Consent", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("consentType")).isAnyOf("INDIVIDUAL", "PARENTAL") assertThat(getString("result")).isAnyOf("ACCEPTED", "DECLINED", "NO_RESPONSE") - assertThat(length()).isEqualTo(5) + assertThat(length()).isEqualTo(4) } } fun validateEnrolmentEventV1ApiModel(json: JSONObject) { - validateCommonParams(json, "Enrolment") + validateCommonParams(json, "Enrolment", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getString("personId").isValidGuid()).isTrue() - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateEnrolmentEventV2ApiModel(json: JSONObject) { - validateCommonParams(json, "Enrolment") + validateCommonParams(json, "Enrolment", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(3) validateTimestamp(getJSONObject("startTime")) assertThat(getString("subjectId")).isNotNull() assertThat(getString("projectId")).isNotNull() assertThat(getString("moduleId")).isNotNull() assertThat(getString("attendantId")).isNotNull() assertThat(getString("personCreationEventId")).isNotNull() - assertThat(length()).isEqualTo(7) + assertThat(length()).isEqualTo(6) } } fun validateIntentParsingEventApiModel(json: JSONObject) { - validateCommonParams(json, "IntentParsing") + validateCommonParams(json, "IntentParsing", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getString("integration")).isIn(listOf("STANDARD", "ODK", "COMMCARE")) - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateFingerprintCaptureEventApiModel(json: JSONObject) { - validateCommonParams(json, "FingerprintCapture") + validateCommonParams(json, "FingerprintCapture", 4) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(4) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("id")).isNotNull() @@ -355,17 +341,16 @@ fun validateFingerprintCaptureEventApiModel(json: JSONObject) { assertThat(getString("format")).isIn(listOf("ISO_19794_2", "NEC_1")) assertThat(length()).isEqualTo(3) } - assertThat(length()).isEqualTo(8) + assertThat(length()).isEqualTo(7) } } fun validateGuidSelectionEventApiModel(json: JSONObject) { - validateCommonParams(json, "GuidSelection") + validateCommonParams(json, "GuidSelection", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getString("selectedId").isValidGuid()).isTrue() - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } @@ -376,10 +361,9 @@ fun validateMatchEntryApiModel(json: JSONObject) { } fun validateOneToManyMatchEventApiModel(json: JSONObject) { - validateCommonParams(json, "OneToManyMatch") + validateCommonParams(json, "OneToManyMatch", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("matcher")).isAnyOf("SIM_AFIS", "RANK_ONE") @@ -392,15 +376,14 @@ fun validateOneToManyMatchEventApiModel(json: JSONObject) { for (i in 0 until matchEntries.length()) { validateMatchEntryApiModel(matchEntries.getJSONObject(i)) } - assertThat(length()).isEqualTo(6) + assertThat(length()).isEqualTo(5) } } fun validateOneToOneMatchEventApiModel(json: JSONObject) { - validateCommonParams(json, "OneToOneMatch") + validateCommonParams(json, "OneToOneMatch", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(3) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("candidateId").isValidGuid()).isTrue() @@ -411,14 +394,13 @@ fun validateOneToOneMatchEventApiModel(json: JSONObject) { with(getJSONObject("result")) { validateMatchEntryApiModel(this) } - assertThat(length()).isEqualTo(7) + assertThat(length()).isEqualTo(6) } } fun validatePersonCreationEvent(json: JSONObject) { - validateCommonParams(json, "PersonCreation") + validateCommonParams(json, "PersonCreation", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getString("fingerprintReferenceId")).isNotNull() val fingerprintCaptureIds = getJSONArray("fingerprintCaptureIds") @@ -431,27 +413,25 @@ fun validatePersonCreationEvent(json: JSONObject) { for (i in 0 until faceCaptureIds.length()) { assertThat(faceCaptureIds.getString(i).isValidGuid()).isTrue() } - assertThat(length()).isEqualTo(6) + assertThat(length()).isEqualTo(5) } } fun validateRefusalEventApiModel(json: JSONObject) { - validateCommonParams(json, "Refusal") + validateCommonParams(json, "Refusal", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("reason")).isIn(ApiRefusalPayload.ApiAnswer.values().valuesAsStrings()) assertThat(getString("otherText")).isNotNull() - assertThat(length()).isEqualTo(5) + assertThat(length()).isEqualTo(4) } } fun validateScannerConnectionEventApiModel(json: JSONObject) { - validateCommonParams(json, "ScannerConnection") + validateCommonParams(json, "ScannerConnection", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) with(getJSONObject("scannerInfo")) { assertThat(getString("scannerId")).isNotEmpty() @@ -460,15 +440,14 @@ fun validateScannerConnectionEventApiModel(json: JSONObject) { assertThat(getString("generation")).isAnyOf("VERO_2", "VERO_1") assertThat(length()).isEqualTo(4) } - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateVero2InfoSnapshotEventApiModel(json: JSONObject) { - validateCommonParams(json, "Vero2InfoSnapshot") + validateCommonParams(json, "Vero2InfoSnapshot", 3) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(3) validateTimestamp(getJSONObject("startTime")) with(getJSONObject("scannerVersion")) { @@ -488,88 +467,63 @@ fun validateVero2InfoSnapshotEventApiModel(json: JSONObject) { assertThat(length()).isEqualTo(4) } - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateScannerFirmwareUpdateEventApiModel(json: JSONObject) { - validateCommonParams(json, "ScannerFirmwareUpdate") + validateCommonParams(json, "ScannerFirmwareUpdate", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("chip")).isNotEmpty() assertThat(getString("targetAppVersion")).isNotEmpty() assertThat(getString("failureReason")).isNotEmpty() - assertThat(length()).isEqualTo(6) + assertThat(length()).isEqualTo(5) } } -fun validateDatabaseInfoApiModel(json: JSONObject) { - assertThat(json.getInt("recordCount")).isNotNull() - assertThat(json.getInt("sessionCount")).isNotNull() - assertThat(json.length()).isEqualTo(2) -} - -fun validateDeviceApiModel(json: JSONObject) { - assertThat(json.getString("androidSdkVersion")).isNotEmpty() - assertThat(json.getString("deviceModel")).isNotEmpty() - assertThat(json.getString("deviceId")).isNotEmpty() - assertThat(json.length()).isEqualTo(3) -} - -fun validateLocationApiModel(json: JSONObject) { - assertThat(json.getDouble("latitude")).isNotNull() - assertThat(json.getDouble("longitude")).isNotNull() - assertThat(json.length()).isEqualTo(2) -} - fun validateSuspiciousIntentEventApiModel(json: JSONObject) { - validateCommonParams(json, "SuspiciousIntent") + validateCommonParams(json, "SuspiciousIntent", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getJSONObject("unexpectedExtras").toString()).isNotNull() - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateInvalidEventApiModel(json: JSONObject) { - validateCommonParams(json, "InvalidIntent") + validateCommonParams(json, "InvalidIntent", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) assertThat(getJSONObject("extras").toString()).isNotNull() assertThat(getString("action")).isNotNull() - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateFaceOnboardingCompleteEventApiModel(json: JSONObject) { - validateCommonParams(json, "FaceOnboardingComplete") + validateCommonParams(json, "FaceOnboardingComplete", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateFaceFallbackCaptureEventApiModel(json: JSONObject) { - validateCommonParams(json, "FaceFallbackCapture") + validateCommonParams(json, "FaceFallbackCapture", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) - assertThat(length()).isEqualTo(3) + assertThat(length()).isEqualTo(2) } } fun validateFaceCaptureEventApiModel(json: JSONObject) { - validateCommonParams(json, "FaceCapture") + validateCommonParams(json, "FaceCapture", 4) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(4) assertThat(getString("id")).isNotNull() validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) @@ -595,43 +549,39 @@ fun validateFaceCaptureEventApiModel(json: JSONObject) { assertThat(length()).isEqualTo(4) } - assertThat(length()).isEqualTo(9) + assertThat(length()).isEqualTo(8) } } fun validateFaceCaptureConfirmationEventApiModel(json: JSONObject) { - validateCommonParams(json, "FaceCaptureConfirmation") + validateCommonParams(json, "FaceCaptureConfirmation", 2) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(2) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("result")).isIn(listOf("CONTINUE", "RECAPTURE")) - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateFaceCaptureBiometricsEventApiModel(json: JSONObject) { - validateCommonParams(json, "FaceCaptureBiometrics") + validateCommonParams(json, "FaceCaptureBiometrics", 1) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(1) validateTimestamp(getJSONObject("startTime")) - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateFingerprintCaptureBiometricsEventApiModel(json: JSONObject) { - validateCommonParams(json, "FingerprintCaptureBiometrics") + validateCommonParams(json, "FingerprintCaptureBiometrics", 1) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(1) validateTimestamp(getJSONObject("startTime")) - assertThat(length()).isEqualTo(4) + assertThat(length()).isEqualTo(3) } } fun validateDownSyncRequestEventApiModel(json: JSONObject) { - validateCommonParams(json, "EventDownSyncRequest") + validateCommonParams(json, "EventDownSyncRequest", 0) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(0) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("requestId")).isNotNull() @@ -644,9 +594,8 @@ fun validateDownSyncRequestEventApiModel(json: JSONObject) { } fun validateUpSyncRequestEventApiModel(json: JSONObject) { - validateCommonParams(json, "EventUpSyncRequest") + validateCommonParams(json, "EventUpSyncRequest", 0) with(json.getJSONObject("payload")) { - assertThat(getInt("version")).isEqualTo(0) validateTimestamp(getJSONObject("startTime")) validateTimestamp(getJSONObject("endTime")) assertThat(getString("requestId")).isNotNull() 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 d7a8c0cff8..76596a3b17 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,17 +7,118 @@ 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.* -import com.simprints.infra.events.sampledata.* -import com.simprints.infra.eventsync.event.* +import com.simprints.infra.events.sampledata.createAlertScreenEvent +import com.simprints.infra.events.sampledata.createAuthenticationEvent +import com.simprints.infra.events.sampledata.createAuthorizationEvent +import com.simprints.infra.events.sampledata.createCandidateReadEvent +import com.simprints.infra.events.sampledata.createCompletionCheckEvent +import com.simprints.infra.events.sampledata.createConfirmationCallbackEvent +import com.simprints.infra.events.sampledata.createConfirmationCalloutEvent +import com.simprints.infra.events.sampledata.createConnectivitySnapshotEvent +import com.simprints.infra.events.sampledata.createConsentEvent +import com.simprints.infra.events.sampledata.createEnrolmentCallbackEvent +import com.simprints.infra.events.sampledata.createEnrolmentCalloutEvent +import com.simprints.infra.events.sampledata.createEnrolmentEventV1 +import com.simprints.infra.events.sampledata.createEnrolmentEventV2 +import com.simprints.infra.events.sampledata.createEventDownSyncRequestEvent +import com.simprints.infra.events.sampledata.createEventUpSyncRequestEvent +import com.simprints.infra.events.sampledata.createFaceCaptureBiometricsEvent +import com.simprints.infra.events.sampledata.createFaceCaptureConfirmationEvent +import com.simprints.infra.events.sampledata.createFaceCaptureEvent +import com.simprints.infra.events.sampledata.createFaceFallbackCaptureEvent +import com.simprints.infra.events.sampledata.createFaceOnboardingCompleteEvent +import com.simprints.infra.events.sampledata.createFingerprintCaptureBiometricsEvent +import com.simprints.infra.events.sampledata.createFingerprintCaptureEvent +import com.simprints.infra.events.sampledata.createGuidSelectionEvent +import com.simprints.infra.events.sampledata.createIdentificationCallbackEvent +import com.simprints.infra.events.sampledata.createIdentificationCalloutEvent +import com.simprints.infra.events.sampledata.createIntentParsingEvent +import com.simprints.infra.events.sampledata.createInvalidIntentEvent +import com.simprints.infra.events.sampledata.createLastBiometricsEnrolmentCalloutEvent +import com.simprints.infra.events.sampledata.createLicenseCheckEvent +import com.simprints.infra.events.sampledata.createOneToManyMatchEvent +import com.simprints.infra.events.sampledata.createOneToOneMatchEvent +import com.simprints.infra.events.sampledata.createPersonCreationEvent +import com.simprints.infra.events.sampledata.createRefusalCallbackEvent +import com.simprints.infra.events.sampledata.createRefusalEvent +import com.simprints.infra.events.sampledata.createScannerConnectionEvent +import com.simprints.infra.events.sampledata.createScannerFirmwareUpdateEvent +import com.simprints.infra.events.sampledata.createSuspiciousIntentEvent +import com.simprints.infra.events.sampledata.createVerificationCallbackEventV1 +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.* +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 +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Callback +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Callout +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.CandidateRead +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.CompletionCheck +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ConnectivitySnapshot +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Consent +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Enrolment +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.EventDownSyncRequest +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.EventUpSyncRequest +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCapture +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCaptureBiometrics +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceCaptureConfirmation +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceFallbackCapture +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FaceOnboardingComplete +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FingerprintCapture +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.FingerprintCaptureBiometrics +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.GuidSelection +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.IntentParsing +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.InvalidIntent +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.LicenseCheck +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.OneToManyMatch +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.OneToOneMatch +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.PersonCreation +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.Refusal +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ScannerConnection +import com.simprints.infra.eventsync.event.remote.models.ApiEventPayloadType.ScannerFirmwareUpdate +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.validateAlertScreenEventApiModel +import com.simprints.infra.eventsync.event.validateAuthenticationEventApiModel +import com.simprints.infra.eventsync.event.validateAuthorizationEventApiModel +import com.simprints.infra.eventsync.event.validateCallbackV1EventApiModel +import com.simprints.infra.eventsync.event.validateCallbackV2EventApiModel +import com.simprints.infra.eventsync.event.validateCalloutEventApiModel +import com.simprints.infra.eventsync.event.validateCandidateReadEventApiModel +import com.simprints.infra.eventsync.event.validateCommonParams +import com.simprints.infra.eventsync.event.validateCompletionCheckEventApiModel +import com.simprints.infra.eventsync.event.validateConnectivitySnapshotEventApiModel +import com.simprints.infra.eventsync.event.validateConsentEventApiModel +import com.simprints.infra.eventsync.event.validateDownSyncRequestEventApiModel +import com.simprints.infra.eventsync.event.validateEnrolmentEventV1ApiModel +import com.simprints.infra.eventsync.event.validateEnrolmentEventV2ApiModel +import com.simprints.infra.eventsync.event.validateFaceCaptureBiometricsEventApiModel +import com.simprints.infra.eventsync.event.validateFaceCaptureConfirmationEventApiModel +import com.simprints.infra.eventsync.event.validateFaceCaptureEventApiModel +import com.simprints.infra.eventsync.event.validateFaceFallbackCaptureEventApiModel +import com.simprints.infra.eventsync.event.validateFaceOnboardingCompleteEventApiModel +import com.simprints.infra.eventsync.event.validateFingerprintCaptureBiometricsEventApiModel +import com.simprints.infra.eventsync.event.validateFingerprintCaptureEventApiModel +import com.simprints.infra.eventsync.event.validateGuidSelectionEventApiModel +import com.simprints.infra.eventsync.event.validateIntentParsingEventApiModel +import com.simprints.infra.eventsync.event.validateInvalidEventApiModel +import com.simprints.infra.eventsync.event.validateOneToManyMatchEventApiModel +import com.simprints.infra.eventsync.event.validateOneToOneMatchEventApiModel +import com.simprints.infra.eventsync.event.validatePersonCreationEvent +import com.simprints.infra.eventsync.event.validateRefusalEventApiModel +import com.simprints.infra.eventsync.event.validateScannerConnectionEventApiModel +import com.simprints.infra.eventsync.event.validateScannerFirmwareUpdateEventApiModel +import com.simprints.infra.eventsync.event.validateSuspiciousIntentEventApiModel +import com.simprints.infra.eventsync.event.validateUpSyncRequestEventApiModel +import com.simprints.infra.eventsync.event.validateVero2InfoSnapshotEventApiModel import org.json.JSONObject import org.junit.Test import org.junit.runner.RunWith -@Suppress("IMPLICIT_CAST_TO_ANY") +@Suppress("IMPLICIT_CAST_TO_ANY", "KotlinConstantConditions") @RunWith(AndroidJUnit4::class) class ApiEventTest { @@ -110,7 +211,7 @@ class ApiEventTest { val apiEvent = event.fromDomainToApi() val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateCallbackV1EventApiModel(json, 2) + validateCallbackV1EventApiModel(json) } @Test @@ -119,7 +220,7 @@ class ApiEventTest { val apiEvent = event.fromDomainToApi() val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateCallbackV1EventApiModel(json, 2) + validateCallbackV1EventApiModel(json) } @Test @@ -137,7 +238,7 @@ class ApiEventTest { val apiEvent = event.fromDomainToApi() val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateCallbackV1EventApiModel(json, 3) + validateCallbackV1EventApiModel(json) } @Test @@ -155,7 +256,7 @@ class ApiEventTest { val apiEvent = event.fromDomainToApi() val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateCallbackV1EventApiModel(json, 2) + validateCallbackV1EventApiModel(json) } @Test @@ -164,7 +265,7 @@ class ApiEventTest { val apiEvent = event.fromDomainToApi() val json = JSONObject(jackson.writeValueAsString(apiEvent)) - validateCallbackV1EventApiModel(json, 2) + validateCallbackV1EventApiModel(json) } @Test @@ -411,7 +512,19 @@ class ApiEventTest { fun `when event contains raw module id, then tokenizedField in ApiEvent should be empty`() { validateModuleIdTokenization(moduleId = "moduleId".asTokenizableRaw()) } + @Test + fun validate_licenseCheckEventApiModel() { + val event = createLicenseCheckEvent() + val apiEvent = event.fromDomainToApi() + val json = JSONObject(jackson.writeValueAsString(apiEvent)) + validateLicenseCheckEventApiModel(json) + } + + private fun validateLicenseCheckEventApiModel(json: JSONObject) { + validateCommonParams(json,"LicenseCheck",1) + assertThat(json.getJSONObject("payload").getString("status")).isEqualTo("VALID") + } private fun validateModuleIdTokenization(moduleId: TokenizableString) { val event = createEnrolmentEventV2().let { it.copy(payload = it.payload.copy(moduleId = moduleId)) @@ -475,6 +588,7 @@ class ApiEventTest { FaceCaptureBiometrics -> validate_FaceCaptureBiometricsEventApiModel() EventDownSyncRequest -> validate_DownSyncRequestEventApiModel() EventUpSyncRequest -> validate_UpSyncRequestEventApiModel() + LicenseCheck -> validate_licenseCheckEventApiModel() null -> TODO() }.safeSealedWhens } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt index 48f0354949..9e64a8a321 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/EventRemoteDataSourceTest.kt @@ -5,7 +5,6 @@ import com.simprints.core.tools.json.JsonHelper import com.simprints.infra.authstore.AuthStore import com.simprints.infra.events.event.domain.EventCount import com.simprints.infra.events.event.domain.models.Event -import com.simprints.infra.events.event.domain.models.scope.EventScopeType import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEvent import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEventType import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID @@ -16,22 +15,26 @@ import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createSessionScope import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException -import com.simprints.infra.eventsync.event.remote.models.ApiEventCount import com.simprints.infra.eventsync.event.remote.models.session.ApiEventScope -import com.simprints.infra.eventsync.event.remote.models.subject.ApiEnrolmentRecordPayloadType import com.simprints.infra.network.SimNetwork import com.simprints.infra.network.exceptions.BackendMaintenanceException import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import com.simprints.testtools.common.alias.InterfaceInvocation import com.simprints.testtools.common.syntax.assertThrows -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.excludeRecords import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.produce import kotlinx.coroutines.test.runTest -import okhttp3.Headers -import okhttp3.Headers.Companion.headersOf import okhttp3.Headers.Companion.toHeaders import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.After @@ -84,31 +87,15 @@ class EventRemoteDataSourceTest { @Test fun count_shouldMakeANetworkRequest() = runTest { coEvery { - eventRemoteInterface.countEvents( - any(), - any(), - any(), - any(), - any(), - any() - ) - } returns listOf( - ApiEventCount( - ApiEnrolmentRecordPayloadType.EnrolmentRecordCreation, - 1 - ) + eventRemoteInterface.countEvents(any(), any(), any(), any(), any(), any()) + } returns Response.success( + null, + mapOf("x-event-count" to "6", "x-event-count-is-lower-bound" to "true").toHeaders() ) val count = eventRemoteDataSource.count(query) - assertThat(count).isEqualTo( - listOf( - EventCount( - EnrolmentRecordEventType.EnrolmentRecordCreation, - 1 - ) - ) - ) + assertThat(count).isEqualTo(EventCount(6, true)) coVerify(exactly = 1) { eventRemoteInterface.countEvents( projectId = DEFAULT_PROJECT_ID, @@ -189,12 +176,13 @@ class EventRemoteDataSourceTest { any(), any(), any(), + any(), any() ) } throws exception val exceptionThrown = assertThrows { - eventRemoteDataSource.getEvents(query, this) + eventRemoteDataSource.getEvents(GUID1, query, this) } assertThat(exceptionThrown).isEqualTo(exception) } @@ -217,12 +205,13 @@ class EventRemoteDataSourceTest { any(), any(), any(), + any(), any() ) } throws exception assertThrows { - eventRemoteDataSource.getEvents(query, this) + eventRemoteDataSource.getEvents(GUID1, query, this) } } @@ -236,18 +225,19 @@ class EventRemoteDataSourceTest { any(), any(), any(), + any(), any() ) } returns Response.success("".toResponseBody()) val mockedScope: CoroutineScope = mockk() every { mockedScope.produce(capacity = 2000, block = any()) } returns mockk() - eventRemoteDataSource.getEvents(query, mockedScope) + eventRemoteDataSource.getEvents(GUID1, query, mockedScope) with(query) { coVerify { eventRemoteInterface.downloadEvents( - projectId, moduleId, userId, subjectId, modes, lastEventId + GUID1, projectId, moduleId, userId, subjectId, modes, lastEventId ) } } @@ -256,7 +246,7 @@ class EventRemoteDataSourceTest { @Test fun getEvents_shouldReturnCorrectTotalHeader() = runTest { coEvery { - eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any()) + eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any(), any()) } returns Response.success( "".toResponseBody(), mapOf("x-event-count" to "22").toHeaders() @@ -265,45 +255,27 @@ class EventRemoteDataSourceTest { val mockedScope: CoroutineScope = mockk() every { mockedScope.produce(capacity = 2000, block = any()) } returns mockk() - assertThat(eventRemoteDataSource.getEvents(query, mockedScope).totalCount).isEqualTo(22) - } - - @Test - fun getEvents_shouldReturnCorrectRequestId() = runTest { - coEvery { - eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any()) - } returns Response.success( - "".toResponseBody(), - mapOf("x-request-id" to "requestId").toHeaders() + assertThat(eventRemoteDataSource.getEvents(GUID1, query, mockedScope).totalCount).isEqualTo( + 22 ) - - val mockedScope: CoroutineScope = mockk() - - every { mockedScope.produce(capacity = 2000, block = any()) } returns mockk() - assertThat( - eventRemoteDataSource.getEvents( - query, - mockedScope - ).requestId - ).isEqualTo("requestId") } @Test fun getEvents_shouldReturnCorrectStatus() = runTest { coEvery { - eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any()) + eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any(), any()) } returns Response.success(205, "".toResponseBody()) val mockedScope: CoroutineScope = mockk() every { mockedScope.produce(capacity = 2000, block = any()) } returns mockk() - assertThat(eventRemoteDataSource.getEvents(query, mockedScope).status).isEqualTo(205) + assertThat(eventRemoteDataSource.getEvents(GUID1, query, mockedScope).status).isEqualTo(205) } @Test fun getEvents_shouldNotReturnTotalHeaderWhenLowerBound() = runTest { coEvery { - eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any()) + eventRemoteInterface.downloadEvents(any(), any(), any(), any(), any(), any(), any()) } returns Response.success( "".toResponseBody(), mapOf("x-event-count" to "22", "x-event-count-is-lower-bound" to "true").toHeaders() @@ -312,12 +284,19 @@ class EventRemoteDataSourceTest { val mockedScope: CoroutineScope = mockk() every { mockedScope.produce(capacity = 2000, block = any()) } returns mockk() - assertThat(eventRemoteDataSource.getEvents(query, mockedScope).totalCount).isNull() + assertThat(eventRemoteDataSource.getEvents(GUID1, query, mockedScope).totalCount).isNull() } @Test fun postEvent_shouldUploadEvents() = runTest { - coEvery { eventRemoteInterface.uploadEvents(any(), any(), any()) } returns Response.success( + coEvery { + eventRemoteInterface.uploadEvents( + any(), + any(), + any(), + any() + ) + } returns Response.success( "".toResponseBody(), mapOf("x-request-id" to "requestId").toHeaders() ) @@ -325,6 +304,7 @@ class EventRemoteDataSourceTest { val events = listOf(createAlertScreenEvent()) val scope = createSessionScope() eventRemoteDataSource.post( + GUID1, DEFAULT_PROJECT_ID, ApiUploadEventsBody( sessions = listOf(ApiEventScope.fromDomain(scope, events)) @@ -333,6 +313,7 @@ class EventRemoteDataSourceTest { coVerify(exactly = 1) { eventRemoteInterface.uploadEvents( + GUID1, DEFAULT_PROJECT_ID, true, match { body -> @@ -350,6 +331,7 @@ class EventRemoteDataSourceTest { runTest { coEvery { eventRemoteInterface.uploadEvents( + any(), any(), any(), any() @@ -358,6 +340,7 @@ class EventRemoteDataSourceTest { assertThrows { eventRemoteDataSource.post( + GUID1, DEFAULT_PROJECT_ID, ApiUploadEventsBody() ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/ApiAlertScreenEventTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/ApiAlertScreenEventTest.kt index 4640b93286..fdb3644677 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/ApiAlertScreenEventTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/ApiAlertScreenEventTest.kt @@ -2,9 +2,44 @@ package com.simprints.infra.eventsync.event.remote.events import com.google.common.truth.Truth.assertThat import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType -import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BACKEND_MAINTENANCE_ERROR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NOT_ENABLED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NOT_SUPPORTED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.BLUETOOTH_NO_PERMISSION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DIFFERENT_PROJECT_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DIFFERENT_USER_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.DISCONNECTED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GOOGLE_PLAY_SERVICES_OUTDATED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GUID_NOT_FOUND_OFFLINE +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.GUID_NOT_FOUND_ONLINE +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INTEGRITY_SERVICE_ERROR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_INTENT_ACTION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_METADATA +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_MODULE_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_PROJECT_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_SELECTED_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_SESSION_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_STATE_FOR_INTENT_ACTION +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_USER_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.INVALID_VERIFY_ID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LICENSE_INVALID +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LICENSE_MISSING +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.LOW_BATTERY +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MISSING_GOOGLE_PLAY_SERVICES +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MISSING_OR_OUTDATED_GOOGLE_PLAY_STORE_APP +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.MULTIPLE_PAIRED_SCANNERS +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NFC_NOT_ENABLED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NFC_PAIR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.NOT_PAIRED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA_FAILED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.OTA_RECOVERY +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.PROJECT_ENDING +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.PROJECT_PAUSED +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.SERIAL_ENTRY_PAIR +import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.UNEXPECTED_ERROR import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi -import com.simprints.infra.eventsync.event.remote.models.ApiAlertScreenPayload.ApiAlertScreenEventType.* import org.junit.Test class ApiAlertScreenEventTest { @@ -41,8 +76,8 @@ class ApiAlertScreenEventTest { AlertScreenEventType.INTEGRITY_SERVICE_ERROR to INTEGRITY_SERVICE_ERROR, AlertScreenEventType.ENROLMENT_LAST_BIOMETRICS_FAILED to ENROLMENT_LAST_BIOMETRICS_FAILED, AlertScreenEventType.INVALID_STATE_FOR_INTENT_ACTION to INVALID_STATE_FOR_INTENT_ACTION, - AlertScreenEventType.FACE_LICENSE_INVALID to FACE_LICENSE_INVALID, - AlertScreenEventType.FACE_LICENSE_MISSING to FACE_LICENSE_MISSING, + AlertScreenEventType.LICENSE_INVALID to LICENSE_INVALID, + AlertScreenEventType.LICENSE_MISSING to LICENSE_MISSING, AlertScreenEventType.BACKEND_MAINTENANCE_ERROR to BACKEND_MAINTENANCE_ERROR, AlertScreenEventType.GOOGLE_PLAY_SERVICES_OUTDATED to GOOGLE_PLAY_SERVICES_OUTDATED, AlertScreenEventType.MISSING_GOOGLE_PLAY_SERVICES to MISSING_GOOGLE_PLAY_SERVICES, diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt index b171ec79e7..87ab3ced26 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/events/callback/ApiErrorCallbackTest.kt @@ -1,12 +1,12 @@ package com.simprints.infra.eventsync.event.remote.events.callback import com.google.common.truth.Truth.assertThat -import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason as ErrorReason import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback -import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason.* +import com.simprints.infra.eventsync.event.remote.models.callback.ApiErrorCallback.ApiReason.SCANNER_LOW_BATTERY import com.simprints.infra.eventsync.event.remote.models.callback.fromApiToDomain import com.simprints.infra.eventsync.event.remote.models.callback.fromDomainToApi import org.junit.Test +import com.simprints.infra.events.event.domain.models.callback.ErrorCallbackEvent.ErrorCallbackPayload.Reason as ErrorReason class ApiErrorCallbackTest { @@ -23,8 +23,8 @@ class ApiErrorCallbackTest { SCANNER_LOW_BATTERY to ErrorReason.UNEXPECTED_ERROR, ApiErrorCallback.ApiReason.LOGIN_NOT_COMPLETE to ErrorReason.LOGIN_NOT_COMPLETE, ApiErrorCallback.ApiReason.ENROLMENT_LAST_BIOMETRICS_FAILED to ErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED, - ApiErrorCallback.ApiReason.FACE_LICENSE_MISSING to ErrorReason.FACE_LICENSE_MISSING, - ApiErrorCallback.ApiReason.FACE_LICENSE_INVALID to ErrorReason.FACE_LICENSE_INVALID, + ApiErrorCallback.ApiReason.LICENSE_MISSING to ErrorReason.LICENSE_MISSING, + ApiErrorCallback.ApiReason.LICENSE_INVALID to ErrorReason.LICENSE_INVALID, ApiErrorCallback.ApiReason.BACKEND_MAINTENANCE_ERROR to ErrorReason.BACKEND_MAINTENANCE_ERROR, ApiErrorCallback.ApiReason.PROJECT_ENDING to ErrorReason.PROJECT_ENDING, ApiErrorCallback.ApiReason.PROJECT_PAUSED to ErrorReason.PROJECT_PAUSED, @@ -45,8 +45,8 @@ class ApiErrorCallbackTest { ErrorReason.BLUETOOTH_NOT_SUPPORTED to ApiErrorCallback.ApiReason.BLUETOOTH_NOT_SUPPORTED, ErrorReason.LOGIN_NOT_COMPLETE to ApiErrorCallback.ApiReason.LOGIN_NOT_COMPLETE, ErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED to ApiErrorCallback.ApiReason.ENROLMENT_LAST_BIOMETRICS_FAILED, - ErrorReason.FACE_LICENSE_MISSING to ApiErrorCallback.ApiReason.FACE_LICENSE_MISSING, - ErrorReason.FACE_LICENSE_INVALID to ApiErrorCallback.ApiReason.FACE_LICENSE_INVALID, + ErrorReason.LICENSE_MISSING to ApiErrorCallback.ApiReason.LICENSE_MISSING, + ErrorReason.LICENSE_INVALID to ApiErrorCallback.ApiReason.LICENSE_INVALID, ErrorReason.FINGERPRINT_CONFIGURATION_ERROR to ApiErrorCallback.ApiReason.UNEXPECTED_ERROR, ErrorReason.FACE_CONFIGURATION_ERROR to ApiErrorCallback.ApiReason.UNEXPECTED_ERROR, ErrorReason.BACKEND_MAINTENANCE_ERROR to ApiErrorCallback.ApiReason.BACKEND_MAINTENANCE_ERROR, diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayloadTest.kt index 3b12e34043..02772b6275 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayloadTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiFingerprintCapturePayloadTest.kt @@ -22,7 +22,6 @@ class ApiFingerprintCapturePayloadTest { val payload = ApiFingerprintCapturePayload( id = randomUUID(), startTime = ApiTimestamp(1), - version = 3, endTime = ApiTimestamp(1), qualityThreshold = 23, finger = IFingerIdentifier.LEFT_3RD_FINGER, @@ -33,7 +32,6 @@ class ApiFingerprintCapturePayloadTest { with(payload) { assertThat(id).isNotNull() assertThat(startTime).isEqualTo(ApiTimestamp(1),) - assertThat(version).isEqualTo(3) assertThat(endTime).isEqualTo(ApiTimestamp(1),) assertThat(qualityThreshold).isEqualTo(23) assertThat(finger).isEqualTo(IFingerIdentifier.LEFT_3RD_FINGER) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayloadTest.kt index 167e351783..18b7228bbe 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayloadTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/ApiOneToManyMatchPayloadTest.kt @@ -1,7 +1,6 @@ package com.simprints.infra.eventsync.event.remote.models import com.google.common.truth.Truth.assertThat -import com.simprints.core.tools.time.Timestamp import com.simprints.infra.config.store.models.TokenKeyType import io.mockk.mockk import org.junit.Test @@ -12,7 +11,6 @@ class ApiOneToManyMatchPayloadTest { fun `when getTokenizedFieldJsonPath is invoked, null is returned`() { val payload = ApiOneToManyMatchPayload( startTime = ApiTimestamp(0L, false, 0L), - version = 0, endTime = ApiTimestamp(0L, false, 0L), pool = mockk(), matcher = "", diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayloadTest.kt new file mode 100644 index 0000000000..c6c9e70ab1 --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayloadTest.kt @@ -0,0 +1,35 @@ +package com.simprints.infra.eventsync.event.remote.models.downsync + +import com.simprints.infra.config.store.models.TokenKeyType +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApiEventDownSyncRequestPayloadTest { + + @Test + fun testGetTokenizedFieldJsonPath() { + // Arrange + val payload = ApiEventDownSyncRequestPayload( + startTime = mockk(), + endTime = mockk(), + requestId = "requestId", + queryParameters = ApiEventDownSyncRequestPayload.ApiQueryParameters( + moduleId = "moduleId", + attendantId = "attendantId", + subjectId = null, + modes = null, + lastEventId = null, + ), + responseStatus = 200, + errorType = null, + msToFirstResponseByte = 1000L, + eventsRead = 10 + ) + + // Act & Assert + assertEquals("queryParameters.attendantId", payload.getTokenizedFieldJsonPath(TokenKeyType.AttendantId)) + assertEquals("queryParameters.moduleId", payload.getTokenizedFieldJsonPath(TokenKeyType.ModuleId)) + assertEquals(null, payload.getTokenizedFieldJsonPath(TokenKeyType.Unknown)) + } +} \ No newline at end of file diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayloadTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayloadTest.kt index 730e6528a7..4c077bd250 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayloadTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/face/ApiFaceCapturePayloadTest.kt @@ -21,7 +21,6 @@ class ApiFaceCapturePayloadTest { id = randomUUID(), startTime = ApiTimestamp(1), endTime = ApiTimestamp(2), - version = 3, attemptNb = 2, qualityThreshold = 1.2f, result = ApiFaceCapturePayload.ApiResult.VALID, @@ -32,7 +31,6 @@ class ApiFaceCapturePayloadTest { assertThat(id).isNotNull() assertThat(startTime).isEqualTo(ApiTimestamp(1),) assertThat(endTime).isEqualTo(ApiTimestamp(2),) - assertThat(version).isEqualTo(3) assertThat(attemptNb).isEqualTo(2) assertThat(qualityThreshold).isEqualTo(1.2f) assertThat(result).isEqualTo(ApiFaceCapturePayload.ApiResult.VALID) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScopeTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScopeTest.kt new file mode 100644 index 0000000000..582805226c --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScopeTest.kt @@ -0,0 +1,63 @@ +package com.simprints.infra.eventsync.event.remote.models.session + +import com.google.common.truth.Truth +import com.simprints.core.tools.json.JsonHelper +import com.simprints.infra.eventsync.event.remote.models.ApiTimestamp +import org.junit.Test + +class ApiEventScopeTest { + + @Test + fun `projectConfigurationUpdatedAt is not included in JSON when empty`() { + // Arrange + val apiEventScope = ApiEventScope( + id = "testId", + projectId = "testProjectId", + startTime = ApiTimestamp(0), + endTime = null, + endCause = ApiEventScopeEndCause.WORKFLOW_ENDED, + modalities = emptyList(), + sidVersion = "testSidVersion", + libSimprintsVersion = "testLibSimprintsVersion", + language = "testLanguage", + device = ApiDevice("testDeviceId", "testDeviceModel"), + databaseInfo = ApiDatabaseInfo(0, 0), + location = null, + projectConfigurationUpdatedAt = "", + events = emptyList() + ) + + // Act + val json = JsonHelper.toJson(apiEventScope) + + // Assert + Truth.assertThat(json).doesNotContain("projectConfigurationUpdatedAt") + } + + @Test + fun `projectConfigurationUpdatedAt is included in JSON when not empty`() { + // Arrange + val apiEventScope = ApiEventScope( + id = "testId", + projectId = "testProjectId", + startTime = ApiTimestamp(0), + endTime = null, + endCause = ApiEventScopeEndCause.WORKFLOW_ENDED, + modalities = emptyList(), + sidVersion = "testSidVersion", + libSimprintsVersion = "testLibSimprintsVersion", + language = "testLanguage", + device = ApiDevice("testDeviceId", "testDeviceModel"), + databaseInfo = ApiDatabaseInfo(0, 0), + location = null, + projectConfigurationUpdatedAt = "123", + events = emptyList() + ) + + // Act + val json = JsonHelper.toJson(apiEventScope) + + // Assert + Truth.assertThat(json).contains("projectConfigurationUpdatedAt") + } +} \ No newline at end of file diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepositoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepositoryTest.kt index ca88e6987d..99ef048def 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepositoryTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/down/EventDownSyncScopeRepositoryTest.kt @@ -3,8 +3,11 @@ package com.simprints.infra.eventsync.status.down import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.Partitioning import com.simprints.core.domain.modality.Modes +import com.simprints.core.domain.tokenization.TokenizableString import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.config.store.ConfigRepository +import com.simprints.infra.config.store.tokenization.TokenizationProcessor import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODES import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULES import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_MODULE_ID @@ -58,6 +61,12 @@ internal class EventDownSyncScopeRepositoryTest { @MockK lateinit var downSyncOperationOperationDao: DbEventDownSyncOperationStateDao + @MockK + lateinit var configRepository: ConfigRepository + + @MockK + lateinit var tokenizationProcessor: TokenizationProcessor + private lateinit var eventDownSyncScopeRepository: EventDownSyncScopeRepository @get:Rule @@ -71,6 +80,8 @@ internal class EventDownSyncScopeRepositoryTest { authStore, recentUserActivityManager, downSyncOperationOperationDao, + configRepository, + tokenizationProcessor, ) every { authStore.signedInProjectId } returns DEFAULT_PROJECT_ID @@ -94,6 +105,7 @@ internal class EventDownSyncScopeRepositoryTest { @Test fun buildUserDownSyncScope() = runTest(UnconfinedTestDispatcher()) { every { authStore.signedInUserId } returns DEFAULT_USER_ID + every { tokenizationProcessor.encrypt(any(), any(), any()) } returns TokenizableString.Tokenized(DEFAULT_USER_ID.value) val syncScope = eventDownSyncScopeRepository.getDownSyncScope( listOf(Modes.FINGERPRINT), @@ -107,6 +119,20 @@ internal class EventDownSyncScopeRepositoryTest { @Test fun buildUserDownSyncScopeWhenNoSaved() = runTest(UnconfinedTestDispatcher()) { every { authStore.signedInUserId } returns null + every { tokenizationProcessor.encrypt(any(), any(), any()) } returns TokenizableString.Tokenized(DEFAULT_USER_ID.value) + + val syncScope = eventDownSyncScopeRepository.getDownSyncScope( + listOf(Modes.FINGERPRINT), + DEFAULT_MODULES.toList(), + Partitioning.USER + ) + + assertUserSyncScope(syncScope) + } + + @Test + fun buildUserDownSyncScopeWhenUserTokenised() = runTest(UnconfinedTestDispatcher()) { + every { authStore.signedInUserId } returns TokenizableString.Tokenized(DEFAULT_USER_ID.value) val syncScope = eventDownSyncScopeRepository.getDownSyncScope( listOf(Modes.FINGERPRINT), diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/models/EventSyncStateTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/models/EventSyncStateTest.kt index 0b867b8821..55bfd06f9e 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/models/EventSyncStateTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/status/models/EventSyncStateTest.kt @@ -2,13 +2,33 @@ package com.simprints.infra.eventsync.status.models import com.google.common.truth.Truth.assertThat import com.simprints.infra.eventsync.status.models.EventSyncState.SyncWorkerInfo -import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.* +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Blocked +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Cancelled +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Enqueued +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Failed +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Running +import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Succeeded import org.junit.Test class EventSyncStateTest { @Test - fun `is not running when there are no workers`() { + fun `isThereNotSyncHistory() is true when there are no workers`() { + assertThat(createState( + up = emptyList(), + down = emptyList(), + ).isThereNotSyncHistory()).isTrue() + } + @Test + fun `isThereNotSyncHistory() is false when there are workers`() { + assertThat(createState( + up = listOf(createWorker(Succeeded)), + down = emptyList(), + ).isThereNotSyncHistory()).isFalse() + } + + @Test + fun `isSyncRunning() is false when there are no workers`() { assertThat(createState( up = emptyList(), down = emptyList(), @@ -16,7 +36,7 @@ class EventSyncStateTest { } @Test - fun `is not running when when all workers completed`() { + fun `isSyncRunning() is false when when all workers completed`() { assertThat(createState( up = listOf(createWorker(Succeeded)), down = listOf(createWorker(Succeeded)), @@ -24,7 +44,7 @@ class EventSyncStateTest { } @Test - fun `is running when there are running workers`() { + fun `isSyncRunning() is true when there are running workers`() { assertThat(createState( up = listOf(createWorker(Running)), down = listOf(createWorker(Succeeded)), @@ -40,7 +60,7 @@ class EventSyncStateTest { } @Test - fun `is running when there are enqueued workers`() { + fun `isSyncRunning() is true when there are enqueued workers`() { assertThat(createState( up = listOf(createWorker(Enqueued)), down = listOf(createWorker(Succeeded)), @@ -55,6 +75,179 @@ class EventSyncStateTest { ).isSyncRunning()).isTrue() } + @Test + fun `isSyncCompleted() is true when all workers are completed`() { + assertThat(createState( + up = listOf(createWorker(Succeeded)), + down = listOf(createWorker(Succeeded)), + ).isSyncCompleted()).isTrue() + } + + @Test + fun `isSyncCompleted() is false when there are enqueued workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued)), + down = listOf(createWorker(Succeeded)), + ).isSyncCompleted()).isFalse() + } + + @Test + fun `isSyncCompleted() is false when there are running workers`() { + assertThat(createState( + up = listOf(createWorker(Running)), + down = listOf(createWorker(Succeeded)), + ).isSyncCompleted()).isFalse() + } + + @Test + fun `isSyncInProgress() is false when there are no running workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued)), + down = listOf(createWorker(Succeeded)), + ).isSyncInProgress()).isFalse() + } + + @Test + fun `isSyncInProgress() is true when there are running workers`() { + assertThat(createState( + up = listOf(createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncInProgress()).isTrue() + } + + @Test + fun `isSyncConnecting() is false when there are no enqueued workers`() { + assertThat(createState( + up = listOf(createWorker(Running)), + down = listOf(createWorker(Succeeded)), + ).isSyncConnecting()).isFalse() + } + + @Test + fun `isSyncConnecting() is true when there are enqueued workers`() { + assertThat(createState( + up = listOf(createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncConnecting()).isTrue() + } + + @Test + fun `isSyncFailedBecauseReloginRequired() is false when there are no workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed())), + down = listOf(createWorker(Succeeded)), + ).isSyncFailedBecauseReloginRequired()).isFalse() + } + + @Test + fun `isSyncFailedBecauseReloginRequired() is true when there are workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed(failedBecauseReloginRequired = true))), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncFailedBecauseReloginRequired()).isTrue() + } + + @Test + fun `isSyncFailedBecauseTooManyRequests() is false when there are no workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed())), + down = listOf(createWorker(Succeeded)), + ).isSyncFailedBecauseTooManyRequests()).isFalse() + } + + @Test + fun `isSyncFailedBecauseTooManyRequests() is true when there are workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed(failedBecauseTooManyRequest = true))), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncFailedBecauseTooManyRequests()).isTrue() + } + + @Test + fun `isSyncFailedBecauseCloudIntegration() is false when there are no workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed())), + down = listOf(createWorker(Succeeded)), + ).isSyncFailedBecauseCloudIntegration()).isFalse() + } + + @Test + fun `isSyncFailedBecauseBackendMaintenance() is true when there are workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed(failedBecauseBackendMaintenance = true))), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncFailedBecauseBackendMaintenance()).isTrue() + } + + @Test + fun `isSyncFailedBecauseBackendMaintenance() is false when there are no workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed())), + down = listOf(createWorker(Succeeded)), + ).isSyncFailedBecauseBackendMaintenance()).isFalse() + } + + @Test + fun `isSyncFailedBecauseCloudIntegration() is true when there are workers with that status`() { + assertThat(createState( + up = listOf(createWorker(Failed(failedBecauseCloudIntegration = true))), + down = listOf(createWorker(Succeeded), createWorker(Enqueued)), + ).isSyncFailedBecauseCloudIntegration()).isTrue() + } + + @Test + fun `isSyncFailed() is false when there are no Failed, Blocked or Cancelled workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued), createWorker(Running)), + down = listOf(createWorker(Succeeded)), + ).isSyncFailed()).isFalse() + } + + @Test + fun `isSyncFailed() is true when there are no Failed workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued), createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Failed())), + ).isSyncFailed()).isTrue() + } + + @Test + fun `isSyncFailed() is true when there are no Blocked workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued), createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Blocked)), + ).isSyncFailed()).isTrue() + } + + @Test + fun `isSyncFailed() is true when there are no Cancelled workers`() { + assertThat(createState( + up = listOf(createWorker(Enqueued), createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Cancelled)), + ).isSyncFailed()).isTrue() + } + + @Test + fun `getEstimatedBackendMaintenanceOutage() returns outage value when there is a worker with that status`() { + val outage: Long = 666 + assertThat(createState( + up = listOf( + createWorker(Enqueued), + createWorker(Running), + createWorker(Failed(failedBecauseBackendMaintenance = true, estimatedOutage = outage))) + , + down = listOf(createWorker(Succeeded), createWorker(Cancelled)), + ).getEstimatedBackendMaintenanceOutage()).isEqualTo(outage) + } + + @Test + fun `getEstimatedBackendMaintenanceOutage() returns null when there is no worker with that status`() { + assertThat(createState( + up = listOf(createWorker(Enqueued), createWorker(Running)), + down = listOf(createWorker(Succeeded), createWorker(Cancelled)), + ).getEstimatedBackendMaintenanceOutage()).isNull() + } + private fun createState( up: List, down: List, diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index d1b7b4d7b7..f6a77f7709 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt @@ -3,13 +3,13 @@ package com.simprints.infra.eventsync.sync.down.tasks import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.tokenization.asTokenizableRaw import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.enrolment.records.store.EnrolmentRecordRepository import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction.Creation import com.simprints.infra.enrolment.records.store.domain.models.SubjectAction.Deletion import com.simprints.infra.events.EventRepository -import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.events.event.domain.models.downsync.EventDownSyncRequestEvent import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordCreationEvent @@ -34,14 +34,13 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.Mock +import java.util.UUID class EventDownSyncTaskTest { @@ -161,7 +160,7 @@ class EventDownSyncTaskTest { eventScope, match { it is EventDownSyncRequestEvent && - it.payload.requestId == "requestId" && + UUID.fromString(it.payload.requestId) != null && it.payload.eventsRead == eventsToDownload.size && it.payload.responseStatus == 200 } @@ -182,7 +181,7 @@ class EventDownSyncTaskTest { @Test fun downSync_shouldEmitAFailureIfDownloadFails() = runTest { - coEvery { eventRemoteDataSource.getEvents(any(), any()) } throws Throwable("IO Exception") + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws Throwable("IO Exception") val progress = eventDownSyncTask.downSync(this, projectOp, eventScope).toList() @@ -190,9 +189,16 @@ class EventDownSyncTaskTest { coVerify(exactly = 2) { eventDownSyncScopeRepository.insertOrUpdate(any()) } } + @Test(expected = RemoteDbNotSignedInException::class) + fun downSync_shouldThrowUpIfRemoteDbNotSignedInExceptionOccurs() = runTest { + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws RemoteDbNotSignedInException() + + eventDownSyncTask.downSync(this, projectOp, eventScope).toList() + } + @Test fun downSync_shouldAddEventWithErrorIfDownloadFails() = runTest { - coEvery { eventRemoteDataSource.getEvents(any(), any()) } throws Throwable("IO Exception") + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws Throwable("IO Exception") eventDownSyncTask.downSync(this, projectOp, eventScope).toList() coVerify(exactly = 1) { @@ -328,9 +334,8 @@ class EventDownSyncTaskTest { private suspend fun mockProgressEmission(progressEvents: List) { downloadEventsChannel = Channel(capacity = Channel.UNLIMITED) - coEvery { eventRemoteDataSource.getEvents(any(), any()) } returns EventDownSyncResult( + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } returns EventDownSyncResult( 0, - requestId = "requestId", status = 200, downloadEventsChannel ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorkerTest.kt index 5d0da71cd6..03cbeefb5d 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/EventDownSyncDownloaderWorkerTest.kt @@ -8,12 +8,18 @@ import androidx.work.WorkInfo.State.SUCCEEDED import androidx.work.workDataOf import com.google.common.truth.Truth.assertThat import com.simprints.core.tools.json.JsonHelper +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.eventsync.SampleSyncScopes.projectDownSyncScope import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository -import com.simprints.infra.eventsync.sync.common.* +import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TIME +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS import com.simprints.infra.eventsync.sync.down.tasks.EventDownSyncTask import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.INPUT_DOWN_SYNC_OPS import com.simprints.infra.eventsync.sync.down.workers.EventDownSyncDownloaderWorker.Companion.INPUT_EVENT_DOWN_SYNC_SCOPE_ID @@ -32,7 +38,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.* +import java.util.UUID @RunWith(AndroidJUnit4::class) internal class EventDownSyncDownloaderWorkerTest { @@ -162,6 +168,24 @@ internal class EventDownSyncDownloaderWorkerTest { ) } + @Test + fun worker_failForRemoteDbNotSignedInException_shouldFail() = runTest { + coEvery { eventRepository.getEventScope(any()) } returns eventScope + coEvery { + downSyncTask.downSync(any(), any(), any()) + } throws RemoteDbNotSignedInException() + + val result = eventDownSyncDownloaderWorker.doWork() + + assertThat(result).isEqualTo( + ListenableWorker.Result.failure( + workDataOf( + OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true + ) + ) + ) + } + @Test fun worker_failForNetworkIssue_shouldRetry() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt index 21535b0a0a..abf1a0d38b 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt @@ -7,6 +7,7 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.randomUUID import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.config.store.models.SynchronizationConfiguration @@ -15,11 +16,18 @@ import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.events.event.domain.models.scope.EventScopeType import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent -import com.simprints.infra.events.sampledata.* import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import com.simprints.infra.events.sampledata.SampleDefaults.GUID3 +import com.simprints.infra.events.sampledata.createAlertScreenEvent +import com.simprints.infra.events.sampledata.createAuthenticationEvent +import com.simprints.infra.events.sampledata.createEnrolmentEventV2 +import com.simprints.infra.events.sampledata.createEventWithSessionId +import com.simprints.infra.events.sampledata.createFaceCaptureBiometricsEvent +import com.simprints.infra.events.sampledata.createFingerprintCaptureBiometricsEvent +import com.simprints.infra.events.sampledata.createPersonCreationEvent +import com.simprints.infra.events.sampledata.createSessionScope import com.simprints.infra.eventsync.SampleSyncScopes import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource import com.simprints.infra.eventsync.exceptions.TryToUploadEventsForNotSignedProject @@ -83,6 +91,9 @@ internal class EventUpSyncTaskTest { every { timeHelper.now() } returns NOW every { authStore.signedInProjectId } returns DEFAULT_PROJECT_ID + every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( + 10, 10, 10 + ) every { projectConfiguration.synchronization } returns synchronizationConfiguration coEvery { configRepository.getProjectConfiguration() } returns projectConfiguration @@ -118,6 +129,55 @@ internal class EventUpSyncTaskTest { coVerify(exactly = 2) { eventRepo.getEventsFromScope(any()) } } + @Test + fun `upload events in batches of provided size`() = runTest { + setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) + every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( + 2, 2, 2 + ) + + coEvery { eventRepo.getClosedEventScopes(any()) } returns emptyList() + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SESSION) } returns listOf( + createSessionScope(GUID1), + createSessionScope(GUID2), + createSessionScope(GUID3) + ) + coEvery { + eventRepo.getEventsFromScope(any()) + } returns listOf(createEventWithSessionId(GUID1, GUID1)) + + eventUpSyncTask.upSync(operation, eventScope).toList() + + coVerify(exactly = 2) { eventRemoteDataSource.post(any(), any(), any()) } + } + + @Test + fun `upload out-of-session events in correct fields`() = runTest { + setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) + every { synchronizationConfiguration.up.simprints.batchSizes } returns UpSynchronizationConfiguration.UpSyncBatchSizes( + 2, 2, 2 + ) + + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SESSION) } returns emptyList() + coEvery { eventRepo.getClosedEventScopes(EventScopeType.DOWN_SYNC) } returns listOf( + createSessionScope(GUID1) + ) + coEvery { eventRepo.getClosedEventScopes(EventScopeType.UP_SYNC) } returns listOf( + createSessionScope(GUID2), + createSessionScope(GUID3) + ) + coEvery { + eventRepo.getEventsFromScope(any()) + } returns listOf(createEventWithSessionId(GUID1, GUID1)) + + eventUpSyncTask.upSync(operation, eventScope).toList() + + coVerify { + eventRemoteDataSource.post(any(), any(), match { it.eventDownSyncs.size == 1 }) + eventRemoteDataSource.post(any(), any(), match { it.eventUpSyncs.size == 2 }) + } + } + @Test fun `upload should not filter any session events on upload`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) @@ -135,6 +195,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -167,6 +228,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -198,6 +260,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -219,7 +282,7 @@ internal class EventUpSyncTaskTest { fun `when upload succeeds it should delete session events`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) - coEvery { eventRepo.getClosedEventScopes(any()) } returns listOf( + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SESSION) } returns listOf( createSessionScope(GUID1), createSessionScope(GUID2) ) @@ -233,8 +296,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() coVerify { - eventRepo.deleteEventScope(GUID1) - eventRepo.deleteEventScope(GUID2) + eventRepo.deleteEventScopes(listOf(GUID1, GUID2)) } } @@ -271,7 +333,7 @@ internal class EventUpSyncTaskTest { eventRepo.getEventsFromScope(GUID1) } returns listOf(createEventWithSessionId(GUID1, GUID1)) - coEvery { eventRemoteDataSource.post(any(), any()) } throws Throwable("") + coEvery { eventRemoteDataSource.post(any(), any(), any()) } throws Throwable("") eventUpSyncTask.upSync(operation, eventScope).toList() @@ -290,7 +352,9 @@ internal class EventUpSyncTaskTest { eventRepo.getEventsFromScope(GUID1) } returns listOf(createEventWithSessionId(GUID1, GUID1)) - coEvery { eventRemoteDataSource.post(any(), any()) } throws NetworkConnectionException( + coEvery { + eventRemoteDataSource.post(any(), any(), any()) + } throws NetworkConnectionException( cause = Exception() ) @@ -309,7 +373,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() coVerify(exactly = 0) { - eventRemoteDataSource.post(any(), any()) + eventRemoteDataSource.post(any(), any(), any()) eventRemoteDataSource.dumpInvalidEvents(any(), any()) eventRepo.deleteEventScope(GUID1) } @@ -328,7 +392,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() - coVerify(exactly = 0) { eventRemoteDataSource.post(any(), any()) } + coVerify(exactly = 0) { eventRemoteDataSource.post(any(), any(), any()) } coVerify { eventRepo.getEventsJsonFromScope(any()) eventRemoteDataSource.dumpInvalidEvents(any(), any()) @@ -352,7 +416,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() coVerify(exactly = 0) { - eventRemoteDataSource.post(any(), any()) + eventRemoteDataSource.post(any(), any(), any()) eventRepo.deleteEventScope(GUID1) } @@ -379,7 +443,7 @@ internal class EventUpSyncTaskTest { coEvery { eventRepo.getClosedEventScopes(EventScopeType.SESSION) } returns listOf( createSessionScope(GUID1), ) - coEvery { eventRemoteDataSource.post(any(), any()) } throws HttpException( + coEvery { eventRemoteDataSource.post(any(), any(), any()) } throws HttpException( Response.error(427, "".toResponseBody(null)) ) @@ -398,7 +462,7 @@ internal class EventUpSyncTaskTest { fun `upload should save request event for each upload request`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) - coEvery { eventRepo.getClosedEventScopes(any()) } returns listOf( + coEvery { eventRepo.getClosedEventScopes(EventScopeType.SESSION) } returns listOf( createSessionScope(GUID1), createSessionScope(GUID2) ) @@ -461,20 +525,24 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() + coVerify(exactly = 3) { eventRepo.getClosedEventScopes(any()) } + coVerify(exactly = 3) { eventRepo.addOrUpdateEvent(any(), any()) } + coVerify(exactly = 1) { eventRepo.addOrUpdateEvent(any(), match { - it is EventUpSyncRequestEvent && - it.payload.content == EventUpSyncRequestEvent.UpSyncContent(1, 0, 0) + it is EventUpSyncRequestEvent && it.payload.content.sessionCount == 1 + }) + eventRepo.addOrUpdateEvent(any(), match { + it is EventUpSyncRequestEvent && it.payload.content.eventUpSyncCount == 1 }) eventRepo.addOrUpdateEvent(any(), match { - it is EventUpSyncRequestEvent && - it.payload.content == EventUpSyncRequestEvent.UpSyncContent(0, 1, 1) + it is EventUpSyncRequestEvent && it.payload.content.eventDownSyncCount == 1 }) } } @Test - fun `upload does not reports events if no scopes to upload`() = runTest { + fun `upload does not reports events if only up-sync scopes are present to upload`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) coEvery { eventRepo.getClosedEventScopes(any()) } returns emptyList() coEvery { eventRepo.getClosedEventScopes(EventScopeType.UP_SYNC) } returns listOf( @@ -490,7 +558,7 @@ internal class EventUpSyncTaskTest { } @Test - fun `upload does not reports events if only up-sync scopes are present to upload`() = runTest { + fun `upload does not reports events if no scopes to upload`() = runTest { setUpSyncKind(UpSynchronizationConfiguration.UpSynchronizationKind.ALL) coEvery { eventRepo.getClosedEventScopes(any()) } returns emptyList() @@ -499,6 +567,13 @@ internal class EventUpSyncTaskTest { coVerify(exactly = 0) { eventRepo.addOrUpdateEvent(any(), any()) } } + @Test(expected = RemoteDbNotSignedInException::class) + fun `upSync should throw up if RemoteDbNotSignedInException occurs`() = runTest { + coEvery { eventRepo.getClosedEventScopes(any()) } throws RemoteDbNotSignedInException() + + eventUpSyncTask.upSync(operation, eventScope).toList() + } + private fun setUpSyncKind(kind: UpSynchronizationConfiguration.UpSynchronizationKind) { every { synchronizationConfiguration.up.simprints.kind } returns kind } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt index 154053796d..05966c0b41 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt @@ -11,6 +11,7 @@ import androidx.work.workDataOf import com.google.common.truth.Truth.assertThat import com.simprints.core.tools.json.JsonHelper import com.simprints.infra.authstore.AuthStore +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation @@ -19,11 +20,11 @@ import com.simprints.infra.eventsync.sync.common.EventSyncCache import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TIME import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED import com.simprints.infra.eventsync.sync.up.EventUpSyncProgress import com.simprints.infra.eventsync.sync.up.tasks.EventUpSyncTask import com.simprints.infra.eventsync.sync.up.workers.EventUpSyncUploaderWorker.Companion.INPUT_EVENT_UP_SYNC_SCOPE_ID import com.simprints.infra.eventsync.sync.up.workers.EventUpSyncUploaderWorker.Companion.INPUT_UP_SYNC -import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.BackendMaintenanceException import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import com.simprints.testtools.common.coroutines.TestCoroutineRule @@ -33,12 +34,9 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -170,6 +168,25 @@ internal class EventUpSyncUploaderWorkerTest { ) } + @Test + fun worker_shouldSetFailCorrectlyIfRemoteDbNotSignedInException() = runTest { + val eventUpSyncUploaderWorker = init(projectScope) + + coEvery { + upSyncTask.upSync(any(), any()) + } throws RemoteDbNotSignedInException() + + val result = eventUpSyncUploaderWorker.doWork() + + assertThat(result).isEqualTo( + ListenableWorker.Result.failure( + workDataOf( + OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true + ) + ) + ) + } + @Test fun worker_shouldRetryIfNotBackendMaintenanceOrSyncIssue() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope 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 c2499aca8b..fa1d2697fe 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,6 +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, null, -> { // ADD TEST FOR NEW EVENT IN THIS CLASS } diff --git a/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/16.json b/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/16.json new file mode 100644 index 0000000000..a5659ec864 --- /dev/null +++ b/infra/events/schemas/com.simprints.infra.events.event.local.EventRoomDatabase/16.json @@ -0,0 +1,150 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "d5dd0e6fc8f6d48c5f58e7b191bc8d3d", + "entities": [ + { + "tableName": "DbEvent", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `projectId` TEXT, `scopeId` TEXT, `eventJson` TEXT NOT NULL, `createdAt_unixMs` INTEGER NOT NULL, `createdAt_isTrustworthy` INTEGER NOT NULL, `createdAt_msSinceBoot` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeId", + "columnName": "scopeId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "eventJson", + "columnName": "eventJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt.unixMs", + "columnName": "createdAt_unixMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.isTrustworthy", + "columnName": "createdAt_isTrustworthy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.msSinceBoot", + "columnName": "createdAt_msSinceBoot", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DbEventScope", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `projectId` TEXT NOT NULL, `type` TEXT NOT NULL, `payloadJson` TEXT NOT NULL, `start_unixMs` INTEGER NOT NULL, `start_isTrustworthy` INTEGER NOT NULL, `start_msSinceBoot` INTEGER, `end_unixMs` INTEGER, `end_isTrustworthy` INTEGER, `end_msSinceBoot` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "projectId", + "columnName": "projectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payloadJson", + "columnName": "payloadJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt.unixMs", + "columnName": "start_unixMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.isTrustworthy", + "columnName": "start_isTrustworthy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt.msSinceBoot", + "columnName": "start_msSinceBoot", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endedAt.unixMs", + "columnName": "end_unixMs", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endedAt.isTrustworthy", + "columnName": "end_isTrustworthy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endedAt.msSinceBoot", + "columnName": "end_msSinceBoot", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd5dd0e6fc8f6d48c5f58e7b191bc8d3d')" + ] + } +} \ No newline at end of file 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 dc8fb17140..dc25a94217 100644 --- a/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt +++ b/infra/events/src/debug/java/com/simprints/infra/events/sampledata/EventFactoryUtils.kt @@ -8,22 +8,46 @@ import com.simprints.core.tools.time.Timestamp import com.simprints.core.tools.utils.SimNetworkUtils import com.simprints.core.tools.utils.SimNetworkUtils.Connection import com.simprints.infra.config.store.models.GeneralConfiguration.Modality -import com.simprints.infra.events.event.domain.models.* +import com.simprints.infra.events.event.domain.models.AlertScreenEvent import com.simprints.infra.events.event.domain.models.AlertScreenEvent.AlertScreenPayload.AlertScreenEventType.BLUETOOTH_NOT_ENABLED +import com.simprints.infra.events.event.domain.models.AuthenticationEvent import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload.Result.AUTHENTICATED import com.simprints.infra.events.event.domain.models.AuthenticationEvent.AuthenticationPayload.UserInfo +import com.simprints.infra.events.event.domain.models.AuthorizationEvent import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload import com.simprints.infra.events.event.domain.models.AuthorizationEvent.AuthorizationPayload.AuthorizationResult.AUTHORIZED +import com.simprints.infra.events.event.domain.models.CandidateReadEvent import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload.LocalResult.FOUND import com.simprints.infra.events.event.domain.models.CandidateReadEvent.CandidateReadPayload.RemoteResult.NOT_FOUND +import com.simprints.infra.events.event.domain.models.CompletionCheckEvent +import com.simprints.infra.events.event.domain.models.ConnectivitySnapshotEvent +import com.simprints.infra.events.event.domain.models.ConsentEvent import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload.Result.ACCEPTED import com.simprints.infra.events.event.domain.models.ConsentEvent.ConsentPayload.Type.INDIVIDUAL +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.Event +import com.simprints.infra.events.event.domain.models.EventType +import com.simprints.infra.events.event.domain.models.FingerComparisonStrategy +import com.simprints.infra.events.event.domain.models.GuidSelectionEvent +import com.simprints.infra.events.event.domain.models.IntentParsingEvent import com.simprints.infra.events.event.domain.models.IntentParsingEvent.IntentParsingPayload.IntegrationInfo.COMMCARE +import com.simprints.infra.events.event.domain.models.InvalidIntentEvent +import com.simprints.infra.events.event.domain.models.LicenseCheckEvent +import com.simprints.infra.events.event.domain.models.MatchEntry +import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.MatchPool import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload.MatchPoolType.PROJECT +import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent +import com.simprints.infra.events.event.domain.models.PersonCreationEvent +import com.simprints.infra.events.event.domain.models.RefusalEvent import com.simprints.infra.events.event.domain.models.RefusalEvent.RefusalPayload.Answer.OTHER +import com.simprints.infra.events.event.domain.models.ScannerConnectionEvent import com.simprints.infra.events.event.domain.models.ScannerConnectionEvent.ScannerConnectionPayload.ScannerGeneration.VERO_1 import com.simprints.infra.events.event.domain.models.ScannerConnectionEvent.ScannerConnectionPayload.ScannerInfo +import com.simprints.infra.events.event.domain.models.ScannerFirmwareUpdateEvent +import com.simprints.infra.events.event.domain.models.SuspiciousIntentEvent +import com.simprints.infra.events.event.domain.models.Vero2InfoSnapshotEvent import com.simprints.infra.events.event.domain.models.Vero2InfoSnapshotEvent.BatteryInfo import com.simprints.infra.events.event.domain.models.Vero2InfoSnapshotEvent.Vero2Version import com.simprints.infra.events.event.domain.models.callback.CallbackComparisonScore @@ -50,10 +74,10 @@ 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.scope.DatabaseInfo import com.simprints.infra.events.event.domain.models.scope.Device -import com.simprints.infra.events.event.domain.models.scope.Location import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.events.event.domain.models.scope.EventScopePayload import com.simprints.infra.events.event.domain.models.scope.EventScopeType +import com.simprints.infra.events.event.domain.models.scope.Location import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestEvent import com.simprints.infra.events.sampledata.SampleDefaults.CREATED_AT import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_METADATA @@ -404,9 +428,17 @@ fun createEventUpSyncRequestEvent() = EventUpSyncRequestEvent( createdAt = CREATED_AT, endedAt = ENDED_AT, requestId = GUID1, - sessionCount = 1, - eventUpSyncCount = 2, - eventDownSyncCount = 3, + content = EventUpSyncRequestEvent.UpSyncContent( + sessionCount = 1, + eventUpSyncCount = 2, + eventDownSyncCount = 3, + ), responseStatus = 200, errorType = "OK", ) +fun createLicenseCheckEvent() = LicenseCheckEvent( + createdAt = CREATED_AT, + status = LicenseCheckEvent.LicenseStatus.VALID, + vendor = "NEC_FINGERPRINT", + +) diff --git a/infra/events/src/main/java/com/simprints/infra/events/EventRepositoryImpl.kt b/infra/events/src/main/java/com/simprints/infra/events/EventRepositoryImpl.kt index fa4c65ab31..3433378a7a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/EventRepositoryImpl.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/EventRepositoryImpl.kt @@ -86,6 +86,12 @@ internal open class EventRepositoryImpl @Inject constructor( eventLocalDataSource.loadEventScope(downSyncEventScopeId) override suspend fun closeEventScope(eventScope: EventScope, reason: EventScopeEndCause?) { + if (eventScope.projectId == PROJECT_ID_FOR_NOT_SIGNED_IN) { + eventLocalDataSource.deleteEventScope(scopeId = eventScope.id) + eventLocalDataSource.deleteEventsInScope(scopeId = eventScope.id) + return + } + val events = eventLocalDataSource.loadEventsInScope(eventScope.id) if (events.isEmpty()) { eventLocalDataSource.deleteEventScope(scopeId = eventScope.id) @@ -157,7 +163,7 @@ internal open class EventRepositoryImpl @Inject constructor( it.validate(currentEvents, event) } - event.scopeId = scope.id + event.scopeId = event.scopeId ?: scope.id event.projectId = scope.projectId eventLocalDataSource.saveEvent(event) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEnrolmentRecordEvents.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordEvents.kt similarity index 63% rename from feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEnrolmentRecordEvents.kt rename to infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordEvents.kt index 4273c3e10f..afba92c8aa 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEnrolmentRecordEvents.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEnrolmentRecordEvents.kt @@ -1,9 +1,9 @@ -package com.simprints.feature.clientapi.models +package com.simprints.infra.events.event.cosync import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEvent @Keep -internal data class CoSyncEnrolmentRecordEvents( +data class CoSyncEnrolmentRecordEvents( val events: List, ) diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEvents.kt b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEvents.kt similarity index 61% rename from feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEvents.kt rename to infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEvents.kt index 4dd3672214..cf970457f4 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/models/CoSyncEvents.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/cosync/CoSyncEvents.kt @@ -1,9 +1,9 @@ -package com.simprints.feature.clientapi.models +package com.simprints.infra.events.event.cosync import androidx.annotation.Keep import com.simprints.infra.events.event.domain.models.Event @Keep -internal data class CoSyncEvents( +data class CoSyncEvents( val events: List, ) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/EventCount.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/EventCount.kt index f9d015fdb9..e95db5382d 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/EventCount.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/EventCount.kt @@ -1,7 +1,16 @@ package com.simprints.infra.events.event.domain import androidx.annotation.Keep -import com.simprints.infra.events.event.domain.models.subject.EnrolmentRecordEventType @Keep -data class EventCount(val type: EnrolmentRecordEventType, val count: Int) +data class EventCount( + val count: Int, + val isLowerBound: Boolean, +) { + + /** + * Returns exact count or null if the count is a lower bound. + */ + val exactCount: Int? + get() = if (isLowerBound) null else count +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AlertScreenEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AlertScreenEvent.kt index 0087616cd9..7811f2c75a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AlertScreenEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/AlertScreenEvent.kt @@ -70,8 +70,8 @@ data class AlertScreenEvent( INTEGRITY_SERVICE_ERROR, ENROLMENT_LAST_BIOMETRICS_FAILED, INVALID_STATE_FOR_INTENT_ACTION, - FACE_LICENSE_INVALID,// Todo rename to LICENSE_INVALID CORE-3206 - FACE_LICENSE_MISSING,// Todo rename to LICENSE_MISSING CORE-3206 + LICENSE_INVALID, + LICENSE_MISSING, GOOGLE_PLAY_SERVICES_OUTDATED, MISSING_GOOGLE_PLAY_SERVICES, MISSING_OR_OUTDATED_GOOGLE_PLAY_STORE_APP, 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 512cdb07d2..09509c3ab1 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 @@ -23,9 +23,9 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.CANDID import com.simprints.infra.events.event.domain.models.EventType.Companion.COMPLETION_CHECK_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONNECTIVITY_SNAPSHOT_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.CONSENT_KEY -import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_DOWN_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V1_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ENROLMENT_V2_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_DOWN_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.EVENT_UP_SYNC_REQUEST_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_BIOMETRICS_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.FACE_CAPTURE_CONFIRMATION_KEY @@ -37,6 +37,7 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion.FINGER import com.simprints.infra.events.event.domain.models.EventType.Companion.GUID_SELECTION_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.INTENT_PARSING_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.INVALID_INTENT_KEY +import com.simprints.infra.events.event.domain.models.EventType.Companion.LICENSE_CHECK_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ONE_TO_MANY_MATCH_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.ONE_TO_ONE_MATCH_KEY import com.simprints.infra.events.event.domain.models.EventType.Companion.PERSON_CREATION_KEY @@ -135,6 +136,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = Vero2InfoSnapshotEvent::class, name = VERO_2_INFO_SNAPSHOT_KEY), 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), ) 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 acb55e4023..7e1195cd41 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 @@ -14,6 +14,7 @@ import com.simprints.infra.events.event.domain.models.EventType.Companion import com.simprints.infra.events.event.domain.models.GuidSelectionEvent.GuidSelectionPayload import com.simprints.infra.events.event.domain.models.IntentParsingEvent.IntentParsingPayload import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.InvalidIntentPayload +import com.simprints.infra.events.event.domain.models.LicenseCheckEvent.LicenseCheckEventPayload import com.simprints.infra.events.event.domain.models.OneToManyMatchEvent.OneToManyMatchPayload import com.simprints.infra.events.event.domain.models.OneToOneMatchEvent.OneToOneMatchPayload import com.simprints.infra.events.event.domain.models.PersonCreationEvent.PersonCreationPayload @@ -85,6 +86,7 @@ import com.simprints.infra.events.event.domain.models.upsync.EventUpSyncRequestE JsonSubTypes.Type(value = SuspiciousIntentPayload::class, name = EventType.SUSPICIOUS_INTENT_KEY), 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), ) 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 bcf3eeb05a..67b58fa474 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 @@ -128,6 +128,9 @@ enum class EventType { /* key added: EVENT_UP_SYNC_REQUEST_KEY */ EVENT_UP_SYNC_REQUEST, + + /* key added: LICENSE_CHECK_KEY */ + LICENSE_CHECK, ; companion object { @@ -171,5 +174,6 @@ enum class EventType { const val VERO_2_INFO_SNAPSHOT_KEY = "VERO_2_INFO_SNAPSHOT" 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" } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/LicenseCheckEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/LicenseCheckEvent.kt new file mode 100644 index 0000000000..58a0aa6310 --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/LicenseCheckEvent.kt @@ -0,0 +1,58 @@ +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.LICENSE_CHECK +import java.util.UUID + +@Keep +data class LicenseCheckEvent( + override val id: String = UUID.randomUUID().toString(), + override val payload: LicenseCheckEventPayload, + override val type: EventType, + override var scopeId: String? = null, + override var projectId: String? = null, +) : Event() { + +constructor( + createdAt: Timestamp, + status: LicenseStatus, + vendor: String, + + ) : this( + UUID.randomUUID().toString(), + LicenseCheckEventPayload( + createdAt = createdAt, + eventVersion = EVENT_VERSION, + status = status, + vendor = vendor + ), + LICENSE_CHECK + ) + enum class LicenseStatus { + VALID, + INVALID, + EXPIRED, + MISSING, + ERROR, + } + + override fun getTokenizedFields(): Map = emptyMap() + + override fun setTokenizedFields(map: Map) = this // No tokenized fields + + @Keep + data class LicenseCheckEventPayload( + override val createdAt: Timestamp, + override val eventVersion: Int, + val status: LicenseStatus, + val vendor: String, + override val endedAt: Timestamp? = null, + override val type: EventType = LICENSE_CHECK, + ) : EventPayload() + companion object { + const val EVENT_VERSION = 1 + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ConfirmationCallbackEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ConfirmationCallbackEvent.kt index 2007c64c75..bc679cbc8a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ConfirmationCallbackEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ConfirmationCallbackEvent.kt @@ -44,6 +44,6 @@ data class ConfirmationCallbackEvent( companion object { - const val EVENT_VERSION = 2 + const val EVENT_VERSION = 3 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/EnrolmentCallbackEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/EnrolmentCallbackEvent.kt index e88e20650b..446ade622d 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/EnrolmentCallbackEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/EnrolmentCallbackEvent.kt @@ -45,6 +45,6 @@ data class EnrolmentCallbackEvent( companion object { - const val EVENT_VERSION = 2 + const val EVENT_VERSION = 3 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt index 0bac4ae631..3e701beba3 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEvent.kt @@ -54,8 +54,8 @@ data class ErrorCallbackEvent( BLUETOOTH_NOT_SUPPORTED, LOGIN_NOT_COMPLETE, ENROLMENT_LAST_BIOMETRICS_FAILED, - FACE_LICENSE_MISSING, - FACE_LICENSE_INVALID, + LICENSE_MISSING, + LICENSE_INVALID, FINGERPRINT_CONFIGURATION_ERROR, BACKEND_MAINTENANCE_ERROR, PROJECT_ENDING, @@ -75,8 +75,8 @@ data class ErrorCallbackEvent( AppErrorReason.BLUETOOTH_NOT_SUPPORTED -> BLUETOOTH_NOT_SUPPORTED AppErrorReason.LOGIN_NOT_COMPLETE -> LOGIN_NOT_COMPLETE AppErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED -> ENROLMENT_LAST_BIOMETRICS_FAILED - AppErrorReason.FACE_LICENSE_MISSING -> FACE_LICENSE_MISSING - AppErrorReason.FACE_LICENSE_INVALID -> FACE_LICENSE_INVALID + AppErrorReason.LICENSE_MISSING -> LICENSE_MISSING + AppErrorReason.LICENSE_INVALID -> LICENSE_INVALID AppErrorReason.FINGERPRINT_CONFIGURATION_ERROR -> FINGERPRINT_CONFIGURATION_ERROR AppErrorReason.FACE_CONFIGURATION_ERROR -> FACE_CONFIGURATION_ERROR AppErrorReason.BACKEND_MAINTENANCE_ERROR -> BACKEND_MAINTENANCE_ERROR @@ -91,6 +91,6 @@ data class ErrorCallbackEvent( companion object { - const val EVENT_VERSION = 2 + const val EVENT_VERSION = 3 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/RefusalCallbackEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/RefusalCallbackEvent.kt index fd530390ff..b7a9900ace 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/RefusalCallbackEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/callback/RefusalCallbackEvent.kt @@ -46,6 +46,6 @@ data class RefusalCallbackEvent( companion object { - const val EVENT_VERSION = 2 + const val EVENT_VERSION = 3 } } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEvent.kt index 6526ca543b..f0ad624974 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEvent.kt @@ -43,7 +43,10 @@ data class EventDownSyncRequestEvent( EventType.EVENT_DOWN_SYNC_REQUEST ) - override fun getTokenizedFields(): Map = emptyMap() + override fun getTokenizedFields(): Map = listOf( + payload.queryParameters.attendantId?.let { TokenKeyType.AttendantId to TokenizableString.Tokenized(it) }, + payload.queryParameters.moduleId?.let { TokenKeyType.ModuleId to TokenizableString.Tokenized(it) } + ).mapNotNull { it }.toMap() override fun setTokenizedFields(map: Map): Event = this @Keep diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt index 5364672439..800914d68a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/subject/EnrolmentRecordEvent.kt @@ -1,7 +1,19 @@ package com.simprints.infra.events.event.domain.models.subject import androidx.annotation.Keep +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = EnrolmentRecordCreationEvent::class, name = "EnrolmentRecordCreation"), + JsonSubTypes.Type(value = EnrolmentRecordMoveEvent::class, name = "EnrolmentRecordMove"), + JsonSubTypes.Type(value = EnrolmentRecordDeletionEvent::class, name = "EnrolmentRecordDeletion") +) @Keep sealed class EnrolmentRecordEvent( open val id: String, diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt index 6a3f5eaf05..413072cb9a 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/upsync/EventUpSyncRequestEvent.kt @@ -22,9 +22,7 @@ data class EventUpSyncRequestEvent( createdAt: Timestamp, endedAt: Timestamp, requestId: String, - sessionCount: Int = 0, - eventUpSyncCount: Int = 0, - eventDownSyncCount: Int = 0, + content: UpSyncContent = UpSyncContent(), responseStatus: Int? = null, errorType: String? = null, ) : this( @@ -33,11 +31,7 @@ data class EventUpSyncRequestEvent( createdAt, endedAt, requestId, - UpSyncContent( - sessionCount, - eventUpSyncCount, - eventDownSyncCount, - ), + content, responseStatus, errorType, EVENT_VERSION, diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt index d47aaf7868..e5cce2486e 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/EventRoomDatabase.kt @@ -7,10 +7,23 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.simprints.infra.events.BuildConfig -import com.simprints.infra.events.event.local.migrations.* +import com.simprints.infra.events.event.local.migrations.EventMigration10to11 +import com.simprints.infra.events.event.local.migrations.EventMigration11to12 +import com.simprints.infra.events.event.local.migrations.EventMigration12to13 +import com.simprints.infra.events.event.local.migrations.EventMigration13to14 +import com.simprints.infra.events.event.local.migrations.EventMigration14to15 +import com.simprints.infra.events.event.local.migrations.EventMigration15to16 +import com.simprints.infra.events.event.local.migrations.EventMigration1to2 +import com.simprints.infra.events.event.local.migrations.EventMigration2to3 +import com.simprints.infra.events.event.local.migrations.EventMigration3to4 +import com.simprints.infra.events.event.local.migrations.EventMigration4to5 +import com.simprints.infra.events.event.local.migrations.EventMigration5to6 +import com.simprints.infra.events.event.local.migrations.EventMigration7to8 +import com.simprints.infra.events.event.local.migrations.EventMigration8to9 +import com.simprints.infra.events.event.local.migrations.EventMigration9to10 import com.simprints.infra.events.event.local.models.DbEvent import com.simprints.infra.events.event.local.models.DbEventScope -import com.simprints.infra.events.local.migrations.* +import com.simprints.infra.events.local.migrations.EventMigration6to7 import net.sqlcipher.database.SupportFactory @@ -19,7 +32,7 @@ import net.sqlcipher.database.SupportFactory DbEvent::class, DbEventScope::class, ], - version = 15, + version = 16, exportSchema = true ) @TypeConverters(Converters::class) @@ -52,6 +65,7 @@ internal abstract class EventRoomDatabase : RoomDatabase() { .addMigrations(EventMigration12to13()) .addMigrations(EventMigration13to14()) .addMigrations(EventMigration14to15()) + .addMigrations(EventMigration15to16()) if (BuildConfig.DB_ENCRYPTION) builder.openHelperFactory(factory) diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration12to13.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration12to13.kt index 7dd69679c6..86b275ffb3 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration12to13.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration12to13.kt @@ -12,9 +12,9 @@ import com.simprints.infra.logging.Simber internal class EventMigration12to13 : Migration(12, 13) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { Simber.d("Migrating room db from schema 12 to schema 13.") - migrateEventJson(database) + migrateEventJson(db) Simber.d("Migration from schema 12 to schema 13 done.") } diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16.kt new file mode 100644 index 0000000000..9e4e0a1a5e --- /dev/null +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16.kt @@ -0,0 +1,61 @@ +package com.simprints.infra.events.event.local.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.simprints.core.tools.extentions.getStringWithColumnName +import com.simprints.infra.logging.Simber + +internal class EventMigration15to16 : Migration(15, 16) { + + override fun migrate(db: SupportSQLiteDatabase) { + Simber.d("Migrating room db from schema 15 to schema 16.") + renameFaceLicenseErrors(db) + Simber.d("Migration from schema 15 to schema 16 done.") + } + + // rename FACE_LICENSE_MISSING and FACE_LICENSE_INVALID to LICENSE_MISSING and LICENSE_INVALID + private fun renameFaceLicenseErrors(database: SupportSQLiteDatabase) { + // select all alert AlertScreenEvent with type FACE_LICENSE_MISSING or FACE_LICENSE_INVALID + val cursor = database.query( + "SELECT * FROM $TABLE_NAME WHERE $EVENT_TYPE =?", arrayOf(ALERT_EVENT_TYPE) + ) + cursor.use { + while (it.moveToNext()) { + val id = it.getStringWithColumnName("id") + val jsonData = it.getStringWithColumnName(DB_EVENT_JSON_FIELD) + // only update the json if it contains FACE_LICENSE_MISSING or FACE_LICENSE_INVALID + if (jsonData?.contains(FACE_LICENSE_MISSING) == true || jsonData?.contains( + FACE_LICENSE_INVALID + ) == true + ) { + jsonData.let { + val updatedJson = jsonData.replace( + "\"$FACE_LICENSE_MISSING\"", + "\"$LICENSE_MISSING\"" + ).replace( + "\"$FACE_LICENSE_INVALID\"", + "\"$LICENSE_INVALID\"" + ) + database.execSQL( + "UPDATE $TABLE_NAME SET $DB_EVENT_JSON_FIELD = ? WHERE id = ?", + arrayOf(updatedJson, id) + ) + } + } + + } + } + } + + companion object { + const val TABLE_NAME = "DbEvent" + const val DB_EVENT_JSON_FIELD = "eventJson" + const val EVENT_TYPE = "type" + const val ALERT_EVENT_TYPE = "ALERT_SCREEN" + const val FACE_LICENSE_MISSING = "FACE_LICENSE_MISSING" + const val LICENSE_MISSING = "LICENSE_MISSING" + const val FACE_LICENSE_INVALID = "FACE_LICENSE_INVALID" + const val LICENSE_INVALID = "LICENSE_INVALID" + + } +} diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/local/models/DbEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/local/models/DbEvent.kt index 095f6c5097..cfbbf3aca0 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/local/models/DbEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/local/models/DbEvent.kt @@ -25,7 +25,6 @@ internal data class DbEvent( companion object { - const val DEFAULT_EVENT_VERSION = 0 val dbSerializationModule = SimpleModule().apply { addSerializer(TokenizableString::class.java, TokenizationClassNameSerializer()) addDeserializer(TokenizableString::class.java, TokenizationClassNameDeserializer()) diff --git a/infra/events/src/test/java/com/simprints/infra/events/EventRepositoryImplTest.kt b/infra/events/src/test/java/com/simprints/infra/events/EventRepositoryImplTest.kt index b05501b9c4..989e045fe5 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/EventRepositoryImplTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/EventRepositoryImplTest.kt @@ -19,6 +19,7 @@ import com.simprints.infra.events.event.local.EventLocalDataSource import com.simprints.infra.events.exceptions.validator.DuplicateGuidSelectEventValidatorException import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_PROJECT_ID import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 +import com.simprints.infra.events.sampledata.SampleDefaults.GUID2 import com.simprints.infra.events.sampledata.createAlertScreenEvent import com.simprints.infra.events.sampledata.createSessionScope import io.mockk.MockKAnnotations @@ -166,22 +167,50 @@ internal class EventRepositoryImplTest { } @Test - fun `should close event scope by id`() = runTest { - val scope = createSessionScope("scopeId", isClosed = false) - val event = createAlertScreenEvent() + fun `should delete scope and events if project not signed in`() = runTest { + val scope = createSessionScope("scopeId", isClosed = false, projectId = PROJECT_ID_FOR_NOT_SIGNED_IN) - coEvery { eventLocalDataSource.loadEventScope(any()) } returns scope - coEvery { eventLocalDataSource.loadEventsInScope(any()) } returns listOf( - event.copy(payload = event.payload.copy(endedAt = Timestamp(5))), - ) - eventRepo.closeEventScope("scopeId", null) + eventRepo.closeEventScope(scope, null) coVerify { - eventLocalDataSource.saveEventScope(match { - assertThat(it.endedAt).isEqualTo(Timestamp(5L)) - true - }) + eventLocalDataSource.deleteEventScope("scopeId") + eventLocalDataSource.deleteEventsInScope("scopeId") + } + } + + @Test + fun `add event to current session should add event related to current session into DB`() = + runTest { + val scope = createSessionScope("scopeId", isClosed = false) + val event = createAlertScreenEvent() + + coEvery { eventLocalDataSource.loadEventScope(any()) } returns scope + coEvery { eventLocalDataSource.loadEventsInScope(any()) } returns listOf( + event.copy(payload = event.payload.copy(endedAt = Timestamp(5))), + ) + eventRepo.closeEventScope("scopeId", null) + + coVerify { + eventLocalDataSource.saveEventScope(match { + assertThat(it.endedAt).isEqualTo(Timestamp(5L)) + true + }) + } } + + @Test + fun `adding event to should not override existing session id in the event`() = runTest { + val scope = createSessionScope("scopeId", isClosed = false) + val event = createAlertScreenEvent().copy( + scopeId = GUID2 + ) + + coEvery { eventLocalDataSource.loadEventScope(any()) } returns scope + coEvery { eventLocalDataSource.loadEventsInScope(any()) } returns listOf(event) + + eventRepo.addOrUpdateEvent(scope, event) + + coVerify { eventLocalDataSource.saveEvent(event.copy(scopeId = GUID2)) } } @Test diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/EventCountTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/EventCountTest.kt new file mode 100644 index 0000000000..31eccdf477 --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/EventCountTest.kt @@ -0,0 +1,14 @@ +package com.simprints.infra.events.event.domain + + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class EventCountTest { + + @Test + fun getExactCount() { + assertThat(EventCount(5, false).exactCount).isEqualTo(5) + assertThat(EventCount(5, true).exactCount).isNull() + } +} diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt index 6c88f8c81d..2078f68d63 100644 --- a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/callback/ErrorCallbackEventTest.kt @@ -49,8 +49,8 @@ class ErrorCallbackEventTest { AppErrorReason.BLUETOOTH_NOT_SUPPORTED to ErrorReason.BLUETOOTH_NOT_SUPPORTED, AppErrorReason.LOGIN_NOT_COMPLETE to ErrorReason.LOGIN_NOT_COMPLETE, AppErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED to ErrorReason.ENROLMENT_LAST_BIOMETRICS_FAILED, - AppErrorReason.FACE_LICENSE_MISSING to ErrorReason.FACE_LICENSE_MISSING, - AppErrorReason.FACE_LICENSE_INVALID to ErrorReason.FACE_LICENSE_INVALID, + AppErrorReason.LICENSE_MISSING to ErrorReason.LICENSE_MISSING, + AppErrorReason.LICENSE_INVALID to ErrorReason.LICENSE_INVALID, AppErrorReason.FINGERPRINT_CONFIGURATION_ERROR to ErrorReason.FINGERPRINT_CONFIGURATION_ERROR, AppErrorReason.FACE_CONFIGURATION_ERROR to ErrorReason.FACE_CONFIGURATION_ERROR, AppErrorReason.BACKEND_MAINTENANCE_ERROR to ErrorReason.BACKEND_MAINTENANCE_ERROR, diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEventTest.kt b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEventTest.kt new file mode 100644 index 0000000000..70de2e0322 --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEventTest.kt @@ -0,0 +1,87 @@ +package com.simprints.infra.events.event.domain.models.downsync + +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 +import com.simprints.infra.events.event.domain.models.downsync.EventDownSyncRequestEvent.QueryParameters +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class EventDownSyncRequestEventTest { + + @Test + fun `getTokenizedFields returns empty map when attendantId and moduleId are null`() { + val event = getEventDownSyncRequestEvent( + attendantId = null, + moduleId = null + ) + + val result = event.getTokenizedFields() + + assertEquals(emptyMap(), result) + } + + @Test + fun `getTokenizedFields returns map with AttendantId when only attendantId is not null`() { + val event = getEventDownSyncRequestEvent( + attendantId = "attendantId", + moduleId = null + ) + + val result = event.getTokenizedFields() + + assertEquals(mapOf(TokenKeyType.AttendantId to TokenizableString.Tokenized("attendantId")), result) + } + + @Test + fun `getTokenizedFields returns map with ModuleId when only moduleId is not null`() { + val event = getEventDownSyncRequestEvent( + attendantId = null, + moduleId = "moduleId" + ) + + val result = event.getTokenizedFields() + + assertEquals(mapOf(TokenKeyType.ModuleId to TokenizableString.Tokenized("moduleId")), result) + } + + @Test + fun `getTokenizedFields returns map with AttendantId and ModuleId when both are not null`() { + val event = getEventDownSyncRequestEvent( + attendantId = "attendantId", + moduleId = "moduleId" + ) + + val result = event.getTokenizedFields() + + assertEquals(mapOf( + TokenKeyType.AttendantId to TokenizableString.Tokenized("attendantId"), + TokenKeyType.ModuleId to TokenizableString.Tokenized("moduleId") + ), result) + } + + private fun getEventDownSyncRequestEvent( + attendantId: String? = null, + moduleId: String? = null + ): EventDownSyncRequestEvent { + return EventDownSyncRequestEvent( + payload = EventDownSyncRequestEvent.EventDownSyncRequestPayload( + createdAt = mockk(), + endedAt = mockk(), + requestId = "requestId", + queryParameters = QueryParameters( + moduleId = moduleId, + attendantId = attendantId + ), + responseStatus = 200, + errorType = "errorType", + msToFirstResponseByte = 1000, + eventsRead = 1, + eventVersion = 1 + ), + type = EventType.EVENT_DOWN_SYNC_REQUEST + ) + } +} \ No newline at end of file diff --git a/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16Test.kt b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16Test.kt new file mode 100644 index 0000000000..a857958bc9 --- /dev/null +++ b/infra/events/src/test/java/com/simprints/infra/events/event/local/migrations/EventMigration15to16Test.kt @@ -0,0 +1,104 @@ +package com.simprints.infra.events.event.local.migrations + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.simprints.core.tools.extentions.getStringWithColumnName +import com.simprints.infra.events.event.local.EventRoomDatabase +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EventMigration15to16Test { + + @get:Rule + val helper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + EventRoomDatabase::class.java, + ) + + @Test + fun `should rename FACE_LICENSE_MISSING and FACE_LICENSE_INVALID to LICENSE_MISSING and LICENSE_INVALID`() { + val missingLicenseEventId = "event-id-missing" + val eventWithFaceLicenseMissing = + createEvent(missingLicenseEventId, "FACE_LICENSE_MISSING") + val invalidLicenseEventId = "event-id-invalid" + val eventWithFaceLicenseInvalid = + createEvent(invalidLicenseEventId, "FACE_LICENSE_INVALID") + + helper.createDatabase(EventMigration15to16.TABLE_NAME, 15).apply { + insert( + EventMigration15to16.TABLE_NAME, + SQLiteDatabase.CONFLICT_NONE, + eventWithFaceLicenseMissing + ) + insert( + EventMigration15to16.TABLE_NAME, + SQLiteDatabase.CONFLICT_NONE, + eventWithFaceLicenseInvalid + ) + close() + } + val db = helper.runMigrationsAndValidate( + EventMigration15to16.TABLE_NAME, + 16, + true, + EventMigration15to16() + ) + MigrationTestingTools.retrieveCursorWithEventById(db, missingLicenseEventId).use { event -> + // the event json doesn't contain FACE_LICENSE_MISSING + assertThat(event.getStringWithColumnName("eventJson")).contains( + "\"type\":\"LICENSE_MISSING\"" + ) + + } + MigrationTestingTools.retrieveCursorWithEventById(db, invalidLicenseEventId).use { event -> + // the event json doesn't contain FACE_LICENSE_INVALID + assertThat(event.getStringWithColumnName("eventJson")).contains( + "\"type\":\"LICENSE_INVALID\"" + ) + } + helper.closeWhenFinished(db) + } + + @Test + fun `should not rename FACE_LICENSE_MISSING and FACE_LICENSE_INVALID to LICENSE_MISSING and LICENSE_INVALID if they are not present`() { + val eventWithDifferentError = createEvent("event-id-different", "DIFFERENT_ERROR") + + helper.createDatabase(EventMigration15to16.TABLE_NAME, 15).apply { + insert( + EventMigration15to16.TABLE_NAME, + SQLiteDatabase.CONFLICT_NONE, + eventWithDifferentError + ) + close() + } + val db = helper.runMigrationsAndValidate( + EventMigration15to16.TABLE_NAME, + 16, + true, + EventMigration15to16() + ) + MigrationTestingTools.retrieveCursorWithEventById(db, "event-id-different").use { event -> + // the event json doesn't contain FACE_LICENSE_MISSING or FACE_LICENSE_INVALID + assertThat(event.getStringWithColumnName("eventJson")).contains( + "\"type\":\"DIFFERENT_ERROR\"" + ) + } + helper.closeWhenFinished(db) + } + + private fun createEvent(id: String, errorType: String): ContentValues { + return ContentValues().apply { + put("id", id) + put("createdAt_unixMs", 0) + put("createdAt_isTrustworthy", 0) + put("type", "ALERT_SCREEN") + put("eventJson", "{\"type\":\"$errorType\"}") + } + } +} diff --git a/infra/events/src/test/resources/all-events/license_check_v1.json b/infra/events/src/test/resources/all-events/license_check_v1.json new file mode 100644 index 0000000000..15b8484999 --- /dev/null +++ b/infra/events/src/test/resources/all-events/license_check_v1.json @@ -0,0 +1,17 @@ +{ + "id": "ab03f6c5-57ce-4518-9346-5b48e90953f0", + "type": "LICENSE_CHECK", + "labels": { + "projectId": "TEST6Oai41ps1pBNrzBL", + "sessionId": "8bc5f4a2-859e-4da7-80e5-a6d233d0a065", + "deviceId": "f2fd8393c0a0be67" + }, + "payload": { + "type": "LICENSE_CHECK", + "eventVersion": 1, + "createdAt": 1345678901, + "endedAt": 0, + "status": "VALID", + "vendor": "NEC_FINGERPRINT" + } +} diff --git a/infra/images/build.gradle.kts b/infra/images/build.gradle.kts index ff130498ee..96e1856e74 100644 --- a/infra/images/build.gradle.kts +++ b/infra/images/build.gradle.kts @@ -7,17 +7,6 @@ plugins { android { namespace = "com.simprints.infra.images" - buildTypes { - getByName("release") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "60L") - } - getByName("staging") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - getByName("debug") { - buildConfigField("long", "SYNC_PERIODIC_WORKER_INTERVAL_MINUTES", "15L") - } - } } dependencies { @@ -30,6 +19,5 @@ dependencies { implementation(libs.androidX.security) implementation(libs.kotlin.coroutinesPlayServices) - implementation(libs.workManager.work) } diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt index ad267970f5..6db5067cda 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImageRepositoryImpl.kt @@ -34,11 +34,12 @@ class ImageRepositoryImpl @Inject internal constructor( localDataSource.deleteImage(imageRef) } else { allImagesUploaded = false + Simber.e("Failed to upload image without exception") } } } catch (t: Throwable) { allImagesUploaded = false - Simber.d(t) + Simber.e(t) } } diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImageUpSyncScheduler.kt b/infra/images/src/main/java/com/simprints/infra/images/ImageUpSyncScheduler.kt deleted file mode 100644 index 5875c58b74..0000000000 --- a/infra/images/src/main/java/com/simprints/infra/images/ImageUpSyncScheduler.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.simprints.infra.images - -interface ImageUpSyncScheduler { - - /** - * Schedules the background worker if there is none. - */ - suspend fun scheduleImageUpSync() - - /** - * Fully reschedule the background worker. - * Should be used in when the configuration that affects scheduling has changed. - */ - suspend fun rescheduleImageUpSync() - fun cancelImageUpSync() - -} diff --git a/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt b/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt index 970bd933d4..8a45f9cfd3 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/ImagesModule.kt @@ -4,7 +4,6 @@ import com.simprints.infra.images.local.ImageLocalDataSource import com.simprints.infra.images.local.ImageLocalDataSourceImpl import com.simprints.infra.images.remote.ImageRemoteDataSource import com.simprints.infra.images.remote.ImageRemoteDataSourceImpl -import com.simprints.infra.images.worker.ImageUpSyncSchedulerImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -17,9 +16,6 @@ abstract class ImagesModule { @Binds internal abstract fun bindImageRepository(impl: ImageRepositoryImpl): ImageRepository - @Binds - internal abstract fun bindImageUpSyncScheduler(impl: ImageUpSyncSchedulerImpl): ImageUpSyncScheduler - @Binds internal abstract fun bindImageLocalDataSource(impl: ImageLocalDataSourceImpl): ImageLocalDataSource diff --git a/infra/images/src/main/java/com/simprints/infra/images/model/Path.kt b/infra/images/src/main/java/com/simprints/infra/images/model/Path.kt index 2feeea5cad..f6d36b6ead 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/model/Path.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/model/Path.kt @@ -1,8 +1,10 @@ package com.simprints.infra.images.model import android.os.Parcelable +import androidx.annotation.Keep import kotlinx.parcelize.Parcelize import java.io.File +import java.io.Serializable /** * An abstraction of a file path @@ -12,9 +14,9 @@ import java.io.File * e.g.: for dir1/dir2/file.txt [parts] should be * @sample [arrayOf("dir1", "dir2", "file.txt")] */ +@Keep @Parcelize -data class Path(val parts: Array) : Parcelable { - +data class Path(val parts: Array) : Parcelable, Serializable { /** * Constructor with a string path * @param pathString the path as a string diff --git a/infra/images/src/main/java/com/simprints/infra/images/model/SecuredImageRef.kt b/infra/images/src/main/java/com/simprints/infra/images/model/SecuredImageRef.kt index fbab87af8a..b2c2cac6ae 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/model/SecuredImageRef.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/model/SecuredImageRef.kt @@ -1,6 +1,6 @@ package com.simprints.infra.images.model -import com.google.errorprone.annotations.Keep +import androidx.annotation.Keep @Keep data class SecuredImageRef(override val relativePath: Path) : ImageRef(relativePath) diff --git a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt index b54d396a12..117e202478 100644 --- a/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt +++ b/infra/images/src/main/java/com/simprints/infra/images/remote/ImageRemoteDataSourceImpl.kt @@ -24,8 +24,10 @@ internal class ImageRemoteDataSourceImpl @Inject constructor( return if (firebaseProjectName != null) { val projectId = authStore.signedInProjectId - if (projectId.isEmpty()) + if (projectId.isEmpty()) { + Simber.i("AuthStore projectId is empty") return UploadResult(imageRef, UploadResult.Status.FAILED) + } val bucketUrl = imageUrlProvider.getProject(projectId).imageBucket @@ -51,6 +53,7 @@ internal class ImageRemoteDataSourceImpl @Inject constructor( UploadResult(imageRef, status) } else { + Simber.i("Firebase projectId is null") UploadResult(imageRef, UploadResult.Status.FAILED) } } diff --git a/infra/images/src/main/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImpl.kt b/infra/images/src/main/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImpl.kt deleted file mode 100644 index f0f3e70d5f..0000000000 --- a/infra/images/src/main/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImpl.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.simprints.infra.images.worker - -import android.content.Context -import androidx.work.* -import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS -import com.simprints.infra.config.store.ConfigRepository -import com.simprints.infra.config.store.models.imagesUploadRequiresUnmeteredConnection -import com.simprints.infra.images.BuildConfig -import com.simprints.infra.images.ImageUpSyncScheduler -import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -internal class ImageUpSyncSchedulerImpl @Inject constructor( - @ApplicationContext context: Context, - private val configRepo: ConfigRepository, -) : ImageUpSyncScheduler { - - private val workManager = WorkManager.getInstance(context) - - override suspend fun scheduleImageUpSync() { - workManager.enqueueUniquePeriodicWork( - WORK_NAME, - ExistingPeriodicWorkPolicy.UPDATE, - buildWork() - ) - } - - override suspend fun rescheduleImageUpSync() { - workManager.enqueueUniquePeriodicWork( - WORK_NAME, - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - buildWork() - ) - } - - override fun cancelImageUpSync() { - workManager.cancelUniqueWork(WORK_NAME) - } - - private suspend fun buildWork(): PeriodicWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType( - if (configRepo.getProjectConfiguration().imagesUploadRequiresUnmeteredConnection()) NetworkType.UNMETERED - else NetworkType.CONNECTED - ) - .build() - - return PeriodicWorkRequestBuilder( - SYNC_REPEAT_INTERVAL, - SYNC_REPEAT_UNIT - ) - .setConstraints(constraints) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - MIN_BACKOFF_MILLIS, - TimeUnit.MILLISECONDS - ).build() - } - - companion object { - - private const val WORK_NAME = "image-upsync-work-v2" - private const val SYNC_REPEAT_INTERVAL = - BuildConfig.SYNC_PERIODIC_WORKER_INTERVAL_MINUTES - private val SYNC_REPEAT_UNIT = TimeUnit.MINUTES - } -} diff --git a/infra/images/src/test/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImplTest.kt b/infra/images/src/test/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImplTest.kt deleted file mode 100644 index 2ef5f7979e..0000000000 --- a/infra/images/src/test/java/com/simprints/infra/images/worker/ImageUpSyncSchedulerImplTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.simprints.infra.images.worker - -import android.content.Context -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest -import androidx.work.WorkManager -import com.simprints.infra.config.store.ConfigRepository -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Assert.* - -import org.junit.Before -import org.junit.Test - -internal class ImageUpSyncSchedulerImplTest { - - @MockK - private lateinit var ctx: Context - - @MockK - private lateinit var workManager: WorkManager - - @MockK - private lateinit var configRepository: ConfigRepository - - private lateinit var scheduler: ImageUpSyncSchedulerImpl - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - mockkStatic(WorkManager::class) - every { WorkManager.getInstance(ctx) } returns workManager - - scheduler = ImageUpSyncSchedulerImpl(ctx, configRepository) - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `should schedule worker to upload on any network`() = runTest { - coEvery { configRepository.getProjectConfiguration().synchronization.up.imagesRequireUnmeteredConnection } returns false - - scheduler.scheduleImageUpSync() - - coVerify { - workManager.enqueueUniquePeriodicWork( - any(), - any(), - match { - it.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED - } - ) - } - } - - @Test - fun `should schedule worker to upload only on unmetered network`() = runTest { - coEvery { configRepository.getProjectConfiguration().synchronization.up.imagesRequireUnmeteredConnection } returns true - - scheduler.scheduleImageUpSync() - - coVerify { - workManager.enqueueUniquePeriodicWork( - any(), - any(), - match { - it.workSpec.constraints.requiredNetworkType == NetworkType.UNMETERED - } - ) - } - } - - @Test - fun `should cancel and reschedule worker`() = runTest { - coEvery { configRepository.getProjectConfiguration().synchronization.up.imagesRequireUnmeteredConnection } returns true - - scheduler.rescheduleImageUpSync() - - coVerify { - workManager.enqueueUniquePeriodicWork( - any(), - ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, - any() - ) - } - } - - @Test - fun `should cancel worker`() = runTest { - scheduler.cancelImageUpSync() - - coVerify { - workManager.cancelUniqueWork(any()) - } - } - -} diff --git a/infra/license/build.gradle.kts b/infra/license/build.gradle.kts index 3af8bd0e47..ab9da09b6f 100644 --- a/infra/license/build.gradle.kts +++ b/infra/license/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { implementation(project(":infra:auth-store")) + implementation(project(":infra:events")) implementation(libs.retrofit.core) implementation(libs.jackson.core) implementation(libs.androidX.security) diff --git a/infra/license/src/androidTest/java/com/simprints/infra/license/LicenseLocalDataSourceImplTest.kt b/infra/license/src/androidTest/java/com/simprints/infra/license/LicenseLocalDataSourceImplTest.kt deleted file mode 100644 index 6b922b1a86..0000000000 --- a/infra/license/src/androidTest/java/com/simprints/infra/license/LicenseLocalDataSourceImplTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.simprints.infra.license - -import android.app.Application -import androidx.test.core.app.ApplicationProvider -import com.google.common.truth.Truth.assertThat -import com.simprints.infra.license.local.LicenseLocalDataSource -import com.simprints.infra.license.local.LicenseLocalDataSourceImpl -import io.mockk.mockk -import io.mockk.spyk -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import java.io.File -import java.util.UUID - -class LicenseLocalDataSourceImplTest { - private val app = ApplicationProvider.getApplicationContext() - private val filesFolder = "${app.filesDir}/${LicenseLocalDataSource.LICENSES_FOLDER}" - private val licenseVendor = Vendor("vendor1") - private val filePath = "$filesFolder/$licenseVendor" - private val licenseLocalDataSourceImpl = spyk( - LicenseLocalDataSourceImpl(app, mockk(), UnconfinedTestDispatcher()) - ) - - @Before - fun setUp() { - File(filesFolder).deleteRecursively() - } - - @Test - fun givenALicense_storeItEncrypted() = runTest { - val license = UUID.randomUUID().toString() - licenseLocalDataSourceImpl.saveLicense(licenseVendor,license) - val savedLicense = String(File(filePath).readBytes()) - - assertThat(savedLicense).isNotEqualTo(license) - } - - @Test - fun givenAnEncryptedFile_decryptIt_shouldReturnTheRightContent() = runTest { - val license = UUID.randomUUID().toString() - licenseLocalDataSourceImpl.saveLicense(licenseVendor,license) - - val licenseRead = licenseLocalDataSourceImpl.getLicense(licenseVendor) - assertThat(licenseRead).isEqualTo(license) - } - - -} diff --git a/infra/license/src/main/java/com/simprints/infra/license/LicenseRepository.kt b/infra/license/src/main/java/com/simprints/infra/license/LicenseRepository.kt index eab0231626..c3afea1902 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/LicenseRepository.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/LicenseRepository.kt @@ -1,5 +1,6 @@ package com.simprints.infra.license +import com.simprints.infra.license.remote.License import kotlinx.coroutines.flow.Flow interface LicenseRepository { @@ -10,7 +11,7 @@ interface LicenseRepository { licenseVendor: Vendor, ): Flow - suspend fun getCachedLicense(licenseVendor: Vendor): String + suspend fun getCachedLicense(licenseVendor: Vendor): License? suspend fun deleteCachedLicense(licenseVendor: Vendor) - + suspend fun deleteCachedLicenses() } diff --git a/infra/license/src/main/java/com/simprints/infra/license/LicenseRepositoryImpl.kt b/infra/license/src/main/java/com/simprints/infra/license/LicenseRepositoryImpl.kt index 6feb887917..6bbc9a4628 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/LicenseRepositoryImpl.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/LicenseRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.simprints.infra.license import com.simprints.infra.license.local.LicenseLocalDataSource import com.simprints.infra.license.remote.ApiLicenseResult +import com.simprints.infra.license.remote.License import com.simprints.infra.license.remote.LicenseRemoteDataSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector @@ -25,16 +26,16 @@ internal class LicenseRepositoryImpl @Inject constructor( if (license == null) { emit(LicenseState.Downloading) licenseRemoteDataSource.getLicense(projectId, deviceId, licenseVendor) - .let { apiLicenseResult -> - when (apiLicenseResult) { + .let { result -> + when (result) { is ApiLicenseResult.Success -> handleLicenseResultSuccess( licenseVendor, - apiLicenseResult + result.license ) - is ApiLicenseResult.Error -> handleLicenseResultError(apiLicenseResult) + is ApiLicenseResult.Error -> handleLicenseResultError(result) is ApiLicenseResult.BackendMaintenanceError -> handleLicenseResultBackendMaintenanceError( - apiLicenseResult + result ) } } @@ -49,17 +50,16 @@ internal class LicenseRepositoryImpl @Inject constructor( * @param licenseVendor * @return cached license as [String] */ - override suspend fun getCachedLicense(licenseVendor: Vendor): String = + override suspend fun getCachedLicense(licenseVendor: Vendor)= licenseLocalDataSource.getLicense(licenseVendor) - ?: throw IllegalStateException("No cached license found") private suspend fun FlowCollector.handleLicenseResultSuccess( licenseVendor: Vendor, - apiLicenseResult: ApiLicenseResult.Success + apiLicenseResult: License ) { - licenseLocalDataSource.saveLicense(licenseVendor, apiLicenseResult.licenseJson) - emit(LicenseState.FinishedWithSuccess(apiLicenseResult.licenseJson)) + licenseLocalDataSource.saveLicense(licenseVendor, apiLicenseResult) + emit(LicenseState.FinishedWithSuccess(apiLicenseResult)) } private suspend fun FlowCollector.handleLicenseResultError(apiLicenseResult: ApiLicenseResult.Error) { @@ -74,4 +74,5 @@ internal class LicenseRepositoryImpl @Inject constructor( override suspend fun deleteCachedLicense(licenseVendor: Vendor) = licenseLocalDataSource.deleteCachedLicense(licenseVendor) + override suspend fun deleteCachedLicenses() = licenseLocalDataSource.deleteCachedLicenses() } diff --git a/infra/license/src/main/java/com/simprints/infra/license/LicenseState.kt b/infra/license/src/main/java/com/simprints/infra/license/LicenseState.kt index ded3ae7b3a..8a5bc29764 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/LicenseState.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/LicenseState.kt @@ -1,9 +1,11 @@ package com.simprints.infra.license +import com.simprints.infra.license.remote.License + sealed class LicenseState { - object Started : LicenseState() - object Downloading : LicenseState() - data class FinishedWithSuccess(val license: String) : LicenseState() + data object Started : LicenseState() + data object Downloading : LicenseState() + data class FinishedWithSuccess(val license: License) : LicenseState() data class FinishedWithError(val errorCode: String) : LicenseState() data class FinishedWithBackendMaintenanceError(val estimatedOutage: Long?) : LicenseState() { } diff --git a/infra/license/src/main/java/com/simprints/infra/license/LicenseUtils.kt b/infra/license/src/main/java/com/simprints/infra/license/LicenseUtils.kt new file mode 100644 index 0000000000..2b45875811 --- /dev/null +++ b/infra/license/src/main/java/com/simprints/infra/license/LicenseUtils.kt @@ -0,0 +1,31 @@ +package com.simprints.infra.license + +import com.simprints.infra.license.remote.License +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + + +private fun License.isExpired(): Boolean { + if (expiration.isEmpty()) return false + + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + format.timeZone = TimeZone.getTimeZone("UTC") + val expiryDate = try { + format.parse(expiration) + } catch (e: ParseException) { + null + } + // if expiry date is null, consider it as not expired to avoid blocking the app and let the bio sdk handle it + // if expiry date is before current date, consider it as expired + return expiryDate?.before(Date()) ?: false +} + +fun License?.determineLicenseStatus() = when { + this == null -> LicenseStatus.MISSING + isExpired() -> LicenseStatus.EXPIRED + data.isEmpty() -> LicenseStatus.INVALID + else -> LicenseStatus.VALID +} diff --git a/infra/license/src/main/java/com/simprints/infra/license/SaveLicenseCheckEventUseCase.kt b/infra/license/src/main/java/com/simprints/infra/license/SaveLicenseCheckEventUseCase.kt new file mode 100644 index 0000000000..e0d5555427 --- /dev/null +++ b/infra/license/src/main/java/com/simprints/infra/license/SaveLicenseCheckEventUseCase.kt @@ -0,0 +1,35 @@ +package com.simprints.infra.license + +import com.simprints.core.tools.time.TimeHelper +import com.simprints.infra.events.SessionEventRepository +import com.simprints.infra.events.event.domain.models.LicenseCheckEvent +import javax.inject.Inject + +class SaveLicenseCheckEventUseCase @Inject constructor( + private val eventRepository: SessionEventRepository, + private val timeHelper: TimeHelper +) { + suspend operator fun invoke(vendor: Vendor, status: LicenseStatus) { + val licenseCheckEvent = LicenseCheckEvent(timeHelper.now(), status.toEventStatus(), vendor.value) + eventRepository.addOrUpdateEvent(licenseCheckEvent) + } + + private fun LicenseStatus.toEventStatus() = when (this) { + LicenseStatus.VALID -> LicenseCheckEvent.LicenseStatus.VALID + LicenseStatus.INVALID -> LicenseCheckEvent.LicenseStatus.INVALID + LicenseStatus.EXPIRED -> LicenseCheckEvent.LicenseStatus.EXPIRED + LicenseStatus.MISSING -> LicenseCheckEvent.LicenseStatus.MISSING + LicenseStatus.ERROR -> LicenseCheckEvent.LicenseStatus.ERROR + } + +} +/** + * Represents the status of a license. + */ +enum class LicenseStatus { + VALID, + INVALID, + EXPIRED, + MISSING, + ERROR, +} diff --git a/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSource.kt b/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSource.kt index 3371797665..d7e05746a6 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSource.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSource.kt @@ -1,14 +1,17 @@ package com.simprints.infra.license.local import com.simprints.infra.license.Vendor +import com.simprints.infra.license.remote.License internal interface LicenseLocalDataSource { - suspend fun getLicense(vendor: Vendor): String? + suspend fun getLicense(vendor: Vendor): License? - suspend fun saveLicense(vendor: Vendor, license: String) + suspend fun saveLicense(vendor: Vendor, license: License) suspend fun deleteCachedLicense(vendor: Vendor) + suspend fun deleteCachedLicenses() + companion object { const val LICENSES_FOLDER = "licenses" } diff --git a/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSourceImpl.kt b/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSourceImpl.kt index 742e1c4e6f..56b2e68def 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSourceImpl.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/local/LicenseLocalDataSourceImpl.kt @@ -4,6 +4,7 @@ import android.content.Context import com.simprints.core.DispatcherIO import com.simprints.infra.license.Vendor import com.simprints.infra.license.local.LicenseLocalDataSource.Companion.LICENSES_FOLDER +import com.simprints.infra.license.remote.License import com.simprints.infra.logging.Simber import com.simprints.infra.security.SecurityManager import dagger.hilt.android.qualifiers.ApplicationContext @@ -20,10 +21,23 @@ internal class LicenseLocalDataSourceImpl @Inject constructor( private val licenseDirectoryPath = "${context.filesDir}/${LICENSES_FOLDER}" - - override suspend fun getLicense(vendor: Vendor): String? = withContext(dispatcherIo) { + override suspend fun getLicense(vendor: Vendor): License? = withContext(dispatcherIo) { renameOldRocLicense()// TODO: remove this after a few releases when all users have migrated to the 2023.3.0 version - getFileFromStorage(vendor) + val expirationDate = getExpirationDate(vendor) + val licenseData = getFileFromStorage(vendor) + licenseData?.let { License(expirationDate, it) } + } + + private fun getExpirationDate(vendor: Vendor): String { + // if the vendor.expiration file exists, read the expiration date from it else return an empty string + // expiration date is stored in a file with the vendor name and .expiration extension + // no need to encrypt the expiration date as it is not sensitive information + val expirationFile = File("$licenseDirectoryPath/${vendor}.expiration") + return if (expirationFile.exists()) { + expirationFile.readText() + } else { + "" + } } private fun renameOldRocLicense() { @@ -39,20 +53,30 @@ internal class LicenseLocalDataSourceImpl @Inject constructor( } - override suspend fun saveLicense(vendor: Vendor, license: String): Unit = + override suspend fun saveLicense(vendor: Vendor, license: License): Unit = withContext(dispatcherIo) { createDirectoryIfNonExistent(licenseDirectoryPath) - - val file = File("$licenseDirectoryPath/${vendor}") - - try { - keyHelper.getEncryptedFileBuilder(file, context).openFileOutput() - .use { it.write(license.toByteArray()) } - } catch (t: Throwable) { - Simber.e(t) - } + saveLicenseData(vendor, license.data) + saveExpirationDate(vendor, license.expiration) } + private fun saveLicenseData(vendor: Vendor, licenseData: String) { + val file = File("$licenseDirectoryPath/${vendor}") + try { + keyHelper.getEncryptedFileBuilder(file, context).openFileOutput() + .use { it.write(licenseData.toByteArray()) } + } catch (t: Throwable) { + Simber.e(t) + } + } + private fun saveExpirationDate(vendor: Vendor, expirationDate: String) { + val expirationFile = File("$licenseDirectoryPath/${vendor}.expiration") + try { + expirationFile.writeText(expirationDate) + } catch (t: Throwable) { + Simber.e(t) + } + } private fun createDirectoryIfNonExistent(path: String) { val directory = File(path) if (!directory.exists()) @@ -68,6 +92,15 @@ internal class LicenseLocalDataSourceImpl @Inject constructor( } } + override suspend fun deleteCachedLicenses(): Unit = withContext(dispatcherIo) { + try { + val deleted = File(licenseDirectoryPath).deleteRecursively() + Simber.d("Deleted all licenses successfully = $deleted") + } catch (t: Throwable) { + Simber.e(t) + } + } + private fun getFileFromStorage(vendor: Vendor): String? = try { val file = File("$licenseDirectoryPath/$vendor") val encryptedFile = keyHelper.getEncryptedFileBuilder(file, context) diff --git a/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicense.kt b/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicense.kt index d295d79f63..043b0fd037 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicense.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicense.kt @@ -9,17 +9,11 @@ import com.simprints.infra.license.Vendor */ @Keep internal data class ApiLicense(val licenses: Map = emptyMap()) { - - /** - * This method gets the correct license data based on which vendor is passed to it. - * If the license doesn't contain data for that vendor, returns an empty string. - */ - fun getLicenseBasedOnVendor(vendor: Vendor) = licenses[vendor]?.data ?: "" - + fun getLicenseBasedOnVendor(vendor: Vendor) = licenses[vendor] } @Keep -internal data class License(val vendor: String, val expiration: String, val data: String) +data class License(val expiration: String, val data: String) /** * BFSID returns an error in the following format: diff --git a/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicenseResult.kt b/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicenseResult.kt index 0774627668..d7e810d4a3 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicenseResult.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/remote/ApiLicenseResult.kt @@ -1,8 +1,7 @@ package com.simprints.infra.license.remote internal sealed class ApiLicenseResult { - data class Success(val licenseJson: String) : ApiLicenseResult() + data class Success(val license: License) : ApiLicenseResult() data class Error(val errorCode: String) : ApiLicenseResult() - //TODO: add a special "Bad network connection" error? data class BackendMaintenanceError(val estimatedOutage: Long? = null) : ApiLicenseResult() } diff --git a/infra/license/src/main/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImpl.kt b/infra/license/src/main/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImpl.kt index 2a178e3fa7..c149778985 100644 --- a/infra/license/src/main/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImpl.kt +++ b/infra/license/src/main/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImpl.kt @@ -13,8 +13,7 @@ import retrofit2.HttpException import javax.inject.Inject internal class LicenseRemoteDataSourceImpl @Inject constructor( - private val authStore: AuthStore, - private val jsonHelper: JsonHelper + private val authStore: AuthStore, private val jsonHelper: JsonHelper ) : LicenseRemoteDataSource { companion object { @@ -23,28 +22,31 @@ internal class LicenseRemoteDataSourceImpl @Inject constructor( } override suspend fun getLicense( - projectId: String, - deviceId: String, - vendor: Vendor + projectId: String, deviceId: String, vendor: Vendor ): ApiLicenseResult = try { getProjectApiClient().executeCall { - val apiLicense = it.getLicense(projectId, deviceId, vendor).parseApiLicense() - ApiLicenseResult.Success(licenseJson = apiLicense.getLicenseBasedOnVendor(vendor)) - } + it.getLicense(projectId, deviceId, vendor).parseApiLicense() + .getLicenseBasedOnVendor(vendor)?.let { apiLicense -> + ApiLicenseResult.Success(apiLicense) + } + } ?: ApiLicenseResult.Error(UNKNOWN_ERROR_CODE) } catch (t: Throwable) { when (t) { is NetworkConnectionException -> { Simber.i(t) ApiLicenseResult.Error(UNKNOWN_ERROR_CODE) } + is BackendMaintenanceException -> { Simber.i(t) ApiLicenseResult.BackendMaintenanceError(t.estimatedOutage) } + is SyncCloudIntegrationException -> { Simber.e(t) handleCloudException(t) } + else -> { Simber.e(t) ApiLicenseResult.Error(UNKNOWN_ERROR_CODE) @@ -58,10 +60,10 @@ internal class LicenseRemoteDataSourceImpl @Inject constructor( * Anything else we can't really recover. */ private fun handleCloudException(exception: SyncCloudIntegrationException): ApiLicenseResult { - return if (exception.httpStatusCode() == AUTHORIZATION_ERROR) - handleRetrofitException(exception.cause as HttpException) - else - ApiLicenseResult.Error(UNKNOWN_ERROR_CODE) + return if (exception.httpStatusCode() == AUTHORIZATION_ERROR) handleRetrofitException( + exception.cause as HttpException + ) + else ApiLicenseResult.Error(UNKNOWN_ERROR_CODE) } private fun handleRetrofitException(exception: HttpException): ApiLicenseResult { diff --git a/infra/license/src/test/java/com/simprints/infra/license/LicenseUtilsTest.kt b/infra/license/src/test/java/com/simprints/infra/license/LicenseUtilsTest.kt new file mode 100644 index 0000000000..5659641ae3 --- /dev/null +++ b/infra/license/src/test/java/com/simprints/infra/license/LicenseUtilsTest.kt @@ -0,0 +1,33 @@ +package com.simprints.infra.license + +import com.google.common.truth.Truth.assertThat +import com.simprints.infra.license.remote.License +import org.junit.Test + +class LicenseUtilsTest { + + + @Test + fun testDetermineLicenseStatus() { + val nullLicense: License? = null + assertThat(nullLicense.determineLicenseStatus()).isEqualTo(LicenseStatus.MISSING) + + val expiredLicense = License("2022-01-01T00:00:00Z", "data") + assertThat(expiredLicense.determineLicenseStatus()).isEqualTo(LicenseStatus.EXPIRED) + + val invalidLicense = License("2223-01-01T00:00:00Z", "") + assertThat(invalidLicense.determineLicenseStatus()).isEqualTo(LicenseStatus.INVALID) + + val validLicense = License("2123-01-01T00:00:00Z", "data") + assertThat(validLicense.determineLicenseStatus()).isEqualTo(LicenseStatus.VALID) + + val validLicenseWithEmptyExpiration = License("", "data") + assertThat(validLicenseWithEmptyExpiration.determineLicenseStatus()).isEqualTo(LicenseStatus.VALID) + + val validLicenseWithBadExpirationDate = License("bad date", "data") + assertThat(validLicenseWithBadExpirationDate.determineLicenseStatus()).isEqualTo( + LicenseStatus.VALID + ) + + } +} diff --git a/infra/license/src/test/java/com/simprints/infra/license/SaveLicenseCheckEventUseCaseTest.kt b/infra/license/src/test/java/com/simprints/infra/license/SaveLicenseCheckEventUseCaseTest.kt new file mode 100644 index 0000000000..8f46958071 --- /dev/null +++ b/infra/license/src/test/java/com/simprints/infra/license/SaveLicenseCheckEventUseCaseTest.kt @@ -0,0 +1,74 @@ +package com.simprints.infra.license + +import com.google.common.truth.Truth +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.LicenseCheckEvent +import io.mockk.MockKAnnotations +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.slot +import kotlinx.coroutines.test.runTest +import org.junit.Before +import kotlin.test.Test + +class SaveLicenseCheckEventUseCaseTest { + + @RelaxedMockK + private lateinit var eventRepository: SessionEventRepository + + @MockK + private lateinit var timeHelper: TimeHelper + + @MockK + private lateinit var now: Timestamp + + private lateinit var saveLicenseCheckEventUseCase: SaveLicenseCheckEventUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this) + saveLicenseCheckEventUseCase = SaveLicenseCheckEventUseCase(eventRepository, timeHelper) + every { timeHelper.now() } returns now + } + + + @Test + fun `invoke adds LicenseCheckEvent with VALID status to eventRepository`() = runTest { + val vendor = Vendor("TestVendor") + val status = LicenseStatus.VALID + + saveLicenseCheckEventUseCase(vendor, status) + + val expectedEvent = + LicenseCheckEvent(now, LicenseCheckEvent.LicenseStatus.VALID, vendor.value) + verifyExpectedEvent(expectedEvent) + + } + + + @Test + fun `invoke adds LicenseCheckEvent with INVALID status to eventRepository`() = runTest { + val status = LicenseStatus.INVALID + val vendor = Vendor("TestVendor") + + saveLicenseCheckEventUseCase(vendor, status) + + val expectedEvent = + LicenseCheckEvent(now, LicenseCheckEvent.LicenseStatus.INVALID, vendor.value) + verifyExpectedEvent(expectedEvent) + } + + private fun verifyExpectedEvent(expectedEvent: LicenseCheckEvent) { + val slot = slot() + coVerify { eventRepository.addOrUpdateEvent(capture(slot)) } + val actualPayload = slot.captured.payload + val expectedPayload = expectedEvent.payload + Truth.assertThat(actualPayload.status).isEqualTo(expectedPayload.status) + Truth.assertThat(actualPayload.vendor).isEqualTo(expectedPayload.vendor) + } + +} diff --git a/infra/license/src/test/java/com/simprints/infra/license/local/LicenseLocalDataSourceImplTest.kt b/infra/license/src/test/java/com/simprints/infra/license/local/LicenseLocalDataSourceImplTest.kt index f82a0e6f2b..86930bd022 100644 --- a/infra/license/src/test/java/com/simprints/infra/license/local/LicenseLocalDataSourceImplTest.kt +++ b/infra/license/src/test/java/com/simprints/infra/license/local/LicenseLocalDataSourceImplTest.kt @@ -5,6 +5,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.security.crypto.EncryptedFile import com.google.common.truth.Truth.assertThat import com.simprints.infra.license.Vendor +import com.simprints.infra.license.remote.License import com.simprints.infra.security.SecurityManager import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations @@ -57,10 +58,10 @@ class LicenseLocalDataSourceImplTest { @Test fun `check saving the file opens a file output`() = runTest { val fileName = "testfile" - localSource.saveLicense(licenseVendor, fileName) + val expirationDate = "2023-01-01" + localSource.saveLicense(licenseVendor, License(expirationDate,fileName)) assert(File("$filesDirPath/${LicenseLocalDataSource.LICENSES_FOLDER}").exists()) - verify(exactly = 1) { encryptedFile.openFileOutput() } } @@ -79,6 +80,13 @@ class LicenseLocalDataSourceImplTest { assertThat(File("${filesDirPath}/${LicenseLocalDataSource.LICENSES_FOLDER}/$licenseVendor").exists()).isFalse() } + @Test + fun `check delete all deletes the dir`() = runTest { + localSource.deleteCachedLicenses() + + assertThat(File("${filesDirPath}/${LicenseLocalDataSource.LICENSES_FOLDER}/$licenseVendor").exists()).isFalse() + } + @Test fun `check getting the file renames old Roc license file to RANK_ONE_FACE `() = runTest { // Create the license folder and the old ROC.lic file diff --git a/infra/license/src/test/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImplTest.kt b/infra/license/src/test/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImplTest.kt index 9345603276..9b2df4961c 100644 --- a/infra/license/src/test/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImplTest.kt +++ b/infra/license/src/test/java/com/simprints/infra/license/remote/LicenseRemoteDataSourceImplTest.kt @@ -19,11 +19,12 @@ import org.junit.Before import org.junit.Test import retrofit2.HttpException import retrofit2.Response -import java.util.* +import java.util.UUID @ExperimentalCoroutinesApi class LicenseRemoteDataSourceImplTest { private val license = UUID.randomUUID().toString() + private val expirationDate = "2023.12.31" private val remoteInterface = mockk() private val simApiClient = mockk>() @@ -48,7 +49,7 @@ class LicenseRemoteDataSourceImplTest { } returns """{ "RANK_ONE_FACE": { "vendor": "RANK_ONE_FACE", - "expiration": "2023.12.31", + "expiration": "$expirationDate", "data": "$license" } } @@ -139,7 +140,7 @@ class LicenseRemoteDataSourceImplTest { RANK_ONE_FACE ) - assertThat(newLicense).isEqualTo(ApiLicenseResult.Success(license)) + assertThat(newLicense).isEqualTo(ApiLicenseResult.Success(License(expirationDate, license))) } @Test diff --git a/infra/license/src/test/java/com/simprints/infra/license/repository/LicenseRepositoryImplTest.kt b/infra/license/src/test/java/com/simprints/infra/license/repository/LicenseRepositoryImplTest.kt index c851bb6738..0f224bbecd 100644 --- a/infra/license/src/test/java/com/simprints/infra/license/repository/LicenseRepositoryImplTest.kt +++ b/infra/license/src/test/java/com/simprints/infra/license/repository/LicenseRepositoryImplTest.kt @@ -6,8 +6,10 @@ import com.simprints.infra.license.LicenseState import com.simprints.infra.license.Vendor import com.simprints.infra.license.local.LicenseLocalDataSource import com.simprints.infra.license.remote.ApiLicenseResult +import com.simprints.infra.license.remote.License import com.simprints.infra.license.remote.LicenseRemoteDataSource import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.test.runTest @@ -16,7 +18,7 @@ import org.junit.Test import java.util.UUID class LicenseRepositoryImplTest { - private val license = UUID.randomUUID().toString() + private val license = License( "2023.12.31", UUID.randomUUID().toString()) private val licenseLocalDataSource: LicenseLocalDataSource = mockk(relaxUnitFun = true) private val licenseRemoteDataSource: LicenseRemoteDataSource = mockk() @@ -147,7 +149,7 @@ class LicenseRepositoryImplTest { } @Test - fun ` test getCachedLicense success`() = runTest { + fun `test getCachedLicense success`() = runTest { // Given coEvery { licenseLocalDataSource.getLicense(RANK_ONE_FACE) } returns license // When @@ -156,14 +158,28 @@ class LicenseRepositoryImplTest { assertThat(cachedLicense).isEqualTo(license) } - @Test(expected = IllegalStateException::class) - fun ` test getCachedLicense failure`() = runTest { + @Test + fun `test getCachedLicense failure`() = runTest { // Given coEvery { licenseLocalDataSource.getLicense(RANK_ONE_FACE) } returns null // When - licenseRepositoryImpl.getCachedLicense(RANK_ONE_FACE) - // Then throw exception + val license= licenseRepositoryImpl.getCachedLicense(RANK_ONE_FACE) + // Then + assertThat(license).isNull() + } + + @Test + fun `deletes cached licence`() = runTest { + licenseRepositoryImpl.deleteCachedLicense(RANK_ONE_FACE) + + coVerify { licenseLocalDataSource.deleteCachedLicense(RANK_ONE_FACE) } + } + + @Test + fun `deletes all cached licence`() = runTest { + licenseRepositoryImpl.deleteCachedLicenses() + coVerify { licenseLocalDataSource.deleteCachedLicenses() } } companion object { diff --git a/infra/logging/build.gradle.kts b/infra/logging/build.gradle.kts index fc00525922..0a3828fd66 100644 --- a/infra/logging/build.gradle.kts +++ b/infra/logging/build.gradle.kts @@ -36,9 +36,7 @@ android { dependencies { implementation(libs.firebase.crashlytics) - implementation(libs.firebase.analytics){ - exclude("com.google.guava") - } + api(libs.firebase.analytics) implementation(libs.firebase.perf) implementation(libs.timber) { exclude("org.jetbrains", "annotations") diff --git a/infra/network/src/main/java/com/simprints/infra/network/SimNetwork.kt b/infra/network/src/main/java/com/simprints/infra/network/SimNetwork.kt index 31404ef262..dbcf83ad00 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/SimNetwork.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/SimNetwork.kt @@ -26,6 +26,7 @@ interface SimNetwork { ): SimApiClient fun getApiBaseUrl(): String + fun getApiBaseUrlPrefix(): String fun setApiBaseUrl(apiBaseUrl: String?) fun resetApiBaseUrl() diff --git a/infra/network/src/main/java/com/simprints/infra/network/SimNetworkImpl.kt b/infra/network/src/main/java/com/simprints/infra/network/SimNetworkImpl.kt index b15df20703..8b9a31448a 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/SimNetworkImpl.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/SimNetworkImpl.kt @@ -29,6 +29,10 @@ internal class SimNetworkImpl @Inject constructor( return baseUrlProvider.getApiBaseUrl() } + override fun getApiBaseUrlPrefix(): String { + return baseUrlProvider.getApiBaseUrlPrefix() + } + override fun setApiBaseUrl(apiBaseUrl: String?) { baseUrlProvider.setApiBaseUrl(apiBaseUrl) } diff --git a/infra/network/src/main/java/com/simprints/infra/network/httpclient/DefaultOkHttpClientBuilder.kt b/infra/network/src/main/java/com/simprints/infra/network/httpclient/DefaultOkHttpClientBuilder.kt index 114283ebcd..3cb20b88bd 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/httpclient/DefaultOkHttpClientBuilder.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/httpclient/DefaultOkHttpClientBuilder.kt @@ -15,7 +15,6 @@ import okio.BufferedSink import okio.GzipSink import okio.buffer import java.io.IOException -import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -28,7 +27,6 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( companion object { const val DEVICE_ID_HEADER = "X-Device-ID" - const val REQUEST_ID_HEADER = "X-Request-ID" const val AUTHORIZATION_HEADER = "Authorization" const val USER_AGENT_HEADER = "User-Agent" @@ -58,23 +56,15 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( addInterceptor(buildAuthenticationInterceptor(authToken)) } } + .addNetworkInterceptor(ChuckerInterceptor.Builder(ctx).build()) + .addInterceptor(buildDeviceIdInterceptor(deviceId)) + .addInterceptor(buildVersionInterceptor(versionName)) + .addInterceptor(buildGZipInterceptor()) .apply { if (BuildConfig.DEBUG_MODE) { addInterceptor(buildSimberLoggingInterceptor()) } } - .addNetworkInterceptor(ChuckerInterceptor.Builder(ctx).build()) - .addInterceptor(buildRequestIdInterceptor()) - .addInterceptor(buildDeviceIdInterceptor(deviceId)) - .addInterceptor(buildVersionInterceptor(versionName)) - .addInterceptor(buildGZipInterceptor()) - - private fun buildRequestIdInterceptor() = Interceptor { chain -> - val newRequest = chain.request().newBuilder() - .addHeader(REQUEST_ID_HEADER, UUID.randomUUID().toString()) - .build() - return@Interceptor chain.proceed(newRequest) - } private fun buildAuthenticationInterceptor(authToken: String) = Interceptor { chain -> val newRequest = chain.request().newBuilder() diff --git a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProvider.kt b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProvider.kt index 2d6d73f461..06475a448a 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProvider.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProvider.kt @@ -2,6 +2,7 @@ package com.simprints.infra.network.url internal interface BaseUrlProvider { fun getApiBaseUrl(): String + fun getApiBaseUrlPrefix(): String fun setApiBaseUrl(apiBaseUrl: String?) fun resetApiBaseUrl() } diff --git a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt index e2f25cf892..8ef127001e 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt @@ -3,18 +3,26 @@ package com.simprints.infra.network.url import android.content.Context import android.content.SharedPreferences import androidx.annotation.VisibleForTesting +import androidx.core.content.edit +import com.simprints.infra.logging.Simber import com.simprints.infra.network.BuildConfig import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +import javax.inject.Singleton -internal class BaseUrlProviderImpl @Inject constructor(@ApplicationContext context: Context) : BaseUrlProvider { +@Singleton +internal class BaseUrlProviderImpl @Inject constructor( + @ApplicationContext context: Context, +) : BaseUrlProvider { companion object { + private const val PREF_FILE_NAME = "b3f0cf9b-4f3f-4c5b-bf85-7b1f44eddd7a" private const val PREF_MODE = Context.MODE_PRIVATE private const val API_BASE_URL_KEY = "ApiBaseUrl" private const val API_VERSION = "v2" private const val BASE_URL_SUFFIX = "/androidapi/$API_VERSION/" + @VisibleForTesting const val DEFAULT_BASE_URL = "https://${BuildConfig.BASE_URL_PREFIX}.simprints-apis.com$BASE_URL_SUFFIX" @@ -24,6 +32,11 @@ internal class BaseUrlProviderImpl @Inject constructor(@ApplicationContext conte val prefs: SharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_MODE) override fun getApiBaseUrl(): String = prefs.getString(API_BASE_URL_KEY, DEFAULT_BASE_URL)!! + .also { Simber.d("API base URL is $it") } + + override fun getApiBaseUrlPrefix(): String = prefs.getString(API_BASE_URL_KEY, DEFAULT_BASE_URL) + ?.removeSuffix(BASE_URL_SUFFIX) + ?.also { Simber.d("API base URL prefix is $it") }!! override fun setApiBaseUrl(apiBaseUrl: String?) { val prefix = "https://" @@ -36,11 +49,15 @@ internal class BaseUrlProviderImpl @Inject constructor(@ApplicationContext conte DEFAULT_BASE_URL } - prefs.edit().putString(API_BASE_URL_KEY, newValue).apply() - } + Simber.e("Setting API base URL to $newValue") - override fun resetApiBaseUrl() = - prefs.edit().putString(API_BASE_URL_KEY, DEFAULT_BASE_URL).apply() + prefs.edit(commit = true) { putString(API_BASE_URL_KEY, newValue) } + Simber.e("Setting API base URL to ${getApiBaseUrl()}") + } + override fun resetApiBaseUrl() { + Simber.e("Resetting API base") + prefs.edit(commit = true) { putString(API_BASE_URL_KEY, DEFAULT_BASE_URL) } + } } diff --git a/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt b/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt index 361ad60672..2d52dc24e2 100644 --- a/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt +++ b/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt @@ -14,7 +14,10 @@ import org.junit.Test class BaseUrlProviderImplTest { companion object { + private const val URL_SUFFIX = "/androidapi/v2/" + private const val URL = "https://test" + private const val URL_WITH_SUFFIX = "https://test$URL_SUFFIX" } @RelaxedMockK @@ -47,6 +50,15 @@ class BaseUrlProviderImplTest { assertThat(url).isEqualTo(URL) } + @Test + fun `get api base url prefix should return the actual url`() { + every { sharedPreferences.getString(any(), any()) } returns URL_WITH_SUFFIX + + val url = baseUrlProviderImpl.getApiBaseUrlPrefix() + + assertThat(url).isEqualTo(URL) + } + @Test fun `set api base url should set the url to the default one when the one passed is null`() { baseUrlProviderImpl.setApiBaseUrl(null) @@ -59,7 +71,7 @@ class BaseUrlProviderImplTest { val url = "https://url.com" baseUrlProviderImpl.setApiBaseUrl(url) - verify(exactly = 1) { editor.putString(any(), "$url/androidapi/v2/") } + verify(exactly = 1) { editor.putString(any(), "$url$URL_SUFFIX") } } @Test @@ -67,7 +79,7 @@ class BaseUrlProviderImplTest { val url = "url.com" baseUrlProviderImpl.setApiBaseUrl(url) - verify(exactly = 1) { editor.putString(any(), "https://$url/androidapi/v2/") } + verify(exactly = 1) { editor.putString(any(), "https://$url$URL_SUFFIX") } } @Test diff --git a/infra/orchestrator-data/build.gradle.kts b/infra/orchestrator-data/build.gradle.kts index 9836315a0d..6d6361d344 100644 --- a/infra/orchestrator-data/build.gradle.kts +++ b/infra/orchestrator-data/build.gradle.kts @@ -13,4 +13,6 @@ dependencies { implementation(project(":infra:events")) implementation(project(":feature:exit-form")) + + implementation(libs.jackson.core) } diff --git a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt index 25bf2fbecc..290908b425 100644 --- a/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt +++ b/infra/orchestrator-data/src/main/java/com/simprints/infra/orchestration/data/ActionRequest.kt @@ -1,10 +1,23 @@ package com.simprints.infra.orchestration.data import androidx.annotation.Keep +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo import com.simprints.core.domain.tokenization.TokenizableString import java.io.Serializable - +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = ActionRequest.EnrolActionRequest::class, name = "EnrolActionRequest"), + JsonSubTypes.Type(value = ActionRequest.IdentifyActionRequest::class, name = "IdentifyActionRequest"), + JsonSubTypes.Type(value = ActionRequest.VerifyActionRequest::class, name = "VerifyActionRequest"), + JsonSubTypes.Type(value = ActionRequest.ConfirmIdentityActionRequest::class, name = "ConfirmIdentityActionRequest"), + JsonSubTypes.Type(value = ActionRequest.EnrolLastBiometricActionRequest::class, name = "EnrolLastBiometricActionRequest") +) sealed class ActionRequest( open val actionIdentifier: ActionRequestIdentifier, open val projectId: String, @@ -18,6 +31,7 @@ sealed class ActionRequest( override val projectId: String, override val userId: TokenizableString, override val moduleId: TokenizableString, + val biometricDataSource: String, val metadata: String, override val unknownExtras: Map, ) : ActionRequest(actionIdentifier, projectId, userId, unknownExtras), FlowAction @@ -28,6 +42,7 @@ sealed class ActionRequest( override val projectId: String, override val userId: TokenizableString, override val moduleId: TokenizableString, + val biometricDataSource: String, val metadata: String, override val unknownExtras: Map, ) : ActionRequest(actionIdentifier, projectId, userId, unknownExtras), FlowAction @@ -38,6 +53,7 @@ sealed class ActionRequest( override val projectId: String, override val userId: TokenizableString, override val moduleId: TokenizableString, + val biometricDataSource: String, val metadata: String, val verifyGuid: String, override val unknownExtras: Map, diff --git a/infra/realm/src/main/java/com/simprints/infra/realm/RealmWrapperImpl.kt b/infra/realm/src/main/java/com/simprints/infra/realm/RealmWrapperImpl.kt index 46957e43db..5467c89e67 100644 --- a/infra/realm/src/main/java/com/simprints/infra/realm/RealmWrapperImpl.kt +++ b/infra/realm/src/main/java/com/simprints/infra/realm/RealmWrapperImpl.kt @@ -102,10 +102,16 @@ class RealmWrapperImpl @Inject constructor( private fun getLocalDbKey(): LocalDbKey = authStore.signedInProjectId.let { return if (it.isNotEmpty()) { - securityManager.getLocalDbKeyOrThrow(it) - } else { - throw RealmUninitialisedException("No signed in project id found") - } + try { + securityManager.getLocalDbKeyOrThrow(it) + } catch (ex: Exception) { + Simber.e(ex) + securityManager.recreateLocalDatabaseKey(it) + securityManager.getLocalDbKeyOrThrow(it) + } + } else { + throw RealmUninitialisedException("No signed in project id found") + } } private fun recreateLocalDbKey() = diff --git a/infra/resources/src/main/res/color/input_text_background.xml b/infra/resources/src/main/res/color/input_text_background.xml new file mode 100644 index 0000000000..a8f23d4fa6 --- /dev/null +++ b/infra/resources/src/main/res/color/input_text_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/infra/resources/src/main/res/values-am-rET/strings.xml b/infra/resources/src/main/res/values-am-rET/strings.xml index 4933537120..9cff19410d 100644 --- a/infra/resources/src/main/res/values-am-rET/strings.xml +++ b/infra/resources/src/main/res/values-am-rET/strings.xml @@ -3,16 +3,25 @@ ማንቂያ + ዝጋ + ፈቃደኝነት + ልመዘግብ%2$s እፈልጋለው%1$s ወደፊት የጣት አሻራዎትን እርስዎን መለየት ይቻል ዘንድ እና የጣት አሻራ መረጃዎትን ለመመዝገብ %1$s እና %2$s መጠቀም እፈልጋለሁ ወደፊት %2$s የእርስዎን መረጃ ለማግኘት የጣት አሻራዎትን መጠቀም እፈልጋለሁ %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። + በተጨማሪም ሲምፕሪንትስ መረጃውን ለጥናት አላማ ሊጠቀምበት ይችላል + ከተስማሙ፤በማንኛዉም ጊዜ ፈቃደኝነቱን በማንኛዉም ጊዜ ማንሳት ይችላሉ እና መረጃውን ማስጠፋት ይችላሉ እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የወላጅ ፈቃደኝነት + ልመዘግብ %2$sእፈልጋለው%1$s እርስዎን %2$s ለመመዝገብ አንዲቻል ዉስጥ %1$s እና ወደፊት መለየት አንዲቻል :: እርስዎን %2$s ወደፊት መለየት አንዲቻል %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። + ከተስማሙ፤ፍቅዱን በማንኛውም ጊዜ ማንሳት ይችላሉ እና የልጅዎንም መረጃ እንዲጠፋ መጠየቅ ይችላሉ የልጅዎን የጣት አሻራ መጠቀም እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የጣት አሻራን ተጠቀም የፊቱን ፎቶግራፍ ማንሳት @@ -34,14 +43,19 @@ የተሳካ ምዝገባ መመዝገብ አልተቻለም 1. %1$s ሪኮርድን ማስቀመጥ አልተሳካም \n 2. ዝጋን መታ ያድርጉ + መመዝገብ አልተቻለም፡%1$sከቆየው ሪከርድ ጋር ድግግሞስ ስለሆነ \n 2. ዝጋ የሚለውን ተጫን ፊት የጣት አሻራ ፊት /የጣት አሻራ የጣት አሻራውን ለምንድን ነው የሚዘሉት? + የጣት አሻራ ለምን ተዘለለ? + ፌስ መመዝገቡ ለምን ተዝለለ? ተጨማሪ ኢንፎርሜሽን + ፊቱን ያዝ የጣት አሻራውን እስካን ያድርጉ + ፊቱን ያዝ ይላኩ እባክዎት ካሉት አማራጮች ይምረጡ እና ይላኩ እባክዎት ቅጽን ይላኩ @@ -59,6 +73,8 @@ ግለሰቡ አልተገኘም አንዲጠራ የተመረጠው ግለሰብ ዳታቤዙ ውስጥ የለም ግለሰቡ የለም፡ እባክዎ ከኢንተርኔት ጋር መገናኘትዎን አረጋግጠው እንደገና ይሞክሩ + እንደገና ይሞክሩ + የፕሮጀክት መለያ ቁጥር የፕሮጀክት ቁልፍ @@ -66,12 +82,19 @@ የፈጣን ምላሽ መለያ እስካን ያድርጉ መግባት የፈጣን ምላሽ ሚስጥር ቁጥርን እስካን በሚያደረግበት ጊዜ ስህተት አጋጥሞታል + ካሜራ ስላልበራ QR code ስካን ማድረግ አልተቻለም + ካሜራዉን ለመክፈት ችግር አጋጥሟል፤እባክዎ ሲስተም አድሚኒስትሬተሩን ያሳዉቁ እስካን የተደረገው የፈጣን ምላሽ ሚስጥር ቁጥር ትክክል አይደለም ተጠቃሚው ተዘሏል ፡ እባክዎ የተጠቃሚውን መለያ ቁጥር ፡ የፕሮጀክት ቁጥር እና ቁልፍ በማረጋገጥ እንደገና ይሞክሩ፡፡ ትክክል ያልሆነ ተጠቃሚ ነው ፡ እባክዎ የፕሮጀክት ቁጥር እና ቁልፍ ያረጋግጡ፡፡ የፕሮጀክት ቁጥሩ አቅራቢው ውስጥ ካለው ዝርዝር ጋር የተለየ ነው፡ እብክዎት የስስተም አስተዳዳሪውን ያነጋግሩ፡፡ አሁን ከግንኙነት ውጭ ነዎት፡ እባክዎት የኢንተርኔት ግንኙነቱን መኖሩን ያረጋግጡ + Integrity Service ለማገኘት ችግር አጋጥሟል፡እባክዎ እንደግና ይሞክሩ ሰርቨሩን ለማግኘት ሲሞክሩ ስህተት አጋጥሞታል ፡ እባክዎት ተንሽ ቆይተው ይሞክሩት፡፡ + የባኪንድ URL ቀይር + አስቀምጥ + default ተጠቀም  + አቋርጥ የመለየት ሰራውን እስኪጠናቀቅ እባክዎት ትንሽ ይጠብቁ @@ -98,12 +121,19 @@ %d ተመራጮች + + ስህተት አጋጥሟል የተግበቦት ስህተት የተለየ የፕሮጀክት መለያ ስለተጠቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ ከሲስተሙ ዋና ገጽ ይውጡ ወይም የቅርብ ተጠሪዎን ያነጋግሩ + ሲምፕሪንትስ ካልሰራ ፤እባክዎ እንደገና ይሞክሩ እና ለሲስተም አሰተዳዳሪው ያሳውቁ ጊዜው ያለፈበት የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ የተጫነው የጉግል ፕለይ አገልግሎት ስሪት በSID ከሚያስፈልገው በላይ የቆየ ነው። የጎግል ፕሌይ አገልግሎቶች ይጎድላሉ የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ አልተጫነም። + \"Google Play Store app\" ጠፍቷል ወይም ጊዜው አልፎበታል + \"Google Play Store app\" አልተጫንም ወይም አፕዴት አልተደረገም + ፕሮጀክቱ እያለቀ ነው + ሴሽን ማስጀመር አልተቻለም ፕሮጀክቱ እያበቃ ስለሆነ፤ ለተጨማሪ እባክዎ የሲስተም አስተዳዳሪዉን ያሳዉቁ ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል ክፍለ-ጊዜን ማስጀመር አልተቻለም ምክንያቱም ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ የተሳሳተ የፕሮጀክት መለያ ስለተተቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ @@ -121,13 +151,16 @@ ማረጋገጫ ተልኳል እርስዎን መልሰው በማዛወር ላይ - - የፊት ሞዳሊቲ አልተገኘም - አስፈላጊ ፋይሎችን በማውረድ ላይ - ፍቃድ አይሰራም/ትክክል አይደለም - የፊት ላይሰንሱ/ ፍቃድ የማይሰራ ነው፤እባክዎ የሲስተም አስተዳዳሪውን ያሳውቁ - የማዋቀር ስህተት - የፊት ሁነታን በማዋቀር ላይ ስህተት ነበር፣እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ + + ሞዳሊቲ በማዋቀር ላይ + አስፈላጊዉን ፈይል በማውረድ ላይ + ጥገና ሞድ + ሲስተሙ ጥገና ላይ ነው፤እባክዎ እንደገና ይሞከሩ + የማይሰራ ላይሰንስ + ላይሰነሱ አይሰራም፤የሲስተም አስተዳዳሪው ያሳውቁ + የማዋቀር ስህተት %1$s + ላይሰንሱ ለማግኘት ችግር አጋጥሟል፤እባከዎ ለሲስተም አስተዳዳሪው ያሳዉቁ + በቂ ብርሀን ያለበት ቦታ መምረጥ @@ -147,6 +180,7 @@ ሰዉየዉ ቀጥታ ካሜራዉን እንዲያይ አድርግ ለማንሳት ተጫን በማንሳት ላይ.... + የካሜራ ፈቃድ አብራ ባለበት ፖዚሽኑ ላይ አቆይ.... የፊት ምስል በስኬት ተመዝግቧል ማረጋገጥ @@ -155,6 +189,7 @@ ስህተት አጋጥሟል + ሲሞፕሪንትስ ካልሰራ፤እባክዎ እንደገና ይሞክሩ እና ለሲስተም አስተዳዳሪው ያሳዉቁ ይዝጉ እንደገና ይሞክሩ የስልክ ማስተካከያ @@ -182,6 +217,7 @@ የሚጠቀሙበት መሳሪያ ሊገናኝ አልቻለም የተገኘው ስካነር %1$s እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ አስተካክለው እንደገና ይሞክሩ የNFC ቺፑን ማንበብ አልተቻለም፤ እባክዎ እንደገና ይሞክሩ የተሳሳተ NFC ቺፕ ተገኝቷል እሽ @@ -189,6 +225,7 @@ የስካነሩን ቁጥር ቀጥሎ ካለው ሳጥን ላይ ይፃፉ ይህ የሚገኘው በስካነሩ የታችኛው ክፍል ነው እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ዝግጁ ሲሆን እንደገና ይሞክሩ እባክዎ የሲሪያል ቁጥሩን 6 አሃዞች ያስገቡ ስካነሩ አፕዴት አድርግ ስካነሩ አፕዴት እስኪጨርስ እንደበራ ይቆይ @@ -212,11 +249,14 @@ \n5. ወደ ሲምፕሪንት ይመለሱ እስካነሩን ያብሩ 1. እስካነሩን ያስነሱ\n2. እንደገና ይሞክሩ + ፈቃድ የለም + ስልኩ ላይ ብሉቱዝ አብራ የሚጠቀሙበት መሳሪያ ብሉቱዝ ለመጠቀም አያስችልም፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ ብሉቱዝ አልተከፈተም ፡ለመጠቀም አያስችልም፤እባክዎ ማስተካከያው ውስጥ ገብተው ብሉቱዙን ከፍተው እንደገና ይሞክሩ በስልኩ ላይ ከአንድ በላይ እስካነር ተገናኝቷል፣ እባክዎ የማይፈልጉትን እስካነር ግንኙነቱን ያቋርጡ + የጣት አሻራ ሞዳሊቲ በማዋቀር ላይ ወደ ሲስተሙ ማስገባት መለየት ማመሳከር @@ -230,6 +270,8 @@ የቀኝ እጅ ሶስተኛ ጣት የቀኝ እጅ አራተኛ ጣት የቀኝ እጅ አምስተኛ ጣት + አቋርጥ + ስህተት አጋጥሟል እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንቱ መስራት ካለቻለ የሲስተም አስተዳዳሪውን ያነጋግረ እባክዎ እስካን ያድርጉ ለቀጣዩ ጣትዎ ወደ ግራ ያንሸራቱ @@ -262,6 +304,9 @@ ፕሮጀክቱ በአሁኑ ጊዜ እያለቀ ስለሆነ ክፍለ-ጊዜን ማስጀመር አልተቻለም። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ መግባት ከፈለጉ፡ እባክዎ የደንበኛውን አፕልኬሽን ከፍተው የሲም ፕሪንትን መለያቁጥር በመጠቀም መግባት ይችላሉ + የስልክመለያ፡ %s + ዝጋ + እስካነር ይጠቀሙ: %1$s አሁን ያለ ተጠቃሚ: %1$s @@ -278,23 +323,29 @@ ሞጁሎች የስልኩ ሴቲንግ በመግባት የሞባይል ዳታ ያብሩ ማስተካከያ + ብዙ ሞጁሎች ወርደዋል + ሲንክ ለማድረግ በቅድሚያ log in ይደረግ + ያለፈው ግንኙነት: %1$s ሁሉም መዝገቦች ተጭነዋል። + + %1$d ለመስቀል መዝገብ + %1$d ለመስቀል መዝገቦች + እንደገና ይሞክሩ የየዕለት ተግባር: %1$s - - - ወደ ሲስተሙ ማስገባት + + %d ወደ ሲስተሙ ማስገባት ወደ ሲስተሙ ማስገባት - - መለየት + + %d መለየት መለየት - - ማመሳከር + + %d ማመሳከር ማመሳከር @@ -315,12 +366,14 @@ የመሳሪያው መለያ እስካን እንዲደረግ የሚፈልጉተን የጣት አሻራ ይምረጡ ሲንክ ላይ ተጨማሪ ዝርዝር መረጃ + ወደ clipboard ተገልብጧል  + ሲንክ የተደረገ መረጃ ሞጅውሎችን ይምረጡ የተመዘገበውን ለመጫን - የተመዘገበውን ለመማውረድ + ሪከርዶች ለማዉረድ ወይም ለመሰረዝ የተመዘገበውን ለመሰረዝ በስልኩ አጠቃላይ የተመዘገበ የተመረጠ ሞጁል @@ -329,6 +382,8 @@ የሚጫኑ ምስሎች አሁን ግንኙነት ፍጠር በግንኙነት ላይ + የሲንኩን መረጃ በመውሰድ ላይ + የተሳሳተ ምርጫ፡ እባክዎት ካለው የምዕራፍ አማራጭ ውስጥ ቢያንስ አንድ ይምረጡ የተሳሳተ ምርጫ ፡ካሉት ምዕራፎች ውስጥ ከ %1$d በላይ መምረጥ አይችሉም @@ -349,6 +404,7 @@ ለመክፈት የይለፍ ቃል ያስገቡ ለመውጣት የይለፍ ቃል ያስገቡ የይለፍ ቃል + አቋርጥ የተሳሳተ የይለፍ ቃል ለመክፈት መታ ያድርጉ @@ -360,10 +416,14 @@ የእርስዎ ፕሮጀክት ወቅታዊ መዛግብት እንዳለው ለማረጋገጥ የውሂብ ማመሳሰል አስፈላጊ ነው። ያለ ትክክለኛ ምክንያት የእርስዎን ውሂብ ማመሳሰል አለመቻል የስራ ባልደረቦችዎ ጠቃሚ መዝገቦችን ማግኘት እና የፕሮጀክቱን ትክክለኛ ሪፖርት የማድረግ ችሎታ ላይ ተጽእኖ ያሳድራል። እና ለእርስዎ እና ለፕሮጀክቱ የማይፈለጉ ውጤቶችን ሊያስከትል ይችላል. መውጣት ጨርሰው መውጣት ይፈልጋሉ + ዘግቶ መዉጣት ሰርዝ ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ቆየት ብለው ይሞክሩ. ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ከ%s በኋላ እንደገና ይሞክሩ + + ሲንክ ማድረግ በሂደት ላይ + diff --git a/infra/resources/src/main/res/values-am/strings.xml b/infra/resources/src/main/res/values-am/strings.xml index 3ace39ce7b..b5599e66f7 100644 --- a/infra/resources/src/main/res/values-am/strings.xml +++ b/infra/resources/src/main/res/values-am/strings.xml @@ -3,8 +3,11 @@ ማንቂያ + ዝጋ + ፈቃደኝነት + ልመዘግብ I %2$s እፈልጋለው %1$s. ወደፊት የጣት አሻራዎትን እርስዎን መለየት ይቻል ዘንድ እና የጣት አሻራ መረጃዎትን ለመመዝገብ %1$s እና %2$s መጠቀም እፈልጋለሁ ወደፊት %2$s የእርስዎን መረጃ ለማግኘት የጣት አሻራዎትን መጠቀም እፈልጋለሁ %1$s :: ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %1$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። @@ -13,8 +16,10 @@ ከተስማሙ ፈቃድዎትን መቀየር እና እርስዎን በሚመለከት ያለው መረጃ እንዲሰረዝ መጠየቅ ይችላሉ: እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የወላጅ ፈቃደኝነት + ልመዝግብ %2$s እፈልጋለው %1$s. እርስዎን %2$s ለመመዝገብ አንዲቻል ዉስጥ %1$s እና ወደፊት መለየት አንዲቻል :: እርስዎን %2$s ወደፊት መለየት አንዲቻል %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። ከተስማሙ ፈቃድዎትን መቀየር እና ልጁን በሚመለከት ያለው መረጃ እንዲሰረዝ መጠየቅ ይችላሉ: የልጅዎን የጣት አሻራ መጠቀም እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ @@ -38,6 +43,7 @@ ምዝገባው ተሳክቷል የመጨረሻውን ባዮሜትሪክስ ለመመዝግብ ማንቂያ 1. %1$s ሪኮርድን ማስቀመጥ አልተሳካም \n 2. ዝጋን መታ ያድርጉ + መመዝገብ አልተቻለም፡%1$s1. መመዝገብ አልተቻለም፡ከቆየው ሪከርድ ጋር ድግግሞስ ስለሆነ \n 2. ዝጋ የሚለውን ተጫን Face የጣት አሻራ የፊት / የጣት አሻራ @@ -67,6 +73,8 @@ ግለሰቡ አልተገኘም አንዲጠራ የተመረጠው ግለሰብ ዳታቤዙ ውስጥ የለም ግለሰቡ የለም፡ እባክዎ ከኢንተርኔት ጋር መገናኘትዎን አረጋግጠው እንደገና ይሞክሩ + እንደገና ይሞክሩ + የፕሮጀክት መለያ ቁጥር የፕሮጀክት ቁልፍ @@ -74,12 +82,22 @@ የፈጣን ምላሽ መለያ እስካን ያድርጉ መግባት የፈጣን ምላሽ ሚስጥር ቁጥርን እስካን በሚያደረግበት ጊዜ ስህተት አጋጥሞታል + የካሜራ ፈቃድ ስላልበራ QR code ስካን ማድረግ አልተቻለም + ካሜራዉን ለማብራት ችግር አጋጥሟል፤ እባክዎ የሲስተም አስተዳደር ያሳዉቁ እስካን የተደረገው የፈጣን ምላሽ ሚስጥር ቁጥር ትክክል አይደለም ተጠቃሚው ተዘሏል ፡ እባክዎ የተጠቃሚውን መለያ ቁጥር ፡ የፕሮጀክት ቁጥር እና ቁልፍ በማረጋገጥ እንደገና ይሞክሩ፡፡ ትክክል ያልሆነ ተጠቃሚ ነው ፡ እባክዎ የፕሮጀክት ቁጥር እና ቁልፍ ያረጋግጡ፡፡ የፕሮጀክት ቁጥሩ አቅራቢው ውስጥ ካለው ዝርዝር ጋር የተለየ ነው፡ እብክዎት የስስተም አስተዳዳሪውን ያነጋግሩ፡፡ አሁን ከግንኙነት ውጭ ነዎት፡ እባክዎት የኢንተርኔት ግንኙነቱን መኖሩን ያረጋግጡ + የ Integrity Service. ስህተት አጋጥሟል. እባክዎ እንደገና ይሞክሩ ሰርቨሩን ለማግኘት ሲሞክሩ ስህተት አጋጥሞታል ፡ እባክዎት ተንሽ ቆይተው ይሞክሩት፡፡ + የባክንድ URL ቀይር + አስቀምጥ + default ተጠቀም +  +  +  + አቋርጥ የመለየት ሰራውን እስኪጠናቀቅ እባክዎት ትንሽ ይጠብቁ @@ -106,12 +124,19 @@ %d ተዛምዶ + + ስህተት ኣጋጥሟል የተግበቦት ስህተት የተለየ የፕሮጀክት መለያ ስለተጠቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ ከሲስተሙ ዋና ገጽ ይውጡ ወይም የቅርብ ተጠሪዎን ያነጋግሩ + እባከዎ እንደገና ይሞክሩ እና ሲምፕሪንትስ አልሰራም ሲስተም አስተዳዳሪውን ያሳዉቁ ጊዜው ያለፈበት የጉግል ፕሌይ አገልግሎቶች በዚህ መሳሪያ ላይ የተጫነው የጉግል ፕለይ አገልግሎት ስሪት በSID ከሚያስፈልገው በላይ የቆየ ነው። የጎግል ፐሌይ አገልግሎቶች ይጎድላሉ የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ አልተጫነም። + ያገልግሎቱ ጊዜውን ያለፈ ወይም ያልተገኘ \"Google Play Store app\" + የ \"Google Play አፕ\" አልተጫነም ወይም ጊዜው አልፎበታል + ባሁኑ ጊዜ ፕሮጀክቱ እያለቀ ነው + ፕሮጀክቱ እያለቀ ስለሆነ session ማስጀመር አልተቻለም፤እባክዎ ሱፐርቫይዘሩን ያናግሩ   ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል ክፍለ-ጊዜን ማስጀመር አልተቻለም ምክንያቱም ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ የተሳሳተ የፕሮጀክት መለያ ስለተተቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ @@ -129,13 +154,16 @@ ማረጋገጫ ተልኳል እርስዎን መልሰው በማዛወር ላይ - - የፊት ሁነታን በማዋቀር ላይ - አስፈላጊ ፋይሎችን በማውረድ ላይ - ፍቃድ ልክ ያልሆነ - የፊት ፈቃድ ልክ ያልሆነ ነው፣ እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ - የማዋቀር ስህተት - የፊት ሁነታን በማዋቀር ላይ ስህተት ነበር፣እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ + + ሞዳሊቲ በማዋቀር ላይ + አስፈላጊ ፋይሎችን በማውረድ ላይ + ጥገና ሞድ + ሲስተሙ ጥገና ላይ ነው፤ እባክዎ ትንስ ቆይተው ይሞክሩ + ፈቃዱ አይሰራም + የማይሰራ ፈቃድ፤እባክዎ ሲስተም አስተዳዳሪውን ያሳውቁ + የተግባቦት ስህተት%1$s + ፈቃዱን ለማግኘት ስህተት አጋጥሟል፣የሲስተም ሱፕርቫይዘሩን ያማክሩ + በቂ ብርሀን ያለበት ቦታ መምረጥ @@ -155,6 +183,7 @@ ሰዉየዉ ቀጥታ ካሜራዉን እንዲያይ አድርግ ለማንሳት ተጫን በማንሳት ላይ.... + እባክዎ የካሜራ ፍቃድ ያብሩ ባለበት ፖዚሽኑ ላይ አቆይ.... የፊት ምስል በስኬት ተመዝግቧል ማረጋገጥ @@ -163,6 +192,7 @@ ስህተት አጋጥሟል + እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንትሱ ካልሰራ የሲስተም አስተዳደር ያሳውቁ ይዝጉ እንደገና ይሞክሩ የስልክ ማስተካከያ @@ -190,6 +220,7 @@ የሚጠቀሙበት መሳሪያ ሊገናኝ አልቻለም የተገኘው ስካነር %1$s እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ትንስ ቆይተው ይሞክሩ እና የማጣመር ጥያቄውን ይቀበሉ የNFC ቺፑን ማንበብ አልተቻለም፤ እባክዎ እንደገና ይሞክሩ የተሳሳተ NFC ቺፕ ተገኝቷል እሽ @@ -197,6 +228,7 @@ የስካነሩን ቁጥር ቀጥሎ ካለው ሳጥን ላይ ይፃፉ ይህ የሚገኘው በስካነሩ የታችኛው ክፍል ነው እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ትንሽ ቆይተው ይሞክሩ እባክዎ የሲሪያል ቁጥሩን 6 አሃዞች ያስገቡ ስካነሩ አፕዴት አድርግ ስካነሩ አፕዴት እስኪጨርስ እንደበራ ይቆይ @@ -220,11 +252,14 @@ \n5. ወደ ሲምፕሪንት ይመለሱ እስካነሩን ያብሩ 1. እስካነሩን ያስነሱ\n2. እንደገና ይሞክሩ + ፍቃድ የለም + ስልኩ ሴቲንግ ላይ ብሉቱዝ ያብሩ የሚጠቀሙበት መሳሪያ ብሉቱዝ ለመጠቀም አያስችልም፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ ብሉቱዝ አልተከፈተም ፡ለመጠቀም አያስችልም፤እባክዎ ማስተካከያው ውስጥ ገብተው ብሉቱዙን ከፍተው እንደገና ይሞክሩ በስልኩ ላይ ከአንድ በላይ እስካነር ተገናኝቷል፣ እባክዎ የማይፈልጉትን እስካነር ግንኙነቱን ያቋርጡ + የጣት አሻራ ሞዳሊቲ በማዋቀር ላይ ወደ ሲስተሙ ማስገባት መለየት ማመሳከር @@ -238,6 +273,8 @@ የቀኝ እጅ ሶስተኛ ጣት የቀኝ እጅ አራተኛ ጣት የቀኝ እጅ አምስተኛ ጣት + ይሰርዙ + ስህተት አጋጥሟል እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንቱ መስራት ካለቻለ የሲስተም አስተዳዳሪውን ያነጋግረ እባክዎ እስካን ያድርጉ ለቀጣዩ ጣትዎ ወደ ግራ ያንሸራቱ @@ -270,6 +307,9 @@ ፕሮጀክቱ በአሁኑ ጊዜ እያለቀ ስለሆነ ክፍለ-ጊዜን ማስጀመር አልተቻለም። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ መግባት ከፈለጉ፡ እባክዎ የደንበኛውን አፕልኬሽን ከፍተው የሲም ፕሪንትን መለያቁጥር በመጠቀም መግባት ይችላሉ + የስልኩመለያ%s + ዝጋ + እስካነር ይጠቀሙ: %1$s አሁን ያለ ተጠቃሚ: %1$s @@ -286,23 +326,29 @@ ሞጁሎች የስልኩ ሴቲንግ በመግባት የሞባይል ዳታ ያብሩ ማስተካከያ + ከተፈቀደው ሞጁል በላይ ወርዷል + በቅድሚያ መግባት አለበት መረጃዉን ለመላክ + መጨረሻ ጊዜ ወደ ሰርቨር የተላከው : %1$s ሁሉም መዝገቦች ተጭነዋል + + %1$d ለመስቀል መዝገብ + %1$d ለመስቀል መዝገቦች + እንደገና ይሞክሩ የየዕለት ተግባር: %1$s - - - ወደ ሲስተሙ ማስገባት + + %d ወደ ሲስተሙ ማስገባት ወደ ሲስተሙ ማስገባት - - መለየት + + %d መለየት መለየት - - ማመሳከር + + %d ማመሳከር ማመሳከር @@ -323,12 +369,14 @@ የመሳሪያው መለያ እስካን እንዲደረግ የሚፈልጉተን የጣት አሻራ ይምረጡ ሲንክ ላይ ተጨማሪ ዝርዝር መረጃ + ገልብጥ ወደ clipboard + ሲንክ የተደረገ መረጃ ሞጅውሎችን ይምረጡ የተመዘገበውን ለመጫን - የተመዘገበውን ለመማውረድ + ሪክርዶች ለማውረድ ወይም ለመሰረዝ የተመዘገበውን ለመሰረዝ በስልኩ አጠቃላይ የተመዘገበ የተመረጡ ሞጁሎች @@ -337,6 +385,8 @@ የሚጫኑ ምስሎች አሁን ግንኙነት ፍጠር በግንኙነት ላይ + የሲንክ መረጃውን በመውሰድ ላይ + የተሳሳተ ምርጫ፡ እባክዎት ካለው የምዕራፍ አማራጭ ውስጥ ቢያንስ አንድ ይምረጡ የተሳሳተ ምርጫ ፡ካሉት ምዕራፎች ውስጥ ከ %1$d በላይ መምረጥ አይችሉም @@ -357,6 +407,7 @@ ለመክፈት የይለፍ ቃል ያስገቡ ለመውጣት የይለፍ ቃል ያስገቡ የይለፍ ቃል + አቋርጥ የተሳሳተ የይለፍ ቃል ለመክፈት መታ ያድርጉ @@ -368,10 +419,14 @@ የእርስዎ ፕሮጀክት ወቅታዊ መዛግብት እንዳለው ለማረጋገጥ የውሂብ ማመሳሰል አስፈላጊ ነው። ያለ ትክክለኛ ምክንያት የእርስዎን ውሂብ ማመሳሰል አለመቻል የስራ ባልደረቦችዎ ጠቃሚ መዝገቦችን ማግኘት እና የፕሮጀክቱን ትክክለኛ ሪፖርት የማድረግ ችሎታ ላይ ተጽእኖ ያሳድራል። እና ለእርስዎ እና ለፕሮጀክቱ የማይፈለጉ ውጤቶችን ሊያስከትል ይችላል. መውጣት ጨርሰው መውጣት ይፈልጋሉ - ሰርዝ + ዘግተህ ዉጣ + አቋርጥ ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ቆየት ብለው ይሞክሩ. ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ከ%s በኋላ እንደገና ይሞክሩ + + ሲንክ በማድረግ ላይ + diff --git a/infra/resources/src/main/res/values-bn/strings.xml b/infra/resources/src/main/res/values-bn/strings.xml index 2e1bad925e..856441c259 100644 --- a/infra/resources/src/main/res/values-bn/strings.xml +++ b/infra/resources/src/main/res/values-bn/strings.xml @@ -3,13 +3,16 @@ সতর্ক বার্তা + বন্ধ করুন + সাধারন সম্মতি আমি আপনাকে %2$s এ নিবন্ধন করতে চাই %1$s আমি আপনাার %2$s আপনাকে %1$s এ নিবন্ধন করতে চাই এবং ভবিষ্যতে তা দিয়ে আপনাকে সনাক্ত করতে চাই। আমি আপনার %2$s ব্যবহার করে %1$s - এর জন্য আপনার তথ্যাদি খুঁজে বের করতে চাই। - সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %1$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। - %1$s এবং সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %1$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + %1$s এবং সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + সিমপ্রিন্টস গবেষণার কাজেও তথ্যগুলো ব্যবহার করবে সম্মতি দেবার পরেও যেকোনো সময় আপনি অনুমতি ফিরিয়ে নিতে এবং আপনার তথ্যাদি মুছে ফেলতে বলতে পারেন। আমি কি %1$s? অনুগ্রহ করে বলুন- \"আমি অনুমতি দিচ্ছি \", \" আমি প্রত্যাখ্যান করছি\" অথবা \"আমার প্রশ্ন আছে।\" মাতা-পিতার সম্মতি @@ -17,7 +20,7 @@ আমি %2$s তাঁকে %1Ss নিবন্ধন করে, ভবিষ্যতে সনাক্ত করতে চাই। \"আমি %2$s তাঁকে খুঁজে বের করতে চাই %1$s \" সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক প্রতিষ্ঠানের কাছে তাঁর %1$s এবং বর্তমান ঠিকানা সংরক্ষিত আছে। - %1$s এবং সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + %1$s এবং সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। আপনি সম্মতি দিলেও যে কোন সময় অনুমতি সরিয়ে নিয়ে আপনার সন্তানের তথ্য মুছে ফেলতে বলতে পারেন। আমি কি আপনার সন্তানের %1$s ব্যবহার করতে পারি\? অনুগ্রহ করে বলুন- \"আমি অনুমতি দিচ্ছি \", \" আমি প্রত্যাখ্যান করছি\" অথবা \"আমার প্রশ্ন আছে।\" আপনার আঙ্গুলের ছাপ ব্যবহার @@ -40,15 +43,19 @@ নিবন্ধন সফল হয়েছে সেভ করা যায়নি 1. %1$s রেকর্ড সংরক্ষণ করতে ব্যর্থ 2. বন্ধ করুন + 1. একটি বিদ্যমান রেকর্ডের নকল হবার কারণে %1$s রেকর্ডটি সংরক্ষণ করতে ব্যর্থ 2. বন্ধ করুন মুখ আঙ্গুলের ছাপ মুখ/আঙুলের ছাপ আপনি কেন আঙ্গুলের ছাপ দেয়া এড়িয়ে গেছেন? - বায়োমেট্রিক তথ্য এড়িয়ে গেলেন কেন\? + বায়োমেট্রিক তথ্য এড়িয়ে গেলেন কেন? + মুখমণ্ডলের ছবি নেয়া এড়িয়ে গেলেন কেন? অতিরিক্ত তথ্য + মুখমণ্ডলের ছবি সংগ্রহ করুন আঙ্গুলের ছাপ স্ক্যান করুন + মুখমণ্ডলের ছবি সংগ্রহ করুন জমা দিন একটি অপশন সিলেক্ট করুন এবং জমা দিন ফর্ম জমা দিন @@ -56,6 +63,7 @@ তথ্য দিতে আগ্রহী নন অনুমতি নেই অ্যাপ কাজ করছে না + স্ক্যানার কাজ করছে না ব্যক্তি উপস্থিত নয় বয়স কম অন্যান্য @@ -65,6 +73,8 @@ ব্যক্তি খুঁজে পাওয়া যায়নি যাচাইয়ের জন্য নির্বাচিত ব্যক্তি ডাটাবেসে নেই ব্যক্তি খুঁজে পাওয়া যায়নি, দয়া করে নিশ্চিত করুন যে আপনি ইন্টারনেটে সংযুক্ত আছেন এবং আবার চেষ্টা করুন + আবার চেষ্টা করুন + প্রজেক্ট আইডি প্রজেক্ট কী @@ -72,12 +82,19 @@ কিউআর কোড স্ক্যান করুন লগইন করুন কিউআর কোড স্ক্যান- এ সমস্যা হয়েছে + ক্যামেরা ব্যবহারের অনুমতি ছাড়া QR কোড স্ক্যান করা যাবে না + ক্যামেরা ব্যাবহারের চেষ্টা করার সময় একটি সমস্যা দেখা দিয়েছে, সুপারভাইসর এর সাথে যোগাযোগ করুন স্ক্যান করা কিউআর কোডটি সঠিক নয় তথ্য অসম্পূর্ণ। ব্যবহারকারী আইডি, প্রজেক্ট আইডি এবং কী চেক করুন এবং আবার চেষ্টা করুন তথ্য সঠিক নয়। প্রজেক্ট আইডি এবং কী চেক করুন। ভিন্ন প্রজেক্ট আইডি। সিস্টেম অ্যাডমিন এর সাথে যোগাযোগ করুন আপনি এখন অফলাইন। আপনার ইন্টারনেট সংযোগ চেক করুন + ইন্টিগ্রিটি সার্ভিসের সাথে কন্টাক্টের সময় একটি সমস্যা হয়েছে৷ অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন সার্ভার এর সাথে সংযোগ স্থাপনে সমস্যা হয়েছে। পরবর্তীতে আবার চেষ্টা করুন। + ব্যাকএন্ড URL পরিবর্তন করুন + সেভ করুন + ডিফল্ট ব্যাবহার করুন + বাতিল করুন আঙ্গুলের ছাপ মিলানো হচ্ছে, অপেক্ষা করুন @@ -104,12 +121,19 @@ %d মিল + + ত্রুটি দেখা দিয়েছে কনফিগারেশন সমস্যা একটি কনফিগারেশন সমস্যা ঘটেছে (ভিন্ন প্রজেক্ট আই ডি), সাইন আউট করুন এবং আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন + আবার চেষ্টা করুন এবং সিমপ্রিন্টস কাজ না করলে আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন। পুরানো Google Play Service সংস্করণ এই ডিভাইসে ইনস্টল করা Google Play Service সংস্করণটি SID অ্যাপ -এর প্রয়োজনীয় সংস্করণের চেয়ে পুরানো Google play service অনুপস্থিত এই ডিভাইসে Google play service ইনস্টল করা নেই। + \"Google Play Store অ্যাপ\" অনুপস্থিত বা পুরানো সংস্করণ + \"Google Play Store অ্যাপ\" ইনস্টল বা আপডেট করা নেই। + প্রকল্পটি বর্তমানে শেষ হতে চলেছে + প্রকল্পটি বর্তমানে শেষ হতে চলেছে বিধায় সেশন চালু করতে ব্যর্থ হয়েছে। আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন প্রকল্পটি বর্তমানে বিরতিতে আছে প্র্রকল্পটি বর্তমানে বিরতিতে থাকার কারণে সেশনটি চালু করা যাচ্ছেনা। আরও তথ্যের জন্য সুপারভাইসরের সাথে যোগাযোগ করুন। একটি কনফিগারেশন সমস্যা ঘটেছে (অবৈধ প্রোজেক্ট আইডি), আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন @@ -127,13 +151,16 @@ নিশ্চিত করা হয়েছে আপনাকে ফিরিয়ে আনা হচ্ছে - - মুখের মোডালিটি কনফিগার করা হচ্ছে - প্রয়োজনীয় ফাইল ডাউনলোড করা হচ্ছে - অবৈধ লাইসেন্স - মুুখের লাইসেন্স অবৈধ, আপনার সুপেরভাইসার এর সাথে যোগাযোগ করুন - কনফিগারেশন সমস্যা - মুখের মোডালিটি কনফিগার করার সময় সমস্যা হয়েছে। সিস্টেম অ্যাডমিন এর সাথে যোগাযোগ করুন + + পদ্ধতি কনফিগার করা হচ্ছে + প্রয়োজনীয় ফাইল ডাউনলোড করা হচ্ছে + রক্ষণাবেক্ষণ মোড + সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে, অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন + লাইসেন্সটি অকার্যকর + লাইসেন্সটি অকার্যকর, আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন + কনফিগারেশন ত্রুটি %1$s + লাইসেন্সটি সিস্টেম থেকে খুঁজে আনার সময় একটি ত্রুটি দেখা দিয়েছে, আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন + ভালো আলো আছে এমন স্থান খুঁজে বের করুন @@ -153,6 +180,7 @@ ব্যক্তি ক্যামেরার দিকে সোজা তাকাচ্ছে কিনা নিশ্চিত করুন ছবি তুলতে এখানে চাপুন ছবি তোলা হচ্ছে + অনুগ্রহ করে ক্যামেরা ব্যাবহারের অনুমতি দিন স্থির থাকুন ব্যক্তির মুখের ছবি তোলা সফল হয়েছে নিশ্চিত করা হচ্ছে @@ -161,6 +189,7 @@ সমস্যা ঘটেছে + অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন, তখন যদি সিমপ্রিন্টস কাজ না করে তাহলে আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন বন্ধ করুন আবার চেষ্টা করুন ফোন সেটিংস @@ -169,7 +198,7 @@ হ্যা না স্ক্যানার সংযুক্ত হচ্ছে… - ইউএন২০ চালু হচ্ছে… + ইউএন20 চালু হচ্ছে… ভেরো সেট-আপ হচ্ছে… সংযুক্ত হয়েছে ব্লুুটুথ বন্ধ আছে @@ -188,6 +217,7 @@ ডিভাইস গুলো সংযুক্ত করা যাচ্ছেনা %1$s স্ক্যানার পাওয়া গেছে স্ক্যানার %1$s চালুু আছে কিনা নিশ্চিত করুন এবং আবার চেষ্টা করুন + পেয়ারিং বাতিল করা হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন এবং পেয়ারিং অনুরোধ করা হলে সেটি গ্রহণ করুন। এন.এফ.সি চিপ পড়া যাচ্ছেনা, আবার চেষ্টা করুন অবৈধ এন.এফ.সি চিপ ঠিক আছে @@ -195,6 +225,7 @@ নিচের বক্স-এ স্ক্যানার এর নাম্বার লিখুন এই নাম্বারটি স্ক্যানার-এর একদম নিচে পাবেন স্ক্যানার %1$s চালুু আছে কিনা নিশ্চিত করুন এবং আবার চেষ্টা করুন + পেয়ারিং বাতিল করা হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন এবং পেয়ারিং অনুরোধ করা হলে সেটি গ্রহণ করুন। ৬ সংখ্যার সিরিয়াল নাম্বারটি লিখুন স্ক্যানার আপডেট করা প্রয়োজন স্ক্যানার চালু রাখুন, তা আপডেট হচ্ছে @@ -218,11 +249,14 @@ \n৫। সিমপ্রিন্টস অ্যাপ- এ ফেরত যেতে \"ফেরত\" বাটন চাপুন। স্ক্যানার চালু করুন ১। স্ক্যানার চালু করুন।\n২। \"আবার চেষ্টা করুন\" চাপুন। + অনুমতি নেই + অনুগ্রহ করে ফোন সেটিংসে ব্লুটুথ-এর অনুমতি সক্ষম করুন৷ আপনার ফোন/যন্ত্র ব্লুটুথ সাপোর্ট করেনা। আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন ব্লুটুথ সক্রিয় করা নেই, এটি সেটিংস এ সক্রিয় হয়েছে কিনা তা নিশ্চিত করুন এবং আবার চেষ্টা করুন এক এর অধিক স্ক্যানার সংযুক্ত আছে, অপ্রয়োজনীয় স্ক্যানার বাতিল করুন। + ফিঙ্গারপ্রিন্ট পদ্ধতি কনফিগার করা হচ্ছে তালিকাভুক্তকরণ সনাক্তকরণ যাচাইকরণ @@ -236,6 +270,8 @@ ডান হাতের মধ্যমা ডান হাতের অনামিকা ডান হাতের কনিষ্ঠা / কড়ে আঙ্গুল + বাতিল করুন + ত্রুটি হয়েছে আবার চেষ্টা করুন এবং সিমপ্রিন্টস কাজ না করলে আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন। স্ক্যান করুন পরবর্তী আঙ্গুল স্ক্যান করতে বামে সোয়াইপ করুন @@ -268,6 +304,9 @@ প্র্রকল্পটি বর্তমানে শেষ হবার কারণে সেশনটি চালু করা যাচ্ছেনা। আরও তথ্যের জন্য সুপারভাইসরের সাথে যোগাযোগ করুন। আপনাকে লগইন করতে হবে। সিমপ্রিন্টস আইডি খুলতে আপনার ক্লায়েন্ট অ্যাপ ব্যবহার করুন এবং লগইন করুন + ডিভাইস আইডি: %s + বন্ধ করুন + স্ক্যানার ব্যবহার করা হয়েছে: %1$s বর্তমান ব্যবহারকারী: %1$s @@ -284,12 +323,32 @@ মডিউল সমূহ সেটিংস্‌ থেকে ইন্টারনেট সংযোগ চালু করুন স্ক্যানার যোগ করুন + অনেক বেশি মডিউল ডাউনলোড করা হয়েছে + সিঙ্ক করতে পুনরায় লগইন করুন + সর্বশেষ সিঙ্ক: %1$s সমস্ত রেকর্ড আপলোড হয়েছে + + %1$dটি রেকর্ড আপলোড হওয়া বাকি + %1$dটি রেকর্ড আপলোড হওয়া বাকি + আবার চেষ্টা করুন কার্যক্রম তথ্য: %1$s + + তালিকাভুক্তকরণ + তালিকাভুক্তকরণ + + + সনাক্তকরণ + সনাক্তকরণ + + + যাচাইকরণ + যাচাইকরণ + + সেটিংস সম্বন্ধে @@ -307,12 +366,14 @@ ডিভাইস এর আইডি যে আঙ্গুল গুলো স্ক্যান করবেন সেগুলো সিলেক্ট করুন সিঙ্ক- এর জন্য আরও তথ্য + কপি করা হয়েছে + তথ্য সিঙ্ক করুন মডিউল নির্বাচন করুন আপলোড এর জন্য প্রস্তুত রেকর্ড - ডাউনলোড এর জন্য প্রস্তুত রেকর্ড + ডাউনলোড বা মুছে ফেলার রেকর্ড মুছে ফেলার জন্য প্রস্তুত রেকর্ড ফোনে সংরক্ষিত সকল রেকর্ড নির্বাচিত মডিউল @@ -321,6 +382,8 @@ আপলোড বাকি সিঙ্ক করুন সিঙ্ক হচ্ছে… + সিঙ্ক তথ্য পুনরায় নিয়ে আসুন + অবৈধ নির্বাচন। অন্তত একটি মডিউল নির্বাচন করুন। অবৈধ নির্বাচন। %1$d এর বেশি মডিউল নির্বাচন করবেন না। @@ -341,6 +404,7 @@ আনলক করতে পাসওয়ার্ড লিখুন লগ আউট করতে পাসওয়ার্ড লিখুন পাসওয়ার্ড + বাতিল করুন ভুল পাসওয়ার্ড Unlock করতে চাপুন @@ -352,10 +416,14 @@ আপনার প্রকল্পের সকল রেকর্ড আপ-টু-ডেট রয়েছে তা নিশ্চিত করার জন্য ডেটা সিঙ্ক্রোনাইজেশন অপরিহার্য। কোনো বৈধ কারণ ছাড়া ডেটা সিঙ্ক্রোনাইজ করতে ব্যর্থ হলে তা আপনার সহকর্মীদের গুরুত্বপূর্ণ রেকর্ডে অ্যাক্সেস এবং প্রকল্পের সঠিকভাবে রিপোর্ট করার ক্ষমতাকে প্রভাবিত করবে।\n\nকোনও বৈধ কারণ ছাড়া ডেটা সিঙ্ক্রোনাইজ করতে ব্যর্থ হওয়া ডেটা সংগ্রহ প্রোটোকলের লঙ্ঘনও হিসেবে গণ্য হতে পারে যার ফলে প্রকল্পের অবাঞ্ছিত পরিণতি হবার সুযোগ থাকে। লগআউট করুন আপনি কি লগআউট করতে চান? + লগআউট করুন বাতিল করুন সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে। অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন. সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে। %s পরে আবার চেষ্টা করুন + + সিঙ্ক হচ্ছে + diff --git a/infra/resources/src/main/res/values-fr/strings.xml b/infra/resources/src/main/res/values-fr/strings.xml index ad928fd5da..02d71a19d5 100644 --- a/infra/resources/src/main/res/values-fr/strings.xml +++ b/infra/resources/src/main/res/values-fr/strings.xml @@ -3,6 +3,8 @@ Alerte + Fermer + Consentement général Je voudrais %2$s pour vous inscrire à %1$s. @@ -20,13 +22,13 @@ Simprints, une organisation à but non lucratif basée au Royaume-Uni, aura accès à leurs %1$s et à son emplacement actuel. %1$s et Simprints, une organisation à but non lucratif basée au Royaume-Uni, auront accès à leurs %2$s et à leur localisation actuelle. Si vous acceptez, vous pouvez retirer votre autorisation à tout moment et demander que les données de votre enfant soient effacées. - Puis-je %1$s de votre enfant ? Veuillez dire \"Je l\'accepte\", \"Je le refuse\", ou \"J\'ai des questions.\" - scanner vos empreintes digitales + Puis-je utiliser le(s) facteur(s) biométrique(s) \"%1$s\" de votre enfant ? Veuillez dire \"Je l\'accepte\", \"Je le refuse\", ou \"J\'ai des questions. + utiliser vos empreintes digitales prendre des photos de votre visage empreintes digitales photos empreintes digitales, photos - scanner les empreintes digitales de votre enfant + utiliser les empreintes digitales de votre enfant prendre des photos du visage de votre enfant et Refuser @@ -34,24 +36,25 @@ Politique de confidentialité Politique de confidentialité Téléchargez la politique de confidentialité - Téléchargement de l\'avis de confidentialité + Téléchargement de la politique de confidentialité Le téléchargement de la politique de confidentialité a échoué. Inscription réussie Impossible d\'enregistrer - 1. La sauvegarde de l\'enregistrement %1$s a echoué \\n 2. Cliquez fermer + 1. La sauvegarde de l\'enregistrement %1$s a echoué \n 2. Tapez pour fermer + 1. Echec lors de la sauvegarde de l\'enregistrement %1$s puisqu\'il créerait un doublon d\'un enregistrement existant \n 2. Touchez pour fermer Reconnaissance faciale Empreinte digitale - Reconnaissance faciale/Empreinte digitale + Reconnaissance faciale / Empreinte digitale - Pourquoi avez-vous ignoré le scan d\'empreinte digitale ? - Pourquoi avez-vous ignoré la capture biométrique ? - Pourquoi avez-vous ignoré la capture du visage ? + Pourquoi avez-vous passé le scan d\'empreinte digitale ? + Pourquoi avez-vous passé la capture biométrique ? + Pourquoi avez-vous passé la capture du visage ? Informations supplémentaires Retourner à Simprints - Scannez les empreintes digitales + Scanner les empreintes digitales Capturer le visage Soumettre Veuillez sélectionner une option et soumettre @@ -60,6 +63,7 @@ Ne veut pas donner de données N\'a pas la permission L\'application ne fonctionne pas + Le scanner ne fonctionne pas Personne non présente Trop jeune Autre @@ -69,6 +73,8 @@ Personne non trouvée La personne sélectionnée pour la vérification n\'est pas dans la base de données. Personne non trouvée, veuillez vous assurer que vous êtes connecté à l\'internet et réessayer + Réessayez + ID du projet Clé du projet @@ -76,12 +82,19 @@ Scanner QRCode Connexion Un problème est survenu lors du scan du code QR + Impossible de scanner le QR Code sans la permission d\'accéder à l\'appareil photo + Une erreur s\'est produite lors de l\'accès à l\'appareil photo, veuillez contacter votre administrateur système. Le QRCode scanné n\'est pas valide. Informations d\'identification manquantes. Veuillez vérifier l\'ID utilisateur, l\'ID projet et la clé, et réessayer. Informations d\'identification invalides. Veuillez vérifier l\'ID et la clé du projet - L\'ID du projet est différent de celui fourni dans l\'intention. Veuillez contacter votre administrateur système. + L\'ID du projet est différent de celui fourni dans l\'appel depuis l\'autre application. Veuillez contacter votre administrateur système. Actuellement hors ligne. Veuillez vérifier votre connexion Internet. + Une erreur s\'est produite en essayant de contacter le Service d\'Intégrité. Veuillez réessayer plus tard. Un problème est survenu en essayant de contacter le serveur. Veuillez réessayer plus tard. + Changez l\'URL du serveur backend. + Enregistrer + Utiliser les valeurs par défaut + Annuler Veuillez attendre pendant que nous procédons à l\'identification @@ -93,7 +106,7 @@ Comparé à %d candidats. - A retourné %d résultats. + A retourné %d résultat. A retourné %d résultats. A retourné %d résultats. @@ -113,36 +126,46 @@ %d correspondances + + Une erreur est survenue Erreur de configuration Une erreur de configuration s\'est produite (Id de projet différent), veuillez vous déconnecter de l\'écran d\'accueil ou contacter votre administrateur système. + Veuillez réessayer et si Simprints ne fonctionne pas, contactez votre administrateur système Services Google Play obsolètes La version de Services Google Play installé sur cette appareil est plus ancienne de celle nécessaire pour démarrer SID Services Google Play manquants - Services Google Play ne sont pas installés sur cette appareil + Les Services Google Play ne sont pas installés sur cette appareil + Application \"Google Play Store\" manquante ou obsolète + L\'application \"Google Play Store\" n\'est pas installée ou est obsolète. + Le projet se termine actuellement + Impossible de lancer la session car le projet se termine actuellement. Veuillez contacter votre superviseur pour plus d\'informations Le projet est actuellement en pause Impossible de lancer la session car le projet est actuellement en pause. Veuillez contacter votre superviseur pour plus d\'informations Une erreur de configuration s\'est produite (le champ project ID est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ selected Id est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ session ID est invalide), veuillez contacter votre administrateur système. - Une erreur de configuration s\'est produite (action intentionnelle invalide), veuillez contacter votre administrateur système. + Une erreur de configuration s\'est produite (action d\'appel depuis l\'autre application est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ metadata est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ user ID est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ module ID est invalide), veuillez contacter votre administrateur système. Une erreur de configuration s\'est produite (le champ verify GUID est invalide), veuillez contacter votre administrateur système. - Appareil rooter détecté + Appareil rooté détecté \"1. Contactez votre superviseur - appareil rooté détecté 2. Tapez sur Fermer\" Confirmation envoyée Vous allez être redirigé - - Configuration de la modalité de reconnaissance faciale - Téléchargement des fichiers nécessaires - Licence invalide - La licence pour la reconnaissance faciale n\'est pas valide, veuillez contacter votre administrateur système - Erreur de configuration - Une erreur s\'est produite lors de la configuration de la modalité de visage, veuillez contacter votre administrateur système. + + Configuration des modalités + Téléchargement des fichiers nécessaires + Mode de maintenance + Le système est hors ligne pour maintenance. Veuillez réessayer plus tard. + Licence invalide + La licence n\'est pas valide, veuillez contacter votre administrateur système + Erreur de configuration %1$s + Une erreur s\'est produite lors de la récupération de la licence, veuillez contacter votre administrateur système + Trouvez un endroit bien éclairé. @@ -160,7 +183,9 @@ Reculez La personne ne regarde pas droit devant elle Assurez-vous que la personne regarde droit vers la caméra. + Touchez pour capturer Capture en cours... + Veuillez autoriser les permissions de l\'appareil photo Maintenez la position... Le visage a été capturé avec succès. Confirmation @@ -168,9 +193,10 @@ Touchez pour continuer - Erreur survenue - Fermez - Réessayez + Une erreur est survenue + Veuillez réessayer et si Simprints ne fonctionne pas, contactez votre administrateur système + Fermer + Réessayer Réglages du téléphone Réglages Est ce que %1$s est votre scanner? @@ -196,13 +222,15 @@ Impossible de connecter les appareils Scanner %1$s trouvé Veuillez vous assurer que le scanner %1$s est allumé et réessayez. + Connexion annulée, veuillez réessayer et accepter la requête de connexion lorsqu\'elle sera demandée. Impossible de lire la puce NFC, veuillez réessayer La puce NFC détectée est invalid OK Scanner déconnecté - Entrez le numéro du scanner dans la boite ci-dessous + Entrez le numéro du scanner Ceci se trouve sous le bas du scanner Veuillez vous assurer que le scanner %1$s est allumé et réessayez. + Connexion annulée, veuillez réessayer et accepter la requête de connexion lorsqu\'elle sera demandée. Veuillez entrer les 6 chiffres du numéro de série Le scanner doit être mis à jour Laissez le scanner allumé pendant la mise à jour @@ -219,14 +247,17 @@ Batterie faible Batterie du scanner faible, veuillez charger votre scanner Jumelez le scanner au téléphone - 1. Vérifiez que le scanner est allumé \\n 2. Cliquez sur réglages \\n 3. Déjumelez tous les scanners jumelés \\n 4. Jumelez votre scanner au téléphone \\n 5. Cliquez sur retour pour repartir à Simprints. + 1. Vérifiez que le scanner est allumé \n 2. Cliquez sur réglages \n 3. Déjumelez tous les scanners jumelés \n 4. Jumelez votre scanner au téléphone \n 5. Cliquez sur retour pour repartir à Simprints. Allumez le scanner - 1. Allumez le scanner \\n 2. Cliquez Réessayez + 1. Allumez le scanner \n 2. Cliquez Réessayez + Pas de permission + Veuillez activer les permissions Bluetooth dans les réglages du téléphone. Le bluetooth n\'est pas supporté sur ce téléphone, veuillez contacter votre administrateur système. Le bluetooth n\'est pas activé, assurez-vous qu\'il soit activé dans les réglages puis réessayez Plus d\'un scanner est jumelé au téléphone, veuillez déjumeler les scanners non nécessaire + Configuration de la modalité de reconnaissance par empreintes digitales Inscription Identification Vérification @@ -240,19 +271,21 @@ Majeur droit Annulaire droit Auriculaire droit + Annuler + Une erreur est survenue Veuillez réessayer et si Simprints ne fonctionne pas, contactez votre administrateur système. Veuillez scanner Glissez à gauche pour passer au doigt suivant Veuillez rescanner - En cours de scan - En cours de transfert de données + En cours de scan... + En cours de transfert de données... Scan correct Mauvais scan Pas de doigt détecté Doigt ignoré Veuillez scanner au moins 1 doigt requis Doigts scannés - Validez + Valider Recommencer Scan %1$s sur %2$s Veuillez scanner à nouveau @@ -261,7 +294,7 @@ Scanner Veuillez attendre Doigt manquant - Soyez prêt + Préparez vous Veuillez essayer un autre doigt @@ -272,6 +305,9 @@ Impossible de lancer la session car le projet se termine actuellement. Veuillez contacter votre superviseur pour plus d\'informations Vous devez vous connecter. Veuillez utiliser votre application client pour ouvrir Simprints ID et vous connecter. + ID de l\'appareil DeviceID : %s + Fermer + Scanner utilisé %1$s Utilisateur actuel: %1$s @@ -288,25 +324,31 @@ Modules Veuillez activer la connexion Internet dans les réglages Réglages + Trop de modules ont été téléchargés. + Vous devez vous connecter pour synchroniser. + Dernière synchronisation : %1$s Tous enregistrements téléchargés + + %1$d enregistrement à envoyer + %1$d enregistrements à envoyer + %1$d enregistrements à envoyer + Réessayez Activité : %1$s - - - + Inscription Inscriptions Inscriptions - + Identification Identifications Identifications - + Vérification Vérifications Vérifications @@ -315,7 +357,7 @@ Réglages À propos - GÉNÉRALITÉS + GÉNÉRAL DÉTAILS DE L\'APPLICATION À propos Configuration de la synchronisation et de la recherche @@ -327,14 +369,16 @@ Informations sur la synchronisation Version du scanner ID de l\'appareil - Sélectionnez les doigts que vous souhaitez scanner + Voir les doigts qui vont être scannés Plus de détails sur la synchronisation + Copié dans le presse-papier + Informations sur la synchronisation Sélectionner les modules Enregistrements à envoyer - Enregistrements à télécharger + Enregistrements à télécharger ou à supprimer Enregistrements à supprimer Nombre total d\'enregistrements sur l\'appareil Modules sélectionnés @@ -343,6 +387,8 @@ Images à envoyer Synchroniser maintenant Synchronisation… + Re-récupération des informations de synchronisation + Sélection invalide. Veuillez sélectionner au moins un module. Sélection invalide. Veuillez ne pas sélectionner plus de %1$d modules. @@ -351,20 +397,21 @@ Aucun résultat Modules sélectionnés Modules sélectionnés - Validez + Valider Annuler - Choississez les doigts + Voir les doigts Doigt Quantité - écrire le mot de passe pour déverrouiller - écrire le mot de passe pour déconnecter + Entrer le mot de passe pour déverrouiller + Entrer le mot de passe pour déconnecter Mot de passe + Annuler Mot de passe incorrect - Appuyez pour déverrouiller + Tapez pour déverrouiller Déconnexion @@ -374,10 +421,14 @@ La synchronisation des données est essentielle pour garantir que votre projet dispose d\'enregistrements à jour. Ne pas synchroniser vos données sans raison valable aura un impact sur l\'accès de vos collègues aux enregistrements importants et sur la capacité du projet à générer des rapports précis.\n\nNe pas synchroniser les données sans raison valable peut également constituer une violation du protocole de collecte de données et peut entraîner des conséquences indésirables pour vous et le projet. Déconnexion Voulez-vous vous déconnecter ? + Déconnexion Annuler Le système est hors ligne pour maintenance. Réessayez plus tard. Le système est hors ligne pour maintenance. Réessayez dans %s + + Synchronisation en cours + diff --git a/infra/resources/src/main/res/values-hi/strings.xml b/infra/resources/src/main/res/values-hi/strings.xml index 5dd16573c1..bcd30a90c8 100644 --- a/infra/resources/src/main/res/values-hi/strings.xml +++ b/infra/resources/src/main/res/values-hi/strings.xml @@ -3,6 +3,8 @@ अलर्ट + बंद करें + सामान्य सहमति आपको %2$s में पंजीकृत करने के लिए हम आपको %1$s करना चाहेंगे @@ -21,8 +23,12 @@ उनके %2$s व वर्तमान स्थान की जानकारी पर %1$s व सिंप्रिंट्स की पहुँच होगी जोकि एक ब्रिटेन आधारित स्वयं-सेवी संस्था है। यदि आप अभी सहमति देते हैं, तो भी बाद में किसी भी समय अपनी सहमति वापस ले सकते हैं और अपने बच्चे का डाटा मिटाने के लिए कह सकते हैं। क्या हम आपके बच्चे के %1$s का उपयोग कर सकते हैं ? कृपया कहें \"मैं सहमत हूँ \", \"मैं सहमत नहीं हूँ \", or \"मेरे कुछ सवाल हैं.\" + अपनी उंगलियों के निशान का उपयोग करें अपने चेहरे की फ़ोटो लें + फ़िंगरप्रिंट जानकारी चित्र + फिंगरप्रिंट जानकारी, तस्वीरें + अपने बच्चे की उंगलियों के निशान का उपयोग करें अपने बच्चे के चेहरे की फ़ोटो लें और असहमत @@ -37,13 +43,18 @@ पुष्टिकरण भेजा गया सेव नहीं किया जा सकता 1. %1$s रिकॉर्ड सहेजने में विफल \n2. बंद करें टैप करें + 1. %1$s रिकॉर्ड को सहेजने में विफल रहा क्योंकि यह मौजूदा रिकॉर्ड की नकल करता है \n 2. बंद करें पर टैप करें चेहरा + फ़िंगरप्रिंट चेहरा/फ़िंगरप्रिंट + + आपने फ़िंगरप्रिंट कैप्चर करना क्यों छोड़ दिया? आपने बायोमैट्रिक्स जानकारी क्यों नहीं ली? आपने चेहरे की पहचान क्यों नहीं ली? अतिरिक्त जानकारी चेहरे की फ़ोटो लें + फ़िंगरप्रिंट स्कैन करें चेहरे की फ़ोटो लें सबमिट करें कृपया किसी एक विकल्प का चयन करें और सबमिट करें @@ -52,6 +63,7 @@ डाटा नहीं देना चाहते अनुमति नहीं है एप काम नहीं कर रहा + स्कैनर काम नहीं कर रहा व्यक्ति उपस्थित नहीं है आयु बहुत कम है अन्य @@ -61,6 +73,8 @@ व्यक्ति नहीं मिला। सत्यापन के लिए चुना गया व्यक्ति डेटाबेस में नहीं हैं। व्यक्ति नहीं मिला, कृपया इंटरनेट की कनेक्टीवीटी सुनिश्चित करें और पुनः प्रयास करें + पुनः प्रयास करें + प्रोजेक्ट आईडी प्रोजेक्ट कुंजी @@ -68,13 +82,23 @@ क्यूआर कोड को स्कैन करें लॉग-इन क्यूआर कोड स्कैन करने में त्रुटि आ रही है + कैमरे का उपयोग करने की अनुमति के बिना क्यूआर कोड को स्कैन नहीं किया जा सकता + फेस के प्रकार को कॉन्फ़िगर करते समय त्रुटि हुई, कृपया अपने कार्यकारी प्रबंधक से संपर्क करें स्कैन किया गया क्यूआर कोड अमान्य है कोई प्रविष्टि छूट रही है। कृपया यूज़र ID, प्रोजेक्ट ID व कुंजी को जाँच लें व पुनः प्रयास करें। अमान्य प्रविष्टि। कृपया प्रोजेक्ट आईडी व कुंजी की जाँच करें। प्रोजेक्ट आईडी भिन्न डाला गया है। कृपया अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें। वर्तमान में ऑफ़-लाइन। कृपया अपने इंटरनेट कनेक्शन को जाँच लें। + सर्वर से जोड़ने में समस्या आ रही है। कृपया पुनः प्रयास करें। सर्वर से जोड़ने में समस्या आ रही है। कृपया पुनः प्रयास करें। + बैकएंड यूआरएल बदलें + सेव + डिफ़ॉल्ट उपयोग करें + रद्द करें + + जब तक हम पहचान के लिए आगे बढ़ें, कृपया प्रतीक्षा करें + उम्मीदवार में से मिलान हो रहा हैं उम्मीदवार में से मिलान हो रहा हैं %d उम्मीदवार से तुलना की गई @@ -97,12 +121,19 @@ %d मेल खाते हैं + + त्रुटि कॉन्फ़िगरेशन त्रुटि कॉन्फ़िगरेशन त्रुटि हो रही है (भिन्न प्रोजेक्ट ID), कृपया होम स्क्रीन से लॉग-आउट करें या अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें + कृपया पुनः प्रयास करें, और यदि सिंप्रिंट्स फिर भी काम न करे तो कृपया अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें पुरानी Google Play सेवाएं इस डिवाइस पर इंस्टॉल किया गया google play services संस्करण SID द्वारा आवश्यक संस्करण से पुराना है गूगल प्ले सेवाएं अनुपलब्ध इस डिवाइस पर google play सेवाएं इंस्टॉल नहीं हैं। + उपलब्ध नहीं है या पुराना हो गया है \"Google Play Store ऐप\" + उपलब्ध नहीं है या पुराना हो गया है \"Google Play Store ऐप\" + प्रोजेक्ट फिलहाल ख़त्म हो रहा है + सत्र लॉन्च करने में असमर्थ क्योंकि प्रोजेक्ट फिलहाल रुका हुआ है। अधिक जानकारी के लिए कृपया अपने पर्यवेक्षक से संपर्क करें प्रोजेक्ट फिलहाल रुका हुआ है सत्र लॉन्च करने में असमर्थ क्योंकि प्रोजेक्ट फिलहाल रुका हुआ है। अधिक जानकारी के लिए कृपया अपने पर्यवेक्षक से संपर्क करें कॉन्फ़िगरेशन त्रुटि हो रही है (अमान्य प्रोजेक्ट आईडी), कृपया अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें @@ -120,13 +151,16 @@ पुष्टिकरण भेजा गया आपको वापस रीडायरेक्ट कर रहा है - - चेहरे की प्रारूप को कॉन्फ़िगर करना - आवश्यक फ़ाइलें डाउनलोड हो रही हैं - लाइसेंस अमान्य है - बायोमेट्रिक लाइसेंस अमान्य है, कृपया अपने कार्यकारी प्रबंधक से संपर्क करें - कॉन्फ़िगरेशन त्रुटि - फेस के प्रकार को कॉन्फ़िगर करते समय त्रुटि हुई, कृपया अपने कार्यकारी प्रबंधक से संपर्क करें + + बायोमेट्रिक प्रकार कॉन्फ़िगर करना + आवश्यक फ़ाइलें डाउनलोड हो रही हैं + रखरखाव मोड + सिस्टम फिलहाल रखरखाव के लिए ऑफ़लाइन है। कृपया बाद में पुन: प्रयास करें। + लाइसेंस अमान्य है + बायोमेट्रिक लाइसेंस अमान्य है, कृपया अपने कार्यकारी प्रबंधक से संपर्क करें + कॉन्फ़िगरेशन त्रुटि %1$s + बायोमेट्रिक लाइसेंस अमान्य है, कृपया अपने कार्यकारी प्रबंधक से संपर्क करें + अच्छी रोशनी वाली जगह पर जायें। @@ -146,6 +180,7 @@ सुनिश्चित करें कि व्यक्ति सीधे कैमरे में देख रहा है कैप्चर करने के लिए टैप करें कैप्चर हो रहा है....... + कृपया कैमरे की अनुमति दें अपनी जगह पर रहें चेहरे को सफलतापूर्वक कैप्चर कर लिया गया है। पुष्टीकरण @@ -154,12 +189,109 @@ त्रुटि + कृपया पुनः प्रयास करें, और यदि सिंप्रिंट्स फिर भी काम न करे तो कृपया अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें बंद करें पुनः प्रयास करें फोन सेटिंग सेटिंग + क्या %1$s आपका स्कैनर है? + हाँ + नहीं + वेरो से कनेक्ट हो रहा है... + जाग रहा है UN20... + वेरो स्थापित किया जा रहा है... + कनेक्ट है + ब्लूटूथ बंद + ब्लूटूथ चालू करें + ✓ ब्लूटूथ चालू + ब्लूटूथ चालू नहीं किया जा सका. कृपया डिवाइस सेटिंग में ब्लूटूथ चालू करें। + एनएफसी बंद + एनएफसी चालू करें + ✓ एनएफसी चालू + स्कैनर बंद + स्कैनर पावर बटन दबाएँ + ✓ स्कैनर चालू + %1$s मेरा स्कैनर नहीं है + स्कैनर काम नहीं कर रहा + स्कैनर के पीछे से फोन के पीछे तक टैप करें + डिवाइस कनेक्ट नहीं कर सकते + %1$s स्कैनर मिला + कृपया सुनिश्चित करें कि %1$s स्कैनर चालू है और पुनः प्रयास करें। + युग्मन रद्द कर दिया गया, कृपया पुनः प्रयास करें और संकेत मिलने पर युग्मन अनुरोध स्वीकार करें। + एनएफसी चिप नहीं पढ़ सका, कृपया पुनः प्रयास करें + अमान्य एनएफसी चिप का पता चला ठीक + स्कैनर कनेक्ट नहीं है + स्कैनर नंबर दर्ज करें + यह स्कैनर के नीचे पाया जाता है + कृपया सुनिश्चित करें कि %1$s स्कैनर चालू है और पुनः प्रयास करें। + युग्मन रद्द कर दिया गया, कृपया पुनः प्रयास करें और संकेत मिलने पर युग्मन अनुरोध स्वीकार करें। + कृपया क्रमांक के 6 अंक दर्ज करें + स्कैनर को अद्यतन करने की आवश्यकता है + अपडेट होते समय स्कैनर चालू रखें + अपडेट हो रहा है... + %2$s में से प्रयास क्रमांक %1$s अपडेट किया जा रहा है... + ✓ अद्यतन पूर्ण + अद्यतनीकरण शुरू करें + स्कैनर अद्यतन विफल रहा + स्कैनर को बार-बार बंद करें और चालू करें + पावर बटन को 5 सेकंड तक दबाकर रखें + स्कैनर अद्यतन विफल रहा + सहायता के लिए कृपया अपने पर्यवेक्षक से संपर्क करें + जारी रखना + लो बैटरी + स्कैनर की बैटरी कम है, कृपया अपने स्कैनर को चार्ज करें + स्कैनर और डिवाइस को युग्मित करें + 1. जांचें कि स्कैनर चालू है \n 2. सेटिंग्स टैप करें \n 3. किसी भी युग्मित स्कैनर को अनपेयर करें \n 4. अपने स्कैनर को डिवाइस से पेयर करें \n 5. सिमप्रिंट्स पर लौटने के लिए वापस टैप करें + स्कैनर चालू करें + 1. स्कैनर चालू करें \n 2. पुनः प्रयास करें पर टैप करें + अनुमति नहीं + कृपया फ़ोन सेटिंग में ब्लूटूथ अनुमति सक्षम करें + इस डिवाइस पर ब्लूटूथ समर्थित नहीं है, कृपया अपने सिस्टम व्यवस्थापक से संपर्क करें + ब्लूटूथ सक्षम नहीं है, कृपया सुनिश्चित करें कि यह सेटिंग्स में सक्षम है और पुनः प्रयास करें + फ़ोन में एक से अधिक स्कैनर जोड़े गए हैं, कृपया अनावश्यक स्कैनर हटा दें + + + फ़िंगरप्रिंट मोडैलिटी कॉन्फ़िगर करना + पंजीकरण + पहचान + विभिन्न सत्यापन + बायीं पांचवी उंगली + बायीं चौथी उंगली + बायीं तीसरी उंगली + बायीं तर्जनी + बायां अंगूठा + दायां अंगूठा + दाहिनी तर्जनी + दाहिनी तीसरी उंगली + दाहिनी चौथी उंगली + दाहिनी पाँचवीं उंगली + रद्द करें + त्रुटि कृपया पुनः प्रयास करें, और यदि सिंप्रिंट्स फिर भी काम न करे तो कृपया अपने सिस्टम ऐडमिनिस्ट्रेटर से सम्पर्क करें + कृपया स्कैन करें + बाईं ओर अगली उंगली पर स्वाइप करें + कृपया पुनः स्कैन करें + स्कैन किया जा रहा है... + डेटा स्थानांतरित किया जा रहा है... + अच्छा स्कैन + ख़राब स्कैन + कोई उंगली नहीं मिली + उंगली छूट गई + कृपया कम से कम 1 आवश्यक उंगली स्कैन करें + उंगलियां स्कैन की गईं + पुष्टि करना + पुनः आरंभ करें + %2$s में से संख्या %1$s को स्कैन करें + कृपया स्कैन करें + फिर से स्कैन + फिर से स्कैन ? + स्कैन + कृपया प्रतीक्षा करें + उंगली गायब + तैयार हो जाओ + आइए एक और उंगली आज़माएँ! + डैशबोर्ड गोपनीयता की सूचना @@ -168,6 +300,11 @@ सत्र लॉन्च करने में असमर्थ क्योंकि परियोजना वर्तमान में समाप्त हो रही है। अधिक जानकारी के लिए कृपया अपने पर्यवेक्षक से संपर्क करें कृपया लॉग-इन करें। सिंप्रिंट्स ID व लॉग-इन खोलने के लिए कृपया अपने क्लाइंट एप का उपयोग करें। + डिवाइस आईडी:%s + बंद करना + + + स्कैनर का उपयोग किया गया:%1$s वर्तमान उपयोगकर्ता: %1$s @@ -182,23 +319,29 @@ मॉड्यूल्स कृपया सेटिंग में जाकर इंटरनेट को चालू करें सेटिंग + बहुत सारे मॉड्यूल डाउनलोड किए जा चुके हैं. + सिंक करने के लिए आपको फिर से लॉग इन करना होगा + अंतिम बार सिंक: %1$s सभी रिकॉर्ड अपलोड किए गए + + %1$d अपलोड करने हेतु रिकार्ड + %1$dअपलोड करने हेतु रिकार्ड + पुनः प्रयास करें गतिविधि: %1$s - - - पंजीकरण + + %d पंजीकरण पंजीकरण - - विभिन्न पहचान + + %d विभिन्न पहचान विभिन्न पहचान - - विभिन्न सत्यापन + + %d विभिन्न सत्यापन विभिन्न सत्यापन @@ -217,13 +360,16 @@ सिंक सम्बंधित जानकारी स्कैनर संस्करण डिवाइस आईडी + उन उंगलियों को देखें जिन्हें स्कैन किया जाएगा सिंक के बारे में अन्य विवरण + क्लिपबोर्ड पर नकल + सिंक सम्बंधित जानकारी मॉड्यूल का चुनाव करें रिकोर्ड्स अपलोड किए जाने हैं - रिकोर्ड्स डाउनलोड किए जाने हैं + डाउनलोड करने या हटाने के लिए रिकॉर्ड रिकोर्ड्स डिलीट किए जाने हैं डिवाइस में कुल रिकोर्ड्स चुने गए मॉड्यूल @@ -232,6 +378,8 @@ अपलोड करने के लिए चित्र अभी सिंक करें सिंक हो रहा है … + सिंक जानकारी पुनः प्राप्त करें + अमान्य चुनाव। कृपया कम से कम एक मॉड्यूल चुनें अमान्य चुनाव। कृपया %1$d से अधिक मॉड्यूल्स नहीं चुनें। @@ -240,14 +388,19 @@ कोई परिणाम नहीं चुने गए मॉड्यूल मॉड्यूल का चुनाव सफल हुआ + पुष्टि करना रद्द करें उंगलियां देखें + उँगलिया + मात्रा + अनलॉक करने के लिए पासवर्ड दर्ज करें लॉग आउट करने के लिए पासवर्ड दर्ज करें पासवर्ड + रद्द करें गलत पासवर्ड अनलॉक करने के लिए टैप करें @@ -259,10 +412,14 @@ यह सुनिश्चित करने के लिए कि आपके प्रोजेक्ट में अद्यतन रिकॉर्ड हैं, डेटा सिंक्रनाइज़ेशन आवश्यक है। बिना किसी वैध कारण के आपके डेटा को सिंक्रोनाइज़ करने में विफल रहने से आपके सहकर्मियों की महत्वपूर्ण रिकॉर्ड तक पहुंच और प्रोजेक्ट की सटीक रिपोर्ट करने की क्षमता प्रभावित होगी।\n\nबिना किसी वैध कारण के डेटा को सिंक्रोनाइज़ करने में विफल होना भी डेटा संग्रह प्रोटोकॉल का उल्लंघन हो सकता है और इसके परिणामस्वरूप आपके और प्रोजेक्ट के लिए अवांछित परिणाम हो सकते हैं। लॉग-आउट क्या आप लॉग-आउट करना चाहते हैं? + लॉग-आउट रद्द करें सिस्टम वर्तमान में रखरखाव के लिए ऑफ़लाइन है। कृपया बाद में पुन: प्रयास करें। सिस्टम वर्तमान में रखरखाव के लिए ऑफ़लाइन है। कृपया %s के बाद पुन: प्रयास करें + + समन्वयन प्रगति पर है + diff --git a/infra/resources/src/main/res/values/colors.xml b/infra/resources/src/main/res/values/colors.xml index d705426743..5d73616f72 100644 --- a/infra/resources/src/main/res/values/colors.xml +++ b/infra/resources/src/main/res/values/colors.xml @@ -18,6 +18,7 @@ #FFEEEEEE #FF888888 #FFC0C0C0 + #22000000 #000000 #A9B5C2 #384553 diff --git a/infra/resources/src/main/res/values/strings.xml b/infra/resources/src/main/res/values/strings.xml index 6c747d94b4..45b15c5543 100644 --- a/infra/resources/src/main/res/values/strings.xml +++ b/infra/resources/src/main/res/values/strings.xml @@ -91,6 +91,10 @@ Currently offline. Please check your internet connection. A problem occurred trying to contact the Integrity Service. Please try again later. A problem occurred trying to contact the server. Please try again later. + Change backend URL + Save + Use default + Cancel Please wait while we proceed to identification @@ -318,6 +322,8 @@ Please turn on internet connection in settings Settings Too many modules have been downloaded. + You need to log in again in order to sync + Last sync: %1$s All records uploaded @@ -365,7 +371,7 @@ Sync Information Select modules Records to upload - Records to download + Records to download or delete Records to delete Total records on device Selected modules diff --git a/infra/resources/src/main/res/values/styles-widget.xml b/infra/resources/src/main/res/values/styles-widget.xml index c2d46bb6f0..727305ee9e 100644 --- a/infra/resources/src/main/res/values/styles-widget.xml +++ b/infra/resources/src/main/res/values/styles-widget.xml @@ -110,6 +110,7 @@ @color/simprints_text_grey_light @color/simprints_text_grey_light @color/simprints_text_black + @color/input_text_background