Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use preference datastore to monitor syncJobStatus. #2142

Merged
merged 71 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
f0a884e
Update syncJobStatus to preference datastore.
Aug 23, 2023
7c19a83
add fhirdatastore file.
Aug 23, 2023
f915c21
code cleanup.
Aug 23, 2023
55d9a6d
unit tests
Aug 23, 2023
339d808
unit test.
Aug 25, 2023
ce473b8
Cancel ongoing coroutine job before launching new oneTimeSync in demo…
Aug 25, 2023
0248300
Fix android test, pass context instead of datastore in fhir engine co…
Aug 28, 2023
95a486a
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Aug 28, 2023
295c915
fix tests.
Aug 28, 2023
5a0f317
update kotlin doc.
Aug 28, 2023
d2e2644
combine work state with syncjobstatus
Aug 29, 2023
1e663f9
Address review comments.
Sep 6, 2023
c5b60d7
Address review comments.
Sep 6, 2023
d3568e0
Update OneTimeSyncState and PeriodicSyncState.
Oct 5, 2023
c9c7437
missing file.
Oct 5, 2023
399db5e
Merge branch 'master' into sp/issue-2119-datastore
Oct 10, 2023
f0f44b2
Merge branch 'master' into sp/issue-2119-datastore
Oct 10, 2023
66cc4c6
Stores sync job terminal state in the datastore.
Oct 23, 2023
658525b
Address review comments.
Oct 25, 2023
1240cf4
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 25, 2023
b414a68
Address review comments.
Oct 25, 2023
4b77728
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 26, 2023
9ecb215
Address review comments.
Oct 26, 2023
160b53e
Address review comments.
Oct 26, 2023
5423ea8
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 26, 2023
3b0adce
code clean up.
Oct 27, 2023
6b9e02e
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 27, 2023
1750d17
Code clean up.
Oct 30, 2023
3e68201
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 30, 2023
a2ef95e
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Oct 31, 2023
d117cb7
Fix test.
Nov 1, 2023
a08f421
Fix test
Nov 1, 2023
85e13f2
Fix tests.
Nov 2, 2023
568cdf0
Address review comments.
Nov 21, 2023
baf49f6
Address review comments.
Nov 21, 2023
17fab13
Remove DataStoreUtil.
Nov 21, 2023
79b4e7c
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Nov 21, 2023
465b208
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Nov 28, 2023
071561d
Address review comments.
Nov 29, 2023
1a917e5
fix crash.
Nov 30, 2023
860044e
refactoring names
Nov 30, 2023
83f4eeb
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Nov 30, 2023
131682e
spotless apply
Nov 30, 2023
7eb0e88
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 1, 2023
1f47b8a
private access specifier
Dec 1, 2023
b2fa092
Address review comments.
Dec 5, 2023
ca4b039
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 5, 2023
cc21f6b
Address review comment.
Dec 6, 2023
90fcbf7
address review comments.
Dec 6, 2023
e0209ff
fix tests.
Dec 6, 2023
9db67a8
review comment.
Dec 7, 2023
fd3ae6f
Removing existing unwanted call.
Dec 7, 2023
0f33853
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 11, 2023
523806f
Address review comments.
Dec 11, 2023
2c2984b
Add test to assert succedded sync state.
Dec 13, 2023
6ba4c4a
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 13, 2023
29750fd
failed state was nit emitted.
Dec 14, 2023
61222c5
Update api doc.
Dec 14, 2023
b845ba4
Address review comments.
Dec 15, 2023
fe1c64f
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 15, 2023
8d49119
Merge branch 'master' into sp/issue-2119-datastore
Dec 18, 2023
35f86a1
Review address comments.
Dec 18, 2023
68ccb65
Address review comments.
Dec 19, 2023
8c1aa08
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 19, 2023
d806e60
Address review comment.
Dec 26, 2023
34bc508
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 26, 2023
9703722
Address review comments.
Dec 26, 2023
adbb227
Address review comment.
Dec 27, 2023
6890400
Merge branch 'master' into sp/issue-2119-datastore
santosh-pingle Dec 27, 2023
018920f
Address review comments.
Jan 2, 2024
69e4cfc
remove unwanted file.
Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.work.Constraints
import com.google.android.fhir.demo.data.DemoFhirSyncWorker
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.PeriodicSyncJobStatus
import com.google.android.fhir.sync.RepeatInterval
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.SyncJobStatus
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
Expand All @@ -44,10 +46,14 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
val lastSyncTimestampLiveData: LiveData<String>
get() = _lastSyncTimestampLiveData

