diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt index b2837f4358..c2f788b6d0 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/SyncBroadcaster.kt @@ -17,8 +17,12 @@ package org.smartregister.fhircore.engine.sync import android.content.Context +import androidx.lifecycle.asFlow import androidx.work.Constraints import androidx.work.NetworkType +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.hasKeyWithValueOfType import com.google.android.fhir.FhirEngine import com.google.android.fhir.sync.PeriodicSyncConfiguration import com.google.android.fhir.sync.RepeatInterval @@ -31,10 +35,12 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry @@ -64,7 +70,9 @@ constructor( */ suspend fun runOneTimeSync() = coroutineScope { Timber.i("Running one time sync...") - Sync.oneTimeSync(context).handleSyncJobStatus(this) + Sync.oneTimeSync(context) + val uniqueWorkName = "${AppSyncWorker::class.java.name}-oneTimeSync" + handleSyncJobStatus(uniqueWorkName, this) } /** @@ -75,20 +83,49 @@ constructor( suspend fun schedulePeriodicSync(interval: Long = 15) = coroutineScope { Timber.i("Scheduling periodic sync...") Sync.periodicSync( - context = context, - periodicSyncConfiguration = - PeriodicSyncConfiguration( - syncConstraints = - Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), - repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES), - ), - ) - .handleSyncJobStatus(this) + context = context, + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(), + repeat = RepeatInterval(interval = interval, timeUnit = TimeUnit.MINUTES), + ), + ) + val uniqueWorkName = "${AppSyncWorker::class.java.name}-periodicSync" + handleSyncJobStatus(uniqueWorkName, this) } - private fun Flow.handleSyncJobStatus(coroutineScope: CoroutineScope) { - this.onEach { - syncListenerManager.onSyncListeners.forEach { onSyncListener -> onSyncListener.onSync(it) } + // TODO This serves as a workaround to fix issue with getting SyncJobStatus.Finished + // TODO Refactor once https://github.com/google/android-fhir/pull/2142 is merged + private fun handleSyncJobStatus(uniqueWorkName: String, coroutineScope: CoroutineScope) { + WorkManager.getInstance(context) + .getWorkInfosForUniqueWorkLiveData(uniqueWorkName) + .asFlow() + .flatMapConcat { it.asFlow() } + .mapNotNull { it } + .onEach { workInfo -> + // PeriodSync doesn't return state. It's enqueued instead. Finish the sync as workaround. + if (workInfo.state == WorkInfo.State.ENQUEUED) { + syncListenerManager.onSyncListeners.forEach { onSyncListener -> + onSyncListener.onSync(SyncJobStatus.Finished()) + } + } else { + val data = + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + workInfo.outputData + } else workInfo.progress + data + .takeIf { it.keyValueMap.isNotEmpty() && it.hasKeyWithValueOfType("StateType") } + ?.let { + val state = it.getString("StateType")!! + val stateData = it.getString("State") + val syncJobStatus = + Sync.gson.fromJson(stateData, Class.forName(state)) as SyncJobStatus + syncListenerManager.onSyncListeners.forEach { onSyncListener -> + onSyncListener.onSync(syncJobStatus) + } + } + } } .catch { throwable -> Timber.e("Encountered an error during sync:", throwable) } .shareIn(coroutineScope, SharingStarted.Eagerly, 1) diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt index f134a98382..83b9d34123 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/main/AppMainActivityTest.kt @@ -90,10 +90,12 @@ class AppMainActivityTest : ActivityRobolectricTest() { @Test fun testOnSyncWithSyncStateInProgress() { val viewModel = appMainActivity.appMainViewModel + val initialSyncTime = viewModel.appMainUiState.value.lastSyncTime + appMainActivity.onSync(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD)) - // Timestamp will only updated for states Glitch, Finished or Failed. Defaults to empty - Assert.assertTrue(viewModel.appMainUiState.value.lastSyncTime.isEmpty()) + // Timestamp will only updated for Finished. + Assert.assertEquals(initialSyncTime, viewModel.appMainUiState.value.lastSyncTime) } @Test @@ -102,33 +104,31 @@ class AppMainActivityTest : ActivityRobolectricTest() { val timestamp = "2022-05-19" viewModel.sharedPreferencesHelper.write(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, timestamp) + val initialTimestamp = viewModel.appMainUiState.value.lastSyncTime val syncJobStatus = SyncJobStatus.Glitch(exceptions = emptyList()) - val syncJobStatusTimestamp = syncJobStatus.timestamp appMainActivity.onSync(syncJobStatus) // Timestamp last sync timestamp not updated - Assert.assertNotEquals( + Assert.assertEquals( + initialTimestamp, viewModel.appMainUiState.value.lastSyncTime, - viewModel.formatLastSyncTimestamp(syncJobStatusTimestamp), ) } @Test - fun testOnSyncWithSyncStateFailedRendersUpdatedTimestampOnMainUi() { + fun testOnSyncWithSyncStateFailedDoesNotUpdateTimestamp() { val viewModel = appMainActivity.appMainViewModel viewModel.sharedPreferencesHelper.write( SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, "2022-05-19", ) + val initialTimestamp = viewModel.appMainUiState.value.lastSyncTime val syncJobStatus = SyncJobStatus.Failed(listOf()) appMainActivity.onSync(syncJobStatus) - // Timestamp not update if status is Failed - Assert.assertNotEquals( - appMainActivity.appMainViewModel.formatLastSyncTimestamp(syncJobStatus.timestamp), - viewModel.appMainUiState.value.lastSyncTime, - ) + // Timestamp not update if status is Failed. Initial timestamp remains the same + Assert.assertEquals(initialTimestamp, viewModel.appMainUiState.value.lastSyncTime) } @Test