Etapp 3-feedback: GPS-väntfas, offline-kö för aktiviteter, teknisk logg
All checks were successful
release / build-release (push) Successful in 4m10s
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:
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user