Napraw pętlę uruchamiania aplikacji kierowcy

This commit is contained in:
admin
2026-07-29 18:06:24 +02:00
parent 513c1785ef
commit ab68be4c1e
4 changed files with 116 additions and 10 deletions
@@ -5,6 +5,7 @@ import com.google.gson.Gson
import java.io.IOException import java.io.IOException
import java.time.LocalDate import java.time.LocalDate
import pl.firmatpp.kierowca.data.DriverRepository 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.BootstrapResponse
import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RouteResponse import pl.firmatpp.kierowca.data.model.RouteResponse
@@ -38,6 +39,7 @@ class DriverSyncRepository(
saveSyncStates(response.syncState) saveSyncStates(response.syncState)
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis()) CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
}.getOrElse { throwable -> }.getOrElse { throwable ->
throwable.rethrowIfCancellation()
cachedBootstrap(requestedDate, throwable) cachedBootstrap(requestedDate, throwable)
} }
} }
@@ -58,6 +60,7 @@ class DriverSyncRepository(
saveSyncStates(response.syncState) saveSyncStates(response.syncState)
CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis()) CachedValue(response, stale = false, syncedAtEpochMillis = System.currentTimeMillis())
}.getOrElse { throwable -> }.getOrElse { throwable ->
throwable.rethrowIfCancellation()
cachedRoute(routeId, throwable) cachedRoute(routeId, throwable)
} }
@@ -317,7 +317,7 @@ fun DriverApp(
BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) { BackHandler(enabled = state.screen != DriverScreen.Initializing && state.screen != DriverScreen.Phone && state.screen != DriverScreen.Routes) {
viewModel.back() viewModel.back()
} }
DisposableEffect(lifecycleOwner, state.screen) { DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
when (event) { when (event) {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
@@ -23,6 +23,7 @@ import pl.firmatpp.kierowca.data.ApiErrorMapper
import pl.firmatpp.kierowca.data.ApiErrorKind import pl.firmatpp.kierowca.data.ApiErrorKind
import pl.firmatpp.kierowca.data.DriverRepository import pl.firmatpp.kierowca.data.DriverRepository
import pl.firmatpp.kierowca.data.PhotoUploadMetadata 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.DriverSyncRepository
import pl.firmatpp.kierowca.data.sync.NetworkMonitor import pl.firmatpp.kierowca.data.sync.NetworkMonitor
import pl.firmatpp.kierowca.data.sync.StartupOfflineFallbackLoader 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) { class DriverViewModel(application: Application) : AndroidViewModel(application) {
private val appPreferencesStore = AppPreferencesStore(application) private val appPreferencesStore = AppPreferencesStore(application)
private val repository = DriverRepository(application) private val repository = DriverRepository(application)
@@ -236,6 +261,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
private var routesRequestGeneration: Long = 0 private var routesRequestGeneration: Long = 0
private var routeDetailRequestGeneration: Long = 0 private var routeDetailRequestGeneration: Long = 0
private var routesLoadJob: Job? = null private var routesLoadJob: Job? = null
private var startupLoadInProgress: Boolean = false
val state: StateFlow<DriverUiState> = _state val state: StateFlow<DriverUiState> = _state
init { init {
@@ -516,24 +542,35 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
navigateToRoutes: Boolean, navigateToRoutes: Boolean,
startupOfflineOnly: Boolean = false, 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 val generation = ++routesRequestGeneration
routesLoadJob?.cancel() routesLoadJob?.cancel()
routesLoadJob = viewModelScope.launch { val loadJob = viewModelScope.launch {
val isStartup = _state.value.screen == DriverScreen.Initializing
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) } _state.update { it.copy(loading = showLoading, refreshing = !showLoading, feedback = null, error = null) }
runCatching { runCatching {
val cached = when { val cached = when {
startupOfflineOnly -> syncRepository.bootstrapFromCache(date) startupOfflineOnly -> syncRepository.bootstrapFromCache(date)
?: throw IOException("Brak zapisanych danych dla wybranego dnia.") ?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
!_state.value.isOnline -> syncRepository.bootstrapFromCache(date)
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
isStartup -> { isStartup -> {
startupOfflineFallbackLoader.load( startupOfflineFallbackLoader.load(
onlineLoad = { syncRepository.bootstrap(date) }, onlineLoad = { syncRepository.bootstrap(date) },
offlineLoad = { syncRepository.bootstrapFromCache(date) }, offlineLoad = { syncRepository.bootstrapFromCache(date) },
) )
} }
!_state.value.isOnline -> syncRepository.bootstrapFromCache(date)
?: throw IOException("Brak zapisanych danych dla wybranego dnia.")
else -> syncRepository.bootstrap(date) else -> syncRepository.bootstrap(date)
} }
val response = cached.value val response = cached.value
@@ -563,10 +600,11 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
leaveRequestsConfig = settings?.leaveRequests?.let { config -> leaveRequestsConfig = settings?.leaveRequests?.let { config ->
LeaveRequestsConfig(enabled = config.enabled, types = config.types) LeaveRequestsConfig(enabled = config.enabled, types = config.types)
}, },
screen = if ( screen = screenAfterBootstrap(
settings?.leaveRequests?.enabled != true && currentScreen = it.screen,
it.screen in setOf(DriverScreen.LeaveRequests, DriverScreen.LeaveRequestDetail, DriverScreen.AddLeaveRequest) navigateToRoutes = navigateToRoutes,
) DriverScreen.Routes else if (navigateToRoutes) DriverScreen.Routes else it.screen, leaveRequestsEnabled = settings?.leaveRequests?.enabled == true,
),
notifyNewRoutes = response.notificationPreferences?.notifyNewRoutes ?: it.notifyNewRoutes, notifyNewRoutes = response.notificationPreferences?.notifyNewRoutes ?: it.notifyNewRoutes,
imageAuthHeader = repository.imageAuthHeader(), imageAuthHeader = repository.imageAuthHeader(),
isStale = cached.stale || !it.isOnline, isStale = cached.stale || !it.isOnline,
@@ -593,6 +631,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
reconcileTrackingService(response.routes.today, response.session.driver) reconcileTrackingService(response.routes.today, response.session.driver)
} }
}.onFailure { throwable -> }.onFailure { throwable ->
throwable.rethrowIfCancellation()
if (generation != routesRequestGeneration) return@onFailure if (generation != routesRequestGeneration) return@onFailure
reportHandledException("load_routes", throwable, mapOf("date" to date)) reportHandledException("load_routes", throwable, mapOf("date" to date))
if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) { if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
@@ -610,6 +649,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
it.copy(loading = false, refreshing = false) 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) fun openRoute(routeId: String) = openRouteWithTarget(routeId, DriverScreen.Detail)
@@ -1810,7 +1857,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
private suspend fun checkRemoteSyncState() { 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 { syncCheckMutex.withLock {
syncHintMutex.withLock { highestHintVersions.clear() } syncHintMutex.withLock { highestHintVersions.clear() }
val snapshot = _state.value val snapshot = _state.value
@@ -1857,6 +1909,8 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
private suspend fun handleSyncHint(hint: DriverSyncHint) { private suspend fun handleSyncHint(hint: DriverSyncHint) {
val initialSnapshot = _state.value
if (initialSnapshot.screen == DriverScreen.Initializing || initialSnapshot.driver == null) return
syncHintMutex.withLock { syncHintMutex.withLock {
val hintKey = listOf(hint.scope, hint.date ?: "-", hint.routeId ?: "-").joinToString(":") val hintKey = listOf(hint.scope, hint.date ?: "-", hint.routeId ?: "-").joinToString(":")
val previousVersion = highestHintVersions[hintKey] val previousVersion = highestHintVersions[hintKey]
@@ -43,6 +43,55 @@ class DriverUiStateTest {
assertEquals(ServerConnectionState.Degraded, failed.serverConnectionState) 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 @Test
fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() { fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() {
val state = DriverUiState( val state = DriverUiState(