Skip to content

Commit

Permalink
Merge branch 'master' into fix-populate-initial-values
Browse files Browse the repository at this point in the history
  • Loading branch information
parthfloyd authored Jan 29, 2025
2 parents 3c3846e + be94dd5 commit dbb7225
Show file tree
Hide file tree
Showing 10 changed files with 447 additions and 198 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 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 Down Expand Up @@ -298,12 +298,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

private lateinit var currentPageItems: List<QuestionnaireAdapterItem>

/**
* True if the user has tapped the next/previous pagination buttons on the current page. This is
* needed to avoid spewing validation errors before any questions are answered.
*/
private var forceValidation = false

/**
* Map of [QuestionnaireResponseItemAnswerComponent] for
* [Questionnaire.QuestionnaireItemComponent]s that are disabled now. The answers will be used to
Expand Down Expand Up @@ -903,7 +897,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
val validationResult =
if (
modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
forceValidation ||
isInReviewModeFlow.value
) {
questionnaireResponseItemValidator.validate(
Expand Down Expand Up @@ -1124,13 +1117,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
it.item.validationResult is NotValidated
}
) {
// Force update validation results for all questions on the current page. This is needed
// when the user has not answered any questions so no validation has been done.
forceValidation = true
// Add all items on the current page to modifiedQuestionnaireResponseItemSet.
// This will ensure that all fields are validated even when they're not filled by the user
currentPageItems.filterIsInstance<QuestionnaireAdapterItem.Question>().forEach {
modifiedQuestionnaireResponseItemSet.add(it.item.getQuestionnaireResponseItem())
}
// Results in a new questionnaire state being generated synchronously, i.e., the current
// thread will be suspended until the new state is generated.
modificationCount.update { it + 1 }
forceValidation = false
}

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 Google LLC
* Copyright 2024-2025 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 @@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -48,6 +49,7 @@ class PeriodicSyncFragment : Fragment() {
setUpActionBar()
setHasOptionsMenu(true)
refreshPeriodicSynUi()
setUpSyncButtons(view)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
Expand All @@ -67,6 +69,30 @@ class PeriodicSyncFragment : Fragment() {
}
}

private fun setUpSyncButtons(view: View) {
val syncNowButton = view.findViewById<Button>(R.id.sync_now_button)
val cancelSyncButton = view.findViewById<Button>(R.id.cancel_sync_button)
syncNowButton.apply {
setOnClickListener {
periodicSyncViewModel.collectPeriodicSyncJobStatus()
toggleButtonVisibility(hiddenButton = syncNowButton, visibleButton = cancelSyncButton)
visibility = View.GONE
}
}
cancelSyncButton.apply {
setOnClickListener {
periodicSyncViewModel.cancelPeriodicSyncJob()
toggleButtonVisibility(hiddenButton = cancelSyncButton, visibleButton = syncNowButton)
visibility = View.GONE
}
}
}

private fun toggleButtonVisibility(hiddenButton: View, visibleButton: View) {
hiddenButton.visibility = View.GONE
visibleButton.visibility = View.VISIBLE
}

private fun refreshPeriodicSynUi() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 Google LLC
* Copyright 2024-2025 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 Down Expand Up @@ -31,36 +31,43 @@ import com.google.android.fhir.sync.RepeatInterval
import com.google.android.fhir.sync.Sync
import com.google.android.fhir.sync.SyncJobStatus
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import timber.log.Timber

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

val pollPeriodicSyncJobStatus: SharedFlow<PeriodicSyncJobStatus> =
Sync.periodicSync<DemoFhirSyncWorker>(
application.applicationContext,
private val _uiStateFlow = MutableStateFlow(PeriodicSyncUiState())
val uiStateFlow: StateFlow<PeriodicSyncUiState> = _uiStateFlow

private val _pollPeriodicSyncJobStatus = MutableSharedFlow<PeriodicSyncJobStatus>(replay = 10)

init {
viewModelScope.launch { initializePeriodicSync() }
}

private suspend fun initializePeriodicSync() {
val periodicSyncJobStatusFlow =
Sync.periodicSync<DemoFhirSyncWorker>(
context = getApplication<Application>().applicationContext,
periodicSyncConfiguration =
PeriodicSyncConfiguration(
syncConstraints = Constraints.Builder().build(),
repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES),
),
)
.shareIn(viewModelScope, SharingStarted.Eagerly, 10)

private val _uiStateFlow = MutableStateFlow(PeriodicSyncUiState())
val uiStateFlow: StateFlow<PeriodicSyncUiState> = _uiStateFlow

init {
collectPeriodicSyncJobStatus()
periodicSyncJobStatusFlow.collect { status -> _pollPeriodicSyncJobStatus.emit(status) }
}

private fun collectPeriodicSyncJobStatus() {
fun collectPeriodicSyncJobStatus() {
viewModelScope.launch {
pollPeriodicSyncJobStatus.collect { periodicSyncJobStatus ->
_pollPeriodicSyncJobStatus.collect { periodicSyncJobStatus ->
Timber.d(
"currentSyncJobStatus: ${periodicSyncJobStatus.currentSyncJobStatus} lastSyncJobStatus ${periodicSyncJobStatus.lastSyncJobStatus}",
)
val lastSyncStatus = getLastSyncStatus(periodicSyncJobStatus.lastSyncJobStatus)
val lastSyncTime = getLastSyncTime(periodicSyncJobStatus.lastSyncJobStatus)
val currentSyncStatus =
Expand All @@ -83,6 +90,14 @@ class PeriodicSyncViewModel(application: Application) : AndroidViewModel(applica
}
}

fun cancelPeriodicSyncJob() {
viewModelScope.launch {
Sync.cancelPeriodicSync<DemoFhirSyncWorker>(
getApplication<FhirApplication>().applicationContext,
)
}
}

private fun getLastSyncStatus(lastSyncJobStatus: LastSyncJobStatus?): String? {
return when (lastSyncJobStatus) {
is LastSyncJobStatus.Succeeded ->
Expand Down
29 changes: 25 additions & 4 deletions demo/src/main/java/com/google/android/fhir/demo/SyncFragment.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 Google LLC
* Copyright 2024-2025 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 Down Expand Up @@ -30,6 +30,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import com.google.android.fhir.demo.extensions.launchAndRepeatStarted
import com.google.android.fhir.sync.CurrentSyncJobStatus
import timber.log.Timber

class SyncFragment : Fragment() {
private val syncFragmentViewModel: SyncFragmentViewModel by viewModels()
Expand All @@ -49,6 +50,9 @@ class SyncFragment : Fragment() {
view.findViewById<Button>(R.id.sync_now_button).setOnClickListener {
syncFragmentViewModel.triggerOneTimeSync()
}
view.findViewById<Button>(R.id.cancel_sync_button).setOnClickListener {
syncFragmentViewModel.cancelOneTimeSyncWork()
}
observeLastSyncTime()
launchAndRepeatStarted(
{ syncFragmentViewModel.pollState.collect(::currentSyncJobStatus) },
Expand All @@ -73,24 +77,41 @@ class SyncFragment : Fragment() {
}

private fun currentSyncJobStatus(currentSyncJobStatus: CurrentSyncJobStatus) {
requireView().findViewById<TextView>(R.id.current_status).text =
Timber.d("currentSyncJobStatus: $currentSyncJobStatus")
// Update status text
val statusTextView = requireView().findViewById<TextView>(R.id.current_status)
statusTextView.text =
getString(R.string.current_status, currentSyncJobStatus::class.java.simpleName)

// Update progress indicator visibility and handle status-specific actions
// Get views once to avoid repeated lookups
val syncIndicator = requireView().findViewById<ProgressBar>(R.id.sync_indicator)
val syncNowButton = requireView().findViewById<Button>(R.id.sync_now_button)
val cancelSyncButton = requireView().findViewById<Button>(R.id.cancel_sync_button)

// Update view states based on sync status
when (currentSyncJobStatus) {
is CurrentSyncJobStatus.Running -> {
syncIndicator.visibility = View.VISIBLE
syncNowButton.visibility = View.GONE
cancelSyncButton.visibility = View.VISIBLE
}
is CurrentSyncJobStatus.Succeeded -> {
syncIndicator.visibility = View.GONE
syncFragmentViewModel.updateLastSyncTimestamp(currentSyncJobStatus.timestamp)
syncNowButton.visibility = View.VISIBLE
cancelSyncButton.visibility = View.GONE
}
is CurrentSyncJobStatus.Failed,
is CurrentSyncJobStatus.Cancelled, -> {
syncIndicator.visibility = View.GONE
syncNowButton.visibility = View.VISIBLE
cancelSyncButton.visibility = View.GONE
}
is CurrentSyncJobStatus.Enqueued,
is CurrentSyncJobStatus.Cancelled,
is CurrentSyncJobStatus.Blocked, -> {
syncIndicator.visibility = View.GONE
syncNowButton.visibility = View.GONE
cancelSyncButton.visibility = View.VISIBLE
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 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 Down Expand Up @@ -53,15 +53,21 @@ class SyncFragmentViewModel(application: Application) : AndroidViewModel(applica
val pollState: SharedFlow<CurrentSyncJobStatus> =
_oneTimeSyncTrigger
.flatMapLatest {
Sync.oneTimeSync<DemoFhirSyncWorker>(context = application.applicationContext)
Sync.oneTimeSync<DemoFhirSyncWorker>(
context = application.applicationContext,
)
}
.map { it }
.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 0)

fun triggerOneTimeSync() {
viewModelScope.launch { _oneTimeSyncTrigger.emit(true) }
}

fun cancelOneTimeSyncWork() {
viewModelScope.launch { Sync.cancelOneTimeSync<DemoFhirSyncWorker>(getApplication()) }
}

/** Emits last sync time. */
fun updateLastSyncTimestamp(lastSync: OffsetDateTime? = null) {
val formatter =
Expand Down
38 changes: 38 additions & 0 deletions demo/src/main/res/layout/periodic_sync.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,42 @@
android:layout_marginTop="8dp"
/>

<!-- Sync Now Button -->
<Button
android:id="@+id/sync_now_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress_percentage_label"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="64dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:text="Sync Now"
android:backgroundTint="?attr/colorPrimary"
android:textColor="?attr/colorOnPrimary"
app:layout_goneMarginTop="16dp"
/>

<Button
android:id="@+id/cancel_sync_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress_percentage_label"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="64dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:text="Cancel Sync"
android:backgroundTint="?attr/colorPrimary"
android:textColor="?attr/colorOnPrimary"
android:visibility="gone"
app:layout_goneMarginTop="16dp"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
18 changes: 18 additions & 0 deletions demo/src/main/res/layout/sync.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,22 @@
android:textColor="?attr/colorOnPrimary"
/>

<Button
android:id="@+id/cancel_sync_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sync_now_button"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="64dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:text="Cancel Sync"
android:backgroundTint="?attr/colorPrimary"
android:textColor="?attr/colorOnPrimary"
android:visibility="gone"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit dbb7225

Please sign in to comment.