private val _pollState = MutableSharedFlow<SyncJobStatus>()
val pollState: Flow<SyncJobStatus>
private val _pollState = MutableSharedFlow<CurrentSyncJobStatus>()
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
val pollState: Flow<CurrentSyncJobStatus>
get() = _pollState

private val _pollPeriodicSyncJobStatus = MutableSharedFlow<PeriodicSyncJobStatus>()
val pollPeriodicSyncJobStatus: Flow<PeriodicSyncJobStatus>
get() = _pollPeriodicSyncJobStatus

init {
viewModelScope.launch {
Sync.periodicSync<DemoFhirSyncWorker>(
Expand All @@ -59,16 +65,23 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
),
)
.shareIn(this, SharingStarted.Eagerly, 10)
.collect { _pollState.emit(it) }
.collect { _pollPeriodicSyncJobStatus.emit(it) }
}
}

private var oneTimeSyncJob: Job? = null

fun triggerOneTimeSync() {
viewModelScope.launch {
Sync.oneTimeSync<DemoFhirSyncWorker>(getApplication())
.shareIn(this, SharingStarted.Eagerly, 10)
.collect { _pollState.emit(it) }
}
// Cancels any ongoing sync job before starting a new one. Since this function may be called
// more than once, not canceling the ongoing job could result in the creation of multiple jobs
// that emit the same object.
oneTimeSyncJob?.cancel()
omarismail94 marked this conversation as resolved.
Show resolved Hide resolved
oneTimeSyncJob =
viewModelScope.launch {
Sync.oneTimeSync<DemoFhirSyncWorker>(getApplication())
.shareIn(this, SharingStarted.Eagerly, 0)
.collect { result -> result.let { _pollState.emit(it) } }
}
}

