From 879aa814adfd21f029e5948e8c89ef0d4217275c Mon Sep 17 00:00:00 2001 From: Melad Raouf Date: Tue, 13 Jan 2026 09:23:57 +0000 Subject: [PATCH] [MS-1284] Refactor config store models to use Kotlin Serialization --- face/capture/build.gradle.kts | 1 + ...eedbackAutoCaptureFragmentViewModelTest.kt | 36 ++--- .../LiveFeedbackFragmentViewModelTest.kt | 8 +- .../usecases/IsUsingAutoCaptureUseCaseTest.kt | 5 +- .../feature/clientapi/ClientApiViewModel.kt | 2 +- .../clientapi/usecases/SimpleEventReporter.kt | 2 +- .../ConfirmIdentityValidatorTest.kt | 5 +- ...olmentCreationEventForRecordUseCaseTest.kt | 2 - feature/dashboard/build.gradle.kts | 1 + .../settings/SettingsViewModelTest.kt | 5 +- .../ReportActionRequestEventsUseCase.kt | 11 +- feature/orchestrator/build.gradle.kts | 1 + .../usecases/steps/BuildStepsUseCaseTest.kt | 7 +- ...ToCommCareDataSourceIfNeededUseCaseTest.kt | 4 +- infra/config-store/build.gradle.kts | 3 - .../ProjectConfigSharedPrefsMigration.kt | 8 +- .../models/GeneralConsentOptions.kt | 20 +-- .../migrations/models/OldProjectConfig.kt | 150 ++++++++---------- .../models/ParentalConsentOptions.kt | 20 +-- .../local/models/ProjectConfiguration.kt | 6 +- .../ExperimentalProjectConfiguration.kt | 51 ++++-- .../store/models/ProjectConfiguration.kt | 3 +- .../config/store/models/Vero2Configuration.kt | 9 ++ .../remote/models/ApiProjectConfiguration.kt | 4 +- .../ProjectConfigSharedPrefsMigrationTest.kt | 40 ++--- .../local/models/ProjectConfigurationTest.kt | 8 - .../ExperimentalProjectConfigurationTest.kt | 101 ++++++------ .../infra/config/store/testtools/Models.kt | 14 +- .../core/tools/json/AnyPrimitiveSerializer.kt | 100 ------------ .../RealmToRoomMigrationFlagsStoreTest.kt | 10 +- .../remote/models/ApiInvalidIntentPayload.kt | 3 +- .../models/ApiSuspiciousIntentPayload.kt | 3 +- .../event/domain/models/InvalidIntentEvent.kt | 4 +- .../domain/models/SuspiciousIntentEvent.kt | 4 +- .../infra/license/remote/ApiLicense.kt | 1 + .../remote/LicenseRemoteDataSourceImpl.kt | 3 +- infra/sync/build.gradle.kts | 1 + 37 files changed, 295 insertions(+), 361 deletions(-) delete mode 100644 infra/core/src/main/java/com/simprints/core/tools/json/AnyPrimitiveSerializer.kt diff --git a/face/capture/build.gradle.kts b/face/capture/build.gradle.kts index bbb3a84762..fa468ad585 100644 --- a/face/capture/build.gradle.kts +++ b/face/capture/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(libs.androidX.cameraX.view) implementation(libs.androidX.ui.preference) implementation(libs.workManager.work) + implementation(libs.kotlin.serialization) implementation(libs.circleImageView) diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt index 6c5728bcbf..0ce574aa4b 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackAutoCaptureFragmentViewModelTest.kt @@ -15,7 +15,6 @@ import com.simprints.face.infra.basebiosdk.detection.FaceDetector import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.experimental import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.testObserver import io.mockk.* @@ -23,6 +22,7 @@ import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Before import org.junit.Rule import org.junit.Test @@ -74,8 +74,8 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { } returns QUALITY_THRESHOLD every { isUsingAutoCapture.invoke(any()) } returns true coEvery { - configRepository.getProjectConfiguration().experimental().singleQualityFallbackRequired - } returns false + configRepository.getProjectConfiguration().custom + } returns mapOf("singleQualityFallbackRequired" to JsonPrimitive(false)) every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) } justRun { previewFrame.recycle() } val resolveFaceBioSdkUseCase = mockk { @@ -294,8 +294,8 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { val badQuality: Face = getFace(quality = -2f) coEvery { - configRepository.getProjectConfiguration().experimental().singleQualityFallbackRequired - } returns true + configRepository.getProjectConfiguration().custom + } returns mapOf("singleQualityFallbackRequired" to JsonPrimitive(true)) every { faceDetector.analyze(frame) } returnsMany listOf( badQuality, // not a fallback image due to bad quality @@ -325,11 +325,9 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { fun `Use default imaging duration when not configured`() = runTest { coEvery { faceDetector.analyze(frame) } returns getFace() coEvery { - configRepository - .getProjectConfiguration() - .experimental() - .faceAutoCaptureImagingDurationMillis - } returns AUTO_CAPTURE_IMAGING_DURATION_MS + configRepository.getProjectConfiguration().custom + } returns mapOf("faceAutoCaptureImagingDurationMillis" to JsonPrimitive(AUTO_CAPTURE_IMAGING_DURATION_MS)) + val capturingState = viewModel.capturingState.testObserver() viewModel.initAutoCapture() @@ -351,12 +349,12 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { fun `Use custom imaging duration when provided in config`() = runTest { val configDuration = 5000L coEvery { faceDetector.analyze(frame) } returns getFace() - coEvery { - configRepository - .getProjectConfiguration() - .experimental() - .faceAutoCaptureImagingDurationMillis - } returns configDuration + coEvery { configRepository.getProjectConfiguration().custom } returns + mapOf( + "faceAutoCaptureImagingDurationMillis" to JsonPrimitive(configDuration.toInt()), + "singleQualityFallbackRequired" to JsonPrimitive(false), + ) + val capturingState = viewModel.capturingState.testObserver() viewModel.initAutoCapture() @@ -367,8 +365,8 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { Truth .assertThat(capturingState.observedValues.last()) .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.CAPTURING) - - advanceTimeBy(configDuration / 2) + // Add 1ms to account for json deserialization delay + advanceTimeBy((configDuration / 2) + 1) Truth .assertThat(capturingState.observedValues.last()) .isEqualTo(LiveFeedbackFragmentViewModel.CapturingState.FINISHED) @@ -386,7 +384,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest { viewModel.initCapture(FaceConfiguration.BioSdk.SIM_FACE, samplesToKeep, 0) viewModel.process(frame) // won't be observed during the preparation phase viewModel.startCapture() - (1..100).forEach { + repeat(100) { viewModel.process(frame) } advanceTimeBy(AUTO_CAPTURE_IMAGING_DURATION_MS + 1) diff --git a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt index ca76360553..41252a3621 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/screens/livefeedback/LiveFeedbackFragmentViewModelTest.kt @@ -15,12 +15,12 @@ import com.simprints.face.infra.basebiosdk.detection.FaceDetector import com.simprints.face.infra.biosdkresolver.ResolveFaceBioSdkUseCase import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.FaceConfiguration -import com.simprints.infra.config.store.models.experimental import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.testObserver import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Before import org.junit.Rule import org.junit.Test @@ -69,7 +69,8 @@ internal class LiveFeedbackFragmentViewModelTest { ?.qualityThreshold } returns QUALITY_THRESHOLD every { isUsingAutoCapture.invoke(any()) } returns false - coEvery { configRepository.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns false + coEvery { configRepository.getProjectConfiguration().custom } returns + mapOf("singleQualityFallbackRequired" to JsonPrimitive(false)) every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) } justRun { previewFrame.recycle() } val resolveFaceBioSdkUseCase = mockk { @@ -163,7 +164,8 @@ internal class LiveFeedbackFragmentViewModelTest { val validFace: Face = getFace() val badQuality: Face = getFace(quality = -2f) - coEvery { configRepository.getProjectConfiguration().experimental().singleQualityFallbackRequired } returns true + coEvery { configRepository.getProjectConfiguration().custom } returns + mapOf("singleQualityFallbackRequired" to JsonPrimitive(true)) every { faceDetector.analyze(frame) } returnsMany listOf( badQuality, diff --git a/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt b/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt index ad18d09dcd..5b2338da16 100644 --- a/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt +++ b/face/capture/src/test/java/com/simprints/face/capture/usecases/IsUsingAutoCaptureUseCaseTest.kt @@ -4,10 +4,10 @@ import android.content.Context import android.content.SharedPreferences import androidx.preference.PreferenceManager import com.simprints.infra.config.store.models.ProjectConfiguration -import com.simprints.infra.config.store.models.experimental import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -38,7 +38,8 @@ class IsUsingAutoCaptureUseCaseTest { featureEnabled: Boolean, preferenceEnabled: Boolean, ) { - coEvery { projectConfiguration.experimental().faceAutoCaptureEnabled } returns featureEnabled + coEvery { projectConfiguration.custom } returns + mapOf("faceAutoCaptureEnabled" to JsonPrimitive(featureEnabled)) every { sharedPreferences.getBoolean("preference_enable_face_auto_capture", true) } returns preferenceEnabled } diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt index 1d5a119aca..b5a10576ec 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/ClientApiViewModel.kt @@ -77,7 +77,7 @@ class ClientApiViewModel @Inject internal constructor( action: String, extras: Bundle, ): ActionRequest? { - val extrasMap = extras.toMap() + val extrasMap: Map = extras.toMap().mapValues { it.value.toString() } return try { // Session must be created to be able to report invalid intents if mapping fails if (createSessionIfRequiredUseCase(action)) { diff --git a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SimpleEventReporter.kt b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SimpleEventReporter.kt index 927987672d..c9e69f37c5 100644 --- a/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SimpleEventReporter.kt +++ b/feature/client-api/src/main/java/com/simprints/feature/clientapi/usecases/SimpleEventReporter.kt @@ -16,7 +16,7 @@ internal class SimpleEventReporter @Inject constructor( ) { fun addInvalidIntentEvent( action: String, - extras: Map, + extras: Map, ) { sessionCoroutineScope.launch { coreEventRepository.addOrUpdateEvent(InvalidIntentEvent(timeHelper.now(), action, extras)) diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt index b7431115ca..eeced39c3a 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/mappers/request/validators/ConfirmIdentityValidatorTest.kt @@ -14,6 +14,7 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Before import org.junit.Test @@ -199,7 +200,7 @@ internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(Confirm coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) // Mock ConfigManager with feature flag enabled - every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to true) + every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to JsonPrimitive(true)) coEvery { mockConfigRepository.getProjectConfiguration() } returns mockProjectConfig val validator = ConfirmIdentityValidator( @@ -224,7 +225,7 @@ internal class ConfirmIdentityValidatorTest : ActionRequestValidatorTest(Confirm coEvery { mockEventRepository.getEventsFromScope(any()) } returns listOf(mockCallback) // Mock ConfigManager with feature flag disabled - every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to false) + every { mockProjectConfig.custom } returns mapOf("allowConfirmingGuidsNotInCallback" to JsonPrimitive(false)) coEvery { mockConfigRepository.getProjectConfiguration() } returns mockProjectConfig val validator = ConfirmIdentityValidator( diff --git a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt index c9d10e7322..3561959cd7 100644 --- a/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt +++ b/feature/client-api/src/test/java/com/simprints/feature/clientapi/usecases/GetEnrolmentCreationEventForRecordUseCaseTest.kt @@ -37,8 +37,6 @@ class GetEnrolmentCreationEventForRecordUseCaseTest { fun setUp() { MockKAnnotations.init(this, relaxed = true) - every { jsonHelper.toJson(any()) } returns "json" - useCase = GetEnrolmentCreationEventForRecordUseCase( configRepository, enrolmentRecordRepository, diff --git a/feature/dashboard/build.gradle.kts b/feature/dashboard/build.gradle.kts index 8d339309ed..15b754e45a 100644 --- a/feature/dashboard/build.gradle.kts +++ b/feature/dashboard/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { debugImplementation(project(":testing:data-generator")) implementation(libs.fuzzywuzzy.core) + implementation(libs.kotlin.serialization) // UI implementation(libs.androidX.ui.preference) diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/SettingsViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/SettingsViewModelTest.kt index 7612e1bd7b..ac81884e78 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/SettingsViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/SettingsViewModelTest.kt @@ -17,6 +17,7 @@ import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Before import org.junit.Rule import org.junit.Test @@ -66,8 +67,8 @@ class SettingsViewModelTest { @Test fun `experimentalConfiguration live data should follow the project experimental configuration`() = runTest { - val experimentalConfig1 = mapOf("key1" to "value1") - val experimentalConfig2 = mapOf("key2" to "value2") + val experimentalConfig1 = mapOf("key1" to JsonPrimitive("value1")) + val experimentalConfig2 = mapOf("key2" to JsonPrimitive("value2")) coEvery { configRepository.observeProjectConfiguration() } returns flowOf( mockk(relaxed = true) { diff --git a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/ReportActionRequestEventsUseCase.kt b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/ReportActionRequestEventsUseCase.kt index 410c133fe3..ff3e9164c7 100644 --- a/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/ReportActionRequestEventsUseCase.kt +++ b/feature/login-check/src/main/java/com/simprints/feature/logincheck/usecases/ReportActionRequestEventsUseCase.kt @@ -40,7 +40,12 @@ internal class ReportActionRequestEventsUseCase @Inject constructor( private fun reportUnknownExtras(actionRequest: ActionRequest) { if (actionRequest.unknownExtras.isNotEmpty()) { sessionCoroutineScope.launch { - sessionEventRepository.addOrUpdateEvent(SuspiciousIntentEvent(timeHelper.now(), actionRequest.unknownExtras)) + sessionEventRepository.addOrUpdateEvent( + SuspiciousIntentEvent( + timeHelper.now(), + actionRequest.unknownExtras.mapValues { it.value.toString() }, + ), + ) } } } @@ -61,6 +66,7 @@ internal class ReportActionRequestEventsUseCase @Inject constructor( metadata, BiometricDataSource.fromString(biometricDataSource), ) + is ActionRequest.IdentifyActionRequest -> IdentificationCalloutEventV3( startTime, projectId, @@ -69,6 +75,7 @@ internal class ReportActionRequestEventsUseCase @Inject constructor( metadata, BiometricDataSource.fromString(biometricDataSource), ) + is ActionRequest.VerifyActionRequest -> VerificationCalloutEventV3( startTime, projectId, @@ -78,6 +85,7 @@ internal class ReportActionRequestEventsUseCase @Inject constructor( metadata, BiometricDataSource.fromString(biometricDataSource), ) + is ActionRequest.ConfirmIdentityActionRequest -> ConfirmationCalloutEventV3( startTime, projectId, @@ -85,6 +93,7 @@ internal class ReportActionRequestEventsUseCase @Inject constructor( sessionId, metadata, ) + is ActionRequest.EnrolLastBiometricActionRequest -> EnrolmentLastBiometricsCalloutEventV3( startTime, projectId, diff --git a/feature/orchestrator/build.gradle.kts b/feature/orchestrator/build.gradle.kts index 6fa7345088..76ea648484 100644 --- a/feature/orchestrator/build.gradle.kts +++ b/feature/orchestrator/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("simprints.feature") id("kotlin-parcelize") + id("simprints.library.kotlinSerialization") } android { diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt index 354c73ac45..411d72c6db 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/BuildStepsUseCaseTest.kt @@ -3,8 +3,8 @@ package com.simprints.feature.orchestrator.usecases.steps import com.google.common.truth.Truth.* import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality -import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.common.TemplateIdentifier +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential import com.simprints.feature.orchestrator.cache.OrchestratorCache import com.simprints.feature.orchestrator.exceptions.SubjectAgeNotSupportedException @@ -17,12 +17,12 @@ import com.simprints.infra.config.store.models.FingerprintConfiguration import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.NEC import com.simprints.infra.config.store.models.FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER import com.simprints.infra.config.store.models.ProjectConfiguration -import com.simprints.infra.config.store.models.experimental import com.simprints.infra.orchestration.data.ActionRequest import io.mockk.* import io.mockk.impl.annotations.* import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before @@ -240,7 +240,8 @@ class BuildStepsUseCaseTest { val action = mockk(relaxed = true) every { action.getSubjectAgeIfAvailable() } returns null - every { projectConfiguration.experimental().idPoolValidationEnabled } returns true + every { projectConfiguration.custom } returns + mapOf("validateIdentificationPool" to JsonPrimitive(true)) val steps = useCase.build(action, projectConfiguration, enrolmentSubjectId, cachedScannedCredential) diff --git a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/FallbackToCommCareDataSourceIfNeededUseCaseTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/FallbackToCommCareDataSourceIfNeededUseCaseTest.kt index 28d5460609..f4f60b67d0 100644 --- a/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/FallbackToCommCareDataSourceIfNeededUseCaseTest.kt +++ b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/usecases/steps/FallbackToCommCareDataSourceIfNeededUseCaseTest.kt @@ -14,6 +14,7 @@ import io.mockk.every import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonPrimitive import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -44,7 +45,8 @@ class FallbackToCommCareDataSourceIfNeededUseCaseTest { return mockk(relaxed = true) { every { synchronization } returns syncConfig - every { custom } returns mapOf("fallbackToCommCareThresholdDays" to thresholdDays) + every { custom } returns + mapOf("fallbackToCommCareThresholdDays" to JsonPrimitive(thresholdDays)) } } diff --git a/infra/config-store/build.gradle.kts b/infra/config-store/build.gradle.kts index 4aec9b3f06..27eea3048a 100644 --- a/infra/config-store/build.gradle.kts +++ b/infra/config-store/build.gradle.kts @@ -11,9 +11,6 @@ android { dependencies { implementation(project(":infra:auth-store")) implementation(project(":infra:enrolment-records:realm-store")) - implementation(libs.datastore) - implementation(libs.retrofit.core) - implementation(libs.jackson.core) } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigration.kt index 5b7a057712..aeb1af01f4 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/ProjectConfigSharedPrefsMigration.kt @@ -3,7 +3,6 @@ package com.simprints.infra.config.store.local.migrations import android.content.Context import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataMigration -import com.fasterxml.jackson.core.JacksonException import com.simprints.core.tools.json.JsonHelper import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.local.migrations.models.OldProjectConfig @@ -12,6 +11,7 @@ import com.simprints.infra.config.store.local.models.toProto import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION import com.simprints.infra.logging.Simber import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.SerializationException import javax.inject.Inject /** @@ -37,12 +37,12 @@ internal class ProjectConfigSharedPrefsMigration @Inject constructor( if (projectSettingsJson.isNullOrEmpty()) return currentData return try { - JsonHelper - .fromJson(projectSettingsJson) + JsonHelper.json + .decodeFromString(projectSettingsJson) .toDomain(authStore.signedInProjectId) .toProto() } catch (e: Exception) { - if (e is JacksonException) { + if (e is SerializationException) { // Return default value Simber.i("Invalid old configuration for project ${authStore.signedInProjectId}", e, tag = MIGRATION) ProtoProjectConfiguration.getDefaultInstance() diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/GeneralConsentOptions.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/GeneralConsentOptions.kt index 55534116db..937bf9c53a 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/GeneralConsentOptions.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/GeneralConsentOptions.kt @@ -1,19 +1,21 @@ package com.simprints.infra.config.store.local.migrations.models import androidx.annotation.Keep -import com.fasterxml.jackson.annotation.JsonProperty import com.simprints.infra.config.store.models.ConsentConfiguration +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class GeneralConsentOptions( - @JsonProperty("consent_enrol_only") var consentEnrolOnly: Boolean = false, - @JsonProperty("consent_enrol") var consentEnrol: Boolean = true, - @JsonProperty("consent_id_verify") var consentIdVerify: Boolean = true, - @JsonProperty("consent_share_data_no") var consentShareDataNo: Boolean = true, - @JsonProperty("consent_share_data_yes") var consentShareDataYes: Boolean = false, - @JsonProperty("consent_collect_yes") var consentCollectYes: Boolean = false, - @JsonProperty("consent_privacy_rights") var consentPrivacyRights: Boolean = true, - @JsonProperty("consent_confirmation") var consentConfirmation: Boolean = true, + @SerialName("consent_enrol_only") var consentEnrolOnly: Boolean = false, + @SerialName("consent_enrol") var consentEnrol: Boolean = true, + @SerialName("consent_id_verify") var consentIdVerify: Boolean = true, + @SerialName("consent_share_data_no") var consentShareDataNo: Boolean = true, + @SerialName("consent_share_data_yes") var consentShareDataYes: Boolean = false, + @SerialName("consent_collect_yes") var consentCollectYes: Boolean = false, + @SerialName("consent_privacy_rights") var consentPrivacyRights: Boolean = true, + @SerialName("consent_confirmation") var consentConfirmation: Boolean = true, ) { fun toDomain(): ConsentConfiguration.ConsentPromptConfiguration = ConsentConfiguration.ConsentPromptConfiguration( enrolmentVariant = if (consentEnrol) { 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 f49fe591d1..983cf65047 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 @@ -1,7 +1,7 @@ package com.simprints.infra.config.store.local.migrations.models import androidx.annotation.Keep -import com.fasterxml.jackson.annotation.JsonProperty +import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import com.simprints.core.domain.common.Modality import com.simprints.core.domain.common.TemplateIdentifier import com.simprints.core.domain.tokenization.asTokenizableRaw @@ -23,48 +23,55 @@ import com.simprints.infra.config.store.models.Vero1Configuration import com.simprints.infra.config.store.models.Vero2Configuration import com.simprints.infra.config.store.models.Vero2Configuration.LedsMode.BASIC import com.simprints.infra.config.store.models.Vero2Configuration.LedsMode.LIVE_QUALITY_FEEDBACK +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.JsonElement import org.json.JSONObject @Keep +@Serializable +@ExcludedFromGeneratedTestCoverageReports("Data class") internal data class OldProjectConfig( - @JsonProperty("CaptureFingerprintStrategy") private val captureFingerprintStrategy: String?, - @JsonProperty("ConsentRequired") private val consentRequired: String, - @JsonProperty("SaveFingerprintImagesStrategy") private val saveFingerprintImagesStrategy: String?, - @JsonProperty("Vero2FirmwareVersions") private val vero2FirmwareVersions: String?, - @JsonProperty("FingerprintQualityThreshold") private val fingerprintQualityThreshold: String?, - @JsonProperty("FingerprintsToCollect") private val fingerprintsToCollect: String?, - @JsonProperty("ConsentParentalExists") private val consentParentalExists: String, - @JsonProperty("MaxNbOfModules") private val maxNbOfModules: String, - @JsonProperty("FaceQualityThreshold") private val faceQualityThreshold: String?, - @JsonProperty("LogoExists") private val logoExists: String, - @JsonProperty("EnrolmentPlus") private val enrolmentPlus: String, - @JsonProperty("FingerComparisonStrategyForVerification") private val fingerComparisonStrategyForVerification: String?, - @JsonProperty("FingerStatus") private val fingerStatus: String?, - @JsonProperty("ScannerGenerations") private val scannerGenerations: String?, - @JsonProperty("FaceNbOfFramesCaptured") private val faceNbOfFramesCaptured: String?, - @JsonProperty("ProjectSpecificMode") private val projectSpecificMode: String, - @JsonProperty("OrganizationName") private val organizationName: String, - @JsonProperty("SelectedLanguage") private val selectedLanguage: String, - @JsonProperty("ProjectLanguages") private val projectLanguages: String, - @JsonProperty("FingerprintLiveFeedbackOn") private val fingerprintLiveFeedbackOn: String?, - @JsonProperty("FaceConfidenceThresholds") private val faceConfidenceThresholds: String?, - @JsonProperty("MatchGroup") private val matchGroup: String, - @JsonProperty("SyncDestination") private val syncDestination: String?, - @JsonProperty("SimprintsSync") private val simprintsSync: String?, - @JsonProperty("LocationRequired") private val locationRequired: String, - @JsonProperty("FingerImagesExist") private val fingerImagesExist: String?, - @JsonProperty("ConsentParentalOptions") private val consentParentalOptions: String, - @JsonProperty("SaveFaceImages") private val saveFaceImages: String?, - @JsonProperty("ConsentGeneralOptions") private val consentGeneralOptions: String, - @JsonProperty("DownSyncSetting") private val downSyncSetting: String, - @JsonProperty("CoSync") private val coSync: String?, - @JsonProperty("ModuleIdOptions") private val moduleIdOptions: String, - @JsonProperty("FingerprintConfidenceThresholds") private val fingerprintConfidenceThresholds: String?, - @JsonProperty("ProgramName") private val programName: String, - @JsonProperty("SyncGroup") private val syncGroup: String, - @JsonProperty("NbOfIdsInt") private val nbOfIdsInt: String, - @JsonProperty("Modality") private val modality: String, - @JsonProperty("Custom") private val custom: Any?, + @SerialName("CaptureFingerprintStrategy") private val captureFingerprintStrategy: String?, + @SerialName("ConsentRequired") private val consentRequired: String, + @SerialName("SaveFingerprintImagesStrategy") private val saveFingerprintImagesStrategy: String?, + @SerialName("Vero2FirmwareVersions") private val vero2FirmwareVersions: String?, + @SerialName("FingerprintQualityThreshold") private val fingerprintQualityThreshold: String?, + @SerialName("FingerprintsToCollect") private val fingerprintsToCollect: String?, + @SerialName("ConsentParentalExists") private val consentParentalExists: String, + @SerialName("MaxNbOfModules") private val maxNbOfModules: String, + @SerialName("FaceQualityThreshold") private val faceQualityThreshold: String?, + @SerialName("LogoExists") private val logoExists: String, + @SerialName("EnrolmentPlus") private val enrolmentPlus: String, + @SerialName("FingerComparisonStrategyForVerification") private val fingerComparisonStrategyForVerification: String?, + @SerialName("FingerStatus") private val fingerStatus: String?, + @SerialName("ScannerGenerations") private val scannerGenerations: String?, + @SerialName("FaceNbOfFramesCaptured") private val faceNbOfFramesCaptured: String?, + @SerialName("ProjectSpecificMode") private val projectSpecificMode: String, + @SerialName("OrganizationName") private val organizationName: String, + @SerialName("SelectedLanguage") private val selectedLanguage: String, + @SerialName("ProjectLanguages") private val projectLanguages: String, + @SerialName("FingerprintLiveFeedbackOn") private val fingerprintLiveFeedbackOn: String?, + @SerialName("FaceConfidenceThresholds") private val faceConfidenceThresholds: String?, + @SerialName("MatchGroup") private val matchGroup: String, + @SerialName("SyncDestination") private val syncDestination: String?, + @SerialName("SimprintsSync") private val simprintsSync: String?, + @SerialName("LocationRequired") private val locationRequired: String, + @SerialName("FingerImagesExist") private val fingerImagesExist: String?, + @SerialName("ConsentParentalOptions") private val consentParentalOptions: String, + @SerialName("SaveFaceImages") private val saveFaceImages: String?, + @SerialName("ConsentGeneralOptions") private val consentGeneralOptions: String, + @SerialName("DownSyncSetting") private val downSyncSetting: String, + @SerialName("CoSync") private val coSync: String?, + @SerialName("ModuleIdOptions") private val moduleIdOptions: String, + @SerialName("FingerprintConfidenceThresholds") private val fingerprintConfidenceThresholds: String?, + @SerialName("ProgramName") private val programName: String, + @SerialName("SyncGroup") private val syncGroup: String, + @SerialName("NbOfIdsInt") private val nbOfIdsInt: String, + @SerialName("Modality") private val modality: String, + @SerialName("Custom") private val custom: JsonElement?, ) { fun toDomain(projectId: String): ProjectConfiguration = ProjectConfiguration( id = "", @@ -81,10 +88,7 @@ internal data class OldProjectConfig( ) private fun generalConfiguration(): GeneralConfiguration { - val modalities = modality - .split(",") - .map { if (it == "FINGER") "FINGERPRINT" else it } - .map { Modality.valueOf(it) } + val modalities = modality.split(",").map { if (it == "FINGER") "FINGERPRINT" else it }.map { Modality.valueOf(it) } return GeneralConfiguration( modalities = modalities, matchingModalities = modalities, @@ -102,16 +106,14 @@ internal data class OldProjectConfig( FaceConfiguration( allowedSDKs = listOf(FaceConfiguration.BioSdk.RANK_ONE), rankOne = FaceConfiguration.FaceSdkConfiguration( - nbOfImagesToCapture = faceNbOfFramesCaptured?.toIntOrNull() - ?: DEFAULT_FACE_FRAMES_TO_CAPTURE, + nbOfImagesToCapture = faceNbOfFramesCaptured?.toIntOrNull() ?: DEFAULT_FACE_FRAMES_TO_CAPTURE, qualityThreshold = faceQualityThreshold.toFloat(), imageSavingStrategy = if (saveFaceImages.toBoolean()) { FaceConfiguration.ImageSavingStrategy.ONLY_USED_IN_REFERENCE } else { FaceConfiguration.ImageSavingStrategy.NEVER }, - decisionPolicy = faceConfidenceThresholds?.let { parseDecisionPolicy(it) } - ?: DecisionPolicy(0, 0, 0), + decisionPolicy = faceConfidenceThresholds?.let { parseDecisionPolicy(it) } ?: DecisionPolicy(0, 0, 0), version = DEFAULT_FACE_SDK_VERSION, ), simFace = null, @@ -124,29 +126,20 @@ internal data class OldProjectConfig( FingerprintConfiguration( allowedSDKs = listOf(FingerprintConfiguration.BioSdk.SECUGEN_SIM_MATCHER), displayHandIcons = fingerImagesExist.toBoolean(), - allowedScanners = scannerGenerations - ?.split(",") - ?.map { - FingerprintConfiguration.VeroGeneration.valueOf( - it, - ) - } - ?: listOf(FingerprintConfiguration.VeroGeneration.VERO_1), + allowedScanners = scannerGenerations?.split(",")?.map { + FingerprintConfiguration.VeroGeneration.valueOf( + it, + ) + } ?: listOf(FingerprintConfiguration.VeroGeneration.VERO_1), secugenSimMatcher = FingerprintConfiguration.FingerprintSdkConfiguration( - fingersToCapture = fingerprintsToCollect - ?.split(",") - ?.map { TemplateIdentifier.valueOf(it) } - ?: listOf( - TemplateIdentifier.LEFT_THUMB, - TemplateIdentifier.LEFT_INDEX_FINGER, - ), - decisionPolicy = fingerprintConfidenceThresholds?.let { parseDecisionPolicy(it) } - ?: DecisionPolicy(0, 0, 700), - comparisonStrategyForVerification = fingerComparisonStrategyForVerification - ?.let { - FingerprintConfiguration.FingerComparisonStrategy.valueOf(it) - } - ?: FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, + fingersToCapture = fingerprintsToCollect?.split(",")?.map { TemplateIdentifier.valueOf(it) } ?: listOf( + TemplateIdentifier.LEFT_THUMB, + TemplateIdentifier.LEFT_INDEX_FINGER, + ), + decisionPolicy = fingerprintConfidenceThresholds?.let { parseDecisionPolicy(it) } ?: DecisionPolicy(0, 0, 700), + comparisonStrategyForVerification = fingerComparisonStrategyForVerification?.let { + FingerprintConfiguration.FingerComparisonStrategy.valueOf(it) + } ?: FingerprintConfiguration.FingerComparisonStrategy.SAME_FINGER, vero1 = Vero1Configuration(fingerprintQualityThreshold.toInt()), vero2 = vero2Configuration(), maxCaptureAttempts = null, @@ -174,12 +167,13 @@ internal data class OldProjectConfig( emptyMap() } else { // Construct a JavaType instance for Map - val type = JsonHelper.jackson.typeFactory.constructMapType( - Map::class.java, - String::class.java, - Vero2Configuration.Vero2FirmwareVersions::class.java, + JsonHelper.json.decodeFromString( + MapSerializer( + String.serializer(), + Vero2Configuration.Vero2FirmwareVersions.serializer(), + ), + vero2FirmwareVersions, ) - JsonHelper.fromJson(vero2FirmwareVersions, type) }, ) } @@ -190,12 +184,8 @@ internal data class OldProjectConfig( collectConsent = consentRequired.toBoolean(), displaySimprintsLogo = logoExists.toBoolean(), allowParentalConsent = consentParentalExists.toBoolean(), - generalPrompt = JsonHelper - .fromJson(consentGeneralOptions) - .toDomain(), - parentalPrompt = JsonHelper - .fromJson(consentParentalOptions) - .toDomain(), + generalPrompt = JsonHelper.json.decodeFromString(consentGeneralOptions).toDomain(), + parentalPrompt = JsonHelper.json.decodeFromString(consentParentalOptions).toDomain(), ) private fun identificationConfiguration(): IdentificationConfiguration = IdentificationConfiguration( diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/ParentalConsentOptions.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/ParentalConsentOptions.kt index 5f3d016a89..7352a2e674 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/ParentalConsentOptions.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/migrations/models/ParentalConsentOptions.kt @@ -1,19 +1,21 @@ package com.simprints.infra.config.store.local.migrations.models import androidx.annotation.Keep -import com.fasterxml.jackson.annotation.JsonProperty import com.simprints.infra.config.store.models.ConsentConfiguration +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Keep +@Serializable data class ParentalConsentOptions( - @JsonProperty("consent_parent_enrol_only") var consentParentEnrolOnly: Boolean = false, - @JsonProperty("consent_parent_enrol") var consentParentEnrol: Boolean = true, - @JsonProperty("consent_parent_id_verify") var consentParentIdVerify: Boolean = true, - @JsonProperty("consent_parent_share_data_no") var consentParentShareDataNo: Boolean = true, - @JsonProperty("consent_parent_share_data_yes") var consentParentShareDataYes: Boolean = false, - @JsonProperty("consent_parent_collect_yes") var consentParentCollectYes: Boolean = false, - @JsonProperty("consent_parent_privacy_rights") var consentParentPrivacyRights: Boolean = true, - @JsonProperty("consent_parent_confirmation") var consentParentConfirmation: Boolean = true, + @SerialName("consent_parent_enrol_only") var consentParentEnrolOnly: Boolean = false, + @SerialName("consent_parent_enrol") var consentParentEnrol: Boolean = true, + @SerialName("consent_parent_id_verify") var consentParentIdVerify: Boolean = true, + @SerialName("consent_parent_share_data_no") var consentParentShareDataNo: Boolean = true, + @SerialName("consent_parent_share_data_yes") var consentParentShareDataYes: Boolean = false, + @SerialName("consent_parent_collect_yes") var consentParentCollectYes: Boolean = false, + @SerialName("consent_parent_privacy_rights") var consentParentPrivacyRights: Boolean = true, + @SerialName("consent_parent_confirmation") var consentParentConfirmation: Boolean = true, ) { fun toDomain(): ConsentConfiguration.ConsentPromptConfiguration = ConsentConfiguration.ConsentPromptConfiguration( enrolmentVariant = if (consentParentEnrolOnly) { diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt index 93acadfa1a..b755602208 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/local/models/ProjectConfiguration.kt @@ -19,7 +19,7 @@ internal fun ProjectConfiguration.toProto(): ProtoProjectConfiguration = ProtoPr }.also { if (custom != null) { try { - val customJson = JsonHelper.toJson(custom) + val customJson = JsonHelper.json.encodeToString(custom) it.setCustomJson(customJson) } catch (_: Exception) { // It is safer to not have custom config, than broken one @@ -41,8 +41,8 @@ internal fun ProtoProjectConfiguration.toDomain(): ProjectConfiguration = Projec multifactorId = multiFactorId?.toDomain(), custom = customJson?.takeIf { it.isNotBlank() }?.let { try { - JsonHelper.fromJson(it) - } catch (_: Exception) { + JsonHelper.json.decodeFromString(it) + } catch (e: Exception) { // It is safer to not have custom config, than broken one null } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt index 13feeea67c..5e5c10f029 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/ExperimentalProjectConfiguration.kt @@ -1,84 +1,101 @@ package com.simprints.infra.config.store.models +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + /** * Thin wrapper around custom project configuration to keep all the experimental * feature definitions in a single place and make calls explicit and type-safe. */ data class ExperimentalProjectConfiguration( - private val customConfig: Map?, + private val customConfig: Map?, ) { val idPoolValidationEnabled: Boolean get() = customConfig ?.get(ENABLE_ID_POOL_VALIDATION) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val singleQualityFallbackRequired: Boolean get() = customConfig ?.get(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val faceAutoCaptureEnabled: Boolean get() = customConfig ?.get(FACE_AUTO_CAPTURE_ENABLED) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val faceAutoCaptureImagingDurationMillis: Long get() = customConfig ?.get(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS) - ?.let { it as? Int } - ?.toLong() + ?.jsonPrimitive + ?.longOrNull ?.coerceIn(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN, FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX) ?: FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT val recordsDbMigrationFromRealmEnabled: Boolean get() = customConfig ?.get(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val recordsDbMigrationFromRealmMaxRetries: Int - get() = customConfig?.get(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES).let { - (it as? Int) ?: RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES - } + get() = customConfig + ?.get(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES) + ?.jsonPrimitive + ?.intOrNull + ?: RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES val sampleUploadWithSignedUrlEnabled: Boolean get() = customConfig ?.get(SAMPLE_UPLOAD_WITH_URL_ENABLED) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val displayCameraFlashToggle: Boolean get() = customConfig ?.get(CAMERA_FLASH_CONTROLS_ENABLED) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } val fallbackToCommCareThresholdDays: Long get() = customConfig ?.get(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS) - ?.let { it as? Int } - ?.toLong() + ?.jsonPrimitive + ?.longOrNull ?: FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT val ocrUseHighRes: Boolean get() = customConfig ?.get(OCR_USE_HIGH_RES) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull ?: OCR_USE_HIGH_RES_DEFAULT val ocrCaptures: Int get() = customConfig ?.get(OCR_CAPTURES) - ?.let { it as? Int } + ?.jsonPrimitive + ?.intOrNull ?: OCR_CAPTURES_DEFAULT val allowConfirmingGuidsNotInCallback: Boolean get() = customConfig ?.get(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK) - ?.let { it as? Boolean } + ?.jsonPrimitive + ?.booleanOrNull .let { it == true } companion object { 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 d2a656afc0..5b700e041d 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 @@ -3,6 +3,7 @@ package com.simprints.infra.config.store.models import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality import com.simprints.core.domain.common.ModalitySdkType +import kotlinx.serialization.json.JsonElement data class ProjectConfiguration( val id: String, @@ -15,7 +16,7 @@ data class ProjectConfiguration( val identification: IdentificationConfiguration, val synchronization: SynchronizationConfiguration, val multifactorId: MultiFactorIdConfiguration?, - val custom: Map?, + val custom: Map?, ) fun ProjectConfiguration.canCoSyncAllData(): Boolean = diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Vero2Configuration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Vero2Configuration.kt index 8949ecfe4e..7d5c601642 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Vero2Configuration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/models/Vero2Configuration.kt @@ -1,8 +1,10 @@ package com.simprints.infra.config.store.models import androidx.annotation.Keep +import kotlinx.serialization.Serializable @Keep +@Serializable data class Vero2Configuration( val qualityThreshold: Int, val imageSavingStrategy: ImageSavingStrategy, @@ -10,6 +12,8 @@ data class Vero2Configuration( val ledsMode: LedsMode, val firmwareVersions: Map, ) { + @Keep + @Serializable enum class ImageSavingStrategy { NEVER, ONLY_GOOD_SCAN, @@ -17,6 +21,8 @@ data class Vero2Configuration( EAGER, } + @Keep + @Serializable enum class CaptureStrategy { SECUGEN_ISO_500_DPI, SECUGEN_ISO_1000_DPI, @@ -24,6 +30,8 @@ data class Vero2Configuration( SECUGEN_ISO_1700_DPI, } + @Keep + @Serializable enum class LedsMode { BASIC, LIVE_QUALITY_FEEDBACK, @@ -31,6 +39,7 @@ data class Vero2Configuration( } @Keep + @Serializable data class Vero2FirmwareVersions( val cypress: String = "", val stm: String = "", diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt index 31c77f0df7..94df62b87d 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/remote/models/ApiProjectConfiguration.kt @@ -1,9 +1,9 @@ package com.simprints.infra.config.store.remote.models import androidx.annotation.Keep -import com.simprints.core.tools.json.AnyPrimitiveSerializer import com.simprints.infra.config.store.models.ProjectConfiguration import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement @Keep @Serializable @@ -18,7 +18,7 @@ internal data class ApiProjectConfiguration( val identification: ApiIdentificationConfiguration, val synchronization: ApiSynchronizationConfiguration, val multiFactorId: ApiMultiFactorIdConfiguration? = null, - val custom: Map? = null, + val custom: Map? = null, ) { fun toDomain(): ProjectConfiguration = ProjectConfiguration( id = id, 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 4b196909f7..76e0f7a7ad 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 @@ -30,6 +30,7 @@ import com.simprints.infra.config.store.testtools.protoProjectConfiguration import com.simprints.testtools.common.syntax.assertThrows import io.mockk.* import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonElement import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -415,7 +416,7 @@ class ProjectConfigSharedPrefsMigrationTest { // Force an exception val exception = Exception("") mockkObject(JsonHelper) - every { JsonHelper.fromJson(any()) } throws exception + every { JsonHelper.json.decodeFromString(any()) } throws exception val receivedException = assertThrows { projectConfigSharedPrefsMigration.migrate(protoProjectConfiguration) } @@ -436,22 +437,21 @@ class ProjectConfigSharedPrefsMigrationTest { verify(exactly = 1) { editor.apply() } } - private fun concatMapsAsString(vararg maps: Map): String { + private fun concatMapsAsString(vararg maps: Map): String { val m = mutableMapOf() - maps.forEach { - it.keys.forEach { key -> - m[key] = it[key]!! + maps.forEach { map -> + map.forEach { (key, value) -> + m[key] = value.toString() } } - - return JsonHelper.toJson(m) + return JsonHelper.json.encodeToString(m) } companion object { private const val PROJECT_ID = "projectId" private val JSON_GENERAL_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"EnrolmentPlus\":\"false\",\"LocationRequired\":\"true\",\"Modality\":\"FACE,FINGER\",\"ProjectLanguages\":\"en,fr,pt\",\"ProjectSpecificMode\":\"true\",\"SelectedLanguage\":\"en\"}", ) @@ -474,7 +474,7 @@ class ProjectConfigSharedPrefsMigrationTest { .build() private val JSON_CONSENT_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"ConsentGeneralOptions\":\"{\\\"consent_enrol_only\\\":false,\\\"consent_enrol\\\":true,\\\"consent_id_verify\\\":true,\\\"consent_share_data_no\\\":false,\\\"consent_share_data_yes\\\":true,\\\"consent_collect_yes\\\":true,\\\"consent_privacy_rights\\\":true,\\\"consent_confirmation\\\":true}\",\"ConsentParentalExists\":\"true\",\"ConsentParentalOptions\":\"{\\\"consent_parent_enrol_only\\\":true,\\\"consent_parent_enrol\\\":false,\\\"consent_parent_id_verify\\\":true,\\\"consent_parent_share_data_no\\\":true,\\\"consent_parent_share_data_yes\\\":false,\\\"consent_parent_collect_yes\\\":false,\\\"consent_parent_privacy_rights\\\":false,\\\"consent_parent_confirmation\\\":false}\",\"ConsentRequired\":\"true\",\"LogoExists\":\"true\",\"OrganizationName\":\"organization name\",\"ProgramName\":\"program name\"}", ) private val PROTO_CONSENT_CONFIGURATION = ProtoConsentConfiguration @@ -505,19 +505,19 @@ class ProjectConfigSharedPrefsMigrationTest { ).build() private val JSON_SYNCHRONIZATION_CONFIGURATION_EMPTY_SYNC_DESTINATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"DownSyncSetting\":\"ON\",\"MaxNbOfModules\":\"5\",\"ModuleIdOptions\":\"module1|module2\",\"SyncDestination\":\"\",\"SyncGroup\":\"GLOBAL\"}", ) private val JSON_SYNCHRONIZATION_CONFIGURATION_NON_EMPTY_SYNC_DESTINATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"DownSyncSetting\":\"ON\",\"MaxNbOfModules\":\"5\",\"ModuleIdOptions\":\"module1|module2\",\"SimprintsSync\":\"ALL\",\"SyncDestination\":\"SIMPRINTS,COMMCARE\",\"SyncGroup\":\"GLOBAL\"}", ) private val JSON_SYNCHRONIZATION_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"CoSync\":\"ONLY_ANALYTICS\",\"DownSyncSetting\":\"ON\",\"MaxNbOfModules\":\"5\",\"ModuleIdOptions\":\"module1|module2\",\"SimprintsSync\":\"ALL\",\"SyncDestination\":\"SIMPRINTS,COMMCARE\",\"SyncGroup\":\"GLOBAL\"}", ) private val JSON_SYNCHRONIZATION_CONFIGURATION_NULL_VALUES = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"DownSyncSetting\":\"ON\",\"MaxNbOfModules\":\"5\",\"ModuleIdOptions\":\"module1|module2\",\"SyncGroup\":\"GLOBAL\"}", ) private val PROTO_SYNCHRONIZATION_CONFIGURATION = @@ -693,7 +693,7 @@ class ProjectConfigSharedPrefsMigrationTest { ).build() private val JSON_IDENTIFICATION_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"MatchGroup\":\"MODULE\",\"NbOfIdsInt\":\"4\"}", ) private val PROTO_IDENTIFICATION_CONFIGURATION = @@ -703,15 +703,15 @@ class ProjectConfigSharedPrefsMigrationTest { .setPoolType(ProtoIdentificationConfiguration.PoolType.MODULE) .build() - private val JSON_FACE_CONFIGURATION = JsonHelper.fromJson>( + private val JSON_FACE_CONFIGURATION = JsonHelper.json.decodeFromString>( "{\"FaceConfidenceThresholds\":\"{\\\"LOW\\\":\\\"1\\\",\\\"MEDIUM\\\":\\\"20\\\",\\\"HIGH\\\":\\\"100\\\"}\",\"FaceNbOfFramesCaptured\":\"2\",\"FaceQualityThreshold\":\"-1\",\"SaveFaceImages\":\"true\"}", ) private val JSON_FACE_CONFIGURATION_WITHOUT_FIELDS = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"FaceQualityThreshold\":\"-1\"}", ) private val JSON_FACE_CONFIGURATION_WITH_UNEXPECTED_FIELD = - JsonHelper.fromJson>( + JsonHelper.fromJson>( "{\"FaceMatchThreshold\":30, \"FaceConfidenceThresholds\":\"{\\\"LOW\\\":\\\"1\\\",\\\"MEDIUM\\\":\\\"20\\\",\\\"HIGH\\\":\\\"100\\\"}\",\"FaceNbOfFramesCaptured\":\"2\",\"FaceQualityThreshold\":\"-1\",\"SaveFaceImages\":\"true\"}", ) private val PROTO_FACE_CONFIGURATION = ProtoFaceConfiguration @@ -757,7 +757,7 @@ class ProjectConfigSharedPrefsMigrationTest { ).build() private val JSON_VERO_2_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"CaptureFingerprintStrategy\":\"SECUGEN_ISO_500_DPI\",\"FingerComparisonStrategyForVerification\":\"SAME_FINGER\",\"FingerprintLiveFeedbackOn\":\"true\",\"SaveFingerprintImagesStrategy\":\"WSQ_15_EAGER\",\"Vero2FirmwareVersions\":\"{\\\"E-1\\\":{\\\"cypress\\\":\\\"1.1\\\",\\\"stm\\\":\\\"1.0\\\",\\\"un20\\\":\\\"1.3\\\"}}\"}", ) private val PROTO_VERO_2_CONFIGURATION = ProtoVero2Configuration @@ -778,11 +778,11 @@ class ProjectConfigSharedPrefsMigrationTest { ).build() private val JSON_FINGERPRINT_CONFIGURATION = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"FingerComparisonStrategyForVerification\":\"SAME_FINGER\",\"FingerImagesExist\":\"true\",\"FingerStatus\":\"{\\\"RIGHT_5TH_FINGER\\\":\\\"false\\\",\\\"RIGHT_4TH_FINGER\\\":\\\"false\\\",\\\"RIGHT_3RD_FINGER\\\":\\\"false\\\",\\\"RIGHT_INDEX_FINGER\\\":\\\"false\\\",\\\"RIGHT_THUMB\\\":\\\"false\\\",\\\"LEFT_THUMB\\\":\\\"true\\\",\\\"LEFT_INDEX_FINGER\\\":\\\"true\\\",\\\"LEFT_3RD_FINGER\\\":\\\"false\\\",\\\"LEFT_4TH_FINGER\\\":\\\"false\\\",\\\"LEFT_5TH_FINGER\\\":\\\"false\\\"}\",\"FingerprintConfidenceThresholds\":\"{\\\"LOW\\\":\\\"10\\\",\\\"MEDIUM\\\":\\\"40\\\",\\\"HIGH\\\":\\\"200\\\"}\",\"FingerprintQualityThreshold\":\"60\",\"FingerprintsToCollect\":\"LEFT_INDEX_FINGER,LEFT_INDEX_FINGER,LEFT_THUMB\",\"ScannerGenerations\":\"VERO_1,VERO_2\"}", ) private val JSON_FINGERPRINT_CONFIGURATION_WITHOUT_FIELDS = - JsonHelper.fromJson>( + JsonHelper.json.decodeFromString>( "{\"FingerprintQualityThreshold\":\"60\"}", ) private val PROTO_FINGERPRINT_CONFIGURATION = ProtoFingerprintConfiguration diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/ProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/ProjectConfigurationTest.kt index 127631071e..4fc66c1dff 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/ProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/local/models/ProjectConfigurationTest.kt @@ -4,7 +4,6 @@ import com.google.common.truth.Truth.assertThat import com.simprints.infra.config.store.testtools.projectConfiguration import com.simprints.infra.config.store.testtools.protoProjectConfiguration import org.junit.Test -import java.io.InputStream class ProjectConfigurationTest { @Test @@ -24,12 +23,5 @@ class ProjectConfigurationTest { ).isEqualTo( projectConfiguration.copy(custom = null), ) - - assertThat( - // custom map contains class that Jackson cannot convert to string - projectConfiguration.copy(custom = mapOf("test" to InputStream.nullInputStream())).toProto(), - ).isEqualTo( - protoProjectConfiguration.toBuilder().clearCustomJson().build(), - ) } } diff --git a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt index 3419f43f79..01994a3df5 100644 --- a/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt +++ b/infra/config-store/src/test/java/com/simprints/infra/config/store/models/ExperimentalProjectConfigurationTest.kt @@ -16,6 +16,8 @@ import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration. import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.SAMPLE_UPLOAD_WITH_URL_ENABLED import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration.Companion.SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import org.junit.Test internal class ExperimentalProjectConfigurationTest { @@ -23,13 +25,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check pool validation flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(ENABLE_ID_POOL_VALIDATION to 1) to false, + mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(ENABLE_ID_POOL_VALIDATION to false) to false, + mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(ENABLE_ID_POOL_VALIDATION to true) to true, + mapOf(ENABLE_ID_POOL_VALIDATION to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).idPoolValidationEnabled).isEqualTo(result) } @@ -39,13 +41,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check single good face capture fallback flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to 1) to false, + mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to false) to false, + mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to true) to true, + mapOf(SINGLE_GOOD_QUALITY_FALLBACK_REQUIRED to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).singleQualityFallbackRequired).isEqualTo(result) } @@ -55,13 +57,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check face auto capture flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(FACE_AUTO_CAPTURE_ENABLED to 1) to false, + mapOf(FACE_AUTO_CAPTURE_ENABLED to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(FACE_AUTO_CAPTURE_ENABLED to false) to false, + mapOf(FACE_AUTO_CAPTURE_ENABLED to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(FACE_AUTO_CAPTURE_ENABLED to true) to true, + mapOf(FACE_AUTO_CAPTURE_ENABLED to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).faceAutoCaptureEnabled).isEqualTo(result) } @@ -71,15 +73,15 @@ internal class ExperimentalProjectConfigurationTest { fun `check face auto capture imaging duration flag correctly`() { mapOf( // Value not present - emptyMap() to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT, + emptyMap() to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT, // Value not int - mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to true) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT, + mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to JsonPrimitive(true)) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_DEFAULT, // Value present and lesser than min - mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to 0) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN, + mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to JsonPrimitive(0)) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MIN, // Value present and greater than max - mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to 60_001) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX, + mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to JsonPrimitive(60_001)) to FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS_MAX, // Value present and within the range - mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to 1_000) to 1_000L, + mapOf(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS to JsonPrimitive(1_000)) to 1_000L, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).faceAutoCaptureImagingDurationMillis).isEqualTo(result) } @@ -89,13 +91,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check records DB migration flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to 1) to false, + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to false) to false, + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to true) to true, + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).recordsDbMigrationFromRealmEnabled).isEqualTo(result) } @@ -105,12 +107,12 @@ internal class ExperimentalProjectConfigurationTest { fun `check records DB migration max retries parsed correctly`() { mapOf( // Value not present - emptyMap() to RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES, + emptyMap() to RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES, // Value not int - mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to true) to + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to JsonPrimitive(true)) to RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_DEFAULT_MAX_RETRIES, // Value is int - mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to 3) to 3, + mapOf(RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES to JsonPrimitive(3)) to 3, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).recordsDbMigrationFromRealmMaxRetries).isEqualTo(result) } @@ -120,13 +122,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check signed url enabled flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to 1) to false, + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to false) to false, + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to true) to true, + mapOf(SAMPLE_UPLOAD_WITH_URL_ENABLED to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).sampleUploadWithSignedUrlEnabled).isEqualTo(result) } @@ -136,13 +138,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check display camera flash flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(CAMERA_FLASH_CONTROLS_ENABLED to 1) to false, + mapOf(CAMERA_FLASH_CONTROLS_ENABLED to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(CAMERA_FLASH_CONTROLS_ENABLED to false) to false, + mapOf(CAMERA_FLASH_CONTROLS_ENABLED to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(CAMERA_FLASH_CONTROLS_ENABLED to true) to true, + mapOf(CAMERA_FLASH_CONTROLS_ENABLED to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).displayCameraFlashToggle).isEqualTo(result) } @@ -152,14 +154,14 @@ internal class ExperimentalProjectConfigurationTest { fun `check fallback to CommCare threshold days correctly`() { mapOf( // Value not present - emptyMap() to FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT, + emptyMap() to FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT, // Value not int - mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to true) to FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT, - mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to 0) to 0L, - mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to 1) to 1L, - mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to 5) to 5L, + mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to JsonPrimitive(true)) to FALLBACK_TO_COMMCARE_THRESHOLD_DAYS_DEFAULT, + mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to JsonPrimitive(0)) to 0L, + mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to JsonPrimitive(1)) to 1L, + mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to JsonPrimitive(5)) to 5L, // Value present and exactly at default - mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to 3) to 3L, + mapOf(FALLBACK_TO_COMMCARE_THRESHOLD_DAYS to JsonPrimitive(3)) to 3L, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).fallbackToCommCareThresholdDays).isEqualTo(result) } @@ -168,10 +170,10 @@ internal class ExperimentalProjectConfigurationTest { @Test fun `check ocr use high res flag correctly`() { mapOf( - emptyMap() to true, - mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to 1) to true, - mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to false) to false, - mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to true) to true, + emptyMap() to true, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to JsonPrimitive(1)) to true, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to JsonPrimitive(false)) to false, + mapOf(ExperimentalProjectConfiguration.OCR_USE_HIGH_RES to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).ocrUseHighRes).isEqualTo(result) } @@ -181,9 +183,10 @@ internal class ExperimentalProjectConfigurationTest { fun `check ocr captures value correctly`() { val expectedOcrCaptures = 10 mapOf( - emptyMap() to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, - mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to true) to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, - mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to expectedOcrCaptures) to expectedOcrCaptures, + emptyMap() to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, + mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to JsonPrimitive(true)) + to ExperimentalProjectConfiguration.OCR_CAPTURES_DEFAULT, + mapOf(ExperimentalProjectConfiguration.OCR_CAPTURES to JsonPrimitive(expectedOcrCaptures)) to expectedOcrCaptures, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).ocrCaptures).isEqualTo(result) } @@ -193,13 +196,13 @@ internal class ExperimentalProjectConfigurationTest { fun `check allow confirming GUIDs not in callback flag correctly`() { mapOf( // Value not present - emptyMap() to false, + emptyMap() to false, // Value not boolean - mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to 1) to false, + mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to JsonPrimitive(1)) to false, // Value present and FALSE - mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to false) to false, + mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to JsonPrimitive(false)) to false, // Value present and TRUE - mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to true) to true, + mapOf(ALLOW_CONFIRMING_GUIDS_NOT_IN_CALLBACK to JsonPrimitive(true)) to true, ).forEach { (config, result) -> assertThat(ExperimentalProjectConfiguration(config).allowConfirmingGuidsNotInCallback).isEqualTo(result) } 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 71aa590a3c..c2b2f686df 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 @@ -2,8 +2,8 @@ package com.simprints.infra.config.store.testtools import com.simprints.core.domain.common.AgeGroup import com.simprints.core.domain.common.Modality -import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.common.TemplateIdentifier +import com.simprints.core.domain.externalcredential.ExternalCredentialType import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.infra.config.store.local.models.ProtoAllowedAgeRange import com.simprints.infra.config.store.local.models.ProtoConsentConfiguration @@ -68,6 +68,8 @@ 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 +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive internal val apiConsentConfiguration = ApiConsentConfiguration( programName = "programName", @@ -478,11 +480,11 @@ internal val apiMultiFactorIdConfiguration = ApiMultiFactorIdConfiguration( allowedExternalCredentials = listOf(apiAllowedExternalCredential), ) -internal val customKeyMap: Map? = mapOf( - "key1" to 7, - "key2" to 4.2, - "key3" to false, - "key4" to "test", +internal val customKeyMap: Map = mapOf( + "key1" to JsonPrimitive(7), + "key2" to JsonPrimitive(4.2), + "key3" to JsonPrimitive(false), + "key4" to JsonPrimitive("test"), ) internal const val PROTO_CUSTOM_KEY_MAP_JSON = "{\"key1\":7,\"key2\":4.2,\"key3\":false,\"key4\":\"test\"}" diff --git a/infra/core/src/main/java/com/simprints/core/tools/json/AnyPrimitiveSerializer.kt b/infra/core/src/main/java/com/simprints/core/tools/json/AnyPrimitiveSerializer.kt deleted file mode 100644 index 11e7eb32e4..0000000000 --- a/infra/core/src/main/java/com/simprints/core/tools/json/AnyPrimitiveSerializer.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.simprints.core.tools.json - -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.SerialKind -import kotlinx.serialization.descriptors.buildSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.doubleOrNull -import kotlinx.serialization.json.longOrNull - -object AnyPrimitiveSerializer : KSerializer { - @OptIn(InternalSerializationApi::class) - override val descriptor: SerialDescriptor = buildSerialDescriptor("AnyPrimitive", SerialKind.CONTEXTUAL) - - private fun toJsonElement(value: Any): JsonElement = when (value) { - is String -> { - JsonPrimitive(value) - } - - is Number -> { - JsonPrimitive(value) - } - - is Boolean -> { - JsonPrimitive(value) - } - - is List<*> -> { - JsonArray( - value.map { v -> - v ?: throw SerializationException("Null values are not supported for AnyPrimitive") - toJsonElement(v) - }, - ) - } - - is Map<*, *> -> { - val obj = buildJsonObject { - value.forEach { (k, v) -> - require(k is String) { "Only String keys are supported in maps for AnyPrimitiveSerializer" } - require(v != null) { "Null values are not supported for AnyPrimitive" } - put(k, toJsonElement(v)) - } - } - obj - } - - else -> { - throw SerializationException("Unsupported type for AnyPrimitiveSerializer: ${value::class}") - } - } - - private fun fromJsonElement(element: JsonElement): Any = when (element) { - is JsonPrimitive -> { - element.booleanOrNull - ?: element.longOrNull - ?: element.doubleOrNull - ?: element.content - } - - is JsonArray -> { - element.map { fromJsonElement(it) } - } - - is JsonObject -> { - element.mapValues { (_, v) -> - if (v is JsonNull) throw SerializationException("Null values are not supported for AnyPrimitive") - fromJsonElement(v) - } - } - } - - override fun serialize( - encoder: Encoder, - value: Any, - ) { - val jsonEncoder = encoder as? JsonEncoder - ?: throw SerializationException("AnyPrimitiveSerializer can be used only with Json format") - jsonEncoder.encodeJsonElement(toJsonElement(value)) - } - - override fun deserialize(decoder: Decoder): Any { - val jsonDecoder = decoder as? JsonDecoder - ?: throw SerializationException("AnyPrimitiveSerializer can be used only with Json format") - val element = jsonDecoder.decodeJsonElement() - return fromJsonElement(element) - } -} diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/migration/RealmToRoomMigrationFlagsStoreTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/migration/RealmToRoomMigrationFlagsStoreTest.kt index 145cefe169..aaf08892f8 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/migration/RealmToRoomMigrationFlagsStoreTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/local/migration/RealmToRoomMigrationFlagsStoreTest.kt @@ -9,6 +9,8 @@ import com.simprints.infra.security.SecurityManager import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import org.junit.Before import org.junit.Test @@ -30,7 +32,7 @@ class RealmToRoomMigrationFlagsStoreTest { private lateinit var store: RealmToRoomMigrationFlagsStore - private val experimentalFeaturesMap = mutableMapOf() + private val experimentalFeaturesMap = mutableMapOf() @Before fun setUp() { @@ -206,7 +208,7 @@ class RealmToRoomMigrationFlagsStoreTest { fun `canRetry should return true when retries are less than max`() = runTest { // Given val maxRetries = 5 - experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = maxRetries + experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = JsonPrimitive(maxRetries) val currentRetries = maxRetries - 1 every { sharedPreferences.getInt(RealmToRoomMigrationFlagsStore.KEY_MIGRATION_RETRIES, 0) } returns currentRetries @@ -221,7 +223,7 @@ class RealmToRoomMigrationFlagsStoreTest { fun `canRetry should return false when retries reach max`() = runTest { // Given val maxRetries = 5 - experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = maxRetries + experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = JsonPrimitive(maxRetries) val currentRetries = maxRetries every { sharedPreferences.getInt(RealmToRoomMigrationFlagsStore.KEY_MIGRATION_RETRIES, 0) } returns currentRetries @@ -237,7 +239,7 @@ class RealmToRoomMigrationFlagsStoreTest { fun `canRetry should return false when retries exceed max`() = runTest { // Given val maxRetries = 5 - experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = maxRetries + experimentalFeaturesMap[RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_MAX_RETRIES] = JsonPrimitive(maxRetries) val currentRetries = maxRetries + 1 every { sharedPreferences.getInt(RealmToRoomMigrationFlagsStore.KEY_MIGRATION_RETRIES, 0) } returns currentRetries 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 a25eb6a4e0..883a4253be 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 @@ -1,7 +1,6 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep -import com.simprints.core.tools.json.AnyPrimitiveSerializer import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.InvalidIntentEvent.InvalidIntentPayload import kotlinx.serialization.Serializable @@ -11,7 +10,7 @@ import kotlinx.serialization.Serializable internal data class ApiInvalidIntentPayload( override val startTime: ApiTimestamp, val action: String, - val extras: Map, + val extras: Map, ) : ApiEventPayload() { constructor(domainPayload: InvalidIntentPayload) : this( domainPayload.createdAt.fromDomainToApi(), 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 67bcba0fdb..8aafa2813a 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 @@ -1,7 +1,6 @@ package com.simprints.infra.eventsync.event.remote.models import androidx.annotation.Keep -import com.simprints.core.tools.json.AnyPrimitiveSerializer import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.events.event.domain.models.SuspiciousIntentEvent.SuspiciousIntentPayload import kotlinx.serialization.Serializable @@ -10,7 +9,7 @@ import kotlinx.serialization.Serializable @Serializable internal data class ApiSuspiciousIntentPayload( override val startTime: ApiTimestamp, - val unexpectedExtras: Map, + val unexpectedExtras: Map, ) : ApiEventPayload() { constructor(domainPayload: SuspiciousIntentPayload) : this( domainPayload.createdAt.fromDomainToApi(), diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/InvalidIntentEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/InvalidIntentEvent.kt index e2b4957cc8..48ada59fad 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/InvalidIntentEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/InvalidIntentEvent.kt @@ -18,7 +18,7 @@ data class InvalidIntentEvent( constructor( creationTime: Timestamp, action: String, - extras: Map, + extras: Map, ) : this( UUID.randomUUID().toString(), InvalidIntentPayload(creationTime, EVENT_VERSION, action, extras), @@ -34,7 +34,7 @@ data class InvalidIntentEvent( override val createdAt: Timestamp, override val eventVersion: Int, val action: String, - val extras: Map, + val extras: Map, override val endedAt: Timestamp? = null, override val type: EventType = INVALID_INTENT, ) : EventPayload() { diff --git a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/SuspiciousIntentEvent.kt b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/SuspiciousIntentEvent.kt index 05b2ad73d7..380c6453b1 100644 --- a/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/SuspiciousIntentEvent.kt +++ b/infra/events/src/main/java/com/simprints/infra/events/event/domain/models/SuspiciousIntentEvent.kt @@ -17,7 +17,7 @@ data class SuspiciousIntentEvent( ) : Event() { constructor( createdAt: Timestamp, - unexpectedExtras: Map, + unexpectedExtras: Map, ) : this( UUID.randomUUID().toString(), SuspiciousIntentPayload(createdAt, EVENT_VERSION, unexpectedExtras), @@ -32,7 +32,7 @@ data class SuspiciousIntentEvent( data class SuspiciousIntentPayload( override val createdAt: Timestamp, override val eventVersion: Int, - val unexpectedExtras: Map, + val unexpectedExtras: Map, override val endedAt: Timestamp? = null, override val type: EventType = SUSPICIOUS_INTENT, ) : EventPayload() { 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 7589838d61..0e56200674 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 @@ -34,6 +34,7 @@ internal data class LicenseValue( * ``` */ @Keep +@Serializable internal data class ApiLicenseError( val error: String, ) 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 fa03741fcd..b91bbb2433 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 @@ -75,7 +75,8 @@ internal class LicenseRemoteDataSourceImpl @Inject constructor( return ApiLicenseResult.Error(errorCode) } - private fun getLicenseErrorCode(errorBody: ResponseBody): String = jsonHelper.fromJson(errorBody.string()).error + private fun getLicenseErrorCode(errorBody: ResponseBody): String = + jsonHelper.json.decodeFromString(errorBody.string()).error private suspend fun getProjectApiClient(): SimNetwork.SimApiClient = authStore.buildClient(LicenseRemoteInterface::class) diff --git a/infra/sync/build.gradle.kts b/infra/sync/build.gradle.kts index a5e236bbe2..a55c2fc24e 100644 --- a/infra/sync/build.gradle.kts +++ b/infra/sync/build.gradle.kts @@ -43,4 +43,5 @@ dependencies { implementation(project(":fingerprint:infra:image-distortion-config")) implementation(libs.workManager.work) + implementation(libs.kotlin.serialization) }