From 257f12c0bdfd9599773ac4bcacbf29b9ca855a52 Mon Sep 17 00:00:00 2001 From: hoangchungk53qx1 Date: Sat, 2 Dec 2023 19:02:55 +0700 Subject: [PATCH 1/4] refactor demo project --- .../android/fhir/demo/AddPatientViewModel.kt | 11 ++- .../android/fhir/demo/EditPatientViewModel.kt | 18 ++--- .../fhir/demo/MainActivityViewModel.kt | 53 +++++++-------- .../android/fhir/demo/PatientListFragment.kt | 67 ++++++++++--------- .../android/fhir/demo/ScreenerViewModel.kt | 22 +++--- .../android/fhir/demo/extensions/Assets.kt | 22 ++++++ .../fhir/demo/extensions/LifeCycleExt.kt | 35 ++++++++++ 7 files changed, 138 insertions(+), 90 deletions(-) create mode 100644 demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt create mode 100644 demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt diff --git a/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt index 50447b8c28..1cb977da14 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt @@ -27,6 +27,7 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator +import com.google.android.fhir.demo.extensions.readFileFromAssets import java.util.UUID import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.Patient @@ -91,16 +92,12 @@ class AddPatientViewModel(application: Application, private val state: SavedStat _questionnaireJson?.let { return it } - _questionnaireJson = readFileFromAssets(state[AddPatientFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!) + _questionnaireJson = + getApplication() + .readFileFromAssets(state[AddPatientFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!) return _questionnaireJson!! } - private fun readFileFromAssets(filename: String): String { - return getApplication().assets.open(filename).bufferedReader().use { - it.readText() - } - } - private fun generateUuid(): String { return UUID.randomUUID().toString() } diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index 456f480cf9..012d9ad21e 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -26,6 +26,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.mapping.ResourceMapper +import com.google.android.fhir.demo.extensions.readFileFromAssets import com.google.android.fhir.get import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.Patient @@ -47,7 +48,10 @@ class EditPatientViewModel(application: Application, private val state: SavedSta private suspend fun prepareEditPatient(): Pair { val patient = fhirEngine.get(patientId) val launchContexts = mapOf("client" to patient) - val question = readFileFromAssets("new-patient-registration-paginated.json").trimIndent() + val question = + getApplication() + .readFileFromAssets("new-patient-registration-paginated.json") + .trimIndent() val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() val questionnaire = parser.parseResource(Questionnaire::class.java, question) as Questionnaire @@ -101,13 +105,11 @@ class EditPatientViewModel(application: Application, private val state: SavedSta questionnaireJson?.let { return it } - questionnaireJson = readFileFromAssets(state[EditPatientFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!) + questionnaireJson = + getApplication() + .readFileFromAssets( + state[EditPatientFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!, + ) return questionnaireJson!! } - - private fun readFileFromAssets(filename: String): String { - return getApplication().assets.open(filename).bufferedReader().use { - it.readText() - } - } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt index 0097178c39..e1adcbc75b 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt @@ -27,48 +27,45 @@ import com.google.android.fhir.demo.data.DemoFhirSyncWorker import com.google.android.fhir.sync.PeriodicSyncConfiguration 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.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.launch /** View model for [MainActivity]. */ -@OptIn(InternalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) class MainActivityViewModel(application: Application) : AndroidViewModel(application) { private val _lastSyncTimestampLiveData = MutableLiveData() val lastSyncTimestampLiveData: LiveData get() = _lastSyncTimestampLiveData - private val _pollState = MutableSharedFlow() - val pollState: Flow - get() = _pollState + private val _oneTimeSyncTrigger = MutableStateFlow(false) - init { - viewModelScope.launch { - Sync.periodicSync( - application.applicationContext, - periodicSyncConfiguration = - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), - ), - ) - .shareIn(this, SharingStarted.Eagerly, 10) - .collect { _pollState.emit(it) } - } - } + val pollState = + Sync.periodicSync( + context = application.applicationContext, + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), + ), + ) + .shareIn(viewModelScope, SharingStarted.Eagerly, 10) + + val oneTimeSyncState = + _oneTimeSyncTrigger + .combine( + flow = Sync.oneTimeSync(context = application.applicationContext), + ) { _, syncJobStatus -> + syncJobStatus + } + .shareIn(viewModelScope, SharingStarted.Eagerly, 10) fun triggerOneTimeSync() { - viewModelScope.launch { - Sync.oneTimeSync(getApplication()) - .shareIn(this, SharingStarted.Eagerly, 10) - .collect { _pollState.emit(it) } - } + _oneTimeSyncTrigger.value = !_oneTimeSyncTrigger.value } /** Emits last sync time. */ diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index 2f4fc18b64..225617ff26 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt @@ -37,16 +37,15 @@ import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration 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.demo.extensions.launchAndRepeatStarted import com.google.android.fhir.sync.SyncJobStatus import kotlin.math.roundToInt -import kotlinx.coroutines.launch import timber.log.Timber class PatientListFragment : Fragment() { @@ -151,37 +150,39 @@ class PatientListFragment : Fragment() { setHasOptionsMenu(true) (activity as MainActivity).setDrawerEnabled(true) - lifecycleScope.launch { - mainActivityViewModel.pollState.collect { - Timber.d("onViewCreated: pollState Got status $it") - when (it) { - is SyncJobStatus.Started -> { - Timber.i("Sync: ${it::class.java.simpleName}") - fadeInTopBanner(it) - } - is SyncJobStatus.InProgress -> { - Timber.i("Sync: ${it::class.java.simpleName} with data $it") - fadeInTopBanner(it) - } - is SyncJobStatus.Finished -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - } - is SyncJobStatus.Failed -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - } - else -> { - Timber.i("Sync: Unknown state.") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - } - } + launchAndRepeatStarted( + { mainActivityViewModel.pollState.collect(::syncJobStatus) }, + { mainActivityViewModel.oneTimeSyncState.collect(::syncJobStatus) }, + ) + } + + private fun syncJobStatus(it: SyncJobStatus) { + when (it) { + is SyncJobStatus.Started -> { + Timber.i("Sync: ${it::class.java.simpleName}") + fadeInTopBanner(it) + } + is SyncJobStatus.InProgress -> { + Timber.i("Sync: ${it::class.java.simpleName} with data $it") + fadeInTopBanner(it) + } + is SyncJobStatus.Finished -> { + Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp() + fadeOutTopBanner(it) + } + is SyncJobStatus.Failed -> { + Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp() + fadeOutTopBanner(it) + } + else -> { + Timber.i("Sync: Unknown state.") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp() + fadeOutTopBanner(it) } } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt index b2aa0f7335..9ba9b1b1e3 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt @@ -25,6 +25,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.android.fhir.FhirEngine import com.google.android.fhir.datacapture.mapping.ResourceMapper +import com.google.android.fhir.demo.extensions.readFileFromAssets import java.math.BigDecimal import java.util.UUID import kotlinx.coroutines.launch @@ -113,8 +114,7 @@ class ScreenerViewModel(application: Application, private val state: SavedStateH private fun isRequiredFieldMissing(bundle: Bundle): Boolean { bundle.entry.forEach { - val resource = it.resource - when (resource) { + when (val resource = it.resource) { is Observation -> { if (resource.hasValueQuantity() && !resource.valueQuantity.hasValueElement()) { return true @@ -132,18 +132,14 @@ class ScreenerViewModel(application: Application, private val state: SavedStateH private fun getQuestionnaireJson(): String { questionnaireJson?.let { - return it!! + return it } - questionnaireJson = readFileFromAssets(state[ScreenerFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!) + questionnaireJson = + getApplication() + .readFileFromAssets(state[ScreenerFragment.QUESTIONNAIRE_FILE_PATH_KEY]!!) return questionnaireJson!! } - private fun readFileFromAssets(filename: String): String { - return getApplication().assets.open(filename).bufferedReader().use { - it.readText() - } - } - private fun generateUuid(): String { return UUID.randomUUID().toString() } @@ -217,8 +213,7 @@ class ScreenerViewModel(application: Application, private val state: SavedStateH .filter { it.hasCode() && it.code.hasCoding() } .flatMap { it.code.coding } .map { it.code } - .filter { isSymptomPresent(it) } - .count() + .count { isSymptomPresent(it) } return count > 0 } @@ -234,8 +229,7 @@ class ScreenerViewModel(application: Application, private val state: SavedStateH .filter { it.hasCode() && it.code.hasCoding() } .flatMap { it.code.coding } .map { it.code } - .filter { isComorbidityPresent(it) } - .count() + .count { isComorbidityPresent(it) } return count > 0 } diff --git a/demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt b/demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt new file mode 100644 index 0000000000..6f0b0d8efe --- /dev/null +++ b/demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.demo.extensions + +import android.content.Context + +fun Context.readFileFromAssets(fileName: String): String = + assets.open(fileName).bufferedReader().use { it.readText() } diff --git a/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt b/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt new file mode 100644 index 0000000000..ee1f9ae142 --- /dev/null +++ b/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.demo.extensions + +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.launch + +fun Fragment.launchAndRepeatStarted( + vararg launchBlock: suspend () -> Unit, + doAfterLaunch: (() -> Unit)? = null, +) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launchBlock.forEach { launch { it.invoke() } } + doAfterLaunch?.invoke() + } + } +} From 55eed97aff85b4308379f06f1a51f99dd2da404a Mon Sep 17 00:00:00 2001 From: hoangchungk53qx1 Date: Sat, 2 Dec 2023 19:03:18 +0700 Subject: [PATCH 2/4] refactor demo project --- .../android/fhir/demo/extensions/{Assets.kt => AssetsExt.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename demo/src/main/java/com/google/android/fhir/demo/extensions/{Assets.kt => AssetsExt.kt} (100%) diff --git a/demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt b/demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt similarity index 100% rename from demo/src/main/java/com/google/android/fhir/demo/extensions/Assets.kt rename to demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt From e767a9e23396cb35b792393ffee170a404b661e3 Mon Sep 17 00:00:00 2001 From: hoangchungk53qx1 Date: Sat, 24 Feb 2024 11:30:51 +0700 Subject: [PATCH 3/4] splitting the flow into 2 state --- .../fhir/demo/MainActivityViewModel.kt | 64 ++--- .../android/fhir/demo/PatientListFragment.kt | 221 +++++++----------- .../main/res/layout/fragment_patient_list.xml | 23 +- 3 files changed, 111 insertions(+), 197 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt index 6f90615280..f9cf5c83e5 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt @@ -33,10 +33,8 @@ import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import java.time.OffsetDateTime import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.shareIn @@ -50,64 +48,28 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica private val _oneTimeSyncTrigger = MutableStateFlow(false) - val pollState = + val pollPeriodicSyncJobStatus: SharedFlow = Sync.periodicSync( - context = application.applicationContext, - periodicSyncConfiguration = - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), - ), - ) - .shareIn(viewModelScope, SharingStarted.Eagerly, 10) + application.applicationContext, + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), + ), + ).shareIn(viewModelScope, SharingStarted.Eagerly, 10) + - val oneTimeSyncState = + val pollState: SharedFlow = _oneTimeSyncTrigger .combine( flow = Sync.oneTimeSync(context = application.applicationContext), ) { _, syncJobStatus -> syncJobStatus } - .shareIn(viewModelScope, SharingStarted.Eagerly, 10) - - private val _pollState = MutableSharedFlow() - val pollState: Flow - get() = _pollState - - private val _ - = MutableSharedFlow() - val pollPeriodicSyncJobStatus: Flow - get() = _pollPeriodicSyncJobStatus - - init { - viewModelScope.launch { - Sync.periodicSync( - application.applicationContext, - periodicSyncConfiguration = - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), - ), - ) - .shareIn(this, SharingStarted.Eagerly, 10) - .collect { _pollPeriodicSyncJobStatus.emit(it) } - } - } - - private var oneTimeSyncJob: Job? = null + .shareIn(viewModelScope, SharingStarted.Eagerly, 0) fun triggerOneTimeSync() { _oneTimeSyncTrigger.value = !_oneTimeSyncTrigger.value - // 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() - oneTimeSyncJob = - viewModelScope.launch { - Sync.oneTimeSync(getApplication()) - .shareIn(this, SharingStarted.Eagerly, 0) - .collect { result -> result.let { _pollState.emit(it) } } - } } /** Emits last sync time. */ diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index a69245e0bf..7318692f37 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt @@ -1,58 +1,58 @@ - /* - * Copyright 2022-2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* + * Copyright 2022-2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ - package com.google.android.fhir.demo +package com.google.android.fhir.demo - import android.content.Context - import android.graphics.Color - import android.graphics.drawable.ColorDrawable - import android.os.Bundle - import android.os.Handler - import android.os.Looper - import android.view.LayoutInflater - import android.view.MenuItem - import android.view.View - import android.view.ViewGroup - import android.view.animation.AnimationUtils - import android.view.inputmethod.InputMethodManager - import android.widget.LinearLayout - import android.widget.ProgressBar - import android.widget.TextView - import androidx.activity.OnBackPressedCallback - import androidx.appcompat.app.AppCompatActivity - import androidx.appcompat.widget.SearchView - import androidx.fragment.app.Fragment - import androidx.fragment.app.activityViewModels - import androidx.lifecycle.ViewModelProvider - import androidx.lifecycle.lifecycleScope - import androidx.navigation.fragment.NavHostFragment - import androidx.navigation.fragment.findNavController - import androidx.recyclerview.widget.DividerItemDecoration - 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.demo.extensions.launchAndRepeatStarted - 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 timber.log.Timber +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.animation.AnimationUtils +import android.view.inputmethod.InputMethodManager +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +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.demo.extensions.launchAndRepeatStarted +import com.google.android.fhir.sync.CurrentSyncJobStatus +import com.google.android.fhir.sync.LastSyncJobStatus +import com.google.android.fhir.sync.PeriodicSyncJobStatus +import com.google.android.fhir.sync.SyncJobStatus +import kotlin.math.roundToInt +import timber.log.Timber - class PatientListFragment : Fragment() { +class PatientListFragment : Fragment() { private lateinit var fhirEngine: FhirEngine private lateinit var patientListViewModel: PatientListViewModel private lateinit var searchView: SearchView @@ -62,7 +62,7 @@ private lateinit var syncProgress: ProgressBar private var _binding: FragmentPatientListBinding? = null private val binding - get() = _binding!! + get() = _binding!! private val mainActivityViewModel: MainActivityViewModel by activityViewModels() @@ -155,108 +155,67 @@ setHasOptionsMenu(true) (activity as MainActivity).setDrawerEnabled(false) launchAndRepeatStarted( - { mainActivityViewModel.pollState.collect(::syncJobStatus) }, - { mainActivityViewModel.oneTimeSyncState.collect(::syncJobStatus) }, + { mainActivityViewModel.pollState.collect(::currentSyncJobStatus) }, + { mainActivityViewModel.pollPeriodicSyncJobStatus.collect(::periodicSyncJobStatus) }, ) } - private fun syncJobStatus(it: SyncJobStatus) { - when (it) { - is SyncJobStatus.Started -> { - Timber.i("Sync: ${it::class.java.simpleName}") - fadeInTopBanner(it) - } - is SyncJobStatus.InProgress -> { - Timber.i("Sync: ${it::class.java.simpleName} with data $it") - fadeInTopBanner(it) - } - is SyncJobStatus.Finished -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - } - is SyncJobStatus.Failed -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - } - else -> { - Timber.i("Sync: Unknown state.") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp() - fadeOutTopBanner(it) - lifecycleScope.launch { - mainActivityViewModel.pollState.collect { - Timber.d("onViewCreated: pollState Got status $it") - when (it) { + private fun currentSyncJobStatus(currentSyncJobStatus: CurrentSyncJobStatus) { + when (currentSyncJobStatus) { is CurrentSyncJobStatus.Running -> { - Timber.i("Sync: ${it::class.java.simpleName} with data ${it.inProgressSyncJob}") - fadeInTopBanner(it) + Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} with data ${currentSyncJobStatus.inProgressSyncJob}") + fadeInTopBanner(currentSyncJobStatus) } is CurrentSyncJobStatus.Succeeded -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(it.timestamp) - fadeOutTopBanner(it) + Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) + fadeOutTopBanner(currentSyncJobStatus) } is CurrentSyncJobStatus.Failed -> { - Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(it.timestamp) - fadeOutTopBanner(it) + Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) + fadeOutTopBanner(currentSyncJobStatus) } is CurrentSyncJobStatus.Enqueued -> { - Timber.i("Sync: Enqueued") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - fadeOutTopBanner(it) + Timber.i("Sync: Enqueued") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + fadeOutTopBanner(currentSyncJobStatus) } CurrentSyncJobStatus.Cancelled -> TODO() - } } - } + } - lifecycleScope.launch { - mainActivityViewModel.pollPeriodicSyncJobStatus.collect { - Timber.d("onViewCreated: pollState Got status ${it.currentSyncJobStatus}") - when (it.currentSyncJobStatus) { + private fun periodicSyncJobStatus(periodicSyncJobStatus: PeriodicSyncJobStatus) { + when (periodicSyncJobStatus.currentSyncJobStatus) { is CurrentSyncJobStatus.Running -> { - Timber.i( - "Sync: ${it.currentSyncJobStatus::class.java.simpleName} with data ${it.currentSyncJobStatus}", - ) - fadeInTopBanner(it.currentSyncJobStatus) + fadeInTopBanner(periodicSyncJobStatus.currentSyncJobStatus) } is CurrentSyncJobStatus.Succeeded -> { - val lastSyncTimestamp = - (it.currentSyncJobStatus as CurrentSyncJobStatus.Succeeded).timestamp - Timber.i( - "Sync: ${it.currentSyncJobStatus::class.java.simpleName} at $lastSyncTimestamp", - ) - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) - fadeOutTopBanner(it.currentSyncJobStatus) + val lastSyncTimestamp = + (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Succeeded).timestamp + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) } is CurrentSyncJobStatus.Failed -> { - val lastSyncTimestamp = - (it.currentSyncJobStatus as CurrentSyncJobStatus.Failed).timestamp - Timber.i( - "Sync: ${it.currentSyncJobStatus::class.java.simpleName} at $lastSyncTimestamp}", - ) - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) - fadeOutTopBanner(it.currentSyncJobStatus) + val lastSyncTimestamp = + (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Failed).timestamp + Timber.i( + "Sync: ${periodicSyncJobStatus.currentSyncJobStatus::class.java.simpleName} at $lastSyncTimestamp}", + ) + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) } is CurrentSyncJobStatus.Enqueued -> { - Timber.i("Sync: Enqueued") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - fadeOutTopBanner(it.currentSyncJobStatus) + Timber.i("Sync: Enqueued") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) } CurrentSyncJobStatus.Cancelled -> TODO() - } - >>>>>>> master } - } } override fun onDestroyView() { diff --git a/demo/src/main/res/layout/fragment_patient_list.xml b/demo/src/main/res/layout/fragment_patient_list.xml index 12fe261bda..c8409920a6 100644 --- a/demo/src/main/res/layout/fragment_patient_list.xml +++ b/demo/src/main/res/layout/fragment_patient_list.xml @@ -1,36 +1,30 @@ - + android:layout_height="match_parent"> + android:orientation="vertical"> + layout="@layout/sync_status_layout" /> + app:queryHint="@string/query_hint_patient_search" /> + layout="@layout/patient_list_view" /> + app:srcCompat="@drawable/ic_baseline_add_24" /> - + \ No newline at end of file From 021d4c5102c4579c117267f635f93f0f9cbc3f52 Mon Sep 17 00:00:00 2001 From: Jing Tang Date: Mon, 26 Feb 2024 15:42:42 +0000 Subject: [PATCH 4/4] Spotless --- .../android/fhir/demo/AddPatientViewModel.kt | 2 +- .../android/fhir/demo/EditPatientViewModel.kt | 2 +- .../fhir/demo/MainActivityViewModel.kt | 20 +- .../android/fhir/demo/PatientListFragment.kt | 416 +++++++++--------- .../android/fhir/demo/ScreenerViewModel.kt | 2 +- .../android/fhir/demo/extensions/AssetsExt.kt | 2 +- .../fhir/demo/extensions/LifeCycleExt.kt | 2 +- .../main/res/layout/fragment_patient_list.xml | 23 +- 8 files changed, 241 insertions(+), 228 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt index 1cb977da14..8a5c06bc45 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/AddPatientViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index 012d9ad21e..8e843af315 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Google LLC + * Copyright 2021-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt index f9cf5c83e5..e2003ce21b 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt @@ -29,12 +29,12 @@ 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 java.time.OffsetDateTime import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import java.time.OffsetDateTime import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.shareIn @@ -50,14 +50,14 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica val pollPeriodicSyncJobStatus: SharedFlow = Sync.periodicSync( - application.applicationContext, - periodicSyncConfiguration = - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), - ), - ).shareIn(viewModelScope, SharingStarted.Eagerly, 10) - + application.applicationContext, + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES), + ), + ) + .shareIn(viewModelScope, SharingStarted.Eagerly, 10) val pollState: SharedFlow = _oneTimeSyncTrigger @@ -66,7 +66,7 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica ) { _, syncJobStatus -> syncJobStatus } - .shareIn(viewModelScope, SharingStarted.Eagerly, 0) + .shareIn(viewModelScope, SharingStarted.Eagerly, 0) fun triggerOneTimeSync() { _oneTimeSyncTrigger.value = !_oneTimeSyncTrigger.value diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index 7318692f37..080dba3579 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt @@ -53,238 +53,244 @@ import kotlin.math.roundToInt import timber.log.Timber class PatientListFragment : Fragment() { - private lateinit var fhirEngine: FhirEngine - private lateinit var patientListViewModel: PatientListViewModel - private lateinit var searchView: SearchView - private lateinit var topBanner: LinearLayout - private lateinit var syncStatus: TextView - private lateinit var syncPercent: TextView - private lateinit var syncProgress: ProgressBar - private var _binding: FragmentPatientListBinding? = null - private val binding - get() = _binding!! + private lateinit var fhirEngine: FhirEngine + private lateinit var patientListViewModel: PatientListViewModel + private lateinit var searchView: SearchView + private lateinit var topBanner: LinearLayout + private lateinit var syncStatus: TextView + private lateinit var syncPercent: TextView + private lateinit var syncProgress: ProgressBar + private var _binding: FragmentPatientListBinding? = null + private val binding + get() = _binding!! - private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - _binding = FragmentPatientListBinding.inflate(inflater, container, false) - return binding.root - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentPatientListBinding.inflate(inflater, container, false) + return binding.root + } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - (requireActivity() as AppCompatActivity).supportActionBar?.apply { - title = resources.getString(R.string.title_patient_list) - setDisplayHomeAsUpEnabled(true) - } - fhirEngine = FhirApplication.fhirEngine(requireContext()) - patientListViewModel = - ViewModelProvider( - this, - PatientListViewModelFactory(requireActivity().application, fhirEngine), - ) - .get(PatientListViewModel::class.java) - val recyclerView: RecyclerView = binding.patientListContainer.patientList - val adapter = PatientItemRecyclerViewAdapter(this::onPatientItemClicked) - recyclerView.adapter = adapter - recyclerView.addItemDecoration( - DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL).apply { - setDrawable(ColorDrawable(Color.LTGRAY)) - }, - ) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (requireActivity() as AppCompatActivity).supportActionBar?.apply { + title = resources.getString(R.string.title_patient_list) + setDisplayHomeAsUpEnabled(true) + } + fhirEngine = FhirApplication.fhirEngine(requireContext()) + patientListViewModel = + ViewModelProvider( + this, + PatientListViewModelFactory(requireActivity().application, fhirEngine), + ) + .get(PatientListViewModel::class.java) + val recyclerView: RecyclerView = binding.patientListContainer.patientList + val adapter = PatientItemRecyclerViewAdapter(this::onPatientItemClicked) + recyclerView.adapter = adapter + recyclerView.addItemDecoration( + DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL).apply { + setDrawable(ColorDrawable(Color.LTGRAY)) + }, + ) - 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) { - binding.patientListContainer.patientCount.text = "$it Patient(s)" - } + patientListViewModel.patientCount.observe(viewLifecycleOwner) { + binding.patientListContainer.patientCount.text = "$it Patient(s)" + } - searchView = binding.search - topBanner = binding.syncStatusContainer.linearLayoutSyncStatus - topBanner.visibility = View.GONE - syncStatus = binding.syncStatusContainer.tvSyncingStatus - syncPercent = binding.syncStatusContainer.tvSyncingPercent - syncProgress = binding.syncStatusContainer.progressSyncing - searchView.setOnQueryTextListener( - object : SearchView.OnQueryTextListener { - override fun onQueryTextChange(newText: String): Boolean { - patientListViewModel.searchPatientsByName(newText) - return true - } + searchView = binding.search + topBanner = binding.syncStatusContainer.linearLayoutSyncStatus + topBanner.visibility = View.GONE + syncStatus = binding.syncStatusContainer.tvSyncingStatus + syncPercent = binding.syncStatusContainer.tvSyncingPercent + syncProgress = binding.syncStatusContainer.progressSyncing + searchView.setOnQueryTextListener( + object : SearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + patientListViewModel.searchPatientsByName(newText) + return true + } - override fun onQueryTextSubmit(query: String): Boolean { - patientListViewModel.searchPatientsByName(query) - return true - } - }, - ) - searchView.setOnQueryTextFocusChangeListener { view, focused -> - if (!focused) { - // hide soft keyboard - (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(view.windowToken, 0) + override fun onQueryTextSubmit(query: String): Boolean { + patientListViewModel.searchPatientsByName(query) + return true } + }, + ) + searchView.setOnQueryTextFocusChangeListener { view, focused -> + if (!focused) { + // hide soft keyboard + (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) + .hideSoftInputFromWindow(view.windowToken, 0) } - requireActivity() - .onBackPressedDispatcher - .addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (searchView.query.isNotEmpty()) { - searchView.setQuery("", true) - } else { - isEnabled = false - activity?.onBackPressed() - } + } + requireActivity() + .onBackPressedDispatcher + .addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (searchView.query.isNotEmpty()) { + searchView.setQuery("", true) + } else { + isEnabled = false + activity?.onBackPressed() } - }, - ) - - binding.apply { - addPatient.setOnClickListener { onAddPatientClick() } - addPatient.setColorFilter(Color.WHITE) - } - setHasOptionsMenu(true) - (activity as MainActivity).setDrawerEnabled(false) - launchAndRepeatStarted( - { mainActivityViewModel.pollState.collect(::currentSyncJobStatus) }, - { mainActivityViewModel.pollPeriodicSyncJobStatus.collect(::periodicSyncJobStatus) }, + } + }, ) - } - private fun currentSyncJobStatus(currentSyncJobStatus: CurrentSyncJobStatus) { - when (currentSyncJobStatus) { - is CurrentSyncJobStatus.Running -> { - Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} with data ${currentSyncJobStatus.inProgressSyncJob}") - fadeInTopBanner(currentSyncJobStatus) - } - is CurrentSyncJobStatus.Succeeded -> { - Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) - fadeOutTopBanner(currentSyncJobStatus) - } - is CurrentSyncJobStatus.Failed -> { - Timber.i("Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) - fadeOutTopBanner(currentSyncJobStatus) - } - is CurrentSyncJobStatus.Enqueued -> { - Timber.i("Sync: Enqueued") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - fadeOutTopBanner(currentSyncJobStatus) - } - CurrentSyncJobStatus.Cancelled -> TODO() - } + binding.apply { + addPatient.setOnClickListener { onAddPatientClick() } + addPatient.setColorFilter(Color.WHITE) } + setHasOptionsMenu(true) + (activity as MainActivity).setDrawerEnabled(false) + launchAndRepeatStarted( + { mainActivityViewModel.pollState.collect(::currentSyncJobStatus) }, + { mainActivityViewModel.pollPeriodicSyncJobStatus.collect(::periodicSyncJobStatus) }, + ) + } - private fun periodicSyncJobStatus(periodicSyncJobStatus: PeriodicSyncJobStatus) { - when (periodicSyncJobStatus.currentSyncJobStatus) { - is CurrentSyncJobStatus.Running -> { - fadeInTopBanner(periodicSyncJobStatus.currentSyncJobStatus) - } - is CurrentSyncJobStatus.Succeeded -> { - val lastSyncTimestamp = - (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Succeeded).timestamp - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) - fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) - } - is CurrentSyncJobStatus.Failed -> { - val lastSyncTimestamp = - (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Failed).timestamp - Timber.i( - "Sync: ${periodicSyncJobStatus.currentSyncJobStatus::class.java.simpleName} at $lastSyncTimestamp}", - ) - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) - fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) - } - is CurrentSyncJobStatus.Enqueued -> { - Timber.i("Sync: Enqueued") - patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) - fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) - } - CurrentSyncJobStatus.Cancelled -> TODO() - } + private fun currentSyncJobStatus(currentSyncJobStatus: CurrentSyncJobStatus) { + when (currentSyncJobStatus) { + is CurrentSyncJobStatus.Running -> { + Timber.i( + "Sync: ${currentSyncJobStatus::class.java.simpleName} with data ${currentSyncJobStatus.inProgressSyncJob}", + ) + fadeInTopBanner(currentSyncJobStatus) + } + is CurrentSyncJobStatus.Succeeded -> { + Timber.i( + "Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}", + ) + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) + fadeOutTopBanner(currentSyncJobStatus) + } + is CurrentSyncJobStatus.Failed -> { + Timber.i( + "Sync: ${currentSyncJobStatus::class.java.simpleName} at ${currentSyncJobStatus.timestamp}", + ) + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp) + fadeOutTopBanner(currentSyncJobStatus) + } + is CurrentSyncJobStatus.Enqueued -> { + Timber.i("Sync: Enqueued") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + fadeOutTopBanner(currentSyncJobStatus) + } + CurrentSyncJobStatus.Cancelled -> TODO() } + } - override fun onDestroyView() { - super.onDestroyView() - _binding = null + private fun periodicSyncJobStatus(periodicSyncJobStatus: PeriodicSyncJobStatus) { + when (periodicSyncJobStatus.currentSyncJobStatus) { + is CurrentSyncJobStatus.Running -> { + fadeInTopBanner(periodicSyncJobStatus.currentSyncJobStatus) + } + is CurrentSyncJobStatus.Succeeded -> { + val lastSyncTimestamp = + (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Succeeded).timestamp + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) + } + is CurrentSyncJobStatus.Failed -> { + val lastSyncTimestamp = + (periodicSyncJobStatus.currentSyncJobStatus as CurrentSyncJobStatus.Failed).timestamp + Timber.i( + "Sync: ${periodicSyncJobStatus.currentSyncJobStatus::class.java.simpleName} at $lastSyncTimestamp}", + ) + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + mainActivityViewModel.updateLastSyncTimestamp(lastSyncTimestamp) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) + } + is CurrentSyncJobStatus.Enqueued -> { + Timber.i("Sync: Enqueued") + patientListViewModel.searchPatientsByName(searchView.query.toString().trim()) + fadeOutTopBanner(periodicSyncJobStatus.currentSyncJobStatus) + } + CurrentSyncJobStatus.Cancelled -> TODO() } + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - NavHostFragment.findNavController(this).navigateUp() - true - } - else -> false + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + NavHostFragment.findNavController(this).navigateUp() + true } + else -> false } + } - private fun onPatientItemClicked(patientItem: PatientListViewModel.PatientItem) { - findNavController() - .navigate(PatientListFragmentDirections.navigateToProductDetail(patientItem.resourceId)) - } + private fun onPatientItemClicked(patientItem: PatientListViewModel.PatientItem) { + findNavController() + .navigate(PatientListFragmentDirections.navigateToProductDetail(patientItem.resourceId)) + } - private fun onAddPatientClick() { - findNavController() - .navigate(PatientListFragmentDirections.actionPatientListToAddPatientFragment()) - } + private fun onAddPatientClick() { + findNavController() + .navigate(PatientListFragmentDirections.actionPatientListToAddPatientFragment()) + } - private fun fadeInTopBanner(state: CurrentSyncJobStatus) { - if (topBanner.visibility != View.VISIBLE) { - syncStatus.text = resources.getString(R.string.syncing).uppercase() - syncPercent.text = "" - syncProgress.progress = 0 - syncProgress.visibility = View.VISIBLE - topBanner.visibility = View.VISIBLE - val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_in) - topBanner.startAnimation(animation) - } else if ( - state is CurrentSyncJobStatus.Running && state.inProgressSyncJob is SyncJobStatus.InProgress - ) { - val inProgressState = state.inProgressSyncJob as? SyncJobStatus.InProgress - val 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 fadeInTopBanner(state: CurrentSyncJobStatus) { + if (topBanner.visibility != View.VISIBLE) { + syncStatus.text = resources.getString(R.string.syncing).uppercase() + syncPercent.text = "" + syncProgress.progress = 0 + syncProgress.visibility = View.VISIBLE + topBanner.visibility = View.VISIBLE + val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_in) + topBanner.startAnimation(animation) + } else if ( + state is CurrentSyncJobStatus.Running && state.inProgressSyncJob is SyncJobStatus.InProgress + ) { + val inProgressState = state.inProgressSyncJob as? SyncJobStatus.InProgress + val 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: CurrentSyncJobStatus) { - fadeOutTopBanner(state::class.java.simpleName.uppercase()) - } + 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(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()} $statusText".also { syncStatus.text = it } + private fun fadeOutTopBanner(statusText: String) { + syncPercent.text = "" + syncProgress.visibility = View.GONE + if (topBanner.visibility == View.VISIBLE) { + "${resources.getString(R.string.sync).uppercase()} $statusText".also { syncStatus.text = it } - val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_out) - topBanner.startAnimation(animation) - Handler(Looper.getMainLooper()).postDelayed({ topBanner.visibility = View.GONE }, 2000) - } + val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_out) + topBanner.startAnimation(animation) + Handler(Looper.getMainLooper()).postDelayed({ topBanner.visibility = View.GONE }, 2000) } } +} diff --git a/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt index 9ba9b1b1e3..d7228bbcad 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/ScreenerViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Google LLC + * Copyright 2021-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt b/demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt index 6f0b0d8efe..0754188f94 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/extensions/AssetsExt.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt b/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt index ee1f9ae142..437fcb9ac3 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/extensions/LifeCycleExt.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/demo/src/main/res/layout/fragment_patient_list.xml b/demo/src/main/res/layout/fragment_patient_list.xml index c8409920a6..12fe261bda 100644 --- a/demo/src/main/res/layout/fragment_patient_list.xml +++ b/demo/src/main/res/layout/fragment_patient_list.xml @@ -1,30 +1,36 @@ - + android:layout_height="match_parent" +> + android:orientation="vertical" + > + layout="@layout/sync_status_layout" + /> + app:queryHint="@string/query_hint_patient_search" + /> + layout="@layout/patient_list_view" + /> + app:srcCompat="@drawable/ic_baseline_add_24" + /> - \ No newline at end of file +