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 0d81f1fc88..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 @@ -37,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] @@ -66,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() { @@ -96,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 @@ -121,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) { 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/usecase/LogoutUseCase.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt index c206dac577..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 @@ -2,6 +2,7 @@ package com.simprints.feature.dashboard.logout.usecase import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.sync.SyncOrchestrator +import kotlinx.coroutines.runBlocking import javax.inject.Inject internal class LogoutUseCase @Inject constructor( @@ -9,10 +10,12 @@ internal class LogoutUseCase @Inject constructor( private val authManager: AuthManager, ) { - suspend operator fun invoke() { + operator fun invoke() = runBlocking { // Cancel all background sync syncOrchestrator.cancelBackgroundWork() syncOrchestrator.deleteEventSyncInfo() + // sign out the user authManager.signOut() } + } 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 509f2958f6..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 @@ -6,7 +6,6 @@ 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 @@ -56,9 +55,8 @@ internal class SyncViewModel @Inject constructor( private val configRepository: ConfigRepository, private val timeHelper: TimeHelper, private val authStore: AuthStore, - private val logoutUseCase: LogoutUseCase, + private val logout: LogoutUseCase, private val recentUserActivityManager: RecentUserActivityManager, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { companion object { @@ -106,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()) } } 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/test/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModelTest.kt index 23f02a634b..9b001af196 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/LogoutSyncViewModelTest.kt @@ -13,7 +13,6 @@ 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 import org.junit.Test @@ -43,7 +42,6 @@ internal class LogoutSyncViewModelTest { val viewModel = LogoutSyncViewModel( configRepository = configRepository, logoutUseCase = logoutUseCase, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher) ) viewModel.logout() @@ -62,7 +60,6 @@ internal class LogoutSyncViewModelTest { val viewModel = LogoutSyncViewModel( configRepository = configRepository, logoutUseCase = logoutUseCase, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), ) val resultConfig = viewModel.settingsLocked.getOrAwaitValue() assertThat(resultConfig.peekContent()).isEqualTo(config) 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/sync/SyncViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/main/sync/SyncViewModelTest.kt index 3997cec0f7..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 @@ -44,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 @@ -444,8 +443,7 @@ internal class SyncViewModelTest { configRepository = configRepository, timeHelper = timeHelper, authStore = authStore, - logoutUseCase = logoutUseCase, + logout = logoutUseCase, recentUserActivityManager = recentUserActivityManager, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher) ) } 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/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorFragment.kt index ed4e51a1a1..fe2dafb098 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 @@ -74,12 +74,20 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { private val clientApiVm by viewModels() private val orchestratorVm by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + 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) + observeLoginCheckVm() observeClientApiVm() observeOrchestratorVm() @@ -180,6 +188,10 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { 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() { @@ -201,5 +213,6 @@ 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/OrchestratorViewModel.kt b/feature/orchestrator/src/main/java/com/simprints/feature/orchestrator/OrchestratorViewModel.kt index 41cecbf720..cce2dfcd41 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 @@ -95,6 +101,22 @@ internal class OrchestratorViewModel @Inject constructor( 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( + it.fingerIdentifier, + it.format, + 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/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..2dd280c36d 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,6 +1,5 @@ package com.simprints.feature.orchestrator.usecases.response -import android.os.Parcelable import com.simprints.infra.orchestration.data.responses.AppErrorResponse import com.simprints.infra.config.store.models.ProjectConfiguration import com.simprints.infra.orchestration.data.ActionRequest 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..a35de2eced 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 @@ -7,6 +7,7 @@ import com.simprints.fingerprint.capture.FingerprintCaptureResult import com.simprints.infra.eventsync.sync.down.tasks.SubjectFactory import com.simprints.infra.orchestration.data.ActionRequest import com.simprints.core.domain.response.AppErrorReason +import com.simprints.infra.logging.Simber 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/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt b/feature/orchestrator/src/test/java/com/simprints/feature/orchestrator/OrchestratorViewModelTest.kt index 69ec2ae095..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,7 @@ 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 @@ -37,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 @@ -220,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/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 934be8ed92..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 @@ -31,7 +31,7 @@ internal enum class ErrorType( null, IDR.string.configuration_generic_error_message, alertType = AlertScreenEventType.LICENSE_MISSING, - errorReason = AppErrorReason.FACE_CONFIGURATION_ERROR, + errorReason = AppErrorReason.LICENSE_MISSING, ), ; diff --git a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt index 9b0b498fbc..25df12f3db 100644 --- a/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt +++ b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/ConnectScannerViewModel.kt @@ -120,9 +120,7 @@ internal class ConnectScannerViewModel @Inject constructor( fun handleBackPress() { when (backButtonBehaviour.value) { - BackButtonBehaviour.DISABLED, null -> { /* 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/controller/ConnectScannerControllerFragment.kt b/fingerprint/connect/src/main/java/com/simprints/fingerprint/connect/screens/controller/ConnectScannerControllerFragment.kt index d8dc7b9836..a77cbd0cdb 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 @@ -81,8 +81,7 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con } private val hostFragment: Fragment? - get() = childFragmentManager - .findFragmentById(R.id.connect_scanner_host_fragment) + get() = childFragmentManager.findFragmentById(R.id.connect_scanner_host_fragment) private val internalNavController: NavController? get() = hostFragment?.findNavController() @@ -157,13 +156,6 @@ internal class ConnectScannerControllerFragment : Fragment(R.layout.fragment_con } internalNavController?.setGraph(R.navigation.graph_connect_internal) - - if (shouldRequestPermissions) { - shouldRequestPermissions = false - checkBluetoothPermissions() - } else { - alertHelper.handleResume { shouldRequestPermissions = true } - } } private fun showKnownScannerDialog(scannerId: String) { @@ -187,7 +179,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() { 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 f431e0b5c9..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 @@ -52,8 +52,6 @@ internal class SignerManager @Inject constructor( } suspend fun signOut() = withContext(dispatcher) { - authStore.cleanCredentials() - authStore.clearFirebaseToken() simNetwork.resetApiBaseUrl() configRepository.clearData() @@ -65,6 +63,9 @@ internal class SignerManager @Inject constructor( scannerManager.deleteFirmwareFiles() licenseRepository.deleteCachedLicenses() + authStore.cleanCredentials() + authStore.clearFirebaseToken() + Simber.tag(LoggingConstants.CrashReportTag.LOGOUT.name).i("Signed out") } 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 cf9ddf4e16..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 @@ -13,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 @@ -58,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 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 daf3d5658a..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 @@ -20,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 @@ -65,11 +64,12 @@ internal class EventRemoteDataSource @Inject constructor( } suspend fun getEvents( + requestId: String, query: ApiRemoteEventQuery, scope: CoroutineScope, ): EventDownSyncResult { return try { - val response = takeStreaming(query) + val response = takeStreaming(requestId, query) val eventCount = getEventCountFromHeader(response) val streaming = response.body()?.byteStream() ?: ByteArrayInputStream(byteArrayOf()) @@ -77,7 +77,6 @@ internal class EventRemoteDataSource @Inject constructor( EventDownSyncResult( totalCount = eventCount.exactCount, - requestId = getRequestId(response), status = response.code(), eventStream = scope.produce(capacity = CHANNEL_CAPACITY_FOR_PROPAGATION) { parseStreamAndEmitEvents(streaming, this) @@ -118,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, @@ -131,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) } @@ -159,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 1fd0a59188..29c84072cd 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 @@ -16,26 +16,28 @@ internal interface EventRemoteInterface : SimRemoteInterface { @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") @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 @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/downsync/ApiEventDownSyncRequestPayload.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayload.kt index 61fbfa4de4..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 @@ -45,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/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/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/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/down/tasks/EventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTask.kt index ce0beb13cb..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 @@ -35,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( @@ -58,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 ) @@ -118,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 7fa133f457..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 @@ -76,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) 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..dec4b9d700 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 @@ -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 83830bc093..924f631261 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 @@ -63,10 +63,10 @@ class EventSyncMasterWorker @AssistedInject internal 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 662287911c..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 @@ -39,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( @@ -167,7 +168,8 @@ internal class EventUpSyncTask @Inject constructor( eventFilter: (Map?>) -> Map?> = { it }, createUpSyncContentContent: (Int) -> EventUpSyncRequestEvent.UpSyncContent, ) = flow { - Simber.tag(SYNC_LOG_TAG).d("Uploading event scope - $eventScopeTypeToUpload in batches of $batchSize") + 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 @@ -183,13 +185,17 @@ internal class EventUpSyncTask @Inject constructor( val uploadedScopes = mutableListOf() scopesToUpload.chunked(batchSize.coerceAtLeast(1)).forEach { scopes -> + val requestId = UUID.randomUUID().toString() + val requestStartTime = timeHelper.now() try { val result = eventRemoteDataSource.post( + requestId, projectId, scopes.asApiUploadEventsBody(eventScopeTypeToUpload) ) addRequestEvent( + requestId = requestId, eventScope = eventScope, startTime = requestStartTime, result = result, @@ -197,7 +203,7 @@ internal class EventUpSyncTask @Inject constructor( ) uploadedScopes.addAll(scopes.map { it.id }) } catch (ex: Exception) { - handleFailedRequest(ex, eventScope, requestStartTime) + handleFailedRequest(requestId, ex, eventScope, requestStartTime) } } @@ -205,7 +211,9 @@ internal class EventUpSyncTask @Inject constructor( eventRepository.deleteEventScopes(uploadedScopes) } - private fun List.asApiUploadEventsBody(eventScopeTypeToUpload: EventScopeType) = when(eventScopeTypeToUpload) { + 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) @@ -238,6 +246,7 @@ internal class EventUpSyncTask @Inject constructor( } private suspend fun addRequestEvent( + requestId: String, eventScope: EventScope, startTime: Timestamp, result: EventUpSyncResult, @@ -249,7 +258,7 @@ internal class EventUpSyncTask @Inject constructor( EventUpSyncRequestEvent( createdAt = startTime, endedAt = timeHelper.now(), - requestId = result.requestId, + requestId = requestId, content = content, responseStatus = result.status, ) @@ -258,6 +267,7 @@ internal class EventUpSyncTask @Inject constructor( } private suspend fun handleFailedRequest( + requestId: String, ex: Exception, eventScope: EventScope, requestStartTime: Timestamp, @@ -267,13 +277,9 @@ 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 -> { @@ -287,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(), ) 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 acbd0f75d0..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 @@ -74,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) 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 2fa7dc0c7e..ebcc02cfde 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 @@ -168,12 +168,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) } @@ -196,12 +197,13 @@ class EventRemoteDataSourceTest { any(), any(), any(), + any(), any() ) } throws exception assertThrows { - eventRemoteDataSource.getEvents(query, this) + eventRemoteDataSource.getEvents(GUID1, query, this) } } @@ -215,18 +217,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 ) } } @@ -235,7 +238,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() @@ -244,45 +247,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() @@ -291,12 +276,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() ) @@ -304,6 +296,7 @@ class EventRemoteDataSourceTest { val events = listOf(createAlertScreenEvent()) val scope = createSessionScope() eventRemoteDataSource.post( + GUID1, DEFAULT_PROJECT_ID, ApiUploadEventsBody( sessions = listOf(ApiEventScope.fromDomain(scope, events)) @@ -312,6 +305,7 @@ class EventRemoteDataSourceTest { coVerify(exactly = 1) { eventRemoteInterface.uploadEvents( + GUID1, DEFAULT_PROJECT_ID, true, match { body -> @@ -329,6 +323,7 @@ class EventRemoteDataSourceTest { runTest { coEvery { eventRemoteInterface.uploadEvents( + any(), any(), any(), any() @@ -337,6 +332,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/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/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/sync/down/tasks/EventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EventDownSyncTaskTest.kt index 222691ee47..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 @@ -40,6 +40,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test +import java.util.UUID class EventDownSyncTaskTest { @@ -159,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 } @@ -180,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,14 +191,14 @@ class EventDownSyncTaskTest { @Test(expected = RemoteDbNotSignedInException::class) fun downSync_shouldThrowUpIfRemoteDbNotSignedInExceptionOccurs() = runTest { - coEvery { eventRemoteDataSource.getEvents(any(), any()) } throws RemoteDbNotSignedInException() + 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) { @@ -333,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/up/tasks/EventUpSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt index 5982d69a32..849a211b8c 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 @@ -16,18 +16,11 @@ 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 @@ -148,7 +141,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() - coVerify(exactly = 2) { eventRemoteDataSource.post(any(), any()) } + coVerify(exactly = 2) { eventRemoteDataSource.post(any(), any(), any()) } } @Test @@ -173,8 +166,8 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() coVerify { - eventRemoteDataSource.post(any(), match { it.eventDownSyncs.size == 1}) - eventRemoteDataSource.post(any(), match { it.eventUpSyncs.size == 2}) + eventRemoteDataSource.post(any(), any(), match { it.eventDownSyncs.size == 1 }) + eventRemoteDataSource.post(any(), any(), match { it.eventUpSyncs.size == 2 }) } } @@ -195,6 +188,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -227,6 +221,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -258,6 +253,7 @@ internal class EventUpSyncTaskTest { coVerify { eventRemoteDataSource.post( + any(), any(), withArg { assertThat(it.sessions.first().id).isEqualTo(GUID1) @@ -330,7 +326,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() @@ -349,7 +345,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() ) @@ -368,7 +366,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) } @@ -387,7 +385,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()) @@ -411,7 +409,7 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() coVerify(exactly = 0) { - eventRemoteDataSource.post(any(), any()) + eventRemoteDataSource.post(any(), any(), any()) eventRepo.deleteEventScope(GUID1) } @@ -438,7 +436,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)) ) 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/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/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 8e4945a601..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 @@ -4,6 +4,7 @@ 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 @@ -15,8 +16,7 @@ import java.io.File */ @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/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 fccf8c27ed..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" @@ -59,7 +57,6 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( } } .addNetworkInterceptor(ChuckerInterceptor.Builder(ctx).build()) - .addInterceptor(buildRequestIdInterceptor()) .addInterceptor(buildDeviceIdInterceptor(deviceId)) .addInterceptor(buildVersionInterceptor(versionName)) .addInterceptor(buildGZipInterceptor()) @@ -69,13 +66,6 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( } } - 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() .addHeader(AUTHORIZATION_HEADER, "Bearer $authToken") 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 29635161d5..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, diff --git a/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/DeviceConfigDownSyncWorker.kt b/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/DeviceConfigDownSyncWorker.kt index 6d17c81595..d2f46734a8 100644 --- a/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/DeviceConfigDownSyncWorker.kt +++ b/infra/sync/src/main/java/com/simprints/infra/sync/config/worker/DeviceConfigDownSyncWorker.kt @@ -1,6 +1,7 @@ package com.simprints.infra.sync.config.worker import android.content.Context +import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters import com.simprints.core.DispatcherBG import com.simprints.core.workers.SimCoroutineWorker @@ -12,6 +13,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +@HiltWorker internal class DeviceConfigDownSyncWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, @@ -25,8 +27,8 @@ internal class DeviceConfigDownSyncWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { - crashlyticsLog("Fetching device config state") showProgressNotification() + crashlyticsLog("Fetching device config state") try { val state = configRepository.getDeviceState()