From ab68be4c1e30bb391dde8cd47ed9fc0cfbac8d1b Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 29 Jul 2026 18:06:24 +0200 Subject: [PATCH] =?UTF-8?q?Napraw=20p=C4=99tl=C4=99=20uruchamiania=20aplik?= =?UTF-8?q?acji=20kierowcy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/sync/DriverSyncRepository.kt | 3 + .../java/pl/firmatpp/kierowca/ui/DriverApp.kt | 2 +- .../firmatpp/kierowca/ui/DriverViewModel.kt | 72 ++++++++++++++++--- .../firmatpp/kierowca/ui/DriverUiStateTest.kt | 49 +++++++++++++ 4 files changed, 116 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/pl/firmatpp/kierowca/data/sync/DriverSyncRepository.kt b/app/src/main/java/pl/firmatpp/kierowca/data/sync/DriverSyncRepository.kt index ee77139..523049e 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/data/sync/DriverSyncRepository.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/data/sync/DriverSyncRepository.kt @@ -5,6 +5,7 @@ import com.google.gson.Gson import java.io.IOException import java.time.LocalDate import pl.firmatpp.kierowca.data.DriverRepository +import pl.firmatpp.kierowca.data.rethrowIfCancellation import pl.firmatpp.kierowca.data.model.BootstrapResponse import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.RouteResponse @@ -38,6 +39,7 @@ class DriverSyncRepository( saveSyncStates(response.syncState) CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis()) }.getOrElse { throwable -> + throwable.rethrowIfCancellation() cachedBootstrap(requestedDate, throwable) } } @@ -58,6 +60,7 @@ class DriverSyncRepository( saveSyncStates(response.syncState) CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis()) }.getOrElse { throwable -> + throwable.rethrowIfCancellation() cachedRoute(routeId, throwable) } diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index 28f6325..9949ea6 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -317,7 +317,7 @@ fun DriverApp( BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) { viewModel.back() } - DisposableEffect(lifecycleOwner, state.screen) { + DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_START -> { diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt index 53fbbef..0c43dac 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -23,6 +23,7 @@ import pl.firmatpp.kierowca.data.ApiErrorMapper import pl.firmatpp.kierowca.data.ApiErrorKind import pl.firmatpp.kierowca.data.DriverRepository import pl.firmatpp.kierowca.data.PhotoUploadMetadata +import pl.firmatpp.kierowca.data.rethrowIfCancellation import pl.firmatpp.kierowca.data.sync.DriverSyncRepository import pl.firmatpp.kierowca.data.sync.NetworkMonitor import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader @@ -202,6 +203,30 @@ internal fun DriverUiState.withLoadedLeaveRequests( ) } +internal fun shouldStartRoutesLoad( + screen: DriverScreen, + navigateToRoutes: Boolean, + startupOfflineOnly: Boolean, + startupLoadInProgress: Boolean = false, +): Boolean = + (!startupLoadInProgress || navigateToRoutes || startupOfflineOnly) && + (screen != DriverScreen.Initializing || navigateToRoutes || startupOfflineOnly) + +internal fun screenAfterBootstrap( + currentScreen: DriverScreen, + navigateToRoutes: Boolean, + leaveRequestsEnabled: Boolean, +): DriverScreen = when { + currentScreen == DriverScreen.Initializing -> DriverScreen.Routes + !leaveRequestsEnabled && currentScreen in setOf( + DriverScreen.LeaveRequests, + DriverScreen.LeaveRequestDetail, + DriverScreen.AddLeaveRequest, + ) -> DriverScreen.Routes + navigateToRoutes -> DriverScreen.Routes + else -> currentScreen +} + class DriverViewModel(application: Application) : AndroidViewModel(application) { private val appPreferencesStore = AppPreferencesStore(application) private val repository = DriverRepository(application) @@ -236,6 +261,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) private var routesRequestGeneration: Long = 0 private var routeDetailRequestGeneration: Long = 0 private var routesLoadJob: Job? = null + private var startupLoadInProgress: Boolean = false val state: StateFlow = _state init { @@ -516,24 +542,35 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) navigateToRoutes: Boolean, startupOfflineOnly: Boolean = false, ) { + val requestSnapshot = _state.value + if (!shouldStartRoutesLoad( + screen = requestSnapshot.screen, + navigateToRoutes = navigateToRoutes, + startupOfflineOnly = startupOfflineOnly, + startupLoadInProgress = startupLoadInProgress, + ) + ) { + return + } + val isStartup = requestSnapshot.screen == DriverScreen.Initializing + if (isStartup) startupLoadInProgress = true val generation = ++routesRequestGeneration routesLoadJob?.cancel() - routesLoadJob = viewModelScope.launch { - val isStartup = _state.value.screen == DriverScreen.Initializing + val loadJob = viewModelScope.launch { _state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) } runCatching { val cached = when { startupOfflineOnly -> syncRepository.bootstrapFromCache(date) ?: throw IOException("Brak zapisanych danych dla wybranego dnia.") - !_state.value.isOnline -> syncRepository.bootstrapFromCache(date) - ?: throw IOException("Brak zapisanych danych dla wybranego dnia.") isStartup -> { startupOfflineFallbackLoader.load( onlineLoad = { syncRepository.bootstrap(date) }, offlineLoad = { syncRepository.bootstrapFromCache(date) }, ) } + !_state.value.isOnline -> syncRepository.bootstrapFromCache(date) + ?: throw IOException("Brak zapisanych danych dla wybranego dnia.") else -> syncRepository.bootstrap(date) } val response = cached.value @@ -563,10 +600,11 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) leaveRequestsConfig = settings?.leaveRequests?.let { config -> LeaveRequestsConfig(enabled = config.enabled, types = config.types) }, - screen = if ( - settings?.leaveRequests?.enabled != true && - it.screen in setOf(DriverScreen.LeaveRequests, DriverScreen.LeaveRequestDetail, DriverScreen.AddLeaveRequest) - ) DriverScreen.Routes else if (navigateToRoutes) DriverScreen.Routes else it.screen, + screen = screenAfterBootstrap( + currentScreen = it.screen, + navigateToRoutes = navigateToRoutes, + leaveRequestsEnabled = settings?.leaveRequests?.enabled == true, + ), notifyNewRoutes = response.notificationPreferences?.notifyNewRoutes ?: it.notifyNewRoutes, imageAuthHeader = repository.imageAuthHeader(), isStale = cached.stale || !it.isOnline, @@ -593,6 +631,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) reconcileTrackingService(response.routes.today, response.session.driver) } }.onFailure { throwable -> + throwable.rethrowIfCancellation() if (generation != routesRequestGeneration) return@onFailure reportHandledException("load_routes", throwable, mapOf("date" to date)) if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) { @@ -610,6 +649,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) it.copy(loading = false, refreshing = false) } } + routesLoadJob = loadJob + if (isStartup) { + loadJob.invokeOnCompletion { + if (generation == routesRequestGeneration) { + startupLoadInProgress = false + } + } + } } fun openRoute(routeId: String) = openRouteWithTarget(routeId, DriverScreen.Detail) @@ -1810,7 +1857,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) } private suspend fun checkRemoteSyncState() { - if (!_state.value.isOnline) return + val initialSnapshot = _state.value + if ( + !initialSnapshot.isOnline || + initialSnapshot.screen == DriverScreen.Initializing || + initialSnapshot.driver == null + ) return syncCheckMutex.withLock { syncHintMutex.withLock { highestHintVersions.clear() } val snapshot = _state.value @@ -1857,6 +1909,8 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) } private suspend fun handleSyncHint(hint: DriverSyncHint) { + val initialSnapshot = _state.value + if (initialSnapshot.screen == DriverScreen.Initializing || initialSnapshot.driver == null) return syncHintMutex.withLock { val hintKey = listOf(hint.scope, hint.date ?: "-", hint.routeId ?: "-").joinToString(":") val previousVersion = highestHintVersions[hintKey] diff --git a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiStateTest.kt b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiStateTest.kt index 6ae3b08..5d68d19 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiStateTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiStateTest.kt @@ -43,6 +43,55 @@ class DriverUiStateTest { assertEquals(ServerConnectionState.Degraded, failed.serverConnectionState) } + @Test + fun silentRefreshCannotReplaceActiveStartupLoad() { + assertFalse( + shouldStartRoutesLoad( + screen = DriverScreen.Initializing, + navigateToRoutes = false, + startupOfflineOnly = false, + ), + ) + assertFalse( + shouldStartRoutesLoad( + screen = DriverScreen.Routes, + navigateToRoutes = false, + startupOfflineOnly = false, + startupLoadInProgress = true, + ), + ) + } + + @Test + fun startupAndManualOfflineLoadsRemainAvailable() { + assertTrue( + shouldStartRoutesLoad( + screen = DriverScreen.Initializing, + navigateToRoutes = true, + startupOfflineOnly = false, + ), + ) + assertTrue( + shouldStartRoutesLoad( + screen = DriverScreen.Initializing, + navigateToRoutes = false, + startupOfflineOnly = true, + ), + ) + } + + @Test + fun successfulBootstrapAlwaysLeavesInitializingScreen() { + assertEquals( + DriverScreen.Routes, + screenAfterBootstrap( + currentScreen = DriverScreen.Initializing, + navigateToRoutes = false, + leaveRequestsEnabled = true, + ), + ) + } + @Test fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() { val state = DriverUiState(