diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fed2b..1edd9b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed -- **Stats Sync**: Initial sync now starts automatically on first-time setup instead of requiring manual "Force Full Resync" +### Added +- **Stats Sync**: Pull-to-refresh in Stats screen now triggers a background sync +- **Stats Sync**: Automatic sync every 60 seconds while Stats screen is visible ## [0.8.0] - 2026-01-05 diff --git a/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt b/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt index 33bf94c..9a35984 100644 --- a/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt +++ b/app/src/main/java/com/matedroid/ui/screens/stats/StatsScreen.kt @@ -103,6 +103,14 @@ fun StatsScreen( viewModel.setCarId(carId) } + // Periodic sync every 60 seconds while the screen is visible + LaunchedEffect(Unit) { + while (true) { + kotlinx.coroutines.delay(60_000L) // Wait 60 seconds + viewModel.triggerSync() + } + } + LaunchedEffect(uiState.error) { uiState.error?.let { error -> snackbarHostState.showSnackbar(error) diff --git a/app/src/main/java/com/matedroid/ui/screens/stats/StatsViewModel.kt b/app/src/main/java/com/matedroid/ui/screens/stats/StatsViewModel.kt index 688250a..f654e02 100644 --- a/app/src/main/java/com/matedroid/ui/screens/stats/StatsViewModel.kt +++ b/app/src/main/java/com/matedroid/ui/screens/stats/StatsViewModel.kt @@ -1,8 +1,16 @@ package com.matedroid.ui.screens.stats +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager import com.matedroid.data.repository.StatsRepository +import com.matedroid.data.sync.DataSyncWorker import com.matedroid.data.sync.SyncLogCollector import com.matedroid.data.sync.SyncManager import com.matedroid.domain.model.CarStats @@ -10,8 +18,8 @@ import com.matedroid.domain.model.SyncPhase import com.matedroid.domain.model.SyncProgress import com.matedroid.domain.model.YearFilter import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -33,6 +41,7 @@ data class StatsUiState( @HiltViewModel class StatsViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val statsRepository: StatsRepository, private val syncManager: SyncManager, private val syncLogCollector: SyncLogCollector @@ -44,6 +53,9 @@ class StatsViewModel @Inject constructor( /** Sync logs for debug viewing */ val syncLogs: StateFlow> = syncLogCollector.logs + /** Expose sync status for UI to observe */ + val syncStatus = syncManager.syncStatus + private var carId: Int? = null private var syncObserverJob: Job? = null @@ -88,11 +100,40 @@ class StatsViewModel @Inject constructor( fun refresh() { viewModelScope.launch { _uiState.update { it.copy(isRefreshing = true) } + // Trigger sync to fetch new data from server + triggerSync() loadStatsInternal() _uiState.update { it.copy(isRefreshing = false) } } } + /** + * Trigger a background sync to fetch new data from the server. + * Uses KEEP policy to avoid interrupting a running sync. + */ + fun triggerSync() { + // Skip if sync is already running + if (syncManager.syncStatus.value.isAnySyncing) { + return + } + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val syncRequest = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag(DataSyncWorker.TAG) + .build() + + WorkManager.getInstance(context).enqueueUniqueWork( + DataSyncWorker.WORK_NAME, + ExistingWorkPolicy.KEEP, // Don't interrupt running sync + syncRequest + ) + } + fun clearError() { _uiState.update { it.copy(error = null) } }