Skip to content

Commit 5842559

Browse files
committed
Merge branch 'master' into master-release
2 parents 72c2fa6 + 66e14ea commit 5842559

File tree

12 files changed

+457
-206
lines changed

12 files changed

+457
-206
lines changed

buildSrc/src/main/kotlin/Releases.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,13 +50,13 @@ object Releases {
5050

5151
object Engine : LibraryArtifact {
5252
override val artifactId = "engine"
53-
override val version = "1.1.0-preview4-SNAPSHOT"
53+
override val version = "1.2.0"
5454
override val name = "Android FHIR Engine Library"
5555
}
5656

5757
object DataCapture : LibraryArtifact {
5858
override val artifactId = "data-capture"
59-
override val version = "1.2.0-preview9.2-SNAPSHOT"
59+
override val version = "1.3.0"
6060
override val name = "Android FHIR Structured Data Capture Library"
6161
}
6262

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -298,12 +298,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
298298

299299
private lateinit var currentPageItems: List<QuestionnaireAdapterItem>
300300

301-
/**
302-
* True if the user has tapped the next/previous pagination buttons on the current page. This is
303-
* needed to avoid spewing validation errors before any questions are answered.
304-
*/
305-
private var forceValidation = false
306-
307301
/**
308302
* Map of [QuestionnaireResponseItemAnswerComponent] for
309303
* [Questionnaire.QuestionnaireItemComponent]s that are disabled now. The answers will be used to
@@ -903,7 +897,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
903897
val validationResult =
904898
if (
905899
modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
906-
forceValidation ||
907900
isInReviewModeFlow.value
908901
) {
909902
questionnaireResponseItemValidator.validate(
@@ -1124,13 +1117,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
11241117
it.item.validationResult is NotValidated
11251118
}
11261119
) {
1127-
// Force update validation results for all questions on the current page. This is needed
1128-
// when the user has not answered any questions so no validation has been done.
1129-
forceValidation = true
1120+
// Add all items on the current page to modifiedQuestionnaireResponseItemSet.
1121+
// This will ensure that all fields are validated even when they're not filled by the user
1122+
currentPageItems.filterIsInstance<QuestionnaireAdapterItem.Question>().forEach {
1123+
modifiedQuestionnaireResponseItemSet.add(it.item.getQuestionnaireResponseItem())
1124+
}
11301125
// Results in a new questionnaire state being generated synchronously, i.e., the current
11311126
// thread will be suspended until the new state is generated.
11321127
modificationCount.update { it + 1 }
1133-
forceValidation = false
11341128
}
11351129

11361130
if (

demo/src/main/java/com/google/android/fhir/demo/PeriodicSyncFragment.kt

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Google LLC
2+
* Copyright 2024-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import android.view.LayoutInflater
2121
import android.view.MenuItem
2222
import android.view.View
2323
import android.view.ViewGroup
24+
import android.widget.Button
2425
import android.widget.ProgressBar
2526
import android.widget.TextView
2627
import androidx.appcompat.app.AppCompatActivity
@@ -48,6 +49,7 @@ class PeriodicSyncFragment : Fragment() {
4849
setUpActionBar()
4950
setHasOptionsMenu(true)
5051
refreshPeriodicSynUi()
52+
setUpSyncButtons(view)
5153
}
5254

5355
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -67,6 +69,30 @@ class PeriodicSyncFragment : Fragment() {
6769
}
6870
}
6971

72+
private fun setUpSyncButtons(view: View) {
73+
val syncNowButton = view.findViewById<Button>(R.id.sync_now_button)
74+
val cancelSyncButton = view.findViewById<Button>(R.id.cancel_sync_button)
75+
syncNowButton.apply {
76+
setOnClickListener {
77+
periodicSyncViewModel.collectPeriodicSyncJobStatus()
78+
toggleButtonVisibility(hiddenButton = syncNowButton, visibleButton = cancelSyncButton)
79+
visibility = View.GONE
80+
}
81+
}
82+
cancelSyncButton.apply {
83+
setOnClickListener {
84+
periodicSyncViewModel.cancelPeriodicSyncJob()
85+
toggleButtonVisibility(hiddenButton = cancelSyncButton, visibleButton = syncNowButton)
86+
visibility = View.GONE
87+
}
88+
}
89+
}
90+
91+
private fun toggleButtonVisibility(hiddenButton: View, visibleButton: View) {
92+
hiddenButton.visibility = View.GONE
93+
visibleButton.visibility = View.VISIBLE
94+
}
95+
7096
private fun refreshPeriodicSynUi() {
7197
viewLifecycleOwner.lifecycleScope.launch {
7298
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {

demo/src/main/java/com/google/android/fhir/demo/PeriodicSyncViewModel.kt

+30-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Google LLC
2+
* Copyright 2024-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,36 +31,43 @@ import com.google.android.fhir.sync.RepeatInterval
3131
import com.google.android.fhir.sync.Sync
3232
import com.google.android.fhir.sync.SyncJobStatus
3333
import java.util.concurrent.TimeUnit
34+
import kotlinx.coroutines.flow.MutableSharedFlow
3435
import kotlinx.coroutines.flow.MutableStateFlow
35-
import kotlinx.coroutines.flow.SharedFlow
36-
import kotlinx.coroutines.flow.SharingStarted
3736
import kotlinx.coroutines.flow.StateFlow
38-
import kotlinx.coroutines.flow.shareIn
3937
import kotlinx.coroutines.launch
38+
import timber.log.Timber
4039

4140
class PeriodicSyncViewModel(application: Application) : AndroidViewModel(application) {
4241

43-
val pollPeriodicSyncJobStatus: SharedFlow<PeriodicSyncJobStatus> =
44-
Sync.periodicSync<DemoFhirSyncWorker>(
45-
application.applicationContext,
42+
private val _uiStateFlow = MutableStateFlow(PeriodicSyncUiState())
43+
val uiStateFlow: StateFlow<PeriodicSyncUiState> = _uiStateFlow
44+
45+
private val _pollPeriodicSyncJobStatus = MutableSharedFlow<PeriodicSyncJobStatus>(replay = 10)
46+
47+
init {
48+
viewModelScope.launch { initializePeriodicSync() }
49+
}
50+
51+
private suspend fun initializePeriodicSync() {
52+
val periodicSyncJobStatusFlow =
53+
Sync.periodicSync<DemoFhirSyncWorker>(
54+
context = getApplication<Application>().applicationContext,
4655
periodicSyncConfiguration =
4756
PeriodicSyncConfiguration(
4857
syncConstraints = Constraints.Builder().build(),
4958
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES),
5059
),
5160
)
52-
.shareIn(viewModelScope, SharingStarted.Eagerly, 10)
53-
54-
private val _uiStateFlow = MutableStateFlow(PeriodicSyncUiState())
55-
val uiStateFlow: StateFlow<PeriodicSyncUiState> = _uiStateFlow
5661

57-
init {
58-
collectPeriodicSyncJobStatus()
62+
periodicSyncJobStatusFlow.collect { status -> _pollPeriodicSyncJobStatus.emit(status) }
5963
}
6064

61-
private fun collectPeriodicSyncJobStatus() {
65+
fun collectPeriodicSyncJobStatus() {
6266
viewModelScope.launch {
63-
pollPeriodicSyncJobStatus.collect { periodicSyncJobStatus ->
67+
_pollPeriodicSyncJobStatus.collect { periodicSyncJobStatus ->
68+
Timber.d(
69+
"currentSyncJobStatus: ${periodicSyncJobStatus.currentSyncJobStatus} lastSyncJobStatus ${periodicSyncJobStatus.lastSyncJobStatus}",
70+
)
6471
val lastSyncStatus = getLastSyncStatus(periodicSyncJobStatus.lastSyncJobStatus)
6572
val lastSyncTime = getLastSyncTime(periodicSyncJobStatus.lastSyncJobStatus)
6673
val currentSyncStatus =
@@ -83,6 +90,14 @@ class PeriodicSyncViewModel(application: Application) : AndroidViewModel(applica
8390
}
8491
}
8592

93+
fun cancelPeriodicSyncJob() {
94+
viewModelScope.launch {
95+
Sync.cancelPeriodicSync<DemoFhirSyncWorker>(
96+
getApplication<FhirApplication>().applicationContext,
97+
)
98+
}
99+
}
100+
86101
private fun getLastSyncStatus(lastSyncJobStatus: LastSyncJobStatus?): String? {
87102
return when (lastSyncJobStatus) {
88103
is LastSyncJobStatus.Succeeded ->

demo/src/main/java/com/google/android/fhir/demo/SyncFragment.kt

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 Google LLC
2+
* Copyright 2024-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import androidx.fragment.app.viewModels
3030
import androidx.navigation.fragment.NavHostFragment
3131
import com.google.android.fhir.demo.extensions.launchAndRepeatStarted
3232
import com.google.android.fhir.sync.CurrentSyncJobStatus
33+
import timber.log.Timber
3334

3435
class SyncFragment : Fragment() {
3536
private val syncFragmentViewModel: SyncFragmentViewModel by viewModels()
@@ -49,6 +50,9 @@ class SyncFragment : Fragment() {
4950
view.findViewById<Button>(R.id.sync_now_button).setOnClickListener {
5051
syncFragmentViewModel.triggerOneTimeSync()
5152
}
53+
view.findViewById<Button>(R.id.cancel_sync_button).setOnClickListener {
54+
syncFragmentViewModel.cancelOneTimeSyncWork()
55+
}
5256
observeLastSyncTime()
5357
launchAndRepeatStarted(
5458
{ syncFragmentViewModel.pollState.collect(::currentSyncJobStatus) },
@@ -73,24 +77,41 @@ class SyncFragment : Fragment() {
7377
}
7478

7579
private fun currentSyncJobStatus(currentSyncJobStatus: CurrentSyncJobStatus) {
76-
requireView().findViewById<TextView>(R.id.current_status).text =
80+
Timber.d("currentSyncJobStatus: $currentSyncJobStatus")
81+
// Update status text
82+
val statusTextView = requireView().findViewById<TextView>(R.id.current_status)
83+
statusTextView.text =
7784
getString(R.string.current_status, currentSyncJobStatus::class.java.simpleName)
7885

79-
// Update progress indicator visibility and handle status-specific actions
86+
// Get views once to avoid repeated lookups
8087
val syncIndicator = requireView().findViewById<ProgressBar>(R.id.sync_indicator)
88+
val syncNowButton = requireView().findViewById<Button>(R.id.sync_now_button)
89+
val cancelSyncButton = requireView().findViewById<Button>(R.id.cancel_sync_button)
90+
91+
// Update view states based on sync status
8192
when (currentSyncJobStatus) {
8293
is CurrentSyncJobStatus.Running -> {
8394
syncIndicator.visibility = View.VISIBLE
95+
syncNowButton.visibility = View.GONE
96+
cancelSyncButton.visibility = View.VISIBLE
8497
}
8598
is CurrentSyncJobStatus.Succeeded -> {
8699
syncIndicator.visibility = View.GONE
87100
syncFragmentViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp)
101+
syncNowButton.visibility = View.VISIBLE
102+
cancelSyncButton.visibility = View.GONE
88103
}
89104
is CurrentSyncJobStatus.Failed,
105+
is CurrentSyncJobStatus.Cancelled, -> {
106+
syncIndicator.visibility = View.GONE
107+
syncNowButton.visibility = View.VISIBLE
108+
cancelSyncButton.visibility = View.GONE
109+
}
90110
is CurrentSyncJobStatus.Enqueued,
91-
is CurrentSyncJobStatus.Cancelled,
92111
is CurrentSyncJobStatus.Blocked, -> {
93112
syncIndicator.visibility = View.GONE
113+
syncNowButton.visibility = View.GONE
114+
cancelSyncButton.visibility = View.VISIBLE
94115
}
95116
}
96117
}

demo/src/main/java/com/google/android/fhir/demo/SyncFragmentViewModel.kt

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -53,15 +53,21 @@ class SyncFragmentViewModel(application: Application) : AndroidViewModel(applica
5353
val pollState: SharedFlow<CurrentSyncJobStatus> =
5454
_oneTimeSyncTrigger
5555
.flatMapLatest {
56-
Sync.oneTimeSync<DemoFhirSyncWorker>(context = application.applicationContext)
56+
Sync.oneTimeSync<DemoFhirSyncWorker>(
57+
context = application.applicationContext,
58+
)
5759
}
5860
.map { it }
59-
.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
61+
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 0)
6062

6163
fun triggerOneTimeSync() {
6264
viewModelScope.launch { _oneTimeSyncTrigger.emit(true) }
6365
}
6466

67+
fun cancelOneTimeSyncWork() {
68+
viewModelScope.launch { Sync.cancelOneTimeSync<DemoFhirSyncWorker>(getApplication()) }
69+
}
70+
6571
/** Emits last sync time. */
6672
fun updateLastSyncTimestamp(lastSync: OffsetDateTime? = null) {
6773
val formatter =

demo/src/main/res/layout/periodic_sync.xml

+38
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,42 @@
8787
android:layout_marginTop="8dp"
8888
/>
8989

90+
<!-- Sync Now Button -->
91+
<Button
92+
android:id="@+id/sync_now_button"
93+
android:layout_width="wrap_content"
94+
android:layout_height="wrap_content"
95+
app:layout_constraintStart_toStartOf="parent"
96+
app:layout_constraintEnd_toEndOf="parent"
97+
app:layout_constraintTop_toBottomOf="@id/progress_percentage_label"
98+
app:layout_constraintBottom_toBottomOf="parent"
99+
android:layout_marginTop="16dp"
100+
android:layout_marginBottom="64dp"
101+
android:paddingLeft="24dp"
102+
android:paddingRight="24dp"
103+
android:text="Sync Now"
104+
android:backgroundTint="?attr/colorPrimary"
105+
android:textColor="?attr/colorOnPrimary"
106+
app:layout_goneMarginTop="16dp"
107+
/>
108+
109+
<Button
110+
android:id="@+id/cancel_sync_button"
111+
android:layout_width="wrap_content"
112+
android:layout_height="wrap_content"
113+
app:layout_constraintStart_toStartOf="parent"
114+
app:layout_constraintEnd_toEndOf="parent"
115+
app:layout_constraintTop_toBottomOf="@id/progress_percentage_label"
116+
app:layout_constraintBottom_toBottomOf="parent"
117+
android:layout_marginTop="16dp"
118+
android:layout_marginBottom="64dp"
119+
android:paddingLeft="24dp"
120+
android:paddingRight="24dp"
121+
android:text="Cancel Sync"
122+
android:backgroundTint="?attr/colorPrimary"
123+
android:textColor="?attr/colorOnPrimary"
124+
android:visibility="gone"
125+
app:layout_goneMarginTop="16dp"
126+
/>
127+
90128
</androidx.constraintlayout.widget.ConstraintLayout>

demo/src/main/res/layout/sync.xml

+18
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,22 @@
7979
android:textColor="?attr/colorOnPrimary"
8080
/>
8181

82+
<Button
83+
android:id="@+id/cancel_sync_button"
84+
android:layout_width="wrap_content"
85+
android:layout_height="wrap_content"
86+
app:layout_constraintStart_toStartOf="parent"
87+
app:layout_constraintEnd_toEndOf="parent"
88+
app:layout_constraintTop_toBottomOf="@id/sync_now_button"
89+
app:layout_constraintBottom_toBottomOf="parent"
90+
android:layout_marginTop="16dp"
91+
android:layout_marginBottom="64dp"
92+
android:paddingLeft="24dp"
93+
android:paddingRight="24dp"
94+
android:text="Cancel Sync"
95+
android:backgroundTint="?attr/colorPrimary"
96+
android:textColor="?attr/colorOnPrimary"
97+
android:visibility="gone"
98+
/>
99+
82100
</androidx.constraintlayout.widget.ConstraintLayout>

docs/use/api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# API
22

3-
* [Engine](api/engine/1.1.0/index.html)
4-
* [Data Capture](api/data-capture/1.2.0/index.html)
3+
* [Engine](api/engine/1.2.0/index.html)
4+
* [Data Capture](api/data-capture/1.3.0/index.html)
55
* [Workflow](api/workflow/0.1.0-beta01/index.html)
66
* [Knowledge](api/knowledge/0.1.0-beta01/index.html)

0 commit comments

Comments
 (0)