Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062415a75d | ||
|
|
ab68be4c1e |
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 124
|
versionCode = 125
|
||||||
versionName = "1.0.71"
|
versionName = "1.0.72"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user