/** Emits last sync time. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.demo.PatientListViewModel.PatientListViewModelFactory
import com.google.android.fhir.demo.databinding.FragmentPatientListBinding
import com.google.android.fhir.sync.CurrentSyncJobStatus
import com.google.android.fhir.sync.LastSyncJobStatus
import com.google.android.fhir.sync.SyncJobStatus
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -105,6 +107,7 @@ class PatientListFragment : Fragment() {

searchView = binding.search
topBanner = binding.syncStatusContainer.linearLayoutSyncStatus
topBanner.visibility = View.GONE
syncStatus = binding.syncStatusContainer.tvSyncingStatus
syncPercent = binding.syncStatusContainer.tvSyncingPercent
syncProgress = binding.syncStatusContainer.progressSyncing
Expand Down Expand Up @@ -155,28 +158,44 @@ class PatientListFragment : Fragment() {
mainActivityViewModel.pollState.collect {
Timber.d("onViewCreated: pollState Got status $it")
when (it) {
is SyncJobStatus.Started -> {
Timber.i("Sync: ${it::class.java.simpleName}")
is CurrentSyncJobStatus.Running -> {
Timber.i("Sync: ${it::class.java.simpleName} with data ${it.inProgressSyncJob}")
fadeInTopBanner(it)
}
is SyncJobStatus.InProgress -> {
Timber.i("Sync: ${it::class.java.simpleName} with data $it")
fadeInTopBanner(it)
is CurrentSyncJobStatus.Succeeded -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.succeededSyncJob.timestamp}")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
fadeOutTopBanner(it)
}
is SyncJobStatus.Finished -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}")
is CurrentSyncJobStatus.Failed -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.failedSyncJob.timestamp}")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
fadeOutTopBanner(it)
}
is SyncJobStatus.Failed -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}")
is CurrentSyncJobStatus.Enqueued -> {
Timber.i("Sync: Enqueued")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
fadeOutTopBanner(it)
}
else -> {
Timber.i("Sync: Unknown state.")
CurrentSyncJobStatus.Cancelled -> TODO()
}
}
}

lifecycleScope.launch {
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
mainActivityViewModel.pollPeriodicSyncJobStatus.collect {
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
Timber.d("onViewCreated: pollState Got status $it")
if (it.currentSyncJobStatus is CurrentSyncJobStatus.Running) {
Timber.i(
"Sync: ${it.currentSyncJobStatus::class.java.simpleName} with data ${it.currentSyncJobStatus}",
)
fadeInTopBanner(it.currentSyncJobStatus)
} else {
it.lastSyncJobStatus?.let {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
fadeOutTopBanner(it)
Expand Down Expand Up @@ -213,7 +232,7 @@ class PatientListFragment : Fragment() {
.navigate(PatientListFragmentDirections.actionPatientListToAddPatientFragment())
}

private fun fadeInTopBanner(state: SyncJobStatus) {
private fun fadeInTopBanner(state: CurrentSyncJobStatus) {
if (topBanner.visibility != View.VISIBLE) {
syncStatus.text = resources.getString(R.string.syncing).uppercase()
syncPercent.text = ""
Expand All @@ -222,25 +241,35 @@ class PatientListFragment : Fragment() {
topBanner.visibility = View.VISIBLE
val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_in)
topBanner.startAnimation(animation)
} else if (state is SyncJobStatus.InProgress) {
} else if (
state is CurrentSyncJobStatus.Running && state.inProgressSyncJob is SyncJobStatus.InProgress
) {
val inProgressState = state.inProgressSyncJob as? SyncJobStatus.InProgress
val progress =
state
.let { it.completed.toDouble().div(it.total) }
.let { if (it.isNaN()) 0.0 else it }
.times(100)
.roundToInt()
"$progress% ${state.syncOperation.name.lowercase()}ed".also { syncPercent.text = it }
syncProgress.progress = progress
inProgressState
?.let { it.completed.toDouble().div(it.total) }
?.let { if (it.isNaN()) 0.0 else it }
?.times(100)
?.roundToInt()
"$progress% ${inProgressState?.syncOperation?.name?.lowercase()}ed"
.also { syncPercent.text = it }
syncProgress.progress = progress ?: 0
}
}

private fun fadeOutTopBanner(state: SyncJobStatus) {
if (state is SyncJobStatus.Finished) syncPercent.text = ""
syncProgress.visibility = View.GONE
private fun fadeOutTopBanner(state: CurrentSyncJobStatus) {
fadeOutTopBanner(state::class.java.simpleName.uppercase())
}

private fun fadeOutTopBanner(state: LastSyncJobStatus) {
fadeOutTopBanner(state::class.java.simpleName.uppercase())
}

private fun fadeOutTopBanner(statusText: String) {
syncPercent.text = ""
syncProgress.visibility = View.GONE
if (topBanner.visibility == View.VISIBLE) {
"${resources.getString(R.string.sync).uppercase()} ${state::class.java.simpleName.uppercase()}"
.also { syncStatus.text = it }
"${resources.getString(R.string.sync).uppercase()} $statusText".also { syncStatus.text = it }

val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_out)
topBanner.startAnimation(animation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.benchmark.junit4.measureRepeated
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import androidx.work.testing.TestListenableWorkerBuilder
Expand Down Expand Up @@ -138,7 +139,12 @@ class FhirSyncWorkerBenchmark {
private fun oneTimeSync(numberPatients: Int, numberObservations: Int, numberEncounters: Int) =
runBlocking {
val context: Context = ApplicationProvider.getApplicationContext()
val worker = TestListenableWorkerBuilder<BenchmarkTestOneTimeSyncWorker>(context).build()
val inputData =
Data.Builder()
.putString("sync_status_preferences_datastore_key", "BenchmarkTestOneTimeSyncWorker")
.build()
val worker =
TestListenableWorkerBuilder<BenchmarkTestOneTimeSyncWorker>(context, inputData).build()
setupMockServerDispatcher(numberPatients, numberObservations, numberEncounters)
benchmarkRule.measureRepeated {
runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.sync.upload.UploadStrategy
import com.google.android.fhir.testing.TestDataSourceImpl
import com.google.android.fhir.testing.TestDownloadManagerImpl
import com.google.android.fhir.testing.TestFailingDatasource
import com.google.android.fhir.testing.TestFhirEngineImpl
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.TimeUnit
Expand All @@ -36,18 +37,21 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith

/**
* Note : If you are running these tests on a local machine in Android Studio, make sure to clear
* the storage and cache of the `com.google.android.fhir.test` app on the emulator/device before
* running each test individually.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@Ignore("Flaky/fails due to https://github.com/google/android-fhir/issues/2046")
class SyncInstrumentedTest {

private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext

class TestSyncWorker(appContext: Context, workerParams: WorkerParameters) :
open class TestSyncWorker(appContext: Context, workerParams: WorkerParameters) :
FhirSyncWorker(appContext, workerParams) {

override fun getFhirEngine(): FhirEngine = TestFhirEngineImpl
Expand All @@ -61,15 +65,20 @@ class SyncInstrumentedTest {
override fun getUploadStrategy(): UploadStrategy = UploadStrategy.AllChangesSquashedBundlePut
}

class TestSyncWorkerForDownloadFailing(appContext: Context, workerParams: WorkerParameters) :
TestSyncWorker(appContext, workerParams) {
override fun getDataSource(): DataSource = TestFailingDatasource
}

@Test
fun oneTime_worker_runs() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val workManager = WorkManager.getInstance(context)
runBlocking {
Sync.oneTimeSync<TestSyncWorker>(context = context)
.transformWhile {
emit(it is SyncJobStatus.Finished)
it !is SyncJobStatus.Finished
emit(it is CurrentSyncJobStatus.Succeeded)
it !is CurrentSyncJobStatus.Succeeded
}
.shareIn(this, SharingStarted.Eagerly, 5)
}
Expand All @@ -78,6 +87,101 @@ class SyncInstrumentedTest {
.isEqualTo(WorkInfo.State.SUCCEEDED)
}

@Test
fun oneTime_worker_syncState() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val states = mutableListOf<CurrentSyncJobStatus>()
runBlocking {
Sync.oneTimeSync<TestSyncWorker>(context = context)
.transformWhile {
states.add(it)
emit(it is CurrentSyncJobStatus.Succeeded)
it !is CurrentSyncJobStatus.Succeeded
}
.shareIn(this, SharingStarted.Eagerly, 5)
}
assertThat(states.first()).isInstanceOf(CurrentSyncJobStatus.Running::class.java)
assertThat(states.last()).isInstanceOf(CurrentSyncJobStatus.Succeeded::class.java)
}

@Test
fun oneTime_worker_failedSyncState() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val states = mutableListOf<CurrentSyncJobStatus>()
runBlocking {
Sync.oneTimeSync<TestSyncWorkerForDownloadFailing>(context = context)
.transformWhile {
states.add(it)
emit(it is CurrentSyncJobStatus.Failed)
it !is CurrentSyncJobStatus.Failed
}
.shareIn(this, SharingStarted.Eagerly, 5)
}
assertThat(states.first()).isInstanceOf(CurrentSyncJobStatus.Running::class.java)
assertThat(states.last()).isInstanceOf(CurrentSyncJobStatus.Failed::class.java)
}

@Test
fun periodic_worker_periodicSyncState() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val states = mutableListOf<PeriodicSyncJobStatus>()
// run and wait for periodic worker to finish
runBlocking {
Sync.periodicSync<TestSyncWorker>(
context = context,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES),
),
)
.transformWhile {
states.add(it)
emit(it)
it.currentSyncJobStatus !is CurrentSyncJobStatus.Enqueued
}
.shareIn(this, SharingStarted.Eagerly, 5)
}

assertThat(states.first().currentSyncJobStatus)
.isInstanceOf(CurrentSyncJobStatus.Running::class.java)
assertThat(states.first().lastSyncJobStatus).isNull()
assertThat(states.last().currentSyncJobStatus)
.isInstanceOf(CurrentSyncJobStatus.Enqueued::class.java)
assertThat(states.last().lastSyncJobStatus)
.isInstanceOf(LastSyncJobStatus.Succeeded::class.java)
}

@Test
fun periodic_worker_failedPeriodicSyncState() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
val states = mutableListOf<PeriodicSyncJobStatus>()
// run and wait for periodic worker to finish
runBlocking {
Sync.periodicSync<TestSyncWorkerForDownloadFailing>(
context = context,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES),
),
)
.transformWhile {
states.add(it)
emit(it)
it.currentSyncJobStatus !is CurrentSyncJobStatus.Enqueued
}
.shareIn(this, SharingStarted.Eagerly, 5)
}

assertThat(states.first().currentSyncJobStatus)
.isInstanceOf(CurrentSyncJobStatus.Running::class.java)
assertThat(states.first().lastSyncJobStatus).isNull()
assertThat(states.last().currentSyncJobStatus)
.isInstanceOf(CurrentSyncJobStatus.Enqueued::class.java)
assertThat(states.last().lastSyncJobStatus).isInstanceOf(LastSyncJobStatus.Failed::class.java)
}

@Test
fun periodic_worker_still_queued_to_run_after_oneTime_worker_started() {
WorkManagerTestInitHelper.initializeTestWorkManager(context)
Expand All @@ -94,7 +198,7 @@ class SyncInstrumentedTest {
)
.transformWhile {
emit(it)
it !is SyncJobStatus.Finished
it.currentSyncJobStatus !is CurrentSyncJobStatus.Enqueued
}
.shareIn(this, SharingStarted.Eagerly, 5)
}
Expand All @@ -110,7 +214,7 @@ class SyncInstrumentedTest {
Sync.oneTimeSync<TestSyncWorker>(context = context)
.transformWhile {
emit(it)
it !is SyncJobStatus.Finished
it !is CurrentSyncJobStatus.Succeeded
}
.shareIn(this, SharingStarted.Eagerly, 5)
}
Expand Down
Loading
Loading