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

Refactor Sync API #1578

Merged
merged 19 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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 @@ -23,7 +23,7 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineConfiguration
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.ServerConfiguration
import com.google.android.fhir.demo.data.FhirPeriodicSyncWorker
import com.google.android.fhir.demo.data.FhirSyncWorker
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.remote.HttpLogger
import timber.log.Timber
Expand Down Expand Up @@ -52,7 +52,7 @@ class FhirApplication : Application() {
)
)
)
Sync.oneTimeSync<FhirPeriodicSyncWorker>(this)
Sync.oneTimeSync<FhirSyncWorker>(this)
}

private fun constructFhirEngine(): FhirEngine {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.fhir.demo.data.FhirSyncWorker
import com.google.android.fhir.demo.databinding.ActivityMainBinding
import com.google.android.fhir.sync.Sync

const val MAX_RESOURCE_COUNT = 20

Expand Down Expand Up @@ -78,7 +80,7 @@ class MainActivity : AppCompatActivity() {
private fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sync -> {
viewModel.poll()
Sync.oneTimeSync<FhirSyncWorker>(this)
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
true
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Google LLC
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,49 +18,45 @@ package com.google.android.fhir.demo

import android.app.Application
import android.text.format.DateFormat
import android.text.format.DateFormat.is24HourFormat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.work.Constraints
import com.google.android.fhir.demo.data.FhirPeriodicSyncWorker
import com.google.android.fhir.demo.data.FhirSyncWorker
import com.google.android.fhir.sync.PeriodicSyncConfiguration
import com.google.android.fhir.sync.RepeatInterval
import com.google.android.fhir.sync.State
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.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

/** View model for [MainActivity]. */
@OptIn(InternalCoroutinesApi::class)
class MainActivityViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
private val _lastSyncTimestampLiveData = MutableLiveData<String>()
val lastSyncTimestampLiveData: LiveData<String>
get() = _lastSyncTimestampLiveData

private val job = Sync.basicSyncJob(application.applicationContext)
private val _pollState = MutableSharedFlow<State>()
val pollState: Flow<State>
private val _pollState = MutableSharedFlow<SyncJobStatus>()
val pollState: Flow<SyncJobStatus>
get() = _pollState

init {
poll()
}

/** Requests periodic sync. */
fun poll() {
viewModelScope.launch {
job.poll(
Sync.periodicSync<FhirSyncWorker>(
application.applicationContext,
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES)
),
FhirPeriodicSyncWorker::class.java
)
)
.collect { _pollState.emit(it) }
}
Expand All @@ -73,7 +69,7 @@ class MainActivityViewModel(application: Application, private val state: SavedSt
if (DateFormat.is24HourFormat(getApplication())) formatString24 else formatString12
)
_lastSyncTimestampLiveData.value =
job.lastSyncTimestamp()?.toLocalDateTime()?.format(formatter) ?: ""
Sync.getLastSyncTimestamp(getApplication())?.toLocalDateTime()?.format(formatter) ?: ""
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ 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.State
import kotlinx.coroutines.flow.collect
import com.google.android.fhir.sync.SyncJobStatus
import kotlinx.coroutines.launch
import timber.log.Timber

Expand All @@ -63,10 +62,9 @@ class PatientListFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
_binding = FragmentPatientListBinding.inflate(inflater, container, false)
val view = binding.root
return view
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand All @@ -91,13 +89,10 @@ class PatientListFragment : Fragment() {
}
)

patientListViewModel.liveSearchedPatients.observe(
viewLifecycleOwner,
{
Timber.d("Submitting ${it.count()} patient records")
adapter.submitList(it)
}
)
patientListViewModel.liveSearchedPatients.observe(viewLifecycleOwner) {
Timber.d("Submitting ${it.count()} patient records")
adapter.submitList(it)
}

patientListViewModel.patientCount.observe(
viewLifecycleOwner,
Expand Down Expand Up @@ -127,8 +122,7 @@ class PatientListFragment : Fragment() {
}
}
requireActivity()
.onBackPressedDispatcher
.addCallback(
.onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Expand All @@ -153,24 +147,24 @@ class PatientListFragment : Fragment() {
mainActivityViewModel.pollState.collect {
Timber.d("onViewCreated: pollState Got status $it")
when (it) {
is State.Started -> {
is SyncJobStatus.Started -> {
Timber.i("Sync: ${it::class.java.simpleName}")
fadeInTopBanner()
}
is State.InProgress -> {
is SyncJobStatus.InProgress -> {
Timber.i("Sync: ${it::class.java.simpleName} with ${it.resourceType?.name}")
fadeInTopBanner()
}
is State.Finished -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.result.timestamp}")
is SyncJobStatus.Finished -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
// mainActivityViewModel.updateLastSyncTimestamp()
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
fadeOutTopBanner(it)
}
is State.Failed -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.result.timestamp}")
is SyncJobStatus.Failed -> {
Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}")
patientListViewModel.searchPatientsByName(searchView.query.toString().trim())
mainActivityViewModel.updateLastSyncTimestamp()
// mainActivityViewModel.updateLastSyncTimestamp()
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
fadeOutTopBanner(it)
}
else -> Timber.i("Sync: Unknown state.")
Expand Down Expand Up @@ -215,7 +209,7 @@ class PatientListFragment : Fragment() {
}
}

private fun fadeOutTopBanner(state: State) {
private fun fadeOutTopBanner(state: SyncJobStatus) {
if (topBanner.visibility == View.VISIBLE) {
syncStatus.text = state::class.java.simpleName.uppercase()
val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_out)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.google.android.fhir.sync.AcceptLocalConflictResolver
import com.google.android.fhir.sync.DownloadWorkManager
import com.google.android.fhir.sync.FhirSyncWorker

class FhirPeriodicSyncWorker(appContext: Context, workerParams: WorkerParameters) :
class FhirSyncWorker(appContext: Context, workerParams: WorkerParameters) :
FhirSyncWorker(appContext, workerParams) {

override fun getDownloadWorkManager(): DownloadWorkManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import androidx.work.workDataOf
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.OffsetDateTimeTypeAdapter
import com.google.android.fhir.sync.Result.Error
import com.google.android.fhir.sync.Result.Success
import com.google.android.fhir.sync.download.DownloaderImpl
import com.google.android.fhir.sync.upload.BundleUploader
import com.google.android.fhir.sync.upload.LocalChangesPaginator
Expand Down Expand Up @@ -75,15 +73,15 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
)
)

val flow = MutableSharedFlow<State>()
val flow = MutableSharedFlow<SyncJobStatus>()

val job =
CoroutineScope(Dispatchers.IO).launch {
flow.collect {
// now send Progress to work manager so caller app can listen
setProgress(buildWorkData(it))

if (it is State.Finished || it is State.Failed) {
if (it is SyncJobStatus.Finished || it is SyncJobStatus.Failed) {
[email protected]()
}
}
Expand All @@ -104,7 +102,7 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
)
.apply { subscribe(flow) }
.synchronize()
val output = buildOutput(result)
val output = buildWorkData(result)

// await/join is needed to collect states completely
kotlin.runCatching { job.join() }.onFailure(Timber::w)
Expand All @@ -119,7 +117,7 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
*/
val retries = inputData.getInt(MAX_RETRIES_ALLOWED, 0)
return when {
result is Success -> {
result is SyncJobStatus.Finished -> {
Result.success(output)
}
retries > runAttemptCount -> {
Expand All @@ -131,14 +129,7 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
}
}

private fun buildOutput(result: com.google.android.fhir.sync.Result): Data {
return when (result) {
is Success -> buildWorkData(State.Finished(result))
is Error -> buildWorkData(State.Failed(result))
}
}

private fun buildWorkData(state: State): Data {
private fun buildWorkData(state: SyncJobStatus): Data {
return workDataOf(
// send serialized state and type so that consumer can convert it back
"StateType" to state::class.java.name,
Expand All @@ -151,7 +142,7 @@ abstract class FhirSyncWorker(appContext: Context, workerParams: WorkerParameter
}

/**
* Exclusion strategy for [Gson] that handles field exclusions for [State] returned by
* Exclusion strategy for [Gson] that handles field exclusions for [SyncJobStatus] returned by
* FhirSynchronizer. It should skip serializing the exceptions to avoid exceeding WorkManager
* WorkData limit
* @see <a
Expand Down
Loading