Napraw pętlę uruchamiania aplikacji kierowcy
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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<DriverUiState> = _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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user