fix: stop orphaned GPS synchronization
This commit is contained in:
@@ -1,12 +1,23 @@
|
|||||||
package pl.firmatpp.kierowca
|
package pl.firmatpp.kierowca
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import pl.firmatpp.kierowca.data.upload.OfflineOutboxManager
|
||||||
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
|
import pl.firmatpp.kierowca.diagnostics.AppDiagnostics
|
||||||
|
|
||||||
class DriverApplication : Application() {
|
class DriverApplication : Application() {
|
||||||
|
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
AppDiagnostics.installFirebaseCrashlytics()
|
AppDiagnostics.installFirebaseCrashlytics()
|
||||||
AppDiagnostics.log("app_started")
|
AppDiagnostics.log("app_started")
|
||||||
|
applicationScope.launch {
|
||||||
|
runCatching { OfflineOutboxManager(this@DriverApplication).repairGpsQueue() }
|
||||||
|
.onFailure { AppDiagnostics.reportNonFatal(it, "repair_gps_queue") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package pl.firmatpp.kierowca.data.upload
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class OfflineOutboxManager(context: Context) {
|
class OfflineOutboxManager(private val context: Context) {
|
||||||
private val database = DriverDatabase.get(context)
|
private val database = DriverDatabase.get(context)
|
||||||
private val workManager = WorkManager.getInstance(context)
|
private val workManager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
@@ -14,6 +15,27 @@ class OfflineOutboxManager(context: Context) {
|
|||||||
database.routeActionDao().allUnsent().size +
|
database.routeActionDao().allUnsent().size +
|
||||||
database.routePointDao().allUnsent().size
|
database.routePointDao().allUnsent().size
|
||||||
|
|
||||||
|
suspend fun repairGpsQueue() {
|
||||||
|
val retryableStatuses = setOf(
|
||||||
|
RoutePointStatus.Pending,
|
||||||
|
RoutePointStatus.Syncing,
|
||||||
|
RoutePointStatus.FailedRetryable,
|
||||||
|
)
|
||||||
|
val routeIds = database.routePointDao().allUnsent()
|
||||||
|
.filter { it.status in retryableStatuses }
|
||||||
|
.map { it.routeId }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
routeIds.forEach { routeId ->
|
||||||
|
runCatching {
|
||||||
|
workManager.cancelUniqueWork(RoutePointWorker.uniqueWorkName(routeId))
|
||||||
|
.result
|
||||||
|
.get(5, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
RoutePointOutbox(context).enqueueWorker(routeId, delaySeconds = 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearAll() {
|
suspend fun clearAll() {
|
||||||
val photos = database.photoUploadDao().allRecords()
|
val photos = database.photoUploadDao().allRecords()
|
||||||
val dispatchSheets = database.dispatchSheetUploadDao().allRecords()
|
val dispatchSheets = database.dispatchSheetUploadDao().allRecords()
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ interface RoutePointDao {
|
|||||||
@Query("SELECT COUNT(*) FROM route_points WHERE status IN ('PENDING', 'SYNCING', 'FAILED_RETRYABLE')")
|
@Query("SELECT COUNT(*) FROM route_points WHERE status IN ('PENDING', 'SYNCING', 'FAILED_RETRYABLE')")
|
||||||
fun observeUnsentCount(): Flow<Int>
|
fun observeUnsentCount(): Flow<Int>
|
||||||
|
|
||||||
|
@Query("SELECT MIN(createdAtEpochMillis) FROM route_points WHERE status IN ('PENDING', 'SYNCING', 'FAILED_RETRYABLE')")
|
||||||
|
fun observeOldestUnsentAt(): Flow<Long?>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
UPDATE route_points
|
UPDATE route_points
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class RoutePointOutbox(
|
|||||||
|
|
||||||
workManager.enqueueUniqueWork(
|
workManager.enqueueUniqueWork(
|
||||||
RoutePointWorker.uniqueWorkName(routeId),
|
RoutePointWorker.uniqueWorkName(routeId),
|
||||||
ExistingWorkPolicy.APPEND_OR_REPLACE,
|
ExistingWorkPolicy.KEEP,
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import pl.firmatpp.kierowca.data.ApiErrorKind
|
import pl.firmatpp.kierowca.data.ApiErrorKind
|
||||||
import pl.firmatpp.kierowca.data.ApiErrorMapper
|
import pl.firmatpp.kierowca.data.ApiErrorMapper
|
||||||
import pl.firmatpp.kierowca.data.DriverRepository
|
import pl.firmatpp.kierowca.data.DriverRepository
|
||||||
@@ -23,31 +24,34 @@ class RoutePointWorker(
|
|||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (!NetworkMonitor(applicationContext).isCurrentlyValidated()) return Result.retry()
|
if (!NetworkMonitor(applicationContext).isCurrentlyValidated()) return Result.retry()
|
||||||
val routeId = inputData.getString(KEY_ROUTE_ID) ?: return Result.failure()
|
val routeId = inputData.getString(KEY_ROUTE_ID) ?: return Result.failure()
|
||||||
|
while (!isStopped) {
|
||||||
val points = dao.pendingForRoute(routeId, BATCH_LIMIT)
|
val points = dao.pendingForRoute(routeId, BATCH_LIMIT)
|
||||||
if (points.isEmpty()) return Result.success()
|
if (points.isEmpty()) return Result.success()
|
||||||
|
|
||||||
val pointIds = points.map { it.clientPointId }
|
val pointIds = points.map { it.clientPointId }
|
||||||
dao.updateStatus(pointIds, RoutePointStatus.Syncing, null, attemptIncrement = 1)
|
dao.updateStatus(pointIds, RoutePointStatus.Syncing, null, attemptIncrement = 1)
|
||||||
|
|
||||||
return runCatching {
|
val outcome = runCatching {
|
||||||
val response = repository.storeRoutePoints(
|
repository.storeRoutePoints(
|
||||||
routeId = routeId,
|
routeId = routeId,
|
||||||
body = RoutePointBatchBody(
|
body = RoutePointBatchBody(
|
||||||
clientBatchId = UUID.randomUUID().toString(),
|
clientBatchId = UUID.randomUUID().toString(),
|
||||||
points = points.map { it.toDto() },
|
points = points.map { it.toDto() },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
val response = outcome.getOrNull()
|
||||||
|
if (response != null) {
|
||||||
dao.delete(pointIds)
|
dao.delete(pointIds)
|
||||||
|
if (response.arrived) ActiveRouteTrackingService.markArrived(applicationContext, routeId)
|
||||||
if (response.arrived) {
|
if (dao.pendingCount(routeId) == 0) {
|
||||||
ActiveRouteTrackingService.markArrived(applicationContext, routeId)
|
delay(750L)
|
||||||
|
if (dao.pendingCount(routeId) == 0) return Result.success()
|
||||||
}
|
}
|
||||||
if (dao.pendingCount(routeId) > 0) {
|
continue
|
||||||
RoutePointOutbox(applicationContext).enqueueWorker(routeId, delaySeconds = 1L)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.success()
|
val throwable = outcome.exceptionOrNull() ?: return Result.retry()
|
||||||
}.getOrElse { throwable ->
|
|
||||||
val error = ApiErrorMapper.map(throwable)
|
val error = ApiErrorMapper.map(throwable)
|
||||||
AppDiagnostics.reportNonFatal(
|
AppDiagnostics.reportNonFatal(
|
||||||
throwable = throwable,
|
throwable = throwable,
|
||||||
@@ -61,11 +65,18 @@ class RoutePointWorker(
|
|||||||
"retryable" to error.retryable,
|
"retryable" to error.retryable,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (isInactiveRoutePointError(error.code)) {
|
||||||
|
dao.delete(pointIds)
|
||||||
|
ActiveRouteTrackingService.stop(applicationContext, routeId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
val status = if (error.retryable) RoutePointStatus.FailedRetryable else RoutePointStatus.FailedPermanent
|
val status = if (error.retryable) RoutePointStatus.FailedRetryable else RoutePointStatus.FailedPermanent
|
||||||
dao.updateStatus(pointIds, status, error.message, attemptIncrement = 0)
|
dao.updateStatus(pointIds, status, error.message, attemptIncrement = 0)
|
||||||
|
return if (error.retryable && error.kind != ApiErrorKind.Auth) Result.retry() else Result.failure()
|
||||||
if (error.retryable && error.kind != ApiErrorKind.Auth) Result.retry() else Result.failure()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Result.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoutePointEntity.toDto(): RoutePointDto =
|
private fun RoutePointEntity.toDto(): RoutePointDto =
|
||||||
@@ -90,3 +101,5 @@ class RoutePointWorker(
|
|||||||
fun uniqueWorkName(routeId: String): String = "route-point-sync-$routeId"
|
fun uniqueWorkName(routeId: String): String = "route-point-sync-$routeId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun isInactiveRoutePointError(code: String?): Boolean = code == "ROUTE_POINTS_INVALID_STATUS"
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
|
val requestedRouteId = intent.getStringExtra(EXTRA_ROUTE_ID)
|
||||||
|
if (!requestedRouteId.isNullOrBlank() && routeId != requestedRouteId) {
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
@@ -64,13 +68,25 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
if (!id.isNullOrBlank() && (routeId == null || id == routeId)) {
|
if (!id.isNullOrBlank() && (routeId == null || id == routeId)) {
|
||||||
routeId = id
|
routeId = id
|
||||||
arrived = true
|
arrived = true
|
||||||
|
runCatching { locationManager.removeUpdates(this) }
|
||||||
startForegroundCompat(buildNotification())
|
startForegroundCompat(buildNotification())
|
||||||
}
|
}
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
routeId = intent?.getStringExtra(EXTRA_ROUTE_ID) ?: routeId
|
val nextRouteId = intent?.getStringExtra(EXTRA_ROUTE_ID) ?: routeId
|
||||||
driverId = intent?.getStringExtra(EXTRA_DRIVER_ID) ?: driverId
|
val nextDriverId = intent?.getStringExtra(EXTRA_DRIVER_ID) ?: driverId
|
||||||
|
if (nextRouteId.isNullOrBlank() || nextDriverId.isNullOrBlank()) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
val routeChanged = nextRouteId != routeId
|
||||||
|
routeId = nextRouteId
|
||||||
|
driverId = nextDriverId
|
||||||
|
getSharedPreferences(TRACKING_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putString(ACTIVE_ROUTE_ID, nextRouteId)
|
||||||
|
.apply()
|
||||||
routeLabel = intent?.getStringExtra(EXTRA_ROUTE_LABEL) ?: routeLabel
|
routeLabel = intent?.getStringExtra(EXTRA_ROUTE_LABEL) ?: routeLabel
|
||||||
destinationLat = intent?.takeIf { it.hasExtra(EXTRA_DESTINATION_LAT) }?.getDoubleExtra(EXTRA_DESTINATION_LAT, 0.0)
|
destinationLat = intent?.takeIf { it.hasExtra(EXTRA_DESTINATION_LAT) }?.getDoubleExtra(EXTRA_DESTINATION_LAT, 0.0)
|
||||||
?: destinationLat
|
?: destinationLat
|
||||||
@@ -80,10 +96,10 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
?.takeIf { it > 0.0 }
|
?.takeIf { it > 0.0 }
|
||||||
?: totalDistanceMeters
|
?: totalDistanceMeters
|
||||||
progressEnabled = intent?.getBooleanExtra(EXTRA_PROGRESS_ENABLED, true) ?: progressEnabled
|
progressEnabled = intent?.getBooleanExtra(EXTRA_PROGRESS_ENABLED, true) ?: progressEnabled
|
||||||
arrived = false
|
if (routeChanged) arrived = false
|
||||||
startForegroundCompat(buildNotification())
|
startForegroundCompat(buildNotification())
|
||||||
requestLocationUpdates()
|
if (!arrived) requestLocationUpdates()
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +112,7 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
|
|
||||||
if (!arrived && remaining != null && remaining <= arrivalRadiusMeters(location)) {
|
if (!arrived && remaining != null && remaining <= arrivalRadiusMeters(location)) {
|
||||||
arrived = true
|
arrived = true
|
||||||
|
runCatching { locationManager.removeUpdates(this) }
|
||||||
startForegroundCompat(buildNotification())
|
startForegroundCompat(buildNotification())
|
||||||
} else {
|
} else {
|
||||||
startForegroundCompat(buildNotification())
|
startForegroundCompat(buildNotification())
|
||||||
@@ -119,14 +136,19 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
runCatching { locationManager.removeUpdates(this) }
|
runCatching { locationManager.removeUpdates(this) }
|
||||||
|
getSharedPreferences(TRACKING_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.remove(ACTIVE_ROUTE_ID)
|
||||||
|
.apply()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestLocationUpdates() {
|
private fun requestLocationUpdates() {
|
||||||
if (!hasLocationPermission()) {
|
if (!hasLocationPermission() || routeId.isNullOrBlank() || driverId.isNullOrBlank()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
runCatching { locationManager.removeUpdates(this) }
|
||||||
runCatching {
|
runCatching {
|
||||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_INTERVAL_MS, 0f, this)
|
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_INTERVAL_MS, 0f, this)
|
||||||
}
|
}
|
||||||
@@ -266,6 +288,8 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
private const val EXTRA_DESTINATION_LON = "destinationLon"
|
private const val EXTRA_DESTINATION_LON = "destinationLon"
|
||||||
private const val EXTRA_TOTAL_DISTANCE_METERS = "totalDistanceMeters"
|
private const val EXTRA_TOTAL_DISTANCE_METERS = "totalDistanceMeters"
|
||||||
private const val EXTRA_PROGRESS_ENABLED = "progressEnabled"
|
private const val EXTRA_PROGRESS_ENABLED = "progressEnabled"
|
||||||
|
private const val TRACKING_PREFERENCES = "active_route_tracking"
|
||||||
|
private const val ACTIVE_ROUTE_ID = "active_route_id"
|
||||||
|
|
||||||
fun start(context: Context, route: DriverRouteDto, driver: DriverDto, progressEnabled: Boolean = true) {
|
fun start(context: Context, route: DriverRouteDto, driver: DriverDto, progressEnabled: Boolean = true) {
|
||||||
val intent = Intent(context, ActiveRouteTrackingService::class.java).apply {
|
val intent = Intent(context, ActiveRouteTrackingService::class.java).apply {
|
||||||
@@ -281,12 +305,16 @@ class ActiveRouteTrackingService : Service(), LocationListener {
|
|||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context, routeId: String? = null) {
|
||||||
context.startService(
|
val activeRouteId = context.getSharedPreferences(TRACKING_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
Intent(context, ActiveRouteTrackingService::class.java).apply {
|
.getString(ACTIVE_ROUTE_ID, null)
|
||||||
action = ACTION_STOP
|
if (routeId == null || routeId == activeRouteId) {
|
||||||
},
|
context.stopService(Intent(context, ActiveRouteTrackingService::class.java))
|
||||||
)
|
context.getSharedPreferences(TRACKING_PREFERENCES, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.remove(ACTIVE_ROUTE_ID)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markArrived(context: Context, routeId: String) {
|
fun markArrived(context: Context, routeId: String) {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ data class DriverUiState(
|
|||||||
val pendingLogoutItems: Int = 0,
|
val pendingLogoutItems: Int = 0,
|
||||||
val pendingDispatchUploads: Int = 0,
|
val pendingDispatchUploads: Int = 0,
|
||||||
val pendingGpsPoints: Int = 0,
|
val pendingGpsPoints: Int = 0,
|
||||||
|
val oldestPendingGpsAtEpochMillis: Long? = null,
|
||||||
val diagnostics: DiagnosticSnapshot? = null,
|
val diagnostics: DiagnosticSnapshot? = null,
|
||||||
val diagnosticsLoading: Boolean = false,
|
val diagnosticsLoading: Boolean = false,
|
||||||
val diagnosticsError: String? = null,
|
val diagnosticsError: String? = null,
|
||||||
@@ -138,13 +139,20 @@ data class DriverUiState(
|
|||||||
val displaySelectedRoute: DriverRouteDto?
|
val displaySelectedRoute: DriverRouteDto?
|
||||||
get() = selectedRouteProjection?.route
|
get() = selectedRouteProjection?.route
|
||||||
|
|
||||||
val pendingOperationalItems: Int
|
private val pendingUserOperationalItems: Int
|
||||||
get() = queuedPhotoUploads.count { it.status != PhotoUploadStatus.Confirmed.storageValue && it.status != PhotoUploadStatus.Cancelled.storageValue } +
|
get() = queuedPhotoUploads.count { it.status != PhotoUploadStatus.Confirmed.storageValue && it.status != PhotoUploadStatus.Cancelled.storageValue } +
|
||||||
visibleRouteActions.count { it.status != RouteActionStatus.Confirmed } + pendingDispatchUploads + pendingGpsPoints
|
visibleRouteActions.count { it.status != RouteActionStatus.Confirmed } + pendingDispatchUploads
|
||||||
|
|
||||||
|
val gpsQueueNeedsAttention: Boolean
|
||||||
|
get() = isOnline && oldestPendingGpsAtEpochMillis?.let { System.currentTimeMillis() - it >= 2 * 60 * 1000L } == true
|
||||||
|
|
||||||
|
val pendingOperationalItems: Int
|
||||||
|
get() = pendingUserOperationalItems + if (!isOnline || serverConnectionState == ServerConnectionState.Degraded || gpsQueueNeedsAttention) pendingGpsPoints else 0
|
||||||
|
|
||||||
val operationalQueueNeedsAttention: Boolean
|
val operationalQueueNeedsAttention: Boolean
|
||||||
get() = queuedPhotoUploads.any { it.status == PhotoUploadStatus.FailedPermanent.storageValue } ||
|
get() = queuedPhotoUploads.any { it.status == PhotoUploadStatus.FailedPermanent.storageValue } ||
|
||||||
visibleRouteActions.any { it.status == RouteActionStatus.FailedPermanent || it.status == RouteActionStatus.FailedConflict }
|
visibleRouteActions.any { it.status == RouteActionStatus.FailedPermanent || it.status == RouteActionStatus.FailedConflict } ||
|
||||||
|
gpsQueueNeedsAttention
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun DriverUiState.withApiError(throwable: Throwable): DriverUiState {
|
internal fun DriverUiState.withApiError(throwable: Throwable): DriverUiState {
|
||||||
@@ -240,6 +248,11 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
_state.update { it.copy(pendingGpsPoints = count) }
|
_state.update { it.copy(pendingGpsPoints = count) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
DriverDatabase.get(application).routePointDao().observeOldestUnsentAt().collect { oldest ->
|
||||||
|
_state.update { it.copy(oldestPendingGpsAtEpochMillis = oldest) }
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
liveSyncClient.connectionState.collect { connectionState ->
|
liveSyncClient.connectionState.collect { connectionState ->
|
||||||
DriverRuntimeSyncState.realtimeConnected = connectionState == LiveSyncConnectionState.Connected
|
DriverRuntimeSyncState.realtimeConnected = connectionState == LiveSyncConnectionState.Connected
|
||||||
@@ -405,6 +418,9 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
if (_state.value.isOnline && !cached.stale) registerPushTokenIfAvailable(driverId)
|
if (_state.value.isOnline && !cached.stale) registerPushTokenIfAvailable(driverId)
|
||||||
}
|
}
|
||||||
observeDispatchSheetUploads(response.dispatchSheetReminder?.workDate)
|
observeDispatchSheetUploads(response.dispatchSheetReminder?.workDate)
|
||||||
|
if (_state.value.isOnline && !cached.stale && settings?.selectedDate == settings?.today) {
|
||||||
|
reconcileTrackingService(response.routes.today, response.session.driver)
|
||||||
|
}
|
||||||
}.onFailure { throwable ->
|
}.onFailure { throwable ->
|
||||||
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))
|
||||||
@@ -457,6 +473,9 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
observePhotoUploads(routeId)
|
observePhotoUploads(routeId)
|
||||||
observeRouteActions(routeId)
|
observeRouteActions(routeId)
|
||||||
|
if (_state.value.isOnline && !cached.stale) {
|
||||||
|
reconcileTrackingServiceForRoute(response.route, _state.value.driver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshSelectedRoute() {
|
fun refreshSelectedRoute() {
|
||||||
@@ -484,6 +503,9 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (_state.value.isOnline && !cached.stale) {
|
||||||
|
reconcileTrackingServiceForRoute(response.route, _state.value.driver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onFailure { throwable ->
|
.onFailure { throwable ->
|
||||||
if (generation == routeDetailRequestGeneration) _state.update { it.withApiError(throwable) }
|
if (generation == routeDetailRequestGeneration) _state.update { it.withApiError(throwable) }
|
||||||
@@ -1389,6 +1411,35 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reconcileTrackingService(routes: List<DriverRouteDto>, driver: DriverDto) {
|
||||||
|
val activeRoute = routes.firstOrNull { it.status == "W TRAKCIE" || it.trackingStatus == "active" }
|
||||||
|
if (activeRoute == null) {
|
||||||
|
ActiveRouteTrackingService.stop(getApplication())
|
||||||
|
} else {
|
||||||
|
ActiveRouteTrackingService.start(
|
||||||
|
context = getApplication(),
|
||||||
|
route = activeRoute,
|
||||||
|
driver = driver,
|
||||||
|
progressEnabled = _state.value.routeProgressNotificationEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reconcileTrackingServiceForRoute(route: DriverRouteDto, driver: DriverDto?) {
|
||||||
|
if (route.status == "W TRAKCIE" || route.trackingStatus == "active") {
|
||||||
|
if (driver != null) {
|
||||||
|
ActiveRouteTrackingService.start(
|
||||||
|
context = getApplication(),
|
||||||
|
route = route,
|
||||||
|
driver = driver,
|
||||||
|
progressEnabled = _state.value.routeProgressNotificationEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ActiveRouteTrackingService.stop(getApplication(), route.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeRouteActions(routeId: String) {
|
private fun observeRouteActions(routeId: String) {
|
||||||
routeActionsJob?.cancel()
|
routeActionsJob?.cancel()
|
||||||
routeActionsJob = viewModelScope.launch {
|
routeActionsJob = viewModelScope.launch {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package pl.firmatpp.kierowca.data.upload
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoutePointWorkerRulesTest {
|
||||||
|
@Test
|
||||||
|
fun inactiveRouteConflictDiscardsObsoleteGpsPoints() {
|
||||||
|
assertTrue(isInactiveRoutePointError("ROUTE_POINTS_INVALID_STATUS"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun temporaryServerFailureDoesNotDiscardGpsPoints() {
|
||||||
|
assertFalse(isInactiveRoutePointError("SERVER_UNAVAILABLE"))
|
||||||
|
assertFalse(isInactiveRoutePointError(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package pl.firmatpp.kierowca.ui
|
package pl.firmatpp.kierowca.ui
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -130,6 +131,44 @@ class DriverUiStateTest {
|
|||||||
assertEquals(listOf("above-first", "above-second", "span"), state.displayRoutes.map { it.id })
|
assertEquals(listOf("above-first", "above-second", "span"), state.displayRoutes.map { it.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun freshGpsPointsDoNotShowSynchronizationBannerWhenOnlineIsHealthy() {
|
||||||
|
val state = DriverUiState(
|
||||||
|
isOnline = true,
|
||||||
|
serverConnectionState = ServerConnectionState.Reachable,
|
||||||
|
pendingGpsPoints = 48,
|
||||||
|
oldestPendingGpsAtEpochMillis = System.currentTimeMillis() - 30_000L,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(0, state.pendingOperationalItems)
|
||||||
|
assertFalse(state.operationalQueueNeedsAttention)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun staleGpsBacklogRequiresAttentionWhenOnline() {
|
||||||
|
val state = DriverUiState(
|
||||||
|
isOnline = true,
|
||||||
|
serverConnectionState = ServerConnectionState.Reachable,
|
||||||
|
pendingGpsPoints = 48,
|
||||||
|
oldestPendingGpsAtEpochMillis = System.currentTimeMillis() - 3 * 60_000L,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(48, state.pendingOperationalItems)
|
||||||
|
assertTrue(state.operationalQueueNeedsAttention)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun offlineGpsPointsAreShownAsSavedLocallyNotAsFailure() {
|
||||||
|
val state = DriverUiState(
|
||||||
|
isOnline = false,
|
||||||
|
pendingGpsPoints = 48,
|
||||||
|
oldestPendingGpsAtEpochMillis = System.currentTimeMillis() - 3 * 60_000L,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(48, state.pendingOperationalItems)
|
||||||
|
assertFalse(state.operationalQueueNeedsAttention)
|
||||||
|
}
|
||||||
|
|
||||||
private fun leaveRequest(id: String, status: String, version: Long = 1): DriverLeaveRequestDto =
|
private fun leaveRequest(id: String, status: String, version: Long = 1): DriverLeaveRequestDto =
|
||||||
DriverLeaveRequestDto(
|
DriverLeaveRequestDto(
|
||||||
id = id,
|
id = id,
|
||||||
|
|||||||
Reference in New Issue
Block a user