Skip to content

Commit

Permalink
Implement workaround for receiving SyncJobStatus (#2773)
Browse files Browse the repository at this point in the history
* Implement workaround for receiving SyncJobStatus

Signed-off-by: Elly Kitoto <[email protected]>

* Fix comment typo

Signed-off-by: Elly Kitoto <[email protected]>

* check status eqivalence instead of existence

* Resolve failing tests

Signed-off-by: Elly Kitoto <[email protected]>

---------

Signed-off-by: Elly Kitoto <[email protected]>
Co-authored-by: pld <[email protected]>
  • Loading branch information
ellykits and pld authored Sep 21, 2023
1 parent 679314f commit 94dd8da
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -64,7 +70,9 @@ constructor(
*/
suspend fun runOneTimeSync() = coroutineScope {
Timber.i("Running one time sync...")
Sync.oneTimeSync<AppSyncWorker>(context).handleSyncJobStatus(this)
Sync.oneTimeSync<AppSyncWorker>(context)
val uniqueWorkName = "${AppSyncWorker::class.java.name}-oneTimeSync"
handleSyncJobStatus(uniqueWorkName, this)
}

/**
Expand All @@ -75,20 +83,49 @@ constructor(
suspend fun schedulePeriodicSync(interval: Long = 15) = coroutineScope {
Timber.i("Scheduling periodic sync...")
Sync.periodicSync<AppSyncWorker>(
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<SyncJobStatus>.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<String>("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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 94dd8da

Please sign in to comment.