From 26bbaa21f3c4a47570bf355504476143c62af6ee Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 17 Dec 2025 16:22:46 +0200 Subject: [PATCH 1/3] MS-1276 Remove live data from sync management --- .../feature/dashboard/debug/DebugFragment.kt | 3 +- .../dashboard/logout/LogoutSyncViewModel.kt | 3 +- .../settings/syncinfo/SyncInfoViewModel.kt | 5 +- .../usecase/ObserveSyncInfoUseCase.kt | 2 - .../logout/LogoutSyncViewModelTest.kt | 17 +-- .../syncinfo/SyncInfoViewModelTest.kt | 37 +++-- .../usecase/ObserveSyncInfoUseCaseTest.kt | 86 +++++------ .../usecase/RunBlockingEventSyncUseCase.kt | 3 - .../RunBlockingEventSyncUseCaseTest.kt | 32 ++-- .../repository/CandidateRecordDataSource.kt | 8 +- .../infra/eventsync/EventSyncManager.kt | 3 +- .../infra/eventsync/EventSyncManagerImpl.kt | 11 +- .../eventsync/sync/EventSyncStateProcessor.kt | 31 ++-- ...Provider.kt => SyncWorkersInfoProvider.kt} | 10 +- .../infra/eventsync/EventSyncManagerTest.kt | 18 ++- .../sync/EventSyncStateProcessorTest.kt | 138 ++++++++---------- .../SubjectsSyncStateProcessorTestHelper.kt | 14 +- .../common/SyncWorkersInfoProviderTest.kt | 48 ++++++ 18 files changed, 238 insertions(+), 231 deletions(-) rename infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/{SyncWorkersLiveDataProvider.kt => SyncWorkersInfoProvider.kt} (54%) create mode 100644 infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProviderTest.kt diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt index 48ff415af0..fbb71e3b52 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/debug/DebugFragment.kt @@ -8,6 +8,7 @@ import android.text.style.ForegroundColorSpan import android.view.View import androidx.core.graphics.toColorInt import androidx.fragment.app.Fragment +import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import androidx.work.WorkManager import com.simprints.core.DispatcherIO @@ -60,7 +61,7 @@ internal class DebugFragment : Fragment(R.layout.fragment_debug) { super.onViewCreated(view, savedInstanceState) applySystemBarInsets(view) - eventSyncManager.getLastSyncState().observe(viewLifecycleOwner) { state -> + eventSyncManager.getLastSyncState().asLiveData().observe(viewLifecycleOwner) { state -> val states = (state.downSyncWorkersInfo.map { it.state } + state.upSyncWorkersInfo.map { it.state }) val message = 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 c6ca37ee2d..c3c253136d 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 @@ -2,7 +2,6 @@ package com.simprints.feature.dashboard.logout import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope @@ -39,7 +38,7 @@ internal class LogoutSyncViewModel @Inject constructor( .asLiveData(viewModelScope.coroutineContext) val isLogoutWithoutSyncVisibleLiveData: LiveData = combine( - eventSyncManager.getLastSyncState(useDefaultValue = true).asFlow(), + eventSyncManager.getLastSyncState(useDefaultValue = true), syncOrchestrator.observeImageSyncStatus(), ) { eventSyncState, imageSyncStatus -> !eventSyncState.isSyncCompleted() || imageSyncStatus.isSyncing diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt index 69b5af7923..1e0507554d 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModel.kt @@ -3,7 +3,6 @@ package com.simprints.feature.dashboard.settings.syncinfo import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.simprints.core.DispatcherIO @@ -59,7 +58,7 @@ internal class SyncInfoViewModel @Inject constructor( eventSyncManager .getLastSyncState( useDefaultValue = true, // otherwise value not guaranteed - ).asFlow() + ) private val imageSyncStatusFlow = syncOrchestrator.observeImageSyncStatus() @@ -187,7 +186,7 @@ internal class SyncInfoViewModel @Inject constructor( private fun startInitialSyncIfRequired() { viewModelScope.launch { - val isRunning = eventSyncManager.getLastSyncState().value?.isSyncRunning() ?: false + val isRunning = eventSyncManager.getLastSyncState().firstOrNull()?.isSyncRunning() ?: false val lastUpdate = eventSyncManager.getLastSyncTime() val isForceEventSync = when { diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt index 3f8b5b8263..572e5baf59 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt @@ -1,6 +1,5 @@ package com.simprints.feature.dashboard.settings.syncinfo.usecase -import androidx.lifecycle.asFlow import com.simprints.core.DispatcherBG import com.simprints.core.lifecycle.AppForegroundStateTracker import com.simprints.core.tools.extentions.onChange @@ -58,7 +57,6 @@ internal class ObserveSyncInfoUseCase @Inject constructor( ) { private val eventSyncStateFlow = eventSyncManager .getLastSyncState(useDefaultValue = true) // otherwise value not guaranteed - .asFlow() private val imageSyncStatusFlow = syncOrchestrator.observeImageSyncStatus() 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 be1326875e..e81f80614c 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 @@ -1,9 +1,7 @@ package com.simprints.feature.dashboard.logout import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.LiveData -import androidx.lifecycle.asFlow -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase import com.simprints.infra.authstore.AuthStore import com.simprints.infra.config.store.models.ProjectConfiguration @@ -15,13 +13,8 @@ import com.simprints.infra.sync.ImageSyncStatus import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.common.livedata.getOrAwaitValue -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -152,10 +145,8 @@ internal class LogoutSyncViewModelTest { imageSyncStatus: ImageSyncStatus, projectConfig: ProjectConfiguration, ) { - mockkStatic("androidx.lifecycle.FlowLiveDataConversions") - val eventSyncLiveData = mockk>(relaxed = true) - every { eventSyncLiveData.asFlow() } returns flowOf(eventSyncState) - every { eventSyncManager.getLastSyncState(useDefaultValue = true) } returns eventSyncLiveData + val eventSyncFlow = flowOf(eventSyncState) + every { eventSyncManager.getLastSyncState(useDefaultValue = true) } returns eventSyncFlow every { syncOrchestrator.observeImageSyncStatus() } returns flowOf(imageSyncStatus) every { configManager.observeProjectConfiguration() } returns flowOf(projectConfig) } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt index 8723514f61..6861f9a78c 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/SyncInfoViewModelTest.kt @@ -12,7 +12,6 @@ import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase import com.simprints.feature.dashboard.settings.syncinfo.usecase.ObserveSyncInfoUseCase import com.simprints.feature.login.LoginResult import com.simprints.infra.authstore.AuthStore -import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.models.DeviceConfiguration import com.simprints.infra.config.store.models.GeneralConfiguration import com.simprints.infra.config.store.models.Project @@ -125,10 +124,10 @@ class SyncInfoViewModelTest { coEvery { configManager.getDeviceConfiguration() } returns mockDeviceConfiguration coEvery { configManager.getProject() } returns mockProject - val eventSyncLiveData = MutableLiveData(mockEventSyncState) - every { eventSyncManager.getLastSyncState() } returns eventSyncLiveData - every { eventSyncManager.getLastSyncState(any()) } returns eventSyncLiveData - every { eventSyncLiveData.asFlow() } returns flowOf(mockEventSyncState) + val eventSyncFlow = flowOf(mockEventSyncState) + every { eventSyncManager.getLastSyncState() } returns eventSyncFlow + every { eventSyncManager.getLastSyncState(any()) } returns eventSyncFlow + coEvery { eventSyncManager.getLastSyncTime() } returns TEST_TIMESTAMP coEvery { eventSyncManager.countEventsToUpload(any()) } returns flowOf(0) coEvery { eventSyncManager.countEventsToDownload() } returns DownSyncCounts(0, isLowerBound = false) @@ -235,8 +234,8 @@ class SyncInfoViewModelTest { every { progress } returns null every { lastUpdateTimeMillis } returns 0 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) - every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) + every { syncOrchestrator.observeImageSyncStatus() } returns flowOf(mockNotSyncingImageStatus) createViewModel() viewModel.isPreLogoutUpSync = true @@ -261,8 +260,8 @@ class SyncInfoViewModelTest { every { progress } returns null every { lastUpdateTimeMillis } returns 0 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) - every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) + every { syncOrchestrator.observeImageSyncStatus() } returns flowOf(mockNotSyncingImageStatus) createViewModel() viewModel.isPreLogoutUpSync = true @@ -308,8 +307,8 @@ class SyncInfoViewModelTest { every { progress } returns null every { lastUpdateTimeMillis } returns 0 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) - every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) + every { syncOrchestrator.observeImageSyncStatus() } returns flowOf(mockNotSyncingImageStatus) createViewModel() viewModel.isPreLogoutUpSync = false @@ -334,7 +333,7 @@ class SyncInfoViewModelTest { every { progress } returns null every { lastUpdateTimeMillis } returns 0 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockInProgressEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockInProgressEventSyncState) every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) createViewModel() viewModel.isPreLogoutUpSync = true @@ -359,7 +358,7 @@ class SyncInfoViewModelTest { every { progress } returns Pair(1, 2) every { lastUpdateTimeMillis } returns null } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockSyncingImageStatus) createViewModel() viewModel.isPreLogoutUpSync = true @@ -562,7 +561,7 @@ class SyncInfoViewModelTest { val mockInProgressEventSyncState = mockk(relaxed = true) { every { isSyncInProgress() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockInProgressEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockInProgressEventSyncState) createViewModel() val values = viewModel.syncInfoLiveData.getOrAwaitValues(number = 1) { @@ -668,7 +667,7 @@ class SyncInfoViewModelTest { val mockIdleEventSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns null createViewModel() @@ -683,7 +682,7 @@ class SyncInfoViewModelTest { val mockIdleEventSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns oldTimestamp every { timeHelper.msBetweenNowAndTime(oldTimestamp) } returns 600000L // 10 minutes createViewModel() @@ -699,7 +698,7 @@ class SyncInfoViewModelTest { val mockIdleEventSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns recentTimestamp every { timeHelper.msBetweenNowAndTime(recentTimestamp) } returns 60000L // 1 minute createViewModel() @@ -714,7 +713,7 @@ class SyncInfoViewModelTest { val mockRunningSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns true } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockRunningSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockRunningSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns null createViewModel() @@ -729,7 +728,7 @@ class SyncInfoViewModelTest { val mockIdleEventSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns recentTimestamp every { timeHelper.msBetweenNowAndTime(recentTimestamp) } returns 60000L // 1 minute createViewModel() diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt index c97c8e836a..d31e98f3fd 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt @@ -32,6 +32,7 @@ import com.simprints.infra.sync.ImageSyncStatus import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.* +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -110,7 +111,6 @@ internal class ObserveSyncInfoUseCaseTest { @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - mockkStatic("androidx.lifecycle.FlowLiveDataConversions") mockkStatic("com.simprints.infra.config.store.models.ProjectConfigurationKt") mockkStatic("com.simprints.core.tools.extentions.Flow_extKt") setupDefaultMocks() @@ -124,10 +124,10 @@ internal class ObserveSyncInfoUseCaseTest { every { connectivityTracker.observeIsConnected() } returns connectivityLiveData every { connectivityLiveData.asFlow() } returns flowOf(true) - val eventSyncLiveData = MutableLiveData(mockEventSyncState) + val eventSyncLiveData = flowOf(mockEventSyncState) every { eventSyncManager.getLastSyncState() } returns eventSyncLiveData every { eventSyncManager.getLastSyncState(any()) } returns eventSyncLiveData - every { eventSyncLiveData.asFlow() } returns flowOf(mockEventSyncState) + coEvery { eventSyncManager.getLastSyncTime() } returns TEST_TIMESTAMP coEvery { eventSyncManager.countEventsToUpload(any()) } returns flowOf(0) coEvery { eventSyncManager.countEventsToDownload() } returns DownSyncCounts(0, isLowerBound = false) @@ -179,7 +179,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) createUseCase() val result = useCase().first() @@ -203,7 +203,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockFailedEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockFailedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) createUseCase() val result = useCase().first() @@ -216,7 +216,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockFailedEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockFailedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) createUseCase() val result = useCase( @@ -254,7 +254,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns false every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockOfflineEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockOfflineEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) createUseCase() @@ -272,7 +272,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailedBecauseReloginRequired() } returns false every { isSyncFailed() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() @@ -288,7 +288,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockCompletedEventSyncState = mockk(relaxed = true) { every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns TEST_TIMESTAMP createUseCase() @@ -320,7 +320,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() @@ -381,7 +381,7 @@ internal class ObserveSyncInfoUseCaseTest { every { progress } returns 5 every { total } returns 10 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockInProgressEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockInProgressEventSyncState) createUseCase() val result = useCase().first() @@ -398,7 +398,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncCompleted() } returns false every { isThereNotSyncHistory() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockConnectingEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockConnectingEventSyncState) createUseCase() val result = useCase().first() @@ -413,7 +413,7 @@ internal class ObserveSyncInfoUseCaseTest { every { progress } returns 10 every { total } returns 10 } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) createUseCase() val result = useCase().first() @@ -426,7 +426,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockCompletedEventSyncState = mockk(relaxed = true) { every { isSyncCompleted() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) createUseCase() val result = useCase().first() @@ -446,7 +446,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncing } returns false every { progress } returns null } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockInProgressEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockInProgressEventSyncState) every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) createUseCase() @@ -466,7 +466,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncing } returns true every { progress } returns Pair(2, 4) // 2 out of 4 images } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCompletedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCompletedEventSyncState) every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockSyncingImageStatus) createUseCase() @@ -515,7 +515,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) coEvery { enrolmentRecordRepository.count(any()) } returns 25 coEvery { eventSyncManager.countEventsToUpload(any()) } returns flowOf(5) coEvery { eventSyncManager.countEventsToDownload() } returns DownSyncCounts(8, isLowerBound = false) @@ -536,7 +536,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) coEvery { enrolmentRecordRepository.count(any()) } returns 123 createUseCase() @@ -551,7 +551,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockInProgressEventSyncState = mockk(relaxed = true) { every { isSyncInProgress() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockInProgressEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockInProgressEventSyncState) createUseCase() val result = useCase().first() @@ -661,7 +661,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { observeConfigurationFlow.invoke() } returns flowOf(createConfigurationState(projectConfig = mockProjectConfigWithDownSync)) - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.countEventsToDownload() } returns DownSyncCounts(42, isLowerBound = false) every { mockProjectConfigWithDownSync.isSimprintsEventDownSyncAllowed() } returns true every { mockProjectConfigWithDownSync.isModuleSelectionAvailable() } returns false @@ -680,7 +680,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) createUseCase() val result = useCase(isPreLogoutUpSync = true).first() @@ -700,7 +700,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { observeConfigurationFlow.invoke() } returns flowOf(createConfigurationState(projectConfig = mockProjectConfigWithDownSync)) - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.countEventsToDownload() } throws Exception("Timeout") every { mockProjectConfigWithDownSync.isSimprintsEventDownSyncAllowed() } returns true every { mockProjectConfigWithDownSync.isModuleSelectionAvailable() } returns false @@ -723,7 +723,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { observeConfigurationFlow.invoke() } returns flowOf(createConfigurationState(projectConfig = mockProjectConfigWithDownSync)) - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.countEventsToDownload() } throws RuntimeException("Network error") every { mockProjectConfigWithDownSync.isSimprintsEventDownSyncAllowed() } returns true every { mockProjectConfigWithDownSync.isModuleSelectionAvailable() } returns false @@ -832,13 +832,13 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `should handle changes in event sync state stream`() = runTest { - val eventSyncStateFlow = MutableLiveData() + val eventSyncStateFlow = MutableSharedFlow(replay = 1) every { eventSyncManager.getLastSyncState(any()) } returns eventSyncStateFlow createUseCase() val mockIdleState = mockk(relaxed = true) { every { isSyncInProgress() } returns false } - eventSyncStateFlow.value = mockIdleState // started not syncing + eventSyncStateFlow.emit(mockIdleState) // started not syncing val idleResult = useCase().first() @@ -849,7 +849,7 @@ internal class ObserveSyncInfoUseCaseTest { every { progress } returns 1 every { total } returns 2 } - eventSyncStateFlow.value = mockSyncingState // changed to syncing + eventSyncStateFlow.emit(mockSyncingState) // changed to syncing val syncingResult = useCase().first() @@ -952,7 +952,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockIdleEventSyncState = mockk(relaxed = true) { every { isSyncRunning() } returns false } - every { eventSyncManager.getLastSyncState() } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState() } returns flowOf(mockIdleEventSyncState) coEvery { eventSyncManager.getLastSyncTime() } returns TEST_TIMESTAMP every { timeHelper.now() } returnsMany listOf(TEST_TIMESTAMP, Timestamp(TEST_TIMESTAMP.ms + 60_000)) every { timeHelper.readableBetweenNowAndTime(any()) } returnsMany listOf("0 minutes ago", "1 minute ago") @@ -981,7 +981,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns true every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockFailedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) every { mockProjectConfiguration.isCommCareEventDownSyncAllowed() } returns true every { commCarePermissionChecker.hasCommCarePermissions() } returns false // Permission still denied @@ -1004,7 +1004,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns false every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() @@ -1037,7 +1037,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockSyncingEventSyncState = mockk(relaxed = true) { every { isSyncInProgress() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockSyncingEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockSyncingEventSyncState) createUseCase() val result = useCase().first() @@ -1082,7 +1082,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { any().isSimprintsEventDownSyncAllowed() } returns true createUseCase() @@ -1096,7 +1096,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { any().isCommCareEventDownSyncAllowed() } returns true createUseCase() @@ -1122,7 +1122,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockReLoginRequiredEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseReloginRequired() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockReLoginRequiredEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockReLoginRequiredEventSyncState) every { any().isSimprintsEventDownSyncAllowed() } returns true every { any().isCommCareEventDownSyncAllowed() } returns false every { any().canSyncDataToSimprints() } returns false @@ -1138,7 +1138,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockCommCarePermissionErrorEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCommCarePermissionErrorEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCommCarePermissionErrorEventSyncState) every { any().isCommCareEventDownSyncAllowed() } returns true every { any().isSimprintsEventDownSyncAllowed() } returns false every { any().canSyncDataToSimprints() } returns false @@ -1156,7 +1156,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns true every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockCommCarePermissionErrorEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockCommCarePermissionErrorEventSyncState) createUseCase() val result = useCase().first() @@ -1254,7 +1254,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns true every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockFailedEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() @@ -1282,7 +1282,7 @@ internal class ObserveSyncInfoUseCaseTest { createConfigurationState(projectConfig = mockProjectConfigRequiringModules), ) - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) every { mockProjectConfigRequiringModules.isModuleSelectionAvailable() } returns true createUseCase() @@ -1301,7 +1301,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns false every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockIdleEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() @@ -1317,7 +1317,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `should handle failed sync retry indication correctly`() = runTest { - val eventSyncStateFlow = MutableLiveData() + val eventSyncStateFlow = MutableSharedFlow(replay = 1) every { eventSyncManager.getLastSyncState(any()) } returns eventSyncStateFlow createUseCase() val mockFailedState = mockk(relaxed = true) { @@ -1325,7 +1325,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false every { isSyncFailedBecauseReloginRequired() } returns false } - eventSyncStateFlow.value = mockFailedState + eventSyncStateFlow.emit(mockFailedState) val failedResult = useCase().first() @@ -1361,7 +1361,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns false every { isSyncInProgress() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) createUseCase() @@ -1377,7 +1377,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns true } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { mockProjectConfiguration.isCommCareEventDownSyncAllowed() } returns true every { commCarePermissionChecker.hasCommCarePermissions() } returns false // Permission still denied createUseCase() @@ -1394,7 +1394,7 @@ internal class ObserveSyncInfoUseCaseTest { val mockNormalEventSyncState = mockk(relaxed = true) { every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns false } - every { eventSyncManager.getLastSyncState(any()) } returns MutableLiveData(mockNormalEventSyncState) + every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) createUseCase() diff --git a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCase.kt b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCase.kt index f3189be2f0..12d25dd023 100644 --- a/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCase.kt +++ b/feature/validate-subject-pool/src/main/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCase.kt @@ -1,6 +1,5 @@ package com.simprints.feature.validatepool.usecase -import androidx.lifecycle.asFlow import com.simprints.infra.eventsync.EventSyncManager import com.simprints.infra.sync.SyncOrchestrator import kotlinx.coroutines.flow.firstOrNull @@ -15,14 +14,12 @@ internal class RunBlockingEventSyncUseCase @Inject constructor( // so it can be used to as a filter out old sync states val lastSyncId = syncManager .getLastSyncState() - .asFlow() .firstOrNull() ?.syncId syncOrchestrator.startEventSync() syncManager .getLastSyncState() - .asFlow() .firstOrNull { it.syncId != lastSyncId && it.isSyncReporterCompleted() } } } diff --git a/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCaseTest.kt b/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCaseTest.kt index 534f489536..3fe009fb8d 100644 --- a/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCaseTest.kt +++ b/feature/validate-subject-pool/src/test/java/com/simprints/feature/validatepool/usecase/RunBlockingEventSyncUseCaseTest.kt @@ -1,19 +1,15 @@ package com.simprints.feature.validatepool.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData import com.simprints.infra.eventsync.EventSyncManager import com.simprints.infra.eventsync.status.models.EventSyncState import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerType import com.simprints.infra.sync.SyncOrchestrator import com.simprints.testtools.common.coroutines.TestCoroutineRule -import io.mockk.MockKAnnotations -import io.mockk.coJustRun -import io.mockk.coVerify -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Before @@ -49,14 +45,14 @@ class RunBlockingEventSyncUseCaseTest { @Test fun `finishes execution when sync reporters are finished`() = runTest { - val liveData = MutableLiveData() - every { syncManager.getLastSyncState() } returns liveData - liveData.postValue(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) + val syncStateFlow = MutableSharedFlow(replay = 1) + every { syncManager.getLastSyncState() } returns syncStateFlow + syncStateFlow.emit(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) launch { usecase.invoke() } testScheduler.advanceUntilIdle() - liveData.postValue(createSyncState("sync", EventSyncWorkerState.Succeeded)) + syncStateFlow.emit(createSyncState("sync", EventSyncWorkerState.Succeeded)) testScheduler.advanceUntilIdle() coVerify { syncOrchestrator.startEventSync() } @@ -65,14 +61,14 @@ class RunBlockingEventSyncUseCaseTest { @Test fun `finishes execution when sync reporters have failed`() = runTest { - val liveData = MutableLiveData() - every { syncManager.getLastSyncState() } returns liveData - liveData.postValue(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) + val syncStateFlow = MutableSharedFlow(replay = 1) + every { syncManager.getLastSyncState() } returns syncStateFlow + syncStateFlow.emit(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) launch { usecase.invoke() } testScheduler.advanceUntilIdle() - liveData.postValue(createSyncState("sync", EventSyncWorkerState.Failed())) + syncStateFlow.emit(createSyncState("sync", EventSyncWorkerState.Failed())) testScheduler.advanceUntilIdle() coVerify { syncOrchestrator.startEventSync() } @@ -81,14 +77,14 @@ class RunBlockingEventSyncUseCaseTest { @Test fun `finishes execution when sync reporters have been cancelled`() = runTest { - val liveData = MutableLiveData() - every { syncManager.getLastSyncState() } returns liveData - liveData.postValue(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) + val syncStateFlow = MutableSharedFlow(replay = 1) + every { syncManager.getLastSyncState() } returns syncStateFlow + syncStateFlow.emit(createSyncState("oldSync", EventSyncWorkerState.Succeeded)) launch { usecase.invoke() } testScheduler.advanceUntilIdle() - liveData.postValue(createSyncState("sync", EventSyncWorkerState.Cancelled)) + syncStateFlow.emit(createSyncState("sync", EventSyncWorkerState.Cancelled)) testScheduler.advanceUntilIdle() coVerify { syncOrchestrator.startEventSync() } diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/CandidateRecordDataSource.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/CandidateRecordDataSource.kt index 33ea42125d..5b724e77e7 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/CandidateRecordDataSource.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/CandidateRecordDataSource.kt @@ -13,6 +13,9 @@ interface CandidateRecordDataSource { dataSource: BiometricDataSource = BiometricDataSource.Simprints, ): Int + /** + * Loads records concurrently using the provided dispatcher and parallelism level. + */ suspend fun loadCandidateRecords( query: EnrolmentRecordQuery, ranges: List, @@ -21,9 +24,4 @@ interface CandidateRecordDataSource { scope: CoroutineScope, onCandidateLoaded: suspend () -> Unit, ): ReceiveChannel - - /** - * Loads identities concurrently using the provided dispatcher and parallelism level. - * - */ } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt index 0cc1e02f0c..6a213bb013 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManager.kt @@ -1,6 +1,5 @@ package com.simprints.infra.eventsync -import androidx.lifecycle.LiveData import com.simprints.core.tools.time.Timestamp import com.simprints.infra.events.event.domain.models.EventType import com.simprints.infra.eventsync.status.models.DownSyncCounts @@ -16,7 +15,7 @@ interface EventSyncManager { suspend fun getLastSyncTime(): Timestamp? - fun getLastSyncState(useDefaultValue: Boolean = false): LiveData + fun getLastSyncState(useDefaultValue: Boolean = false): Flow suspend fun countEventsToUpload(): Flow diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt index a6e770a933..2067270ce6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/EventSyncManagerImpl.kt @@ -1,7 +1,5 @@ package com.simprints.infra.eventsync -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData import com.simprints.core.DispatcherIO import com.simprints.core.domain.tokenization.values import com.simprints.core.tools.time.TimeHelper @@ -33,6 +31,7 @@ import com.simprints.infra.eventsync.sync.down.tasks.SimprintsEventDownSyncTask import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext import javax.inject.Inject @@ -54,13 +53,11 @@ internal class EventSyncManagerImpl @Inject constructor( ) : EventSyncManager { override suspend fun getLastSyncTime(): Timestamp? = eventSyncCache.readLastSuccessfulSyncTime() - override fun getLastSyncState(useDefaultValue: Boolean): LiveData = MediatorLiveData().apply { + override fun getLastSyncState(useDefaultValue: Boolean): Flow = flow { if (useDefaultValue) { - value = EventSyncState(syncId = "", null, null, emptyList(), emptyList(), emptyList()) - } - addSource(eventSyncStateProcessor.getLastSyncState()) { lastSyncState -> - value = lastSyncState + emit(EventSyncState(syncId = "", null, null, emptyList(), emptyList(), emptyList())) } + eventSyncStateProcessor.getLastSyncState().collect { emit(it) } } override fun getPeriodicWorkTags(): List = listOf( diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt index 5775153007..74d22d7332 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt @@ -1,9 +1,5 @@ package com.simprints.infra.eventsync.sync -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.liveData -import androidx.lifecycle.switchMap import androidx.work.WorkInfo import com.simprints.infra.eventsync.status.models.EventSyncState import com.simprints.infra.eventsync.status.models.EventSyncState.SyncWorkerInfo @@ -15,7 +11,7 @@ import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.END_SYNC_ import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.START_SYNC_REPORTER import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.UPLOADER import com.simprints.infra.eventsync.sync.common.EventSyncCache -import com.simprints.infra.eventsync.sync.common.SyncWorkersLiveDataProvider +import com.simprints.infra.eventsync.sync.common.SyncWorkersInfoProvider import com.simprints.infra.eventsync.sync.common.didFailBecauseBackendMaintenance import com.simprints.infra.eventsync.sync.common.didFailBecauseCloudIntegration import com.simprints.infra.eventsync.sync.common.didFailBecauseCommCarePermissionMissing @@ -31,15 +27,18 @@ import com.simprints.infra.eventsync.sync.up.workers.extractUpSyncMaxCount import com.simprints.infra.eventsync.sync.up.workers.extractUpSyncProgress import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SYNC import com.simprints.infra.logging.Simber +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import javax.inject.Inject internal class EventSyncStateProcessor @Inject constructor( private val eventSyncCache: EventSyncCache, - private val syncWorkersLiveDataProvider: SyncWorkersLiveDataProvider, + private val syncWorkersInfoProvider: SyncWorkersInfoProvider, ) { - fun getLastSyncState(): LiveData = observerForLastSyncId().switchMap { lastSyncId -> - observerForLastSyncIdWorkers(lastSyncId).switchMap { syncWorkers -> - liveData { + fun getLastSyncState(): Flow = observerForLastSyncId().flatMapLatest { lastSyncId -> + observerForLastSyncIdWorkers(lastSyncId).flatMapLatest { syncWorkers -> + flow { val progress = calculateProgressForSync(syncWorkers) val total = calculateTotalForSync(syncWorkers) @@ -57,32 +56,32 @@ internal class EventSyncStateProcessor @Inject constructor( syncReporterStates, ) - emit(syncState) Simber.d("Emitting for sync state: $syncState", tag = SYNC) + emit(syncState) } } } - private fun observerForLastSyncId(): LiveData = syncWorkersLiveDataProvider - .getStartSyncReportersLiveData() - .switchMap { startSyncReporters -> + private fun observerForLastSyncId(): Flow = syncWorkersInfoProvider + .getStartSyncReporters() + .flatMapLatest { startSyncReporters -> Simber.d("Received updated from Master Scheduler", tag = SYNC) val completedSyncMaster = completedWorkers(startSyncReporters) val mostRecentSyncMaster = completedSyncMaster.sortByScheduledTime().lastOrNull() - MutableLiveData().apply { + flow { if (mostRecentSyncMaster != null) { val lastSyncId = mostRecentSyncMaster.outputData.getString(SYNC_ID_STARTED) if (!lastSyncId.isNullOrBlank()) { Simber.d("Received sync id: $lastSyncId", tag = SYNC) - this.postValue(lastSyncId) + emit(lastSyncId) } } } } - private fun observerForLastSyncIdWorkers(lastSyncId: String) = syncWorkersLiveDataProvider.getSyncWorkersLiveData(lastSyncId) + private fun observerForLastSyncIdWorkers(lastSyncId: String) = syncWorkersInfoProvider.getSyncWorkerInfos(lastSyncId) private fun completedWorkers(workInfos: List) = workInfos.filter { it.state == WorkInfo.State.SUCCEEDED } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersLiveDataProvider.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProvider.kt similarity index 54% rename from infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersLiveDataProvider.kt rename to infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProvider.kt index f1c9fb02fd..88b4bbabd3 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersLiveDataProvider.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProvider.kt @@ -1,22 +1,20 @@ package com.simprints.infra.eventsync.sync.common import android.content.Context -import androidx.lifecycle.LiveData import androidx.work.WorkInfo import androidx.work.WorkManager import com.simprints.infra.eventsync.status.models.EventSyncWorkerType import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.Companion.tagForType import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow import javax.inject.Inject -internal class SyncWorkersLiveDataProvider @Inject constructor( +internal class SyncWorkersInfoProvider @Inject constructor( @ApplicationContext ctx: Context, ) { private val wm = WorkManager.getInstance(ctx) - fun getStartSyncReportersLiveData(): LiveData> = - wm.getWorkInfosByTagLiveData((tagForType(EventSyncWorkerType.START_SYNC_REPORTER))) + fun getStartSyncReporters(): Flow> = wm.getWorkInfosByTagFlow(tagForType(EventSyncWorkerType.START_SYNC_REPORTER)) - fun getSyncWorkersLiveData(uniqueSyncId: String): LiveData> = - wm.getWorkInfosByTagLiveData(getUniqueSyncIdTag(uniqueSyncId)) + fun getSyncWorkerInfos(uniqueSyncId: String): Flow> = wm.getWorkInfosByTagFlow(getUniqueSyncIdTag(uniqueSyncId)) } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt index bf16da42fa..b2c959854a 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/EventSyncManagerTest.kt @@ -1,6 +1,5 @@ package com.simprints.infra.eventsync -import androidx.lifecycle.MutableLiveData import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.simprints.core.domain.common.Partitioning @@ -34,7 +33,11 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.junit.Before @@ -130,25 +133,28 @@ internal class EventSyncManagerTest { @Test fun `getLastSyncState should call sync processor`() = runTest { - eventSyncManagerImpl.getLastSyncState() + every { eventSyncStateProcessor.getLastSyncState() } returns flowOf() // Simulate empty flow + + eventSyncManagerImpl.getLastSyncState().firstOrNull() + verify { eventSyncStateProcessor.getLastSyncState() } } @Test fun `getLastSyncState with useDefaultValue true should return an immediate default value`() = runTest { - every { eventSyncStateProcessor.getLastSyncState() } returns MutableLiveData(null) + every { eventSyncStateProcessor.getLastSyncState() } returns MutableSharedFlow() val defaultValue = EventSyncState(syncId = "", null, null, emptyList(), emptyList(), emptyList()) - val result = eventSyncManagerImpl.getLastSyncState(true).value + val result = eventSyncManagerImpl.getLastSyncState(true).firstOrNull() assertThat(result).isEqualTo(defaultValue) } @Test fun `getLastSyncState with useDefaultValue false and no data emission should return null value`() = runTest { - every { eventSyncStateProcessor.getLastSyncState() } returns MutableLiveData(null) + every { eventSyncStateProcessor.getLastSyncState() } returns flowOf() // Simulate empty flow - val result = eventSyncManagerImpl.getLastSyncState(false).value + val result = eventSyncManagerImpl.getLastSyncState(false).firstOrNull() assertThat(result).isEqualTo(null) } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt index d627ea049d..cb4dc25dd2 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt @@ -1,8 +1,7 @@ package com.simprints.infra.eventsync.sync import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.ext.junit.runners.* import androidx.work.WorkInfo import androidx.work.WorkInfo.State.FAILED import androidx.work.WorkInfo.State.SUCCEEDED @@ -11,25 +10,26 @@ import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.Companion.tagForType import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.START_SYNC_REPORTER import com.simprints.infra.eventsync.sync.common.EventSyncCache -import com.simprints.infra.eventsync.sync.common.SyncWorkersLiveDataProvider +import com.simprints.infra.eventsync.sync.common.SyncWorkersInfoProvider import com.simprints.infra.eventsync.sync.common.TAG_MASTER_SYNC_ID import com.simprints.infra.eventsync.sync.common.TAG_SCHEDULED_AT import com.simprints.infra.eventsync.sync.common.TAG_SUBJECTS_DOWN_SYNC_ALL_WORKERS import com.simprints.infra.eventsync.sync.common.TAG_SUBJECTS_SYNC_ALL_WORKERS import com.simprints.infra.eventsync.sync.master.EventStartSyncReporterWorker.Companion.SYNC_ID_STARTED -import com.simprints.testtools.common.livedata.getOrAwaitValue -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.verify -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import io.mockk.* +import io.mockk.impl.annotations.* +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.util.Date +import kotlin.time.Duration.Companion.seconds @RunWith(AndroidJUnit4::class) internal class EventSyncStateProcessorTest { @@ -56,13 +56,13 @@ internal class EventSyncStateProcessorTest { private val failedMasterWorkers: List = listOf(createWorkInfo(FAILED)) - private var startSyncReporterWorker = MutableLiveData>() - private var syncWorkersLiveData = MutableLiveData>() + private var startSyncReporterWorker = MutableSharedFlow>(replay = 1) + private var syncWorkersFlow = MutableSharedFlow>(replay = 1) private lateinit var eventSyncStateProcessor: EventSyncStateProcessor @RelaxedMockK - lateinit var syncWorkersLiveDataProvider: SyncWorkersLiveDataProvider + lateinit var syncWorkersInfoProvider: SyncWorkersInfoProvider @RelaxedMockK lateinit var eventSyncCache: EventSyncCache @@ -70,95 +70,85 @@ internal class EventSyncStateProcessorTest { @Before fun setUp() { MockKAnnotations.init(this) - eventSyncStateProcessor = - EventSyncStateProcessor(eventSyncCache, syncWorkersLiveDataProvider) + eventSyncStateProcessor = EventSyncStateProcessor(eventSyncCache, syncWorkersInfoProvider) mockDependencies() } @Test - fun processor_masterWorkerCompletes_shouldExtractTheUniqueSyncId() = runTest( - UnconfinedTestDispatcher(), - ) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForSuccessfulSync() + fun processor_masterWorkerCompletes_shouldExtractTheUniqueSyncId() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSuccessfulSync()) - eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + eventSyncStateProcessor.getLastSyncState().first() - verify { - syncWorkersLiveDataProvider.getSyncWorkersLiveData(UNIQUE_SYNC_ID) - } + verify { syncWorkersInfoProvider.getSyncWorkerInfos(UNIQUE_SYNC_ID) } } - @Test - fun processor_masterWorkerFails_shouldNotExtractTheUniqueSyncId() = runTest( - UnconfinedTestDispatcher(), - ) { - startSyncReporterWorker.value = failedMasterWorkers + @Test(expected = TimeoutCancellationException::class) + fun processor_masterWorkerFails_shouldNotExtractTheUniqueSyncId() = runTest { + startSyncReporterWorker.emit(failedMasterWorkers) - eventSyncStateProcessor.getLastSyncState() + eventSyncStateProcessor.getLastSyncState().timeout(1.seconds).firstOrNull() - verify(exactly = 0) { syncWorkersLiveDataProvider.getSyncWorkersLiveData(UNIQUE_SYNC_ID) } + // flow will never complete since it the worker flow is not executed } @Test - fun processor_allWorkersSucceed_shouldSyncStateBeSuccess() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForSuccessfulSync() + fun processor_allWorkersSucceed_shouldSyncStateBeSuccess() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSuccessfulSync()) - val syncStates = eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() syncStates.assertSuccessfulSyncState() } @Test - fun processor_oneWorkerStillRunning_shouldSyncStateBeRunning() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForRunningSync() + fun processor_oneWorkerStillRunning_shouldSyncStateBeRunning() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForRunningSync()) - val syncStates = - eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() syncStates.assertRunningSyncState() } @Test - fun processor_oneWorkerRunning_shouldIgnoreCount() = runTest(UnconfinedTestDispatcher()) { + fun processor_oneWorkerRunning_shouldIgnoreCount() = runTest { coEvery { eventSyncCache.shouldIgnoreMax() } returns true - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForRunningSync() + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForRunningSync()) - val syncStates = eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() syncStates.assertRunningSyncStateWithoutProgress() } @Test - fun processor_oneWorkerFailed_shouldSyncStateBeFail() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForFailingSync() + fun processor_oneWorkerFailed_shouldSyncStateBeFail() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForFailingSync()) - val syncStates = eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() syncStates.assertFailingSyncState() } @Test - fun processor_oneWorkerEnqueued_shouldSyncStateBeConnecting() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = createWorkInfosHistoryForConnectingSync() + fun processor_oneWorkerEnqueued_shouldSyncStateBeConnecting() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForConnectingSync()) - val syncStates = - eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() syncStates.assertConnectingSyncState() } @Test - fun getLastSyncState_shouldMapCorrectlyTheBackendMaintenanceFailed() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = - createWorkInfosHistoryForFailingSyncDueBackendMaintenanceError() + fun getLastSyncState_shouldMapCorrectlyTheBackendMaintenanceFailed() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForFailingSyncDueBackendMaintenanceError()) - val syncStates = eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() val expectedState = EventSyncWorkerState.Failed( failedBecauseBackendMaintenance = true, @@ -171,17 +161,13 @@ internal class EventSyncStateProcessorTest { } @Test - fun getLastSyncState_shouldMapCorrectlyTheTooManyRequestsFailed() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = - createWorkInfosHistoryForFailingSyncDueTooManyRequestsError() + fun getLastSyncState_shouldMapCorrectlyTheTooManyRequestsFailed() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForFailingSyncDueTooManyRequestsError()) - val syncStates = - eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() - val expectedState = EventSyncWorkerState.Failed( - failedBecauseTooManyRequest = true, - ) + val expectedState = EventSyncWorkerState.Failed(failedBecauseTooManyRequest = true) syncStates.downSyncWorkersInfo .first() .state @@ -189,17 +175,13 @@ internal class EventSyncStateProcessorTest { } @Test - fun getLastSyncState_shouldMapCorrectlyTheCloudIntegrationFailed() = runTest(UnconfinedTestDispatcher()) { - startSyncReporterWorker.value = successfulMasterWorkers - syncWorkersLiveData.value = - createWorkInfosHistoryForFailingSyncDueCloudIntegrationError() + fun getLastSyncState_shouldMapCorrectlyTheCloudIntegrationFailed() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForFailingSyncDueCloudIntegrationError()) - val syncStates = - eventSyncStateProcessor.getLastSyncState().getOrAwaitValue() + val syncStates = eventSyncStateProcessor.getLastSyncState().first() - val expectedState = EventSyncWorkerState.Failed( - failedBecauseCloudIntegration = true, - ) + val expectedState = EventSyncWorkerState.Failed(failedBecauseCloudIntegration = true) syncStates.downSyncWorkersInfo .first() .state @@ -221,8 +203,8 @@ internal class EventSyncStateProcessorTest { ) private fun mockDependencies() { - every { syncWorkersLiveDataProvider.getStartSyncReportersLiveData() } returns startSyncReporterWorker - every { syncWorkersLiveDataProvider.getSyncWorkersLiveData(any()) } returns syncWorkersLiveData + every { syncWorkersInfoProvider.getStartSyncReporters() } returns startSyncReporterWorker + every { syncWorkersInfoProvider.getSyncWorkerInfos(any()) } returns syncWorkersFlow coEvery { eventSyncCache.readProgress(any()) } returns 0 } } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt index 445e6a9897..7e5bc803cb 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt @@ -7,7 +7,7 @@ import androidx.work.WorkInfo.State.FAILED import androidx.work.WorkInfo.State.RUNNING import androidx.work.WorkInfo.State.SUCCEEDED import androidx.work.workDataOf -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.infra.eventsync.status.models.EventSyncState import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerState.Enqueued @@ -57,25 +57,25 @@ fun EventSyncWorkerState.assertEqualToFailedState(e: Failed) { fun EventSyncState.assertConnectingSyncState() { assertProgressAndTotal(syncId, total, progress) assertThat(downSyncWorkersInfo.count { it.state is Enqueued }).isEqualTo(1) - upSyncWorkersInfo.all { it.state is Succeeded } + assertThat(upSyncWorkersInfo.all { it.state is Succeeded }).isTrue() } fun EventSyncState.assertFailingSyncState() { assertProgressAndTotal(syncId, total, progress) assertThat(downSyncWorkersInfo.count { it.state is Failed }).isEqualTo(1) - upSyncWorkersInfo.all { it.state is Succeeded } + assertThat(upSyncWorkersInfo.all { it.state is Succeeded }).isTrue() } fun EventSyncState.assertSuccessfulSyncState() { assertProgressAndTotal(syncId, total, progress) - downSyncWorkersInfo.all { it.state is Succeeded } - upSyncWorkersInfo.all { it.state is Succeeded } + assertThat(downSyncWorkersInfo.all { it.state is Succeeded }).isTrue() + assertThat(upSyncWorkersInfo.all { it.state is Succeeded }).isTrue() } fun EventSyncState.assertRunningSyncState() { assertProgressAndTotal(syncId, total, progress) assertThat(downSyncWorkersInfo.count { it.state is Running }).isEqualTo(1) - upSyncWorkersInfo.all { it.state is Succeeded } + assertThat(upSyncWorkersInfo.all { it.state is Succeeded }).isTrue() } fun EventSyncState.assertRunningSyncStateWithoutProgress() { @@ -83,7 +83,7 @@ fun EventSyncState.assertRunningSyncStateWithoutProgress() { assertThat(total).isNull() assertThat(progress).isNull() assertThat(downSyncWorkersInfo.count { it.state is Running }).isEqualTo(1) - upSyncWorkersInfo.all { it.state is Succeeded } + assertThat(upSyncWorkersInfo.all { it.state is Succeeded }).isTrue() } private fun assertProgressAndTotal( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProviderTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProviderTest.kt new file mode 100644 index 0000000000..6da8ad73ae --- /dev/null +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/common/SyncWorkersInfoProviderTest.kt @@ -0,0 +1,48 @@ +package com.simprints.infra.eventsync.sync.common + +import android.content.Context +import androidx.test.ext.junit.runners.* +import androidx.work.WorkManager +import com.simprints.testtools.common.coroutines.TestCoroutineRule +import io.mockk.* +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SyncWorkersInfoProviderTest { + @get:Rule + val testCoroutineRule = TestCoroutineRule() + private lateinit var workManager: WorkManager + + private lateinit var syncWorkersInfoProvider: SyncWorkersInfoProvider + + @Before + fun setup() { + workManager = mockk() + every { workManager.getWorkInfosByTagFlow(any()) } returns flowOf(emptyList()) + + mockkObject(WorkManager.Companion) + every { WorkManager.getInstance(any()) } returns workManager + + syncWorkersInfoProvider = SyncWorkersInfoProvider(mockk()) + } + + @Test + fun `getStartSyncReporters requests workers with correct tag`() = runTest { + syncWorkersInfoProvider.getStartSyncReporters().first() + + verify { workManager.getWorkInfosByTagFlow(eq("TAG_PEOPLE_SYNC_WORKER_TYPE_START_SYNC_REPORTER")) } + } + + @Test + fun `getSyncWorkerInfos requests workers with correct tag`() = runTest { + syncWorkersInfoProvider.getSyncWorkerInfos("uniqueTag").first() + + verify { workManager.getWorkInfosByTagFlow(eq("TAG_MASTER_SYNC_ID_uniqueTag")) } + } +} From 0ca387b5d4f347dc202279463f45ce96c4454ed9 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 17 Dec 2025 18:24:45 +0200 Subject: [PATCH 2/3] MS-1276 Convert network connectivity LiveData to Flow --- .../usecase/ObserveSyncInfoUseCase.kt | 2 +- .../usecase/ObserveSyncInfoUseCaseTest.kt | 40 +++--- .../infra/network/ConnectivityTracker.kt | 4 +- .../simprints/infra/network/NetworkModule.kt | 4 +- .../connectivity/ConnectivityLiveData.kt | 35 ----- .../ConnectivityManagerWrapper.kt | 43 +++++- .../connectivity/ConnectivityTrackerImpl.kt | 17 --- .../connectivity/ConnectivityLiveDataTest.kt | 87 ------------ .../ConnectivityManagerWrapperImplTest.kt | 133 +++++++++++++++--- .../ConnectivityTrackerImplTest.kt | 39 ----- 10 files changed, 173 insertions(+), 231 deletions(-) delete mode 100644 infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityLiveData.kt delete mode 100644 infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImpl.kt delete mode 100644 infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityLiveDataTest.kt delete mode 100644 infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImplTest.kt diff --git a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt index 572e5baf59..772d9a5fb4 100644 --- a/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt +++ b/feature/dashboard/src/main/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCase.kt @@ -62,7 +62,7 @@ internal class ObserveSyncInfoUseCase @Inject constructor( // Since we are not using distinctUntilChanged any emission from combined flows will trigger the main flow as well private fun combinedRefreshSignals() = combine( - connectivityTracker.observeIsConnected().asFlow(), + connectivityTracker.observeIsConnected(), appForegroundStateTracker.observeAppInForeground().filter { it }, // only when going to foreground ticker.observeTicks(1.minutes), ) { isOnline, _, _ -> isOnline } diff --git a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt index d31e98f3fd..58d06d1038 100644 --- a/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt +++ b/feature/dashboard/src/test/java/com/simprints/feature/dashboard/settings/syncinfo/usecase/ObserveSyncInfoUseCaseTest.kt @@ -1,8 +1,6 @@ package com.simprints.feature.dashboard.settings.syncinfo.usecase import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asFlow import com.google.common.truth.Truth.* import com.simprints.core.domain.common.Modality import com.simprints.core.lifecycle.AppForegroundStateTracker @@ -120,9 +118,7 @@ internal class ObserveSyncInfoUseCaseTest { private fun setupDefaultMocks() { every { authStore.observeSignedInProjectId() } returns MutableStateFlow(TEST_PROJECT_ID) - val connectivityLiveData = MutableLiveData(true) - every { connectivityTracker.observeIsConnected() } returns connectivityLiveData - every { connectivityLiveData.asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) val eventSyncLiveData = flowOf(mockEventSyncState) every { eventSyncManager.getLastSyncState() } returns eventSyncLiveData @@ -255,7 +251,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockOfflineEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) createUseCase() val result = useCase().first() @@ -273,7 +269,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailed() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() @@ -306,7 +302,7 @@ internal class ObserveSyncInfoUseCaseTest { every { progress } returns null } every { syncOrchestrator.observeImageSyncStatus() } returns MutableStateFlow(mockNotSyncingImageStatus) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) createUseCase() val result = useCase().first() @@ -321,7 +317,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailedBecauseReloginRequired() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() @@ -737,7 +733,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `should handle network errors indication`() = runTest { val connectivityFlow = MutableStateFlow(false) // start offline - every { connectivityTracker.observeIsConnected().asFlow() } returns connectivityFlow + every { connectivityTracker.observeIsConnected() } returns connectivityFlow createUseCase() val offlineResult = useCase().first() @@ -782,7 +778,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `should handle changes in connectivity stream`() = runTest { val connectivityFlow = MutableStateFlow(false) // started offline - every { connectivityTracker.observeIsConnected().asFlow() } returns connectivityFlow + every { connectivityTracker.observeIsConnected() } returns connectivityFlow createUseCase() val offlineResult = useCase().first() @@ -982,7 +978,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) every { mockProjectConfiguration.isCommCareEventDownSyncAllowed() } returns true every { commCarePermissionChecker.hasCommCarePermissions() } returns false // Permission still denied createUseCase() @@ -1005,7 +1001,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() @@ -1047,7 +1043,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `sync button should be disabled when this is logout screen and offline`() = runTest { - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) createUseCase() val result = useCase(isPreLogoutUpSync = true).first() @@ -1057,7 +1053,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `sync button should be enabled when online and there is sync to Simprints`() = runTest { - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) every { any().canSyncDataToSimprints() } returns true createUseCase() @@ -1068,7 +1064,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `sync button should be enabled when offline but CommCare down-sync allowed`() = runTest { - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) every { any().isCommCareEventDownSyncAllowed() } returns true createUseCase() @@ -1236,7 +1232,7 @@ internal class ObserveSyncInfoUseCaseTest { @Test fun `should show correct visibility states for offline instructions`() = runTest { - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) createUseCase() val result = useCase().first() @@ -1255,7 +1251,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockFailedEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() @@ -1283,7 +1279,7 @@ internal class ObserveSyncInfoUseCaseTest { ) every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) every { mockProjectConfigRequiringModules.isModuleSelectionAvailable() } returns true createUseCase() @@ -1302,7 +1298,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockIdleEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() @@ -1362,7 +1358,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncInProgress() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(false) + every { connectivityTracker.observeIsConnected() } returns flowOf(false) createUseCase() val result = useCase().first() @@ -1395,7 +1391,7 @@ internal class ObserveSyncInfoUseCaseTest { every { isSyncFailedBecauseCommCarePermissionIsMissing() } returns false } every { eventSyncManager.getLastSyncState(any()) } returns flowOf(mockNormalEventSyncState) - every { connectivityTracker.observeIsConnected().asFlow() } returns flowOf(true) + every { connectivityTracker.observeIsConnected() } returns flowOf(true) createUseCase() val result = useCase().first() diff --git a/infra/network/src/main/java/com/simprints/infra/network/ConnectivityTracker.kt b/infra/network/src/main/java/com/simprints/infra/network/ConnectivityTracker.kt index f7e6d1eda3..09ea52a41c 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/ConnectivityTracker.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/ConnectivityTracker.kt @@ -1,9 +1,9 @@ package com.simprints.infra.network -import androidx.lifecycle.LiveData +import kotlinx.coroutines.flow.Flow interface ConnectivityTracker { - fun observeIsConnected(): LiveData + fun observeIsConnected(): Flow fun isConnected(): Boolean } diff --git a/infra/network/src/main/java/com/simprints/infra/network/NetworkModule.kt b/infra/network/src/main/java/com/simprints/infra/network/NetworkModule.kt index 3af789fb99..01264fcaaa 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/NetworkModule.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/NetworkModule.kt @@ -1,7 +1,7 @@ package com.simprints.infra.network import android.content.Context -import com.simprints.infra.network.connectivity.ConnectivityTrackerImpl +import com.simprints.infra.network.connectivity.ConnectivityManagerWrapper import com.simprints.infra.network.url.BaseUrlProvider import com.simprints.infra.network.url.BaseUrlProviderImpl import dagger.Binds @@ -23,7 +23,7 @@ abstract class NetworkBindingsModule { internal abstract fun provideBaseUrlProvider(impl: BaseUrlProviderImpl): BaseUrlProvider @Binds - internal abstract fun provideConnectivityTracker(impl: ConnectivityTrackerImpl): ConnectivityTracker + internal abstract fun provideConnectivityTracker(impl: ConnectivityManagerWrapper): ConnectivityTracker } @Module diff --git a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityLiveData.kt b/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityLiveData.kt deleted file mode 100644 index ee94a29ded..0000000000 --- a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityLiveData.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.simprints.infra.network.connectivity - -import android.net.ConnectivityManager -import android.net.Network -import androidx.lifecycle.LiveData - -internal class ConnectivityLiveData( - private val connectivityManagerWrapper: ConnectivityManagerWrapper, -) : LiveData() { - private val networkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - postValue(true) - } - - override fun onLost(network: Network) { - postValue(false) - } - - override fun onUnavailable() { - postValue(false) - } - } - - override fun onActive() { - super.onActive() - connectivityManagerWrapper.registerNetworkCallback(networkCallback) - - postValue(connectivityManagerWrapper.isNetworkAvailable()) - } - - override fun onInactive() { - super.onInactive() - connectivityManagerWrapper.unregisterNetworkCallback(networkCallback) - } -} diff --git a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapper.kt b/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapper.kt index 8f33542000..b7ecd061b7 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapper.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapper.kt @@ -2,17 +2,25 @@ package com.simprints.infra.network.connectivity import android.content.Context import android.net.ConnectivityManager +import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.os.Build +import com.simprints.infra.network.ConnectivityTracker import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject +import javax.inject.Singleton +@Singleton internal class ConnectivityManagerWrapper @Inject constructor( @ApplicationContext private val ctx: Context, -) { +) : ConnectivityTracker { private val connectivityManager by lazy { - ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + ctx.getSystemService(ConnectivityManager::class.java) } /** @@ -24,7 +32,7 @@ internal class ConnectivityManagerWrapper @Inject constructor( * connectivity on this network was successfully validated * False: otherwise. */ - fun isNetworkAvailable(): Boolean { + override fun isConnected(): Boolean { val network = connectivityManager.activeNetwork val capabilities = connectivityManager.getNetworkCapabilities(network) @@ -33,16 +41,37 @@ internal class ConnectivityManagerWrapper @Inject constructor( capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } - fun registerNetworkCallback(networkCallback: ConnectivityManager.NetworkCallback) { + override fun observeIsConnected(): Flow = callbackFlow { + val networkStatusCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(true) + } + + override fun onUnavailable() { + trySend(false) + } + + override fun onLost(network: Network) { + trySend(false) + } + } + + trySend(isConnected()) + registerNetworkCallback(networkStatusCallback) + + awaitClose { unregisterNetworkCallback(networkStatusCallback) } + }.distinctUntilChanged() + + private fun registerNetworkCallback(networkCallback: ConnectivityManager.NetworkCallback) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { connectivityManager.registerDefaultNetworkCallback(networkCallback) } else { - val builder = NetworkRequest.Builder() - connectivityManager.registerNetworkCallback(builder.build(), networkCallback) + val request = NetworkRequest.Builder().build() + connectivityManager.registerNetworkCallback(request, networkCallback) } } - fun unregisterNetworkCallback(networkCallback: ConnectivityManager.NetworkCallback) { + private fun unregisterNetworkCallback(networkCallback: ConnectivityManager.NetworkCallback) { connectivityManager.unregisterNetworkCallback(networkCallback) } } diff --git a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImpl.kt b/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImpl.kt deleted file mode 100644 index e7d61776c7..0000000000 --- a/infra/network/src/main/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.simprints.infra.network.connectivity - -import androidx.lifecycle.LiveData -import com.simprints.infra.network.ConnectivityTracker -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class ConnectivityTrackerImpl @Inject constructor( - private val connectivityManagerWrapper: ConnectivityManagerWrapper, -) : ConnectivityTracker { - private val isConnectedLiveData by lazy { ConnectivityLiveData(connectivityManagerWrapper) } - - override fun observeIsConnected(): LiveData = isConnectedLiveData - - override fun isConnected() = connectivityManagerWrapper.isNetworkAvailable() -} diff --git a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityLiveDataTest.kt b/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityLiveDataTest.kt deleted file mode 100644 index 0328bee39d..0000000000 --- a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityLiveDataTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.simprints.infra.network.connectivity - -import android.net.ConnectivityManager.NetworkCallback -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.google.common.truth.Truth.assertThat -import com.jraska.livedata.test -import io.mockk.CapturingSlot -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -internal class ConnectivityLiveDataTest { - @get:Rule - val rule = InstantTaskExecutorRule() - - @MockK - lateinit var connectivityManagerWrapper: ConnectivityManagerWrapper - - private var networkCallback = CapturingSlot() - - private lateinit var connectivityLiveData: ConnectivityLiveData - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - every { connectivityManagerWrapper.registerNetworkCallback(capture(networkCallback)) } returns Unit - - connectivityLiveData = ConnectivityLiveData(connectivityManagerWrapper) - } - - @Test - fun `live data registers and unregisters callback`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns true - - val testObserver = connectivityLiveData.test() - connectivityLiveData.removeObserver(testObserver) - - verify { connectivityManagerWrapper.registerNetworkCallback(any()) } - verify { connectivityManagerWrapper.unregisterNetworkCallback(any()) } - } - - @Test - fun `live data initialises with current value`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns true - - val testObserver = connectivityLiveData.test() - - verify { connectivityManagerWrapper.isNetworkAvailable() } - assertThat(testObserver.valueHistory()).isEqualTo(listOf(true)) - } - - @Test - fun `live data notified on connection available`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns false - - val testObserver = connectivityLiveData.test() - networkCallback.captured.onAvailable(mockk()) - - assertThat(testObserver.valueHistory()).isEqualTo(listOf(false, true)) - } - - @Test - fun `live data notified on connection lost`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns true - - val testObserver = connectivityLiveData.test() - networkCallback.captured.onLost(mockk()) - - assertThat(testObserver.valueHistory()).isEqualTo(listOf(true, false)) - } - - @Test - fun `live data notified on connection unavailable`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns true - - val testObserver = connectivityLiveData.test() - networkCallback.captured.onUnavailable() - - assertThat(testObserver.valueHistory()).isEqualTo(listOf(true, false)) - } -} diff --git a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapperImplTest.kt b/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapperImplTest.kt index 1926829bbc..5729d22212 100644 --- a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapperImplTest.kt +++ b/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityManagerWrapperImplTest.kt @@ -2,17 +2,33 @@ package com.simprints.infra.network.connectivity import android.content.Context import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback import android.net.NetworkCapabilities import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED -import com.google.common.truth.Truth.assertThat -import io.mockk.MockKAnnotations -import io.mockk.every +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.test.ext.junit.runners.* +import com.google.common.truth.Truth.* +import io.mockk.* import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) internal class ConnectivityManagerWrapperImplTest { + @get:Rule + val rule = InstantTaskExecutorRule() + @MockK lateinit var context: Context @@ -22,19 +38,29 @@ internal class ConnectivityManagerWrapperImplTest { @MockK lateinit var networkCapabilities: NetworkCapabilities + private var networkCallback = CapturingSlot() + private lateinit var connectivityManagerWrapper: ConnectivityManagerWrapper @Before fun setup() { MockKAnnotations.init(this, relaxed = true) + every { context.getSystemService(ConnectivityManager::class.java) } returns connectivityManager + + every { connectivityManager.getNetworkCapabilities(any()) } returns networkCapabilities + every { connectivityManager.activeNetwork } returns mockk() + every { connectivityManager.registerNetworkCallback(any(), capture(networkCallback)) } returns Unit + + connectivityManagerWrapper = ConnectivityManagerWrapper(context) } @Test fun `test isNetworkAvailable should be false if capabilities is null`() { // Given - setupNetworkCapabilities(null) + every { connectivityManager.getNetworkCapabilities(any()) } returns null + // When - val actual = connectivityManagerWrapper.isNetworkAvailable() + val actual = connectivityManagerWrapper.isConnected() // Then assertThat(actual).isEqualTo(false) } @@ -43,9 +69,8 @@ internal class ConnectivityManagerWrapperImplTest { fun `test isNetworkAvailable should be false if network can't reach internet`() { // Given every { networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns false - setupNetworkCapabilities(networkCapabilities) // When - val actual = connectivityManagerWrapper.isNetworkAvailable() + val actual = connectivityManagerWrapper.isConnected() // Then assertThat(actual).isEqualTo(false) } @@ -55,9 +80,8 @@ internal class ConnectivityManagerWrapperImplTest { // Given every { networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true every { networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false - setupNetworkCapabilities(networkCapabilities) // When - val actual = connectivityManagerWrapper.isNetworkAvailable() + val actual = connectivityManagerWrapper.isConnected() // Then assertThat(actual).isEqualTo(false) } @@ -65,19 +89,90 @@ internal class ConnectivityManagerWrapperImplTest { @Test fun `test isNetworkAvailable success`() { // Given - every { networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true - every { networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true - setupNetworkCapabilities(networkCapabilities) - // When - val actual = connectivityManagerWrapper.isNetworkAvailable() + setCapabilities(internet = true, validated = true) // Then - assertThat(actual).isEqualTo(true) + assertThat(connectivityManagerWrapper.isConnected()).isTrue() } - private fun setupNetworkCapabilities(mockedNetworkCapabilities: NetworkCapabilities?) { - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager - every { connectivityManager.getNetworkCapabilities(any()) } returns mockedNetworkCapabilities + @Test + fun `connection flow registers and unregisters callback`() = runTest { + setCapabilities(internet = true, validated = true) - connectivityManagerWrapper = ConnectivityManagerWrapper(context) + connectivityManagerWrapper.observeIsConnected().first() + + verify { connectivityManager.registerNetworkCallback(any(), any()) } + verify { connectivityManager.unregisterNetworkCallback(any()) } + } + + @Test + fun `connection flow initialises with current value`() = runTest { + setCapabilities(internet = true, validated = true) + + assertThat(connectivityManagerWrapper.observeIsConnected().first()).isTrue() + } + + @Test + fun `connection flow notified on connection available`() = runTest { + setCapabilities(internet = false, validated = false) + + val collectedValues = mutableListOf() + val collectJob = launch { + connectivityManagerWrapper.observeIsConnected().onEach { collectedValues.add(it) }.collect() + } + advanceUntilIdle() + + networkCallback.captured.onAvailable(mockk()) + advanceUntilIdle() + + collectJob.cancel() + advanceUntilIdle() + + assertThat(collectedValues).containsExactly(false, true) + } + + @Test + fun `connection flow notified on connection lost`() = runTest { + setCapabilities(internet = true, validated = true) + + val collectedValues = mutableListOf() + val collectJob = launch { + connectivityManagerWrapper.observeIsConnected().onEach { collectedValues.add(it) }.collect() + } + advanceUntilIdle() + + networkCallback.captured.onLost(mockk()) + advanceUntilIdle() + + collectJob.cancel() + advanceUntilIdle() + + assertThat(collectedValues).containsExactly(true, false) + } + + @Test + fun `connection flow notified on connection unavailable`() = runTest { + setCapabilities(internet = true, validated = true) + + val collectedValues = mutableListOf() + val collectJob = launch { + connectivityManagerWrapper.observeIsConnected().onEach { collectedValues.add(it) }.collect() + } + advanceUntilIdle() + + networkCallback.captured.onUnavailable() + advanceUntilIdle() + + collectJob.cancel() + advanceUntilIdle() + + assertThat(collectedValues).containsExactly(true, false) + } + + private fun setCapabilities( + internet: Boolean, + validated: Boolean, + ) { + every { networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns internet + every { networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns validated } } diff --git a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImplTest.kt b/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImplTest.kt deleted file mode 100644 index 6f2ad1ad2e..0000000000 --- a/infra/network/src/test/java/com/simprints/infra/network/connectivity/ConnectivityTrackerImplTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.simprints.infra.network.connectivity - -import com.google.common.truth.Truth.assertThat -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.verify -import org.junit.Before -import org.junit.Test - -internal class ConnectivityTrackerImplTest { - @MockK - lateinit var connectivityManagerWrapper: ConnectivityManagerWrapper - - lateinit var connectivityTracker: ConnectivityTrackerImpl - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - - connectivityTracker = ConnectivityTrackerImpl(connectivityManagerWrapper) - } - - @Test - fun `returns same live data to observe`() { - val data1 = connectivityTracker.observeIsConnected() - val data2 = connectivityTracker.observeIsConnected() - - assertThat(data1).isEqualTo(data2) - } - - @Test - fun `redirects connection request to connectivity manager`() { - every { connectivityManagerWrapper.isNetworkAvailable() } returns true - - assertThat(connectivityTracker.isConnected()).isTrue() - verify { connectivityManagerWrapper.isNetworkAvailable() } - } -} From 2b5d9e08462e58d0ddb2eeef6ed813a1bdc4a028 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Wed, 17 Dec 2025 18:25:39 +0200 Subject: [PATCH 3/3] MS-1276 Move LiveData dependency to be strictly in UI modules --- infra/core/build.gradle.kts | 1 - infra/ui-base/build.gradle.kts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/core/build.gradle.kts b/infra/core/build.gradle.kts index a0a1282075..d799aac30f 100644 --- a/infra/core/build.gradle.kts +++ b/infra/core/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { api(libs.androidX.appcompat) api(libs.androidX.multidex) api(libs.androidX.annotation.annotation) - api(libs.androidX.lifecycle.livedata.ktx) api(libs.androidX.lifecycle.process) api(libs.androidX.cameraX.core) diff --git a/infra/ui-base/build.gradle.kts b/infra/ui-base/build.gradle.kts index a9ccecb631..98745379d8 100644 --- a/infra/ui-base/build.gradle.kts +++ b/infra/ui-base/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { api(libs.androidX.appcompat) api(libs.androidX.lifecycle) api(libs.androidX.lifecycle.scope) + api(libs.androidX.lifecycle.livedata.ktx) api(libs.support.material)