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

api support for sync job cancellation. #2717

Merged
merged 14 commits into from
Jan 29, 2025
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
Loading