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 458845c14fd..0ca5046b740 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,7 +20,7 @@ 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 @@ -48,12 +48,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 +98,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldSuccessfullySyncSavedEvent() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) eventWithoutRegistrationRobot(composeTestRule) { @@ -118,6 +123,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldShowErrorWhenSyncEventFails() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareMalariaEventIntentAndLaunchActivity(ruleEventWithoutRegistration) eventWithoutRegistrationRobot(composeTestRule) { @@ -141,6 +148,8 @@ class SyncFlowTest : BaseTest() { @Test fun shouldSuccessfullySyncSavedDataSet() { + mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) + prepareFacilityDataSetIntentAndLaunchActivity(ruleDataSet) dataSetRobot { @@ -223,5 +232,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 aabd81897a4..2e5bee9767a 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 9a42713a10f..e05bbfe1f9a 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 00000000000..ed53c213589 --- /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 9a42713a10f..ecedce9eedc 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 2fd93e5577d..60ba0133611 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 c0ea8fcde6e..3d65aa19147 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/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java index 3aed5a82aac..2d77cda9928 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.java @@ -240,11 +240,9 @@ public void setProgram(@NonNull Program program) { private void setUpActivityTitle() { String activityTitle; if (eventCreationType == EventCreationType.REFERAL) { - activityTitle = program.displayName() + " - " + getString(R.string.referral); + activityTitle = getString(R.string.referral); } else { - activityTitle = eventUid == null ? - program.displayName() + " - " + resourceManager.formatWithEventLabel(R.string.new_event_label, programStageUid, 1, false) : program.displayName(); } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index 3371769c23c..a468985923c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -29,7 +29,6 @@ class DashboardViewModel( private val selectedEventUid = MutableLiveData() - val updateEnrollment = MutableLiveData(false) val showStatusErrorMessages = MutableLiveData(StatusChangeResultCode.CHANGED) private var _showFollowUpBar = MutableStateFlow(false) @@ -143,7 +142,7 @@ class DashboardViewModel( _showStatusBar.value = status _syncNeeded.value = true _state.value = State.TO_UPDATE - updateEnrollment.postValue(true) + fetchDashboardModel() } else { showStatusErrorMessages.postValue(result) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt index 27893cf19df..7928e186867 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt @@ -35,7 +35,6 @@ class TEIDataContracts { fun showProgramRuleErrorMessage() fun goToEventInitial(eventCreationType: EventCreationType, programStage: ProgramStage) - fun updateEnrollment(update: Boolean) fun displayOrgUnitSelectorForNewEvent(programUid: String, programStageUid: String) fun goToEventDetails( diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt index 806c1c589a4..b817c42e0d2 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt @@ -133,9 +133,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { with(dashboardViewModel) { eventUid().observe(viewLifecycleOwner, ::displayGenerateEvent) - updateEnrollment.observe(viewLifecycleOwner) { update -> - updateEnrollment(update) - } noEnrollmentSelected.observe(viewLifecycleOwner) { noEnrollmentSelected -> if (noEnrollmentSelected) { showLegacyCard(dashboardModel.value as DashboardTEIModel) @@ -383,7 +380,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { programStageUid, ), ) - presenter.fetchEvents(true) + presenter.fetchEvents() }, ).show(parentFragmentManager, SCHEDULING_DIALOG) } @@ -458,18 +455,18 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { override fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) = contractHandler.scheduleEvent(intent, options).observe(viewLifecycleOwner) { - updateEnrollment(true) + presenter.fetchEvents() } override fun openEventInitial(intent: Intent) = contractHandler.editEvent(intent).observe(viewLifecycleOwner) { - updateEnrollment(true) + presenter.fetchEvents() } override fun openEventCapture(intent: Intent) { if (dashboardActivity is TeiDashboardMobileActivity) { contractHandler.editEvent(intent).observe(viewLifecycleOwner) { - updateEnrollment(true) + presenter.fetchEvents() } } if (dashboardActivity is EventCaptureActivity) { @@ -570,13 +567,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { dashboardActivity.executeOnUIThread() } - override fun updateEnrollment(update: Boolean) { - if (update) { - presenter.fetchEvents(update) - dashboardViewModel.updateDashboard() - } - } - companion object { const val RC_EVENTS_COMPLETED = 1601 const val PREF_COMPLETED_EVENT = "COMPLETED_EVENT" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index 7216e851ba9..84dd97073a5 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -285,7 +285,7 @@ class TEIDataPresenter( val intent = Intent(view.context, ProgramStageSelectionActivity::class.java) intent.putExtras(bundle) contractHandler.createEvent(intent).observe(view.viewLifecycleOwner()) { - view.updateEnrollment(true) + fetchEvents() } } @@ -395,10 +395,8 @@ class TEIDataPresenter( return options?.let { eventCreationOptionsMapper.mapToEventsByStage(it) } ?: emptyList() } - fun fetchEvents(updateEnrollment: Boolean) { - if (updateEnrollment) { - groupingProcessor.onNext(dashboardRepository.getGrouping()) - } + fun fetchEvents() { + groupingProcessor.onNext(dashboardRepository.getGrouping()) } fun getEnrollment(): Enrollment? { 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 3c651178c55..8b22365b852 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 59a0f0ef833..61ae035dd14 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 088f59c5b03..5f3581c6226 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,23 @@ 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() { + dismiss() + onNoConnectionListener?.onNoConnection() } private fun showSnackbar() { diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt index c40aef0da8f..1e0c57fe87a 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/DashboardViewModelTest.kt @@ -120,13 +120,13 @@ class DashboardViewModelTest { whenever(repository.updateEnrollmentStatus(any(), any())) doReturn Observable.just( StatusChangeResultCode.CHANGED, ) + whenever(mockedEnrollmentModel.currentEnrollment) doReturn mockedCompletedEnrollment updateEnrollmentStatus(EnrollmentStatus.COMPLETED) testingDispatcher.scheduler.advanceUntilIdle() verify(repository).updateEnrollmentStatus("enrollmentUid", EnrollmentStatus.COMPLETED) assertTrue(showStatusBar.value == EnrollmentStatus.COMPLETED) assertTrue(syncNeeded.value) assertTrue(state.value == State.TO_UPDATE) - assertTrue(updateEnrollment.value == true) } } @@ -187,4 +187,10 @@ class DashboardViewModelTest { on { aggregatedSyncState() } doReturn State.SYNCED on { status() } doReturn EnrollmentStatus.ACTIVE } + + private val mockedCompletedEnrollment: Enrollment = mock { + on { uid() } doReturn "enrollmentUid" + on { aggregatedSyncState() } doReturn State.TO_UPDATE + on { status() } doReturn EnrollmentStatus.COMPLETED + } } 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 a000609fd85..9214df8b142 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/compose-table/src/main/java/org/dhis2/composetable/ui/MultiOptionSelector.kt b/compose-table/src/main/java/org/dhis2/composetable/ui/MultiOptionSelector.kt index a7e8bf5572a..37a55f5ddf6 100644 --- a/compose-table/src/main/java/org/dhis2/composetable/ui/MultiOptionSelector.kt +++ b/compose-table/src/main/java/org/dhis2/composetable/ui/MultiOptionSelector.kt @@ -28,6 +28,7 @@ fun MultiOptionSelector( }, title = title, noResultsFoundString = stringResource(R.string.no_results_found), + searchToFindMoreString = stringResource(id = R.string.search_to_see_more), doneButtonText = stringResource(id = R.string.done), onItemsSelected = { checkBoxes -> val checkedCodes = checkBoxes diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselLayoutManager.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselLayoutManager.kt index f41d96715bd..0913f289d9c 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselLayoutManager.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselLayoutManager.kt @@ -2,6 +2,7 @@ package org.dhis2.maps.carousel import android.content.Context import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView class CarouselLayoutManager( context: Context, @@ -22,4 +23,23 @@ class CarouselLayoutManager( fun setEnabled(enabled: Boolean) { this.enabled = enabled } + + override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { + super.onLayoutChildren(recycler, state) + alignItemsToBottom() + } + + private fun alignItemsToBottom() { + val parentBottom = height - paddingBottom + for (i in 0 until childCount) { + val child = getChildAt(i) + child?.let { + val childBottom = it.bottom + val offset = parentBottom - childBottom + if (offset > 0) { + it.offsetTopAndBottom(offset) + } + } + } + } } diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselTeiHolder.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselTeiHolder.kt index 7f211082553..e688b9afc65 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselTeiHolder.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/carousel/CarouselTeiHolder.kt @@ -4,6 +4,8 @@ import android.view.View import android.widget.Toast import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.floatingactionbutton.FloatingActionButton.OnVisibilityChangedListener import org.dhis2.bindings.getEnrollmentIconsData import org.dhis2.bindings.hasFollowUp import org.dhis2.bindings.paintAllEnrollmentIcons @@ -57,7 +59,7 @@ class CarouselTeiHolder( mapNavigateFab.visibility = if (data.shouldShowNavigationButton()) { View.VISIBLE } else { - View.GONE + View.INVISIBLE } executePendingBindings() } @@ -157,6 +159,11 @@ class CarouselTeiHolder( override fun hideNavigateButton() { dataModel?.setShowNavigationButton(false) - binding.mapNavigateFab.hide() + binding.mapNavigateFab.hide(object : OnVisibilityChangedListener() { + override fun onHidden(fab: FloatingActionButton?) { + super.onHidden(fab) + binding.mapNavigateFab.visibility = View.INVISIBLE + } + }) } } diff --git a/dhis2_android_maps/src/main/res/layout/item_carousel_tei.xml b/dhis2_android_maps/src/main/res/layout/item_carousel_tei.xml index e843927dddd..bf40adcbf99 100644 --- a/dhis2_android_maps/src/main/res/layout/item_carousel_tei.xml +++ b/dhis2_android_maps/src/main/res/layout/item_carousel_tei.xml @@ -44,40 +44,38 @@ type="String" /> - + android:layout_height="wrap_content"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"> - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 510f6acb03a..a3fcea4135d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,15 +3,15 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" vCode = "130" -vName = "3.0-RC1" +vName = "3.0" kotlinCompilerExtensionVersion = "1.5.6" gradle = "8.2.2" kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "0.2-20240426.091900-58" -dhis2sdk = "1.10.0-20240322.092247-38" +designSystem = "0.2-20240430.062706-60" +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" @@ -230,7 +230,6 @@ dispatcher-dispatchCore = { group = "com.rickbusarow.dispatch", name = "dispatch dispatcher-dispatchEspresso = { group = "com.rickbusarow.dispatch", name = "dispatch-android-espresso", version = "1.0.0-beta10" } deprecated-autoValueParcel = { group = "com.ryanharter.auto.value", name = "auto-value-parcel", version.ref = "autovalueparcel" } #TODO: Remove alongside AutoValue [plugins] - [bundles] uicomponents-implementation = ["androidx-coreKtx", "androidx-appcompat", "androidx-activity-compose", "androidx-material3", "google-material", "lottie-compose", "dhis2-mobile-designsystem"] uicomponents-api = ["dhis2-mobile-designsystem", "androidx-compose-constraintlayout", "androidx-compose-preview", "androidx-compose-ui", "androidx-compose-viewbinding", "androidx-compose-livedata", "google-material-themeadapter", "google-material3-themeadapter"]