From b0ce658b19403aa87ee99bdf8e81a40772d4af6a Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Thu, 11 Apr 2024 18:10:07 +0300 Subject: [PATCH 01/29] MS-402 Prevent a navigation race condition on logout --- .../dashboard/logout/LogoutSyncViewModel.kt | 6 +-- .../dashboard/logout/usecase/LogoutUseCase.kt | 14 +++-- .../dashboard/main/sync/SyncViewModel.kt | 3 +- .../settings/about/AboutViewModel.kt | 5 +- .../logout/LogoutSyncViewModelTest.kt | 3 -- .../logout/usecase/LogoutUseCaseTest.kt | 53 +++++++++++++++++++ .../dashboard/main/sync/SyncViewModelTest.kt | 2 - .../settings/about/AboutViewModelTest.kt | 3 -- 8 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCaseTest.kt 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..11d0a07760 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,14 +4,11 @@ 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 @@ -19,7 +16,6 @@ import javax.inject.Inject internal class LogoutSyncViewModel @Inject constructor( private val configRepository: ConfigRepository, private val logoutUseCase: LogoutUseCase, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { val settingsLocked: LiveData> @@ -29,6 +25,6 @@ internal class LogoutSyncViewModel @Inject constructor( fun logout() { - externalScope.launch { logoutUseCase() } + viewModelScope.launch { 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..9e2f206322 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt @@ -1,18 +1,26 @@ package com.simprints.feature.dashboard.logout.usecase +import com.simprints.core.ExternalScope import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.sync.SyncOrchestrator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject internal class LogoutUseCase @Inject constructor( private val syncOrchestrator: SyncOrchestrator, private val authManager: AuthManager, + @ExternalScope private val externalScope: CoroutineScope, ) { suspend operator fun invoke() { - // Cancel all background sync - syncOrchestrator.cancelBackgroundWork() - syncOrchestrator.deleteEventSyncInfo() authManager.signOut() + + // Cancel all background sync + externalScope.launch { + syncOrchestrator.cancelBackgroundWork() + syncOrchestrator.deleteEventSyncInfo() + } } } 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..dc9d5afc72 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 @@ -58,7 +58,6 @@ internal class SyncViewModel @Inject constructor( private val authStore: AuthStore, private val logoutUseCase: LogoutUseCase, private val recentUserActivityManager: RecentUserActivityManager, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { companion object { @@ -106,7 +105,7 @@ internal class SyncViewModel @Inject constructor( configRepository.getProject(authStore.signedInProjectId).state == ProjectState.PROJECT_ENDING if (isSyncComplete && isProjectEnding) { - externalScope.launch { + viewModelScope.launch { logoutUseCase() _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..9dc3e199b1 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 @@ -27,7 +25,6 @@ internal class AboutViewModel @Inject constructor( private val logoutUseCase: LogoutUseCase, private val eventSyncManager: EventSyncManager, private val recentUserActivityManager: RecentUserActivityManager, - @ExternalScope private val externalScope: CoroutineScope, ) : ViewModel() { val syncAndSearchConfig: LiveData @@ -76,7 +73,7 @@ internal class AboutViewModel @Inject constructor( configRepository.getProjectConfiguration().canSyncDataToSimprints() private fun logout() { - externalScope.launch { logoutUseCase() } + viewModelScope.launch { logoutUseCase() } } private fun load() = viewModelScope.launch { 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..4d90c5c510 --- /dev/null +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCaseTest.kt @@ -0,0 +1,53 @@ +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.CoroutineScope +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, + CoroutineScope(testCoroutineRule.testCoroutineDispatcher), + ) + } + + @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..4237765e41 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 @@ -446,6 +445,5 @@ internal class SyncViewModelTest { authStore = authStore, logoutUseCase = 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..39f2ae2120 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 @@ -67,7 +66,6 @@ class AboutViewModelTest { eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, logoutUseCase = logoutUseCase, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), ) assertThat(viewModel.modalities.value).isEqualTo(MODALITIES) @@ -186,7 +184,6 @@ class AboutViewModelTest { configRepository = configRepository, eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, - externalScope = CoroutineScope(testCoroutineRule.testCoroutineDispatcher), logoutUseCase = logoutUseCase, ) } From 19763f163f47e484e2eef246d227d838b29476fc Mon Sep 17 00:00:00 2001 From: melad Date: Mon, 15 Apr 2024 11:54:09 +0200 Subject: [PATCH 02/29] [MS-409] fix LICENSE_MISSING mapping --- .../src/main/java/com/simprints/feature/setup/data/ErrorType.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, ), ; From e08618a8683b867cac5d23dd2e57e6ccf1dc8f65 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Mon, 15 Apr 2024 19:32:54 +0300 Subject: [PATCH 03/29] [MS-412] Untie camera initialization from fragment initialization --- .../livefeedback/LiveFeedbackFragment.kt | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) 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) { From 27b33740157d2fe59f0f6b314c17682b86d73cc3 Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 16 Apr 2024 10:03:40 +0200 Subject: [PATCH 04/29] [MS-413] sign out after stopping the background sync --- .../dashboard/logout/usecase/LogoutUseCase.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 9e2f206322..0b5def525c 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 @@ -5,7 +5,6 @@ import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.sync.SyncOrchestrator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject internal class LogoutUseCase @Inject constructor( @@ -14,13 +13,12 @@ internal class LogoutUseCase @Inject constructor( @ExternalScope private val externalScope: CoroutineScope, ) { - suspend operator fun invoke() { - authManager.signOut() - + suspend operator fun invoke() = externalScope.launch { // Cancel all background sync - externalScope.launch { - syncOrchestrator.cancelBackgroundWork() - syncOrchestrator.deleteEventSyncInfo() - } + syncOrchestrator.cancelBackgroundWork() + syncOrchestrator.deleteEventSyncInfo() + // sign out the user + authManager.signOut() } + } From d446880662f212a1712e077c5432f24716e395fc Mon Sep 17 00:00:00 2001 From: alexandr Date: Tue, 16 Apr 2024 11:42:04 +0300 Subject: [PATCH 05/29] [MS-405] Saving the Action Request in the bundle so it can survive the process death --- .../orchestrator/OrchestratorFragment.kt | 7 +++++++ .../orchestrator/OrchestratorViewModel.kt | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) 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..efc7261191 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 @@ -79,6 +79,8 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState != null) { orchestratorVm.requestProcessed = savedInstanceState.getBoolean(KEY_REQUEST_PROCESSED) + savedInstanceState.getString(KEY_ACTION_REQUEST) + ?.run(orchestratorVm::setActionRequestFromJson) } observeLoginCheckVm() observeClientApiVm() @@ -180,6 +182,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 +207,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..7de858510c 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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 @@ -161,4 +162,20 @@ internal class OrchestratorViewModel @Inject constructor( } } } + fun setActionRequestFromJson(json: String) { + try { + actionRequest = JsonHelper.fromJson(json) + } catch (e: Exception) { + Simber.e(e) + } + } + + fun getActionRequestJson(): String? { + return try { + actionRequest?.run(JsonHelper::toJson) + } catch (e: Exception) { + Simber.e(e) + null + } + } } From 467a68985baa64e37f9a92b720612fd984a2a91b Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Tue, 16 Apr 2024 15:39:31 +0300 Subject: [PATCH 06/29] [MS-416] Move Bluetooth permissions check in onResume() to reflect changes when user navigates to Settings app and back --- .../connect/screens/ConnectScannerViewModel.kt | 4 +--- .../ConnectScannerControllerFragment.kt | 18 ++++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) 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() { From fc58d27008a174f50da76d8847a9939b5cf6172c Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 16 Apr 2024 15:36:51 +0200 Subject: [PATCH 07/29] [MS-413] Force callers to logout to wait for the logout operation to complete --- .../feature/dashboard/logout/LogoutSyncViewModel.kt | 6 ++---- .../dashboard/logout/sync/LogoutSyncFragment.kt | 8 ++++++-- .../logout/syncdecline/LogoutSyncDeclineFragment.kt | 8 ++++++-- .../feature/dashboard/logout/usecase/LogoutUseCase.kt | 6 ++++-- .../feature/dashboard/main/sync/SyncViewModel.kt | 5 ++--- .../dashboard/settings/about/AboutViewModel.kt | 7 ++----- .../dashboard/logout/LogoutSyncViewModelTest.kt | 4 +++- .../dashboard/logout/sync/LogoutSyncFragmentTest.kt | 11 ++++++++--- .../feature/dashboard/main/sync/SyncViewModelTest.kt | 3 ++- .../dashboard/settings/about/AboutViewModelTest.kt | 5 +++-- 10 files changed, 38 insertions(+), 25 deletions(-) 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 11d0a07760..d2be312664 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 @@ -9,7 +9,6 @@ import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.config.store.models.SettingsPasswordConfig import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -24,7 +23,6 @@ internal class LogoutSyncViewModel @Inject constructor( } - fun logout() { - viewModelScope.launch { logoutUseCase() } - } + fun logout() = logoutUseCase() } + diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt index ed46d76baf..1c98b1a108 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt @@ -8,6 +8,7 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.dashboard.R @@ -20,6 +21,7 @@ import com.simprints.feature.login.LoginResult import com.simprints.infra.uibase.navigation.handleResult import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { @@ -54,8 +56,10 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { findNavController().navigate(R.id.action_logoutSyncFragment_to_logoutSyncDeclineFragment) } logoutButton.setOnClickListener { - logoutSyncViewModel.logout() - findNavController().navigate(R.id.action_logoutSyncFragment_to_requestLoginFragment) + lifecycleScope.launch { + logoutSyncViewModel.logout().await() + findNavController().navigate(R.id.action_logoutSyncFragment_to_requestLoginFragment) + } } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt index 514d959125..be8a869d25 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt @@ -5,6 +5,7 @@ import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.simprints.core.livedata.LiveDataEventWithContentObserver @@ -14,6 +15,7 @@ import com.simprints.feature.dashboard.logout.LogoutSyncViewModel import com.simprints.feature.dashboard.settings.password.SettingsPasswordDialogFragment import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import com.simprints.infra.resources.R as IDR @AndroidEntryPoint @@ -62,7 +64,9 @@ class LogoutSyncDeclineFragment : Fragment(R.layout.fragment_logout_sync_decline } private fun processLogoutConfirmation() { - viewModel.logout() - findNavController().navigate(R.id.action_logoutSyncDeclineFragment_to_requestLoginFragment) + lifecycleScope.launch { + viewModel.logout().await() + findNavController().navigate(R.id.action_logoutSyncDeclineFragment_to_requestLoginFragment) + } } } 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 0b5def525c..0504be371c 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 @@ -4,7 +4,7 @@ import com.simprints.core.ExternalScope import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.sync.SyncOrchestrator import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.async import javax.inject.Inject internal class LogoutUseCase @Inject constructor( @@ -13,7 +13,9 @@ internal class LogoutUseCase @Inject constructor( @ExternalScope private val externalScope: CoroutineScope, ) { - suspend operator fun invoke() = externalScope.launch { + // Callers to this fun should wait for the returned Deferred to complete + // to ensure that the logout process is complete + operator fun invoke() = externalScope.async { // Cancel all background sync syncOrchestrator.cancelBackgroundWork() syncOrchestrator.deleteEventSyncInfo() 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 dc9d5afc72..c7d298b5f3 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,7 +55,7 @@ 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, ) : ViewModel() { @@ -106,7 +105,7 @@ internal class SyncViewModel @Inject constructor( if (isSyncComplete && isProjectEnding) { viewModelScope.launch { - logoutUseCase() + logout().await() _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 9dc3e199b1..be16ae0241 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 @@ -22,7 +22,7 @@ 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, ) : ViewModel() { @@ -58,7 +58,7 @@ internal class AboutViewModel @Inject constructor( when (canSyncDataToSimprints() && hasEventsToUpload()) { true -> LogoutDestination.LogoutDataSyncScreen false -> { - logout() + logout().await() LogoutDestination.LoginScreen } } @@ -72,9 +72,6 @@ internal class AboutViewModel @Inject constructor( private suspend fun canSyncDataToSimprints(): Boolean = configRepository.getProjectConfiguration().canSyncDataToSimprints() - private fun logout() { - viewModelScope.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 9b001af196..862113a473 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,6 +13,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -37,8 +38,9 @@ internal class LogoutSyncViewModelTest { MockKAnnotations.init(this, relaxed = true) } + @Suppress("DeferredResultUnused") @Test - fun `should logout correctly`() { + fun `should logout correctly`()= runTest { val viewModel = LogoutSyncViewModel( configRepository = configRepository, logoutUseCase = logoutUseCase, diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt index 499beb07a6..78d03c9937 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt @@ -24,6 +24,7 @@ import dagger.hilt.android.testing.HiltTestApplication import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.CompletableDeferred import org.hamcrest.core.IsNot.not import org.junit.Rule import org.junit.Test @@ -49,7 +50,9 @@ internal class LogoutSyncFragmentTest { @BindValue @JvmField - internal val logoutSyncViewModel = mockk(relaxed = true) + internal val logoutSyncViewModel = mockk(relaxed = true){ + every { logout() } returns CompletableDeferred() + } private val context = InstrumentationRegistry.getInstrumentation().context private val navController = testNavController(R.navigation.graph_dashboard) @@ -423,6 +426,7 @@ internal class LogoutSyncFragmentTest { .check(matches(isDisplayed())) } + @Suppress("DeferredResultUnused") @Test fun `should navigate to requestLoginFragment when logout button is pressed`() { mockSyncToBFSIDAllowed(true) @@ -431,8 +435,9 @@ internal class LogoutSyncFragmentTest { launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.logoutButton)).perform(scrollTo(), click()) - assertThat(navController.currentDestination?.id) - .isEqualTo(R.id.requestLoginFragment) + + verify(exactly = 1) { logoutSyncViewModel.logout() } + } @Test 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 4237765e41..05e84e4380 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 @@ -414,6 +414,7 @@ internal class SyncViewModelTest { assertThat(syncCardLiveData).isEqualTo(SyncTryAgain(DATE)) } + @Suppress("DeferredResultUnused") @Test fun `should logout when project is ending and sync is complete`() { coEvery { configRepository.getDeviceConfiguration() } returns deviceConfiguration @@ -443,7 +444,7 @@ internal class SyncViewModelTest { configRepository = configRepository, timeHelper = timeHelper, authStore = authStore, - logoutUseCase = logoutUseCase, + logout = logoutUseCase, recentUserActivityManager = recentUserActivityManager, ) } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/about/AboutViewModelTest.kt index 39f2ae2120..a267cb70a5 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 @@ -25,6 +25,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +@Suppress("DeferredResultUnused") class AboutViewModelTest { companion object { @@ -65,7 +66,7 @@ class AboutViewModelTest { configRepository = configRepository, eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, - logoutUseCase = logoutUseCase, + logout = logoutUseCase, ) assertThat(viewModel.modalities.value).isEqualTo(MODALITIES) @@ -184,7 +185,7 @@ class AboutViewModelTest { configRepository = configRepository, eventSyncManager = eventSyncManager, recentUserActivityManager = recentUserActivityManager, - logoutUseCase = logoutUseCase, + logout = logoutUseCase, ) } } From 3013f01b2157b8d9d718e08f7a3383175fc0e589 Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 16 Apr 2024 15:38:43 +0200 Subject: [PATCH 08/29] [MS-417] Mark DeviceConfigDownSyncWorker as HiltWorker --- .../infra/sync/config/worker/DeviceConfigDownSyncWorker.kt | 2 ++ 1 file changed, 2 insertions(+) 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..22184eeecc 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, From 71974c8a77e495db761044bc67bef1678d892ddc Mon Sep 17 00:00:00 2001 From: melad Date: Wed, 17 Apr 2024 13:13:19 +0200 Subject: [PATCH 09/29] [MS-413] Use runBlocking to simplify the code --- .../feature/dashboard/logout/LogoutSyncViewModel.kt | 4 +++- .../dashboard/logout/sync/LogoutSyncFragment.kt | 8 ++------ .../logout/syncdecline/LogoutSyncDeclineFragment.kt | 8 ++------ .../feature/dashboard/logout/usecase/LogoutUseCase.kt | 9 ++------- .../feature/dashboard/main/sync/SyncViewModel.kt | 2 +- .../dashboard/settings/about/AboutViewModel.kt | 2 +- .../dashboard/logout/LogoutSyncViewModelTest.kt | 4 +--- .../dashboard/logout/sync/LogoutSyncFragmentTest.kt | 11 +++-------- .../feature/dashboard/main/sync/SyncViewModelTest.kt | 1 - .../dashboard/settings/about/AboutViewModelTest.kt | 1 - 10 files changed, 15 insertions(+), 35 deletions(-) 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 d2be312664..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 @@ -23,6 +23,8 @@ internal class LogoutSyncViewModel @Inject constructor( } - fun logout() = logoutUseCase() + fun logout() { + logoutUseCase() + } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt index 1c98b1a108..ed46d76baf 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragment.kt @@ -8,7 +8,6 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.simprints.core.livedata.LiveDataEventWithContentObserver import com.simprints.feature.dashboard.R @@ -21,7 +20,6 @@ import com.simprints.feature.login.LoginResult import com.simprints.infra.uibase.navigation.handleResult import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { @@ -56,10 +54,8 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) { findNavController().navigate(R.id.action_logoutSyncFragment_to_logoutSyncDeclineFragment) } logoutButton.setOnClickListener { - lifecycleScope.launch { - logoutSyncViewModel.logout().await() - findNavController().navigate(R.id.action_logoutSyncFragment_to_requestLoginFragment) - } + logoutSyncViewModel.logout() + findNavController().navigate(R.id.action_logoutSyncFragment_to_requestLoginFragment) } } diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt index be8a869d25..514d959125 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/syncdecline/LogoutSyncDeclineFragment.kt @@ -5,7 +5,6 @@ import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.simprints.core.livedata.LiveDataEventWithContentObserver @@ -15,7 +14,6 @@ import com.simprints.feature.dashboard.logout.LogoutSyncViewModel import com.simprints.feature.dashboard.settings.password.SettingsPasswordDialogFragment import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import com.simprints.infra.resources.R as IDR @AndroidEntryPoint @@ -64,9 +62,7 @@ class LogoutSyncDeclineFragment : Fragment(R.layout.fragment_logout_sync_decline } private fun processLogoutConfirmation() { - lifecycleScope.launch { - viewModel.logout().await() - findNavController().navigate(R.id.action_logoutSyncDeclineFragment_to_requestLoginFragment) - } + viewModel.logout() + findNavController().navigate(R.id.action_logoutSyncDeclineFragment_to_requestLoginFragment) } } 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 0504be371c..13608a5afb 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt @@ -1,21 +1,16 @@ package com.simprints.feature.dashboard.logout.usecase -import com.simprints.core.ExternalScope import com.simprints.infra.authlogic.AuthManager import com.simprints.infra.sync.SyncOrchestrator -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import javax.inject.Inject internal class LogoutUseCase @Inject constructor( private val syncOrchestrator: SyncOrchestrator, private val authManager: AuthManager, - @ExternalScope private val externalScope: CoroutineScope, ) { - // Callers to this fun should wait for the returned Deferred to complete - // to ensure that the logout process is complete - operator fun invoke() = externalScope.async { + operator fun invoke() =runBlocking{ // Cancel all background sync syncOrchestrator.cancelBackgroundWork() syncOrchestrator.deleteEventSyncInfo() 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 c7d298b5f3..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 @@ -105,7 +105,7 @@ internal class SyncViewModel @Inject constructor( if (isSyncComplete && isProjectEnding) { viewModelScope.launch { - logout().await() + 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 be16ae0241..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 @@ -58,7 +58,7 @@ internal class AboutViewModel @Inject constructor( when (canSyncDataToSimprints() && hasEventsToUpload()) { true -> LogoutDestination.LogoutDataSyncScreen false -> { - logout().await() + logout() LogoutDestination.LoginScreen } } 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 862113a473..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.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test @@ -38,9 +37,8 @@ internal class LogoutSyncViewModelTest { MockKAnnotations.init(this, relaxed = true) } - @Suppress("DeferredResultUnused") @Test - fun `should logout correctly`()= runTest { + fun `should logout correctly`() { val viewModel = LogoutSyncViewModel( configRepository = configRepository, logoutUseCase = logoutUseCase, diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt index 78d03c9937..499beb07a6 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/logout/sync/LogoutSyncFragmentTest.kt @@ -24,7 +24,6 @@ import dagger.hilt.android.testing.HiltTestApplication import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.CompletableDeferred import org.hamcrest.core.IsNot.not import org.junit.Rule import org.junit.Test @@ -50,9 +49,7 @@ internal class LogoutSyncFragmentTest { @BindValue @JvmField - internal val logoutSyncViewModel = mockk(relaxed = true){ - every { logout() } returns CompletableDeferred() - } + internal val logoutSyncViewModel = mockk(relaxed = true) private val context = InstrumentationRegistry.getInstrumentation().context private val navController = testNavController(R.navigation.graph_dashboard) @@ -426,7 +423,6 @@ internal class LogoutSyncFragmentTest { .check(matches(isDisplayed())) } - @Suppress("DeferredResultUnused") @Test fun `should navigate to requestLoginFragment when logout button is pressed`() { mockSyncToBFSIDAllowed(true) @@ -435,9 +431,8 @@ internal class LogoutSyncFragmentTest { launchFragmentInHiltContainer(navController = navController) onView(withId(R.id.logoutButton)).perform(scrollTo(), click()) - - verify(exactly = 1) { logoutSyncViewModel.logout() } - + assertThat(navController.currentDestination?.id) + .isEqualTo(R.id.requestLoginFragment) } @Test 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 05e84e4380..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 @@ -414,7 +414,6 @@ internal class SyncViewModelTest { assertThat(syncCardLiveData).isEqualTo(SyncTryAgain(DATE)) } - @Suppress("DeferredResultUnused") @Test fun `should logout when project is ending and sync is complete`() { coEvery { configRepository.getDeviceConfiguration() } returns deviceConfiguration 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 a267cb70a5..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 @@ -25,7 +25,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -@Suppress("DeferredResultUnused") class AboutViewModelTest { companion object { From 9ec2b96006fb42edfd62124ea62343ab99c618f3 Mon Sep 17 00:00:00 2001 From: melad Date: Wed, 17 Apr 2024 13:17:24 +0200 Subject: [PATCH 10/29] [MS-413] [skip ci] reformat --- .../simprints/feature/dashboard/logout/usecase/LogoutUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 13608a5afb..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 @@ -10,7 +10,7 @@ internal class LogoutUseCase @Inject constructor( private val authManager: AuthManager, ) { - operator fun invoke() =runBlocking{ + operator fun invoke() = runBlocking { // Cancel all background sync syncOrchestrator.cancelBackgroundWork() syncOrchestrator.deleteEventSyncInfo() From 8a3c1705ebb37ac5f069379a05d1495b84bac9b5 Mon Sep 17 00:00:00 2001 From: melad Date: Wed, 17 Apr 2024 13:23:54 +0200 Subject: [PATCH 11/29] [MS-413] Fix tests --- .../feature/dashboard/logout/usecase/LogoutUseCaseTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 index 4d90c5c510..93b95ca6cc 100644 --- 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 @@ -7,7 +7,6 @@ import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.MockKAnnotations import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -36,7 +35,6 @@ class LogoutUseCaseTest { useCase = LogoutUseCase( syncOrchestrator = syncOrchestrator, authManager = authManager, - CoroutineScope(testCoroutineRule.testCoroutineDispatcher), ) } From 8ae336fd4d80de9de82040d8edb45665a52dcb7e Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Wed, 17 Apr 2024 19:42:55 +0300 Subject: [PATCH 12/29] [MS-418] Don't include projectConfigurationUpdatedAt if it's empty --- .../remote/models/session/ApiEventScope.kt | 3 + .../models/session/ApiEventScopeTest.kt | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/session/ApiEventScopeTest.kt 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/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 From 186c0721513e56ca77b2860a34c7a625ebc1ef0a Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Thu, 18 Apr 2024 20:47:16 +0300 Subject: [PATCH 13/29] [MS-425] Add attendantId and moduleId to tokenizedFields if they are present in the query parameters --- .../downsync/EventDownSyncRequestEvent.kt | 5 +- .../downsync/EventDownSyncRequestEventTest.kt | 87 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 infra/events/src/test/java/com/simprints/infra/events/event/domain/models/downsync/EventDownSyncRequestEventTest.kt 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 From 51d9a3fb6efd06e19c91993a686b3fac02945b31 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Mon, 22 Apr 2024 10:21:24 +0300 Subject: [PATCH 14/29] MS-427 Add request ID to response headers locally --- .../network/httpclient/DefaultOkHttpClientBuilder.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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..408583c813 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 @@ -59,7 +59,7 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( } } .addNetworkInterceptor(ChuckerInterceptor.Builder(ctx).build()) - .addInterceptor(buildRequestIdInterceptor()) + .addNetworkInterceptor(buildRequestIdInterceptor()) .addInterceptor(buildDeviceIdInterceptor(deviceId)) .addInterceptor(buildVersionInterceptor(versionName)) .addInterceptor(buildGZipInterceptor()) @@ -70,10 +70,12 @@ 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) + val requestId = UUID.randomUUID().toString() + + // Adding same header to both request and response to + // track the request across network logs and analytics events. + val newRequest = chain.request().newBuilder().addHeader(REQUEST_ID_HEADER, requestId).build() + return@Interceptor chain.proceed(newRequest).newBuilder().addHeader(REQUEST_ID_HEADER, requestId).build() } private fun buildAuthenticationInterceptor(authToken: String) = Interceptor { chain -> From 622e1e8b1d8ac62e73e1ebca092ed32e8d064d95 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:20:14 +0000 Subject: [PATCH 15/29] Translate strings.xml in bn 100% translated source file: 'strings.xml' on 'bn'. --- .../src/main/res/values-bn/strings.xml | 94 ++++++++++++++++--- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/infra/resources/src/main/res/values-bn/strings.xml b/infra/resources/src/main/res/values-bn/strings.xml index 2e1bad925e..856441c259 100644 --- a/infra/resources/src/main/res/values-bn/strings.xml +++ b/infra/resources/src/main/res/values-bn/strings.xml @@ -3,13 +3,16 @@ সতর্ক বার্তা + বন্ধ করুন + সাধারন সম্মতি আমি আপনাকে %2$s এ নিবন্ধন করতে চাই %1$s আমি আপনাার %2$s আপনাকে %1$s এ নিবন্ধন করতে চাই এবং ভবিষ্যতে তা দিয়ে আপনাকে সনাক্ত করতে চাই। আমি আপনার %2$s ব্যবহার করে %1$s - এর জন্য আপনার তথ্যাদি খুঁজে বের করতে চাই। - সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %1$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। - %1$s এবং সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %1$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + %1$s এবং সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + সিমপ্রিন্টস গবেষণার কাজেও তথ্যগুলো ব্যবহার করবে সম্মতি দেবার পরেও যেকোনো সময় আপনি অনুমতি ফিরিয়ে নিতে এবং আপনার তথ্যাদি মুছে ফেলতে বলতে পারেন। আমি কি %1$s? অনুগ্রহ করে বলুন- \"আমি অনুমতি দিচ্ছি \", \" আমি প্রত্যাখ্যান করছি\" অথবা \"আমার প্রশ্ন আছে।\" মাতা-পিতার সম্মতি @@ -17,7 +20,7 @@ আমি %2$s তাঁকে %1Ss নিবন্ধন করে, ভবিষ্যতে সনাক্ত করতে চাই। \"আমি %2$s তাঁকে খুঁজে বের করতে চাই %1$s \" সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক প্রতিষ্ঠানের কাছে তাঁর %1$s এবং বর্তমান ঠিকানা সংরক্ষিত আছে। - %1$s এবং সিমপ্রিন্টস (একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান) আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। + %1$s এবং সিমপ্রিন্টস, একটি যুক্তরাজ্য ভিত্তিক অলাভজনক প্রতিষ্ঠান, আপনার %2$s ও আপনার বর্তমান ঠিকানা সংগ্রহ করতে পারবে। আপনি সম্মতি দিলেও যে কোন সময় অনুমতি সরিয়ে নিয়ে আপনার সন্তানের তথ্য মুছে ফেলতে বলতে পারেন। আমি কি আপনার সন্তানের %1$s ব্যবহার করতে পারি\? অনুগ্রহ করে বলুন- \"আমি অনুমতি দিচ্ছি \", \" আমি প্রত্যাখ্যান করছি\" অথবা \"আমার প্রশ্ন আছে।\" আপনার আঙ্গুলের ছাপ ব্যবহার @@ -40,15 +43,19 @@ নিবন্ধন সফল হয়েছে সেভ করা যায়নি 1. %1$s রেকর্ড সংরক্ষণ করতে ব্যর্থ 2. বন্ধ করুন + 1. একটি বিদ্যমান রেকর্ডের নকল হবার কারণে %1$s রেকর্ডটি সংরক্ষণ করতে ব্যর্থ 2. বন্ধ করুন মুখ আঙ্গুলের ছাপ মুখ/আঙুলের ছাপ আপনি কেন আঙ্গুলের ছাপ দেয়া এড়িয়ে গেছেন? - বায়োমেট্রিক তথ্য এড়িয়ে গেলেন কেন\? + বায়োমেট্রিক তথ্য এড়িয়ে গেলেন কেন? + মুখমণ্ডলের ছবি নেয়া এড়িয়ে গেলেন কেন? অতিরিক্ত তথ্য + মুখমণ্ডলের ছবি সংগ্রহ করুন আঙ্গুলের ছাপ স্ক্যান করুন + মুখমণ্ডলের ছবি সংগ্রহ করুন জমা দিন একটি অপশন সিলেক্ট করুন এবং জমা দিন ফর্ম জমা দিন @@ -56,6 +63,7 @@ তথ্য দিতে আগ্রহী নন অনুমতি নেই অ্যাপ কাজ করছে না + স্ক্যানার কাজ করছে না ব্যক্তি উপস্থিত নয় বয়স কম অন্যান্য @@ -65,6 +73,8 @@ ব্যক্তি খুঁজে পাওয়া যায়নি যাচাইয়ের জন্য নির্বাচিত ব্যক্তি ডাটাবেসে নেই ব্যক্তি খুঁজে পাওয়া যায়নি, দয়া করে নিশ্চিত করুন যে আপনি ইন্টারনেটে সংযুক্ত আছেন এবং আবার চেষ্টা করুন + আবার চেষ্টা করুন + প্রজেক্ট আইডি প্রজেক্ট কী @@ -72,12 +82,19 @@ কিউআর কোড স্ক্যান করুন লগইন করুন কিউআর কোড স্ক্যান- এ সমস্যা হয়েছে + ক্যামেরা ব্যবহারের অনুমতি ছাড়া QR কোড স্ক্যান করা যাবে না + ক্যামেরা ব্যাবহারের চেষ্টা করার সময় একটি সমস্যা দেখা দিয়েছে, সুপারভাইসর এর সাথে যোগাযোগ করুন স্ক্যান করা কিউআর কোডটি সঠিক নয় তথ্য অসম্পূর্ণ। ব্যবহারকারী আইডি, প্রজেক্ট আইডি এবং কী চেক করুন এবং আবার চেষ্টা করুন তথ্য সঠিক নয়। প্রজেক্ট আইডি এবং কী চেক করুন। ভিন্ন প্রজেক্ট আইডি। সিস্টেম অ্যাডমিন এর সাথে যোগাযোগ করুন আপনি এখন অফলাইন। আপনার ইন্টারনেট সংযোগ চেক করুন + ইন্টিগ্রিটি সার্ভিসের সাথে কন্টাক্টের সময় একটি সমস্যা হয়েছে৷ অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন সার্ভার এর সাথে সংযোগ স্থাপনে সমস্যা হয়েছে। পরবর্তীতে আবার চেষ্টা করুন। + ব্যাকএন্ড URL পরিবর্তন করুন + সেভ করুন + ডিফল্ট ব্যাবহার করুন + বাতিল করুন আঙ্গুলের ছাপ মিলানো হচ্ছে, অপেক্ষা করুন @@ -104,12 +121,19 @@ %d মিল + + ত্রুটি দেখা দিয়েছে কনফিগারেশন সমস্যা একটি কনফিগারেশন সমস্যা ঘটেছে (ভিন্ন প্রজেক্ট আই ডি), সাইন আউট করুন এবং আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন + আবার চেষ্টা করুন এবং সিমপ্রিন্টস কাজ না করলে আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন। পুরানো Google Play Service সংস্করণ এই ডিভাইসে ইনস্টল করা Google Play Service সংস্করণটি SID অ্যাপ -এর প্রয়োজনীয় সংস্করণের চেয়ে পুরানো Google play service অনুপস্থিত এই ডিভাইসে Google play service ইনস্টল করা নেই। + \"Google Play Store অ্যাপ\" অনুপস্থিত বা পুরানো সংস্করণ + \"Google Play Store অ্যাপ\" ইনস্টল বা আপডেট করা নেই। + প্রকল্পটি বর্তমানে শেষ হতে চলেছে + প্রকল্পটি বর্তমানে শেষ হতে চলেছে বিধায় সেশন চালু করতে ব্যর্থ হয়েছে। আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন প্রকল্পটি বর্তমানে বিরতিতে আছে প্র্রকল্পটি বর্তমানে বিরতিতে থাকার কারণে সেশনটি চালু করা যাচ্ছেনা। আরও তথ্যের জন্য সুপারভাইসরের সাথে যোগাযোগ করুন। একটি কনফিগারেশন সমস্যা ঘটেছে (অবৈধ প্রোজেক্ট আইডি), আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন @@ -127,13 +151,16 @@ নিশ্চিত করা হয়েছে আপনাকে ফিরিয়ে আনা হচ্ছে - - মুখের মোডালিটি কনফিগার করা হচ্ছে - প্রয়োজনীয় ফাইল ডাউনলোড করা হচ্ছে - অবৈধ লাইসেন্স - মুুখের লাইসেন্স অবৈধ, আপনার সুপেরভাইসার এর সাথে যোগাযোগ করুন - কনফিগারেশন সমস্যা - মুখের মোডালিটি কনফিগার করার সময় সমস্যা হয়েছে। সিস্টেম অ্যাডমিন এর সাথে যোগাযোগ করুন + + পদ্ধতি কনফিগার করা হচ্ছে + প্রয়োজনীয় ফাইল ডাউনলোড করা হচ্ছে + রক্ষণাবেক্ষণ মোড + সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে, অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন + লাইসেন্সটি অকার্যকর + লাইসেন্সটি অকার্যকর, আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন + কনফিগারেশন ত্রুটি %1$s + লাইসেন্সটি সিস্টেম থেকে খুঁজে আনার সময় একটি ত্রুটি দেখা দিয়েছে, আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন + ভালো আলো আছে এমন স্থান খুঁজে বের করুন @@ -153,6 +180,7 @@ ব্যক্তি ক্যামেরার দিকে সোজা তাকাচ্ছে কিনা নিশ্চিত করুন ছবি তুলতে এখানে চাপুন ছবি তোলা হচ্ছে + অনুগ্রহ করে ক্যামেরা ব্যাবহারের অনুমতি দিন স্থির থাকুন ব্যক্তির মুখের ছবি তোলা সফল হয়েছে নিশ্চিত করা হচ্ছে @@ -161,6 +189,7 @@ সমস্যা ঘটেছে + অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন, তখন যদি সিমপ্রিন্টস কাজ না করে তাহলে আপনার সুপারভাইসর এর সাথে যোগাযোগ করুন বন্ধ করুন আবার চেষ্টা করুন ফোন সেটিংস @@ -169,7 +198,7 @@ হ্যা না স্ক্যানার সংযুক্ত হচ্ছে… - ইউএন২০ চালু হচ্ছে… + ইউএন20 চালু হচ্ছে… ভেরো সেট-আপ হচ্ছে… সংযুক্ত হয়েছে ব্লুুটুথ বন্ধ আছে @@ -188,6 +217,7 @@ ডিভাইস গুলো সংযুক্ত করা যাচ্ছেনা %1$s স্ক্যানার পাওয়া গেছে স্ক্যানার %1$s চালুু আছে কিনা নিশ্চিত করুন এবং আবার চেষ্টা করুন + পেয়ারিং বাতিল করা হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন এবং পেয়ারিং অনুরোধ করা হলে সেটি গ্রহণ করুন। এন.এফ.সি চিপ পড়া যাচ্ছেনা, আবার চেষ্টা করুন অবৈধ এন.এফ.সি চিপ ঠিক আছে @@ -195,6 +225,7 @@ নিচের বক্স-এ স্ক্যানার এর নাম্বার লিখুন এই নাম্বারটি স্ক্যানার-এর একদম নিচে পাবেন স্ক্যানার %1$s চালুু আছে কিনা নিশ্চিত করুন এবং আবার চেষ্টা করুন + পেয়ারিং বাতিল করা হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন এবং পেয়ারিং অনুরোধ করা হলে সেটি গ্রহণ করুন। ৬ সংখ্যার সিরিয়াল নাম্বারটি লিখুন স্ক্যানার আপডেট করা প্রয়োজন স্ক্যানার চালু রাখুন, তা আপডেট হচ্ছে @@ -218,11 +249,14 @@ \n৫। সিমপ্রিন্টস অ্যাপ- এ ফেরত যেতে \"ফেরত\" বাটন চাপুন। স্ক্যানার চালু করুন ১। স্ক্যানার চালু করুন।\n২। \"আবার চেষ্টা করুন\" চাপুন। + অনুমতি নেই + অনুগ্রহ করে ফোন সেটিংসে ব্লুটুথ-এর অনুমতি সক্ষম করুন৷ আপনার ফোন/যন্ত্র ব্লুটুথ সাপোর্ট করেনা। আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন ব্লুটুথ সক্রিয় করা নেই, এটি সেটিংস এ সক্রিয় হয়েছে কিনা তা নিশ্চিত করুন এবং আবার চেষ্টা করুন এক এর অধিক স্ক্যানার সংযুক্ত আছে, অপ্রয়োজনীয় স্ক্যানার বাতিল করুন। + ফিঙ্গারপ্রিন্ট পদ্ধতি কনফিগার করা হচ্ছে তালিকাভুক্তকরণ সনাক্তকরণ যাচাইকরণ @@ -236,6 +270,8 @@ ডান হাতের মধ্যমা ডান হাতের অনামিকা ডান হাতের কনিষ্ঠা / কড়ে আঙ্গুল + বাতিল করুন + ত্রুটি হয়েছে আবার চেষ্টা করুন এবং সিমপ্রিন্টস কাজ না করলে আপনার সুপারভাইসর এর সঙ্গে যোগাযোগ করুন। স্ক্যান করুন পরবর্তী আঙ্গুল স্ক্যান করতে বামে সোয়াইপ করুন @@ -268,6 +304,9 @@ প্র্রকল্পটি বর্তমানে শেষ হবার কারণে সেশনটি চালু করা যাচ্ছেনা। আরও তথ্যের জন্য সুপারভাইসরের সাথে যোগাযোগ করুন। আপনাকে লগইন করতে হবে। সিমপ্রিন্টস আইডি খুলতে আপনার ক্লায়েন্ট অ্যাপ ব্যবহার করুন এবং লগইন করুন + ডিভাইস আইডি: %s + বন্ধ করুন + স্ক্যানার ব্যবহার করা হয়েছে: %1$s বর্তমান ব্যবহারকারী: %1$s @@ -284,12 +323,32 @@ মডিউল সমূহ সেটিংস্‌ থেকে ইন্টারনেট সংযোগ চালু করুন স্ক্যানার যোগ করুন + অনেক বেশি মডিউল ডাউনলোড করা হয়েছে + সিঙ্ক করতে পুনরায় লগইন করুন + সর্বশেষ সিঙ্ক: %1$s সমস্ত রেকর্ড আপলোড হয়েছে + + %1$dটি রেকর্ড আপলোড হওয়া বাকি + %1$dটি রেকর্ড আপলোড হওয়া বাকি + আবার চেষ্টা করুন কার্যক্রম তথ্য: %1$s + + তালিকাভুক্তকরণ + তালিকাভুক্তকরণ + + + সনাক্তকরণ + সনাক্তকরণ + + + যাচাইকরণ + যাচাইকরণ + + সেটিংস সম্বন্ধে @@ -307,12 +366,14 @@ ডিভাইস এর আইডি যে আঙ্গুল গুলো স্ক্যান করবেন সেগুলো সিলেক্ট করুন সিঙ্ক- এর জন্য আরও তথ্য + কপি করা হয়েছে + তথ্য সিঙ্ক করুন মডিউল নির্বাচন করুন আপলোড এর জন্য প্রস্তুত রেকর্ড - ডাউনলোড এর জন্য প্রস্তুত রেকর্ড + ডাউনলোড বা মুছে ফেলার রেকর্ড মুছে ফেলার জন্য প্রস্তুত রেকর্ড ফোনে সংরক্ষিত সকল রেকর্ড নির্বাচিত মডিউল @@ -321,6 +382,8 @@ আপলোড বাকি সিঙ্ক করুন সিঙ্ক হচ্ছে… + সিঙ্ক তথ্য পুনরায় নিয়ে আসুন + অবৈধ নির্বাচন। অন্তত একটি মডিউল নির্বাচন করুন। অবৈধ নির্বাচন। %1$d এর বেশি মডিউল নির্বাচন করবেন না। @@ -341,6 +404,7 @@ আনলক করতে পাসওয়ার্ড লিখুন লগ আউট করতে পাসওয়ার্ড লিখুন পাসওয়ার্ড + বাতিল করুন ভুল পাসওয়ার্ড Unlock করতে চাপুন @@ -352,10 +416,14 @@ আপনার প্রকল্পের সকল রেকর্ড আপ-টু-ডেট রয়েছে তা নিশ্চিত করার জন্য ডেটা সিঙ্ক্রোনাইজেশন অপরিহার্য। কোনো বৈধ কারণ ছাড়া ডেটা সিঙ্ক্রোনাইজ করতে ব্যর্থ হলে তা আপনার সহকর্মীদের গুরুত্বপূর্ণ রেকর্ডে অ্যাক্সেস এবং প্রকল্পের সঠিকভাবে রিপোর্ট করার ক্ষমতাকে প্রভাবিত করবে।\n\nকোনও বৈধ কারণ ছাড়া ডেটা সিঙ্ক্রোনাইজ করতে ব্যর্থ হওয়া ডেটা সংগ্রহ প্রোটোকলের লঙ্ঘনও হিসেবে গণ্য হতে পারে যার ফলে প্রকল্পের অবাঞ্ছিত পরিণতি হবার সুযোগ থাকে। লগআউট করুন আপনি কি লগআউট করতে চান? + লগআউট করুন বাতিল করুন সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে। অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন. সিস্টেমটি বর্তমানে রক্ষণাবেক্ষণের জন্য অফলাইনে রয়েছে। %s পরে আবার চেষ্টা করুন + + সিঙ্ক হচ্ছে + From 57c0c1759c2e8f0a7e7bd16744b11f7d141063c1 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 23 Apr 2024 09:26:01 +0400 Subject: [PATCH 16/29] [MS-405] Adding serialization modules when marshaling/unmarshalling actionRequest --- .../orchestrator/OrchestratorViewModel.kt | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) 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 7de858510c..3af5e17f04 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,6 +4,11 @@ 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 @@ -151,7 +156,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) @@ -162,9 +173,13 @@ internal class OrchestratorViewModel @Inject constructor( } } } + fun setActionRequestFromJson(json: String) { try { - actionRequest = JsonHelper.fromJson(json) + actionRequest = JsonHelper.fromJson( + json = json, + module = dbSerializationModule, + type = object : TypeReference() {}) } catch (e: Exception) { Simber.e(e) } @@ -172,10 +187,19 @@ internal class OrchestratorViewModel @Inject constructor( fun getActionRequestJson(): String? { return try { - actionRequest?.run(JsonHelper::toJson) + 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()) + } + } } From c8f1c802fd1a19d68311cebfaed6fe5cc8b2aec5 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:23:04 +0300 Subject: [PATCH 17/29] Updates for project Simprints ID (#692) * Translate strings.xml in am * Translate strings.xml in am_ET --- .../src/main/res/values-am-rET/strings.xml | 90 +++++++++++++++---- .../src/main/res/values-am/strings.xml | 87 ++++++++++++++---- 2 files changed, 146 insertions(+), 31 deletions(-) diff --git a/infra/resources/src/main/res/values-am-rET/strings.xml b/infra/resources/src/main/res/values-am-rET/strings.xml index 4933537120..9cff19410d 100644 --- a/infra/resources/src/main/res/values-am-rET/strings.xml +++ b/infra/resources/src/main/res/values-am-rET/strings.xml @@ -3,16 +3,25 @@ ማንቂያ + ዝጋ + ፈቃደኝነት + ልመዘግብ%2$s እፈልጋለው%1$s ወደፊት የጣት አሻራዎትን እርስዎን መለየት ይቻል ዘንድ እና የጣት አሻራ መረጃዎትን ለመመዝገብ %1$s እና %2$s መጠቀም እፈልጋለሁ ወደፊት %2$s የእርስዎን መረጃ ለማግኘት የጣት አሻራዎትን መጠቀም እፈልጋለሁ %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። + በተጨማሪም ሲምፕሪንትስ መረጃውን ለጥናት አላማ ሊጠቀምበት ይችላል + ከተስማሙ፤በማንኛዉም ጊዜ ፈቃደኝነቱን በማንኛዉም ጊዜ ማንሳት ይችላሉ እና መረጃውን ማስጠፋት ይችላሉ እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የወላጅ ፈቃደኝነት + ልመዘግብ %2$sእፈልጋለው%1$s እርስዎን %2$s ለመመዝገብ አንዲቻል ዉስጥ %1$s እና ወደፊት መለየት አንዲቻል :: እርስዎን %2$s ወደፊት መለየት አንዲቻል %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። + ከተስማሙ፤ፍቅዱን በማንኛውም ጊዜ ማንሳት ይችላሉ እና የልጅዎንም መረጃ እንዲጠፋ መጠየቅ ይችላሉ የልጅዎን የጣት አሻራ መጠቀም እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የጣት አሻራን ተጠቀም የፊቱን ፎቶግራፍ ማንሳት @@ -34,14 +43,19 @@ የተሳካ ምዝገባ መመዝገብ አልተቻለም 1. %1$s ሪኮርድን ማስቀመጥ አልተሳካም \n 2. ዝጋን መታ ያድርጉ + መመዝገብ አልተቻለም፡%1$sከቆየው ሪከርድ ጋር ድግግሞስ ስለሆነ \n 2. ዝጋ የሚለውን ተጫን ፊት የጣት አሻራ ፊት /የጣት አሻራ የጣት አሻራውን ለምንድን ነው የሚዘሉት? + የጣት አሻራ ለምን ተዘለለ? + ፌስ መመዝገቡ ለምን ተዝለለ? ተጨማሪ ኢንፎርሜሽን + ፊቱን ያዝ የጣት አሻራውን እስካን ያድርጉ + ፊቱን ያዝ ይላኩ እባክዎት ካሉት አማራጮች ይምረጡ እና ይላኩ እባክዎት ቅጽን ይላኩ @@ -59,6 +73,8 @@ ግለሰቡ አልተገኘም አንዲጠራ የተመረጠው ግለሰብ ዳታቤዙ ውስጥ የለም ግለሰቡ የለም፡ እባክዎ ከኢንተርኔት ጋር መገናኘትዎን አረጋግጠው እንደገና ይሞክሩ + እንደገና ይሞክሩ + የፕሮጀክት መለያ ቁጥር የፕሮጀክት ቁልፍ @@ -66,12 +82,19 @@ የፈጣን ምላሽ መለያ እስካን ያድርጉ መግባት የፈጣን ምላሽ ሚስጥር ቁጥርን እስካን በሚያደረግበት ጊዜ ስህተት አጋጥሞታል + ካሜራ ስላልበራ QR code ስካን ማድረግ አልተቻለም + ካሜራዉን ለመክፈት ችግር አጋጥሟል፤እባክዎ ሲስተም አድሚኒስትሬተሩን ያሳዉቁ እስካን የተደረገው የፈጣን ምላሽ ሚስጥር ቁጥር ትክክል አይደለም ተጠቃሚው ተዘሏል ፡ እባክዎ የተጠቃሚውን መለያ ቁጥር ፡ የፕሮጀክት ቁጥር እና ቁልፍ በማረጋገጥ እንደገና ይሞክሩ፡፡ ትክክል ያልሆነ ተጠቃሚ ነው ፡ እባክዎ የፕሮጀክት ቁጥር እና ቁልፍ ያረጋግጡ፡፡ የፕሮጀክት ቁጥሩ አቅራቢው ውስጥ ካለው ዝርዝር ጋር የተለየ ነው፡ እብክዎት የስስተም አስተዳዳሪውን ያነጋግሩ፡፡ አሁን ከግንኙነት ውጭ ነዎት፡ እባክዎት የኢንተርኔት ግንኙነቱን መኖሩን ያረጋግጡ + Integrity Service ለማገኘት ችግር አጋጥሟል፡እባክዎ እንደግና ይሞክሩ ሰርቨሩን ለማግኘት ሲሞክሩ ስህተት አጋጥሞታል ፡ እባክዎት ተንሽ ቆይተው ይሞክሩት፡፡ + የባኪንድ URL ቀይር + አስቀምጥ + default ተጠቀም  + አቋርጥ የመለየት ሰራውን እስኪጠናቀቅ እባክዎት ትንሽ ይጠብቁ @@ -98,12 +121,19 @@ %d ተመራጮች + + ስህተት አጋጥሟል የተግበቦት ስህተት የተለየ የፕሮጀክት መለያ ስለተጠቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ ከሲስተሙ ዋና ገጽ ይውጡ ወይም የቅርብ ተጠሪዎን ያነጋግሩ + ሲምፕሪንትስ ካልሰራ ፤እባክዎ እንደገና ይሞክሩ እና ለሲስተም አሰተዳዳሪው ያሳውቁ ጊዜው ያለፈበት የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ የተጫነው የጉግል ፕለይ አገልግሎት ስሪት በSID ከሚያስፈልገው በላይ የቆየ ነው። የጎግል ፕሌይ አገልግሎቶች ይጎድላሉ የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ አልተጫነም። + \"Google Play Store app\" ጠፍቷል ወይም ጊዜው አልፎበታል + \"Google Play Store app\" አልተጫንም ወይም አፕዴት አልተደረገም + ፕሮጀክቱ እያለቀ ነው + ሴሽን ማስጀመር አልተቻለም ፕሮጀክቱ እያበቃ ስለሆነ፤ ለተጨማሪ እባክዎ የሲስተም አስተዳዳሪዉን ያሳዉቁ ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል ክፍለ-ጊዜን ማስጀመር አልተቻለም ምክንያቱም ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ የተሳሳተ የፕሮጀክት መለያ ስለተተቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ @@ -121,13 +151,16 @@ ማረጋገጫ ተልኳል እርስዎን መልሰው በማዛወር ላይ - - የፊት ሞዳሊቲ አልተገኘም - አስፈላጊ ፋይሎችን በማውረድ ላይ - ፍቃድ አይሰራም/ትክክል አይደለም - የፊት ላይሰንሱ/ ፍቃድ የማይሰራ ነው፤እባክዎ የሲስተም አስተዳዳሪውን ያሳውቁ - የማዋቀር ስህተት - የፊት ሁነታን በማዋቀር ላይ ስህተት ነበር፣እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ + + ሞዳሊቲ በማዋቀር ላይ + አስፈላጊዉን ፈይል በማውረድ ላይ + ጥገና ሞድ + ሲስተሙ ጥገና ላይ ነው፤እባክዎ እንደገና ይሞከሩ + የማይሰራ ላይሰንስ + ላይሰነሱ አይሰራም፤የሲስተም አስተዳዳሪው ያሳውቁ + የማዋቀር ስህተት %1$s + ላይሰንሱ ለማግኘት ችግር አጋጥሟል፤እባከዎ ለሲስተም አስተዳዳሪው ያሳዉቁ + በቂ ብርሀን ያለበት ቦታ መምረጥ @@ -147,6 +180,7 @@ ሰዉየዉ ቀጥታ ካሜራዉን እንዲያይ አድርግ ለማንሳት ተጫን በማንሳት ላይ.... + የካሜራ ፈቃድ አብራ ባለበት ፖዚሽኑ ላይ አቆይ.... የፊት ምስል በስኬት ተመዝግቧል ማረጋገጥ @@ -155,6 +189,7 @@ ስህተት አጋጥሟል + ሲሞፕሪንትስ ካልሰራ፤እባክዎ እንደገና ይሞክሩ እና ለሲስተም አስተዳዳሪው ያሳዉቁ ይዝጉ እንደገና ይሞክሩ የስልክ ማስተካከያ @@ -182,6 +217,7 @@ የሚጠቀሙበት መሳሪያ ሊገናኝ አልቻለም የተገኘው ስካነር %1$s እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ አስተካክለው እንደገና ይሞክሩ የNFC ቺፑን ማንበብ አልተቻለም፤ እባክዎ እንደገና ይሞክሩ የተሳሳተ NFC ቺፕ ተገኝቷል እሽ @@ -189,6 +225,7 @@ የስካነሩን ቁጥር ቀጥሎ ካለው ሳጥን ላይ ይፃፉ ይህ የሚገኘው በስካነሩ የታችኛው ክፍል ነው እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ዝግጁ ሲሆን እንደገና ይሞክሩ እባክዎ የሲሪያል ቁጥሩን 6 አሃዞች ያስገቡ ስካነሩ አፕዴት አድርግ ስካነሩ አፕዴት እስኪጨርስ እንደበራ ይቆይ @@ -212,11 +249,14 @@ \n5. ወደ ሲምፕሪንት ይመለሱ እስካነሩን ያብሩ 1. እስካነሩን ያስነሱ\n2. እንደገና ይሞክሩ + ፈቃድ የለም + ስልኩ ላይ ብሉቱዝ አብራ የሚጠቀሙበት መሳሪያ ብሉቱዝ ለመጠቀም አያስችልም፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ ብሉቱዝ አልተከፈተም ፡ለመጠቀም አያስችልም፤እባክዎ ማስተካከያው ውስጥ ገብተው ብሉቱዙን ከፍተው እንደገና ይሞክሩ በስልኩ ላይ ከአንድ በላይ እስካነር ተገናኝቷል፣ እባክዎ የማይፈልጉትን እስካነር ግንኙነቱን ያቋርጡ + የጣት አሻራ ሞዳሊቲ በማዋቀር ላይ ወደ ሲስተሙ ማስገባት መለየት ማመሳከር @@ -230,6 +270,8 @@ የቀኝ እጅ ሶስተኛ ጣት የቀኝ እጅ አራተኛ ጣት የቀኝ እጅ አምስተኛ ጣት + አቋርጥ + ስህተት አጋጥሟል እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንቱ መስራት ካለቻለ የሲስተም አስተዳዳሪውን ያነጋግረ እባክዎ እስካን ያድርጉ ለቀጣዩ ጣትዎ ወደ ግራ ያንሸራቱ @@ -262,6 +304,9 @@ ፕሮጀክቱ በአሁኑ ጊዜ እያለቀ ስለሆነ ክፍለ-ጊዜን ማስጀመር አልተቻለም። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ መግባት ከፈለጉ፡ እባክዎ የደንበኛውን አፕልኬሽን ከፍተው የሲም ፕሪንትን መለያቁጥር በመጠቀም መግባት ይችላሉ + የስልክመለያ፡ %s + ዝጋ + እስካነር ይጠቀሙ: %1$s አሁን ያለ ተጠቃሚ: %1$s @@ -278,23 +323,29 @@ ሞጁሎች የስልኩ ሴቲንግ በመግባት የሞባይል ዳታ ያብሩ ማስተካከያ + ብዙ ሞጁሎች ወርደዋል + ሲንክ ለማድረግ በቅድሚያ log in ይደረግ + ያለፈው ግንኙነት: %1$s ሁሉም መዝገቦች ተጭነዋል። + + %1$d ለመስቀል መዝገብ + %1$d ለመስቀል መዝገቦች + እንደገና ይሞክሩ የየዕለት ተግባር: %1$s - - - ወደ ሲስተሙ ማስገባት + + %d ወደ ሲስተሙ ማስገባት ወደ ሲስተሙ ማስገባት - - መለየት + + %d መለየት መለየት - - ማመሳከር + + %d ማመሳከር ማመሳከር @@ -315,12 +366,14 @@ የመሳሪያው መለያ እስካን እንዲደረግ የሚፈልጉተን የጣት አሻራ ይምረጡ ሲንክ ላይ ተጨማሪ ዝርዝር መረጃ + ወደ clipboard ተገልብጧል  + ሲንክ የተደረገ መረጃ ሞጅውሎችን ይምረጡ የተመዘገበውን ለመጫን - የተመዘገበውን ለመማውረድ + ሪከርዶች ለማዉረድ ወይም ለመሰረዝ የተመዘገበውን ለመሰረዝ በስልኩ አጠቃላይ የተመዘገበ የተመረጠ ሞጁል @@ -329,6 +382,8 @@ የሚጫኑ ምስሎች አሁን ግንኙነት ፍጠር በግንኙነት ላይ + የሲንኩን መረጃ በመውሰድ ላይ + የተሳሳተ ምርጫ፡ እባክዎት ካለው የምዕራፍ አማራጭ ውስጥ ቢያንስ አንድ ይምረጡ የተሳሳተ ምርጫ ፡ካሉት ምዕራፎች ውስጥ ከ %1$d በላይ መምረጥ አይችሉም @@ -349,6 +404,7 @@ ለመክፈት የይለፍ ቃል ያስገቡ ለመውጣት የይለፍ ቃል ያስገቡ የይለፍ ቃል + አቋርጥ የተሳሳተ የይለፍ ቃል ለመክፈት መታ ያድርጉ @@ -360,10 +416,14 @@ የእርስዎ ፕሮጀክት ወቅታዊ መዛግብት እንዳለው ለማረጋገጥ የውሂብ ማመሳሰል አስፈላጊ ነው። ያለ ትክክለኛ ምክንያት የእርስዎን ውሂብ ማመሳሰል አለመቻል የስራ ባልደረቦችዎ ጠቃሚ መዝገቦችን ማግኘት እና የፕሮጀክቱን ትክክለኛ ሪፖርት የማድረግ ችሎታ ላይ ተጽእኖ ያሳድራል። እና ለእርስዎ እና ለፕሮጀክቱ የማይፈለጉ ውጤቶችን ሊያስከትል ይችላል. መውጣት ጨርሰው መውጣት ይፈልጋሉ + ዘግቶ መዉጣት ሰርዝ ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ቆየት ብለው ይሞክሩ. ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ከ%s በኋላ እንደገና ይሞክሩ + + ሲንክ ማድረግ በሂደት ላይ + diff --git a/infra/resources/src/main/res/values-am/strings.xml b/infra/resources/src/main/res/values-am/strings.xml index 3ace39ce7b..b5599e66f7 100644 --- a/infra/resources/src/main/res/values-am/strings.xml +++ b/infra/resources/src/main/res/values-am/strings.xml @@ -3,8 +3,11 @@ ማንቂያ + ዝጋ + ፈቃደኝነት + ልመዘግብ I %2$s እፈልጋለው %1$s. ወደፊት የጣት አሻራዎትን እርስዎን መለየት ይቻል ዘንድ እና የጣት አሻራ መረጃዎትን ለመመዝገብ %1$s እና %2$s መጠቀም እፈልጋለሁ ወደፊት %2$s የእርስዎን መረጃ ለማግኘት የጣት አሻራዎትን መጠቀም እፈልጋለሁ %1$s :: ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %1$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። @@ -13,8 +16,10 @@ ከተስማሙ ፈቃድዎትን መቀየር እና እርስዎን በሚመለከት ያለው መረጃ እንዲሰረዝ መጠየቅ ይችላሉ: እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ የወላጅ ፈቃደኝነት + ልመዝግብ %2$s እፈልጋለው %1$s. እርስዎን %2$s ለመመዝገብ አንዲቻል ዉስጥ %1$s እና ወደፊት መለየት አንዲቻል :: እርስዎን %2$s ወደፊት መለየት አንዲቻል %1$s :: + ሲምፕሪንትስ፤በዩኬ የሚገኝ ለትርፍ ያልተቋቆመ ድርጅት የቦታ መገኛ መረጃ ያገኛል %1$s %1$s አና ሲምፕሪንትስ, በእንግሊዝ ሀገር የሚገኝ ለትርፍ ያልተቃቑመ ድርጅት, የ %2$s አና የጂፒኤስ መረጃዉን ሊያገኙ ይችላሉ። ከተስማሙ ፈቃድዎትን መቀየር እና ልጁን በሚመለከት ያለው መረጃ እንዲሰረዝ መጠየቅ ይችላሉ: የልጅዎን የጣት አሻራ መጠቀም እችላለሁ %1$s? እባክዎ መቀበል, አለመቀበል, ወይም ጥያቄ አለኝ ይበሉ @@ -38,6 +43,7 @@ ምዝገባው ተሳክቷል የመጨረሻውን ባዮሜትሪክስ ለመመዝግብ ማንቂያ 1. %1$s ሪኮርድን ማስቀመጥ አልተሳካም \n 2. ዝጋን መታ ያድርጉ + መመዝገብ አልተቻለም፡%1$s1. መመዝገብ አልተቻለም፡ከቆየው ሪከርድ ጋር ድግግሞስ ስለሆነ \n 2. ዝጋ የሚለውን ተጫን Face የጣት አሻራ የፊት / የጣት አሻራ @@ -67,6 +73,8 @@ ግለሰቡ አልተገኘም አንዲጠራ የተመረጠው ግለሰብ ዳታቤዙ ውስጥ የለም ግለሰቡ የለም፡ እባክዎ ከኢንተርኔት ጋር መገናኘትዎን አረጋግጠው እንደገና ይሞክሩ + እንደገና ይሞክሩ + የፕሮጀክት መለያ ቁጥር የፕሮጀክት ቁልፍ @@ -74,12 +82,22 @@ የፈጣን ምላሽ መለያ እስካን ያድርጉ መግባት የፈጣን ምላሽ ሚስጥር ቁጥርን እስካን በሚያደረግበት ጊዜ ስህተት አጋጥሞታል + የካሜራ ፈቃድ ስላልበራ QR code ስካን ማድረግ አልተቻለም + ካሜራዉን ለማብራት ችግር አጋጥሟል፤ እባክዎ የሲስተም አስተዳደር ያሳዉቁ እስካን የተደረገው የፈጣን ምላሽ ሚስጥር ቁጥር ትክክል አይደለም ተጠቃሚው ተዘሏል ፡ እባክዎ የተጠቃሚውን መለያ ቁጥር ፡ የፕሮጀክት ቁጥር እና ቁልፍ በማረጋገጥ እንደገና ይሞክሩ፡፡ ትክክል ያልሆነ ተጠቃሚ ነው ፡ እባክዎ የፕሮጀክት ቁጥር እና ቁልፍ ያረጋግጡ፡፡ የፕሮጀክት ቁጥሩ አቅራቢው ውስጥ ካለው ዝርዝር ጋር የተለየ ነው፡ እብክዎት የስስተም አስተዳዳሪውን ያነጋግሩ፡፡ አሁን ከግንኙነት ውጭ ነዎት፡ እባክዎት የኢንተርኔት ግንኙነቱን መኖሩን ያረጋግጡ + የ Integrity Service. ስህተት አጋጥሟል. እባክዎ እንደገና ይሞክሩ ሰርቨሩን ለማግኘት ሲሞክሩ ስህተት አጋጥሞታል ፡ እባክዎት ተንሽ ቆይተው ይሞክሩት፡፡ + የባክንድ URL ቀይር + አስቀምጥ + default ተጠቀም +  +  +  + አቋርጥ የመለየት ሰራውን እስኪጠናቀቅ እባክዎት ትንሽ ይጠብቁ @@ -106,12 +124,19 @@ %d ተዛምዶ + + ስህተት ኣጋጥሟል የተግበቦት ስህተት የተለየ የፕሮጀክት መለያ ስለተጠቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ ከሲስተሙ ዋና ገጽ ይውጡ ወይም የቅርብ ተጠሪዎን ያነጋግሩ + እባከዎ እንደገና ይሞክሩ እና ሲምፕሪንትስ አልሰራም ሲስተም አስተዳዳሪውን ያሳዉቁ ጊዜው ያለፈበት የጉግል ፕሌይ አገልግሎቶች በዚህ መሳሪያ ላይ የተጫነው የጉግል ፕለይ አገልግሎት ስሪት በSID ከሚያስፈልገው በላይ የቆየ ነው። የጎግል ፐሌይ አገልግሎቶች ይጎድላሉ የጉግል ፕለይ አገልግሎቶች በዚህ መሳሪያ ላይ አልተጫነም። + ያገልግሎቱ ጊዜውን ያለፈ ወይም ያልተገኘ \"Google Play Store app\" + የ \"Google Play አፕ\" አልተጫነም ወይም ጊዜው አልፎበታል + ባሁኑ ጊዜ ፕሮጀክቱ እያለቀ ነው + ፕሮጀክቱ እያለቀ ስለሆነ session ማስጀመር አልተቻለም፤እባክዎ ሱፐርቫይዘሩን ያናግሩ   ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል ክፍለ-ጊዜን ማስጀመር አልተቻለም ምክንያቱም ፕሮጀክቱ በአሁኑ ጊዜ ባለበት ቆሟል። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ የተሳሳተ የፕሮጀክት መለያ ስለተተቀሙ የተግባቦት ስህተት አጋጥሟል ፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ @@ -129,13 +154,16 @@ ማረጋገጫ ተልኳል እርስዎን መልሰው በማዛወር ላይ - - የፊት ሁነታን በማዋቀር ላይ - አስፈላጊ ፋይሎችን በማውረድ ላይ - ፍቃድ ልክ ያልሆነ - የፊት ፈቃድ ልክ ያልሆነ ነው፣ እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ - የማዋቀር ስህተት - የፊት ሁነታን በማዋቀር ላይ ስህተት ነበር፣እባክዎ የስርዓት አስተዳዳሪዎን ያግኙ + + ሞዳሊቲ በማዋቀር ላይ + አስፈላጊ ፋይሎችን በማውረድ ላይ + ጥገና ሞድ + ሲስተሙ ጥገና ላይ ነው፤ እባክዎ ትንስ ቆይተው ይሞክሩ + ፈቃዱ አይሰራም + የማይሰራ ፈቃድ፤እባክዎ ሲስተም አስተዳዳሪውን ያሳውቁ + የተግባቦት ስህተት%1$s + ፈቃዱን ለማግኘት ስህተት አጋጥሟል፣የሲስተም ሱፕርቫይዘሩን ያማክሩ + በቂ ብርሀን ያለበት ቦታ መምረጥ @@ -155,6 +183,7 @@ ሰዉየዉ ቀጥታ ካሜራዉን እንዲያይ አድርግ ለማንሳት ተጫን በማንሳት ላይ.... + እባክዎ የካሜራ ፍቃድ ያብሩ ባለበት ፖዚሽኑ ላይ አቆይ.... የፊት ምስል በስኬት ተመዝግቧል ማረጋገጥ @@ -163,6 +192,7 @@ ስህተት አጋጥሟል + እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንትሱ ካልሰራ የሲስተም አስተዳደር ያሳውቁ ይዝጉ እንደገና ይሞክሩ የስልክ ማስተካከያ @@ -190,6 +220,7 @@ የሚጠቀሙበት መሳሪያ ሊገናኝ አልቻለም የተገኘው ስካነር %1$s እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ትንስ ቆይተው ይሞክሩ እና የማጣመር ጥያቄውን ይቀበሉ የNFC ቺፑን ማንበብ አልተቻለም፤ እባክዎ እንደገና ይሞክሩ የተሳሳተ NFC ቺፕ ተገኝቷል እሽ @@ -197,6 +228,7 @@ የስካነሩን ቁጥር ቀጥሎ ካለው ሳጥን ላይ ይፃፉ ይህ የሚገኘው በስካነሩ የታችኛው ክፍል ነው እባክዎ ስካነር ቁጥር %1$s መብራቱን አረጋግጠው እንደገና ይሞክሩ + ማጣመሩ ተቋርጧል፤ እባክዎ ትንሽ ቆይተው ይሞክሩ እባክዎ የሲሪያል ቁጥሩን 6 አሃዞች ያስገቡ ስካነሩ አፕዴት አድርግ ስካነሩ አፕዴት እስኪጨርስ እንደበራ ይቆይ @@ -220,11 +252,14 @@ \n5. ወደ ሲምፕሪንት ይመለሱ እስካነሩን ያብሩ 1. እስካነሩን ያስነሱ\n2. እንደገና ይሞክሩ + ፍቃድ የለም + ስልኩ ሴቲንግ ላይ ብሉቱዝ ያብሩ የሚጠቀሙበት መሳሪያ ብሉቱዝ ለመጠቀም አያስችልም፤እባክዎ የቅርብ ተጠሪዎን ያነጋግሩ ብሉቱዝ አልተከፈተም ፡ለመጠቀም አያስችልም፤እባክዎ ማስተካከያው ውስጥ ገብተው ብሉቱዙን ከፍተው እንደገና ይሞክሩ በስልኩ ላይ ከአንድ በላይ እስካነር ተገናኝቷል፣ እባክዎ የማይፈልጉትን እስካነር ግንኙነቱን ያቋርጡ + የጣት አሻራ ሞዳሊቲ በማዋቀር ላይ ወደ ሲስተሙ ማስገባት መለየት ማመሳከር @@ -238,6 +273,8 @@ የቀኝ እጅ ሶስተኛ ጣት የቀኝ እጅ አራተኛ ጣት የቀኝ እጅ አምስተኛ ጣት + ይሰርዙ + ስህተት አጋጥሟል እባክዎ እንደገና ይሞክሩ እና ሲምፕሪንቱ መስራት ካለቻለ የሲስተም አስተዳዳሪውን ያነጋግረ እባክዎ እስካን ያድርጉ ለቀጣዩ ጣትዎ ወደ ግራ ያንሸራቱ @@ -270,6 +307,9 @@ ፕሮጀክቱ በአሁኑ ጊዜ እያለቀ ስለሆነ ክፍለ-ጊዜን ማስጀመር አልተቻለም። ለበለጠ መረጃ እባክዎን ተቆጣጣሪዎን ያነጋግሩ መግባት ከፈለጉ፡ እባክዎ የደንበኛውን አፕልኬሽን ከፍተው የሲም ፕሪንትን መለያቁጥር በመጠቀም መግባት ይችላሉ + የስልኩመለያ%s + ዝጋ + እስካነር ይጠቀሙ: %1$s አሁን ያለ ተጠቃሚ: %1$s @@ -286,23 +326,29 @@ ሞጁሎች የስልኩ ሴቲንግ በመግባት የሞባይል ዳታ ያብሩ ማስተካከያ + ከተፈቀደው ሞጁል በላይ ወርዷል + በቅድሚያ መግባት አለበት መረጃዉን ለመላክ + መጨረሻ ጊዜ ወደ ሰርቨር የተላከው : %1$s ሁሉም መዝገቦች ተጭነዋል + + %1$d ለመስቀል መዝገብ + %1$d ለመስቀል መዝገቦች + እንደገና ይሞክሩ የየዕለት ተግባር: %1$s - - - ወደ ሲስተሙ ማስገባት + + %d ወደ ሲስተሙ ማስገባት ወደ ሲስተሙ ማስገባት - - መለየት + + %d መለየት መለየት - - ማመሳከር + + %d ማመሳከር ማመሳከር @@ -323,12 +369,14 @@ የመሳሪያው መለያ እስካን እንዲደረግ የሚፈልጉተን የጣት አሻራ ይምረጡ ሲንክ ላይ ተጨማሪ ዝርዝር መረጃ + ገልብጥ ወደ clipboard + ሲንክ የተደረገ መረጃ ሞጅውሎችን ይምረጡ የተመዘገበውን ለመጫን - የተመዘገበውን ለመማውረድ + ሪክርዶች ለማውረድ ወይም ለመሰረዝ የተመዘገበውን ለመሰረዝ በስልኩ አጠቃላይ የተመዘገበ የተመረጡ ሞጁሎች @@ -337,6 +385,8 @@ የሚጫኑ ምስሎች አሁን ግንኙነት ፍጠር በግንኙነት ላይ + የሲንክ መረጃውን በመውሰድ ላይ + የተሳሳተ ምርጫ፡ እባክዎት ካለው የምዕራፍ አማራጭ ውስጥ ቢያንስ አንድ ይምረጡ የተሳሳተ ምርጫ ፡ካሉት ምዕራፎች ውስጥ ከ %1$d በላይ መምረጥ አይችሉም @@ -357,6 +407,7 @@ ለመክፈት የይለፍ ቃል ያስገቡ ለመውጣት የይለፍ ቃል ያስገቡ የይለፍ ቃል + አቋርጥ የተሳሳተ የይለፍ ቃል ለመክፈት መታ ያድርጉ @@ -368,10 +419,14 @@ የእርስዎ ፕሮጀክት ወቅታዊ መዛግብት እንዳለው ለማረጋገጥ የውሂብ ማመሳሰል አስፈላጊ ነው። ያለ ትክክለኛ ምክንያት የእርስዎን ውሂብ ማመሳሰል አለመቻል የስራ ባልደረቦችዎ ጠቃሚ መዝገቦችን ማግኘት እና የፕሮጀክቱን ትክክለኛ ሪፖርት የማድረግ ችሎታ ላይ ተጽእኖ ያሳድራል። እና ለእርስዎ እና ለፕሮጀክቱ የማይፈለጉ ውጤቶችን ሊያስከትል ይችላል. መውጣት ጨርሰው መውጣት ይፈልጋሉ - ሰርዝ + ዘግተህ ዉጣ + አቋርጥ ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ቆየት ብለው ይሞክሩ. ስርዓቱ በአሁኑ ጊዜ ለጥገና ከመስመር ውጭ ነው። እባክዎ ከ%s በኋላ እንደገና ይሞክሩ + + ሲንክ በማድረግ ላይ + From cc3015a4bbdd807383e2de08767f872b6b1b05d3 Mon Sep 17 00:00:00 2001 From: melad Date: Tue, 23 Apr 2024 12:19:00 +0200 Subject: [PATCH 18/29] [MS-428] order the signout steps to avoid cleanCredentials before deleting all db records --- .../simprints/infra/authlogic/authenticator/SignerManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt b/infra/auth-logic/src/main/java/com/simprints/infra/authlogic/authenticator/SignerManager.kt index cd1e1c6bb2..033afba617 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 @@ -50,8 +50,6 @@ internal class SignerManager @Inject constructor( } suspend fun signOut() = withContext(dispatcher) { - authStore.cleanCredentials() - authStore.clearFirebaseToken() simNetwork.resetApiBaseUrl() configRepository.clearData() @@ -62,6 +60,9 @@ internal class SignerManager @Inject constructor( enrolmentRecordRepository.deleteAll() scannerManager.deleteFirmwareFiles() + authStore.cleanCredentials() + authStore.clearFirebaseToken() + Simber.tag(LoggingConstants.CrashReportTag.LOGOUT.name).i("Signed out") } From 7722b619781facfe9e77374b4b75d4b4a64b7a72 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 23 Apr 2024 11:31:00 +0100 Subject: [PATCH 19/29] MS-422 Immediate foreground service type for preventing ForegroundServiceDidNotStartInTimeException --- .../main/java/com/simprints/core/workers/SimCoroutineWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 78b6d51256..cf9ddf4e16 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 @@ -95,7 +95,7 @@ abstract class SimCoroutineWorker( .setOnlyAlertOnce(true) .apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_DEFERRED + foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE } } .build() From a19e52ffd70e06c78e94eac3fee17198de24b666 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Tue, 23 Apr 2024 18:38:25 +0300 Subject: [PATCH 20/29] [MS-425] Return JSON path for attendantId and moduleId tokenizedFields --- .../ApiEventDownSyncRequestPayload.kt | 7 +++- .../ApiEventDownSyncRequestPayloadTest.kt | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 infra/event-sync/src/test/java/com/simprints/infra/eventsync/event/remote/models/downsync/ApiEventDownSyncRequestPayloadTest.kt 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/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 From 87ee602457abe10335f9b1299be18cf7a4834c5e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Apr 2024 12:39:16 +0100 Subject: [PATCH 21/29] MS-368 Opting out of setting worker service foreground when battery optimization on --- .../core/tools/utils/BatteryOptimizationUtils.kt | 13 +++++++++++++ .../simprints/core/workers/SimCoroutineWorker.kt | 4 ++++ 2 files changed, 17 insertions(+) create mode 100644 infra/core/src/main/java/com/simprints/core/tools/utils/BatteryOptimizationUtils.kt 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..ffe023fa3e --- /dev/null +++ b/infra/core/src/main/java/com/simprints/core/tools/utils/BatteryOptimizationUtils.kt @@ -0,0 +1,13 @@ +package com.simprints.core.tools.utils + +import android.content.Context +import android.os.PowerManager + +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..0854c68f05 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 @@ -57,6 +58,9 @@ abstract class SimCoroutineWorker( } protected suspend fun showProgressNotification() { + if (BatteryOptimizationUtils.isFollowingBatteryOptimizations(context)) { + return + } try { setForeground(getForegroundInfo()) } catch (setForegroundException: Throwable) { From 8ab5915f31ce989a1246a8e29417d19647159421 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Apr 2024 13:06:44 +0100 Subject: [PATCH 22/29] MS-368 Foreground status for workers to be set as early as possible --- .../sync/down/workers/EventDownSyncDownloaderWorker.kt | 2 +- .../infra/eventsync/sync/master/EventEndSyncReporterWorker.kt | 2 +- .../infra/eventsync/sync/master/EventStartSyncReporterWorker.kt | 2 +- .../infra/eventsync/sync/master/EventSyncMasterWorker.kt | 2 +- .../eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt | 2 +- .../infra/sync/config/worker/DeviceConfigDownSyncWorker.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/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/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 22184eeecc..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 @@ -27,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() From 3843763a91908cef7809cf9cf05f58fa79905555 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Apr 2024 13:08:37 +0100 Subject: [PATCH 23/29] MS-368 Battery optimization check potential exceptions catching --- .../java/com/simprints/core/workers/SimCoroutineWorker.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 0854c68f05..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 @@ -58,10 +58,10 @@ abstract class SimCoroutineWorker( } protected suspend fun showProgressNotification() { - if (BatteryOptimizationUtils.isFollowingBatteryOptimizations(context)) { - return - } try { + if (BatteryOptimizationUtils.isFollowingBatteryOptimizations(context)) { + return + } setForeground(getForegroundInfo()) } catch (setForegroundException: Throwable) { // Setting foreground (showing the notification) may be restricted by the system From 9edf2cb9b48dca58082d0ae09c04a5e0d6147977 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Apr 2024 14:35:14 +0100 Subject: [PATCH 24/29] MS-368 Battery optimization check platform code excluded from test coverage --- .../com/simprints/core/tools/utils/BatteryOptimizationUtils.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index ffe023fa3e..61e5b4842f 100644 --- 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 @@ -2,7 +2,9 @@ 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 = From a13204cb5de0ef0d18fe66833b92786fdcec1873 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Tue, 7 May 2024 15:25:47 +0300 Subject: [PATCH 25/29] MS-439 Move requestID logic from network interceptors to sync tasks --- .../event/remote/EventRemoteDataSource.kt | 15 ++--- .../event/remote/EventRemoteInterface.kt | 8 ++- .../status/down/domain/EventDownSyncResult.kt | 1 - .../status/up/domain/EventUpSyncResult.kt | 1 - .../sync/down/tasks/EventDownSyncTask.kt | 5 +- .../sync/up/tasks/EventUpSyncTask.kt | 28 ++++++---- .../event/remote/EventRemoteDataSourceTest.kt | 56 +++++++++---------- .../sync/down/tasks/EventDownSyncTaskTest.kt | 12 ++-- .../sync/up/tasks/EventUpSyncTaskTest.kt | 32 +++++------ .../httpclient/DefaultOkHttpClientBuilder.kt | 12 ---- 10 files changed, 79 insertions(+), 91 deletions(-) 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/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/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/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/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/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 408583c813..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()) - .addNetworkInterceptor(buildRequestIdInterceptor()) .addInterceptor(buildDeviceIdInterceptor(deviceId)) .addInterceptor(buildVersionInterceptor(versionName)) .addInterceptor(buildGZipInterceptor()) @@ -69,15 +66,6 @@ internal class DefaultOkHttpClientBuilder @Inject constructor( } } - private fun buildRequestIdInterceptor() = Interceptor { chain -> - val requestId = UUID.randomUUID().toString() - - // Adding same header to both request and response to - // track the request across network logs and analytics events. - val newRequest = chain.request().newBuilder().addHeader(REQUEST_ID_HEADER, requestId).build() - return@Interceptor chain.proceed(newRequest).newBuilder().addHeader(REQUEST_ID_HEADER, requestId).build() - } - private fun buildAuthenticationInterceptor(authToken: String) = Interceptor { chain -> val newRequest = chain.request().newBuilder() .addHeader(AUTHORIZATION_HEADER, "Bearer $authToken") From b67f0a71527432cd9d4e5378153639cf50bfe980 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Wed, 8 May 2024 14:24:16 +0300 Subject: [PATCH 26/29] [MS-437] Make Path (also) Serializable so it can be properly serialized in JSON --- .../src/main/java/com/simprints/infra/images/model/Path.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 386da20da7c224d6634ea82b0c42d4a54b7552e7 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Wed, 8 May 2024 14:25:32 +0300 Subject: [PATCH 27/29] [MS-437] Add JSON subtype info for ActionRequest so it can be properly (de)serialized --- infra/orchestrator-data/build.gradle.kts | 2 ++ .../infra/orchestration/data/ActionRequest.kt | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) 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, From c0d196d303a5c36bd72a92cc20ec3058f417b2e0 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Wed, 8 May 2024 14:27:30 +0300 Subject: [PATCH 28/29] [MS-437] Ensure OrchestratorViewModel is properly restored when Activity was killed and restored --- .../orchestrator/OrchestratorFragment.kt | 2 + .../orchestrator/OrchestratorViewModel.kt | 16 +++++ .../usecases/ShouldCreatePersonUseCase.kt | 8 ++- .../response/CreateEnrolResponseUseCase.kt | 3 +- .../orchestrator/OrchestratorViewModelTest.kt | 67 +++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) 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 efc7261191..2a7aa68aee 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 @@ -81,6 +81,8 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { orchestratorVm.requestProcessed = savedInstanceState.getBoolean(KEY_REQUEST_PROCESSED) savedInstanceState.getString(KEY_ACTION_REQUEST) ?.run(orchestratorVm::setActionRequestFromJson) + orchestratorVm.restoreStepsIfNeeded() + orchestratorVm.restoreModalitiesIfNeeded() } observeLoginCheckVm() observeClientApiVm() 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 3af5e17f04..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 @@ -101,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() 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/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, From 9539f8d7f40a4affd335f9b5e94fc47256f6c084 Mon Sep 17 00:00:00 2001 From: Milen Marinov Date: Thu, 9 May 2024 17:38:09 +0300 Subject: [PATCH 29/29] [MS-437] Prevent saving of empty VM data when Fragment is destroyed and restored multiple times without being visible --- .../feature/orchestrator/OrchestratorFragment.kt | 10 +++++++--- .../usecases/response/AppResponseBuilderUseCase.kt | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) 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 2a7aa68aee..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,9 +74,8 @@ 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) @@ -84,6 +83,11 @@ internal class OrchestratorFragment : Fragment(R.layout.fragment_orchestrator) { orchestratorVm.restoreStepsIfNeeded() orchestratorVm.restoreModalitiesIfNeeded() } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeLoginCheckVm() observeClientApiVm() observeOrchestratorVm() 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