diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt index 458845c14f..7e24f017dc 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt @@ -20,13 +20,12 @@ import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -import org.junit.Ignore +import org.hisp.dhis.android.core.mockwebserver.ResponseController.GET import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import syncFlowRobot import java.util.UUID -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity @RunWith(AndroidJUnit4::class) class SyncFlowTest : BaseTest() { @@ -48,12 +47,15 @@ class SyncFlowTest : BaseTest() { override fun setUp() { super.setUp() + setupMockServer() workInfoStatusLiveData = ApplicationProvider.getApplicationContext().mutableWorkInfoStatuses } @Test fun shouldShowErrorWhenTEISyncFails() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + val teiName = "Lars" val teiLastName = "Overland" @@ -95,6 +97,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldSuccessfullySyncSavedEvent() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) eventWithoutRegistrationRobot(composeTestRule) { @@ -118,6 +122,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldShowErrorWhenSyncEventFails() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) eventWithoutRegistrationRobot(composeTestRule) { @@ -141,6 +147,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldSuccessfullySyncSavedDataSet() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareFacilityDataSetIntentAndLaunchActivity(ruleDataSet) dataSetRobot { @@ -223,5 +231,6 @@ class SyncFlowTest : BaseTest() { companion object { const val LAB_MONITORING_EVENT_DATE = "28/6/2020" + const val API_PING_RESPONSE_OK = "mocks/systeminfo/ping.txt" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/sync/MockedWorkManagerController.kt b/app/src/androidTest/java/org/dhis2/usescases/sync/MockedWorkManagerController.kt index aabd81897a..2e5bee9767 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/sync/MockedWorkManagerController.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/sync/MockedWorkManagerController.kt @@ -15,14 +15,6 @@ class MockedWorkManagerController(private val workInfoStatuses: LiveData Unit) { class EnrollmentRobot : BaseRobot() { fun clickOnAProgramForEnrollment(composeTestRule: ComposeTestRule, program: String) { - composeTestRule.onNodeWithTag(PROGRAM_TO_ENROLL.format(program)) + composeTestRule.onNodeWithTag(PROGRAM_TO_ENROLL.format(program), useUnmergedTree = true) .performClick() } diff --git a/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt index 9a42713a10..e05bbfe1f9 100644 --- a/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt +++ b/app/src/dhis/java/org/dhis2/utils/granularsync/GranularSyncModule.kt @@ -60,19 +60,22 @@ class GranularSyncModule( view, repository, schedulerProvider, - object : DispatcherProvider { - override fun io() = Dispatchers.IO - - override fun computation() = Dispatchers.Default - - override fun ui() = Dispatchers.Main - }, + provideDispatchers(), syncContext, workManagerController, smsSyncProvider, ) } + @Provides + fun provideDispatchers() = object : DispatcherProvider { + override fun io() = Dispatchers.IO + + override fun computation() = Dispatchers.Default + + override fun ui() = Dispatchers.Main + } + @Provides fun granularSyncRepository( d2: D2, @@ -87,6 +90,7 @@ class GranularSyncModule( dhisProgramUtils, periodUtils, resourceManager, + provideDispatchers(), ) @Provides diff --git a/app/src/dhisUITesting/assets/mocks/systeminfo/ping.txt b/app/src/dhisUITesting/assets/mocks/systeminfo/ping.txt new file mode 100644 index 0000000000..ed53c21358 --- /dev/null +++ b/app/src/dhisUITesting/assets/mocks/systeminfo/ping.txt @@ -0,0 +1 @@ +pong \ No newline at end of file diff --git a/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt b/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt index 9a42713a10..ecedce9eed 100644 --- a/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt +++ b/app/src/dhisUITesting/java/org/dhis2/utils/granularsync/GranularSyncModule.kt @@ -80,6 +80,7 @@ class GranularSyncModule( periodUtils: DhisPeriodUtils, preferenceProvider: PreferenceProvider, resourceManager: ResourceManager, + dispatcherProvider: DispatcherProvider, ): GranularSyncRepository = GranularSyncRepository( d2, syncContext, @@ -87,6 +88,7 @@ class GranularSyncModule( dhisProgramUtils, periodUtils, resourceManager, + dispatcherProvider, ) @Provides diff --git a/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerController.kt b/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerController.kt index 2fd93e5577..60ba013361 100644 --- a/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerController.kt +++ b/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerController.kt @@ -34,7 +34,6 @@ import androidx.work.WorkInfo interface WorkManagerController { fun syncDataForWorker(workerItem: WorkerItem) - fun syncDataForWorkers(metadataWorkerTag: String, dataWorkerTag: String, workName: String) fun syncMetaDataForWorker(metadataWorkerTag: String, workName: String) fun syncDataForWorker(metadataWorkerTag: String, workName: String) fun beginUniqueWork(workerItem: WorkerItem) diff --git a/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerControllerImpl.kt b/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerControllerImpl.kt index c0ea8fcde6..3d65aa1914 100644 --- a/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerControllerImpl.kt +++ b/app/src/main/java/org/dhis2/data/service/workManager/WorkManagerControllerImpl.kt @@ -30,9 +30,7 @@ package org.dhis2.data.service.workManager import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData -import androidx.work.Constraints import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo @@ -56,38 +54,10 @@ class WorkManagerControllerImpl(private val workManager: WorkManager) : WorkMana } } - override fun syncDataForWorkers( - metadataWorkerTag: String, - dataWorkerTag: String, - workName: String, - ) { - val workerOneBuilder = OneTimeWorkRequest.Builder(SyncMetadataWorker::class.java) - workerOneBuilder - .addTag(metadataWorkerTag) - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) - - val workerTwoBuilder = OneTimeWorkRequest.Builder(SyncDataWorker::class.java) - workerTwoBuilder - .addTag(dataWorkerTag) - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) - - workManager - .beginUniqueWork(workName, ExistingWorkPolicy.KEEP, workerOneBuilder.build()) - .then(workerTwoBuilder.build()) - .enqueue() - } - override fun syncMetaDataForWorker(metadataWorkerTag: String, workName: String) { val workerOneBuilder = OneTimeWorkRequest.Builder(SyncMetadataWorker::class.java) workerOneBuilder .addTag(metadataWorkerTag) - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) workManager .beginUniqueWork(workName, ExistingWorkPolicy.KEEP, workerOneBuilder.build()) @@ -98,9 +68,6 @@ class WorkManagerControllerImpl(private val workManager: WorkManager) : WorkMana val workerTwoBuilder = OneTimeWorkRequest.Builder(SyncDataWorker::class.java) workerTwoBuilder .addTag(dataWorkerTag) - .setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) workManager .beginUniqueWork(workName, ExistingWorkPolicy.KEEP, workerTwoBuilder.build()) @@ -163,9 +130,6 @@ class WorkManagerControllerImpl(private val workManager: WorkManager) : WorkMana syncBuilder.apply { addTag(workerItem.workerName) - setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) workerItem.delayInSeconds?.let { setInitialDelay(it, TimeUnit.SECONDS) } @@ -218,9 +182,6 @@ class WorkManagerControllerImpl(private val workManager: WorkManager) : WorkMana syncBuilder.apply { addTag(workerItem.workerName) - setConstraints( - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - ) workerItem.data?.let { setInputData(it) } diff --git a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncPresenter.kt b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncPresenter.kt index 3c651178c5..8b22365b85 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncPresenter.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncPresenter.kt @@ -89,6 +89,9 @@ class GranularSyncPresenter( workerName = workerName() } + private val _serverAvailability = MutableLiveData() + val serverAvailability: LiveData = _serverAvailability + private fun loadSyncInfo(forcedState: State? = null) { viewModelScope.launch(dispatcher.io()) { val syncState = async { @@ -424,4 +427,15 @@ class GranularSyncPresenter( } } } + + fun checkServerAvailability() { + viewModelScope.launch { + try { + repository.checkServerAvailability() + _serverAvailability.value = true + } catch (error: RuntimeException) { + _serverAvailability.value = false + } + } + } } diff --git a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt index 59a0f0ef83..61ae035dd1 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/GranularSyncRepository.kt @@ -1,6 +1,7 @@ package org.dhis2.utils.granularsync import io.reactivex.Single +import kotlinx.coroutines.withContext import org.dhis2.R import org.dhis2.commons.bindings.categoryOptionCombo import org.dhis2.commons.bindings.countEventImportConflicts @@ -41,6 +42,7 @@ import org.dhis2.commons.sync.ConflictType import org.dhis2.commons.sync.SyncContext import org.dhis2.commons.sync.SyncStatusItem import org.dhis2.commons.sync.SyncStatusType +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.DhisProgramUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.State @@ -57,6 +59,7 @@ class GranularSyncRepository( private val dhisProgramUtils: DhisProgramUtils, private val periodUtils: DhisPeriodUtils, private val resourceManager: ResourceManager, + private val dispatcher: DispatcherProvider, ) { fun getUiState(forcedState: State? = null): SyncUiState { @@ -554,7 +557,7 @@ class GranularSyncRepository( val teiMainAttribute = tei.let { d2.teiMainAttributes(it.uid(), programUid) - } ?: emptyList() + } val label = teiMainAttribute.firstOrNull()?.let { (attributeName, value) -> "$attributeName: $value" @@ -980,6 +983,10 @@ class GranularSyncRepository( .map { it.displayName() } } } + + suspend fun checkServerAvailability() = withContext(dispatcher.io()) { + d2.systemInfoModule().ping().blockingGet() + } } fun List.sortedByState(): List { diff --git a/app/src/main/java/org/dhis2/utils/granularsync/SyncStatusDialog.kt b/app/src/main/java/org/dhis2/utils/granularsync/SyncStatusDialog.kt index 088f59c5b0..d054fec435 100644 --- a/app/src/main/java/org/dhis2/utils/granularsync/SyncStatusDialog.kt +++ b/app/src/main/java/org/dhis2/utils/granularsync/SyncStatusDialog.kt @@ -181,14 +181,18 @@ class SyncStatusDialog : BottomSheetDialogFragment(), GranularSyncContracts.View } private fun onSyncClick() { - when { - networkUtils.isOnline() -> syncGranular() - viewModel.canSendSMS() && - viewModel.isSMSEnabled(context?.showSMS() == true) -> syncSms() - - !networkUtils.isOnline() && - !viewModel.isSMSEnabled(context?.showSMS() == true) -> showSnackbar() + viewModel.serverAvailability.observe(viewLifecycleOwner) { isAvailable -> + val canSendSMS = viewModel.canSendSMS() + val isSMSEnabled = viewModel.isSMSEnabled(context?.showSMS() == true) + + when { + isAvailable -> syncGranular() + canSendSMS && isSMSEnabled -> syncSms() + else -> showSnackbar() + } } + + viewModel.checkServerAvailability() } private fun showSnackbar() { diff --git a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt index a000609fd8..9214df8b14 100644 --- a/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt +++ b/app/src/test/java/org/dhis2/utils/granularsync/GranularSyncPresenterTest.kt @@ -8,8 +8,12 @@ import androidx.lifecycle.Observer import androidx.work.WorkInfo import io.reactivex.Completable import io.reactivex.Single +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.dhis2.commons.Constants import org.dhis2.commons.sync.ConflictType import org.dhis2.commons.sync.SyncContext @@ -23,14 +27,18 @@ import org.hisp.dhis.android.core.common.ObjectWithUid import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.dataset.DataSet import org.hisp.dhis.android.core.dataset.DataSetElement +import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyList import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -57,6 +65,16 @@ class GranularSyncPresenterTest { private val context: Context = mock() private val syncContext: SyncContext = SyncContext.Global() + @Before + fun setUp() { + Dispatchers.setMain(testingDispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun `should return tracker program error state`() { val presenter = GranularSyncPresenter( @@ -463,4 +481,44 @@ class GranularSyncPresenterTest { verify(workManager).syncDataForWorker(Constants.DATA_NOW, Constants.INITIAL_SYNC) verify(workInfoObserver).onChanged(anyList()) } + + @Test + fun shouldCheckAvailableConnection() = runBlocking { + whenever(repository.checkServerAvailability()) doReturn "pong" + + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Global(), + workManager, + smsSyncProvider, + ) + + presenter.checkServerAvailability() + + assertTrue(presenter.serverAvailability.value!!) + } + + @Test + fun shouldCheckUnavailableConnection() = runBlocking { + whenever(repository.checkServerAvailability()) doThrow RuntimeException() + + val presenter = GranularSyncPresenter( + d2, + view, + repository, + trampolineSchedulerProvider, + testDispatcher, + SyncContext.Global(), + workManager, + smsSyncProvider, + ) + + presenter.checkServerAvailability() + + assertFalse(presenter.serverAvailability.value!!) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21bedb2746..fbc18b6836 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' designSystem = "0.2-20240426.091900-58" -dhis2sdk = "1.10.0-20240322.092247-38" +dhis2sdk = "1.10.0-20240426.151240-44" ruleEngine = "3.0.0-20240119.134348-12" expressionParser = "1.1.0-20240219.115041-14" appcompat = "1.6.1"