Etapp 3-feedback: GPS-väntfas, offline-kö för aktiviteter, teknisk logg
All checks were successful
release / build-release (push) Successful in 4m10s

1. Start väntar på GPS-fix: klockan startar vid första fixen eller via
   'Starta ändå'-knappen (fasen SEARCHING_GPS → ACTIVE); tid utan fix
   räknas inte längre
2. Offline-sparning: aktiviteter (spårade + manuellt loggade) köas i Room
   (pending_activity, migration v5) och synkas av samma FIFO-kö som passen
   (OpKind.ADD_ACTIVITY); 'väntar på synk'-sektion i aktivitetslistan
3. Teknisk logg (TrackLog): ringbuffert med GPS-fixar (accepterade/förkastade
   + noggrannhet), fasbyten och sparningar — expanderbar på spårskärmen med
   'Dela loggen'-knapp för enkel fältfelsökning

Version 0.8.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
2026-08-25 08:55:34 +02:00
parent 80a758a097
commit 2f6c55e967
11 changed files with 345 additions and 20 deletions

View File

@@ -28,9 +28,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/** Spårningens fas: söker GPS-fix innan klockan startar, sen aktiv. */
enum class TrackingPhase { IDLE, SEARCHING_GPS, ACTIVE }
/** Läget för en pågående (eller nyss avslutad) aktivitetsspårning. */
data class TrackingState(
val isActive: Boolean = false,
val phase: TrackingPhase = TrackingPhase.IDLE,
/** Aktivitetstypen som spåras */
val typeId: Int = 0,
val typeKey: String = "",
@@ -61,6 +65,7 @@ class TrackingService : Service(), LocationListener {
private var locationManager: LocationManager? = null
private var weightKg: Double? = null
private var activeSinceElapsed = 0L
private var lastLocation: Location? = null
// Glidande medel av höjd för att filtrera GPS-brus innan höjdmeter summeras
private val altitudeWindow = ArrayDeque<Double>()
@@ -77,6 +82,10 @@ class TrackingService : Service(), LocationListener {
stopTracking()
return START_NOT_STICKY
}
ACTION_START_NOW -> {
activate("starta ändå")
return START_STICKY
}
else -> {
if (intent == null) return START_NOT_STICKY
startTracking(intent)
@@ -91,6 +100,7 @@ class TrackingService : Service(), LocationListener {
_state.value = TrackingState(
isActive = true,
phase = if (distanceBased) TrackingPhase.SEARCHING_GPS else TrackingPhase.ACTIVE,
typeId = intent.getIntExtra(EXTRA_TYPE_ID, 0),
typeKey = intent.getStringExtra(EXTRA_TYPE_KEY) ?: "",
typeName = intent.getStringExtra(EXTRA_TYPE_NAME) ?: "Aktivitet",
@@ -99,6 +109,11 @@ class TrackingService : Service(), LocationListener {
isDistanceBased = distanceBased,
startedAtEpochMs = System.currentTimeMillis(),
)
TrackLog.clear()
TrackLog.log(
"start: ${_state.value.typeName} (distans=$distanceBased, vikt=${weightKg ?: "?"} kg)" +
if (distanceBased) " — väntar på GPS-fix" else ""
)
val type = if (distanceBased) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
@@ -110,12 +125,16 @@ class TrackingService : Service(), LocationListener {
if (distanceBased) startGps()
// Tick: tid + kcal + notis
// Tick: tid + kcal + notis. Klockan räknar från att fasen blev ACTIVE.
scope.launch {
val startedElapsed = SystemClock.elapsedRealtime()
while (_state.value.isActive) {
val s = _state.value
val elapsed = ((SystemClock.elapsedRealtime() - startedElapsed) / 1000).toInt()
if (s.phase != TrackingPhase.ACTIVE) {
delay(500)
continue
}
if (activeSinceElapsed == 0L) activeSinceElapsed = SystemClock.elapsedRealtime()
val elapsed = ((SystemClock.elapsedRealtime() - activeSinceElapsed) / 1000).toInt()
_state.value = s.copy(
elapsedSeconds = elapsed,
kcal = ActivityKcal.estimate(
@@ -141,8 +160,12 @@ class TrackingService : Service(), LocationListener {
private fun startGps() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
) return
) {
TrackLog.log("FEL: platsbehörighet saknas — ingen GPS-lyssning")
return
}
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
TrackLog.log("GPS-lyssning begärd (GPS_PROVIDER, 2 s / 3 m)")
runCatching {
locationManager?.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
@@ -154,11 +177,31 @@ class TrackingService : Service(), LocationListener {
}
}
/** Gå från GPS-sökning till aktiv (klockan börjar ticka). */
private fun activate(reason: String) {
val s = _state.value
if (!s.isActive || s.phase == TrackingPhase.ACTIVE) return
TrackLog.log("aktiverad ($reason)")
_state.value = s.copy(
phase = TrackingPhase.ACTIVE,
startedAtEpochMs = System.currentTimeMillis(),
)
}
override fun onLocationChanged(location: Location) {
val s = _state.value
if (!s.isActive || !s.isDistanceBased) return
val acc = if (location.hasAccuracy()) location.accuracy else -1f
// Släng riktigt dåliga fixar
if (location.hasAccuracy() && location.accuracy > 30f) return
if (location.hasAccuracy() && location.accuracy > 30f) {
TrackLog.log("fix förkastad: noggrannhet ${"%.0f".format(acc)} m (>30)")
return
}
TrackLog.log(
"fix: acc=${"%.0f".format(acc)} m alt=${if (location.hasAltitude()) "%.0f".format(location.altitude) else "?"}" +
" fart=${if (location.hasSpeed()) "%.1f".format(location.speed * 3.6) else "?"} km/h"
)
if (s.phase == TrackingPhase.SEARCHING_GPS) activate("GPS-fix")
var distance = s.distanceMeters
var elevGain = s.elevationGainMeters
@@ -192,6 +235,11 @@ class TrackingService : Service(), LocationListener {
}
private fun stopTracking() {
val s = _state.value
TrackLog.log(
"stopp: ${s.elapsedSeconds}s, ${"%.0f".format(s.distanceMeters)} m, " +
"+${"%.0f".format(s.elevationGainMeters)} hm, ${s.points.size} punkter, kcal=${s.kcal?.toInt() ?: "?"}"
)
locationManager?.removeUpdates(this)
_state.value = _state.value.copy(isActive = false)
stopForeground(STOP_FOREGROUND_REMOVE)
@@ -248,6 +296,7 @@ class TrackingService : Service(), LocationListener {
private const val NOTIFICATION_ID = 44
const val ACTION_STOP = "eu.brassepc.fitnessdroid.TRACKING_STOP"
const val ACTION_START_NOW = "eu.brassepc.fitnessdroid.TRACKING_START_NOW"
const val EXTRA_TYPE_ID = "typeId"
const val EXTRA_TYPE_KEY = "typeKey"
const val EXTRA_TYPE_NAME = "typeName"
@@ -278,5 +327,12 @@ class TrackingService : Service(), LocationListener {
Intent(context, TrackingService::class.java).apply { action = ACTION_STOP }
)
}
/** "Starta ändå" — börja räkna tid utan att vänta på GPS-fixen. */
fun startNow(context: Context) {
context.startService(
Intent(context, TrackingService::class.java).apply { action = ACTION_START_NOW }
)
}
}
}