From 53e4f14b67923d5307960daa7e01666e5d0e1f31 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 26 Aug 2026 08:01:38 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=B6relsevakt:=20accelerometern=20fryser=20s?= =?UTF-8?q?p=C3=A5ret=20n=C3=A4r=20man=20st=C3=A5r=20stilla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPS-drift vid stillastående gav fejkspår/-distans/-höjdmeter (och 2 km/h snittfart från en soffa). Nu mäter accelerometern vibrationsmönstret (glidande 4 s-fönster av |accel|−g): under tröskeln = stilla → kartpositionen uppdateras men spårpunkter, distans och höjdmeter fryses. GPS-rutan visar 'stilla'. Övergångar loggas i tekniska loggen med uppmätt nivå. Inställningar → GPS-spårning: av/på-switch + känslighet (0,35 m/s² standard, justerbar 0,10–1,50). Version 0.8.4. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg --- app/build.gradle.kts | 2 +- .../fitnessdroid/data/SettingsStore.kt | 18 +++++ .../fitnessdroid/data/TrackingService.kt | 78 ++++++++++++++++++- .../ui/settings/SettingsScreen.kt | 33 +++++++- .../fitnessdroid/ui/track/TrackScreen.kt | 12 ++- 5 files changed, 138 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b94ca06..0661d13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,7 @@ android { minSdk = 31 targetSdk = 35 versionCode = ciRunNumber ?: 1 - versionName = "0.8.3" + (ciRunNumber?.let { "+build.$it" } ?: "-dev") + versionName = "0.8.4" + (ciRunNumber?.let { "+build.$it" } ?: "-dev") } signingConfigs { diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt index 2e84821..530b8ab 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/SettingsStore.kt @@ -40,6 +40,10 @@ data class AppSettings( val gpsSpeedFactor: Double = 2.0, /** GPS-filter: fartgolv (km/h) när Doppler-fart saknas/är noll */ val gpsSpeedFloorKmh: Int = 12, + /** Rörelsevakt: frys spår/distans när accelerometern säger stilla */ + val motionGuardEnabled: Boolean = true, + /** Rörelsevaktens känslighet (m/s² avvikelse från vila; lägre = känsligare) */ + val motionThreshold: Double = 0.35, ) class SettingsStore(private val context: Context) { @@ -70,9 +74,21 @@ class SettingsStore(private val context: Context) { gpsAccuracyLimitM = prefs[KEY_GPS_ACC] ?: 35, gpsSpeedFactor = prefs[KEY_GPS_FACTOR]?.toDoubleOrNull() ?: 2.0, gpsSpeedFloorKmh = prefs[KEY_GPS_FLOOR] ?: 12, + motionGuardEnabled = prefs[KEY_MOTION_GUARD]?.toBooleanStrictOrNull() ?: true, + motionThreshold = prefs[KEY_MOTION_THRESHOLD]?.toDoubleOrNull() ?: 0.35, ) } + suspend fun setMotionGuardEnabled(value: Boolean) { + context.settingsDataStore.edit { it[KEY_MOTION_GUARD] = value.toString() } + } + + suspend fun setMotionThreshold(value: Double) { + context.settingsDataStore.edit { + it[KEY_MOTION_THRESHOLD] = (Math.round(value.coerceIn(0.1, 1.5) * 100) / 100.0).toString() + } + } + suspend fun setGpsAccuracyLimit(value: Int) { context.settingsDataStore.edit { it[KEY_GPS_ACC] = value.coerceIn(10, 100) } } @@ -164,5 +180,7 @@ class SettingsStore(private val context: Context) { private val KEY_GPS_ACC = intPreferencesKey("gps_accuracy_limit_m") private val KEY_GPS_FACTOR = stringPreferencesKey("gps_speed_factor") private val KEY_GPS_FLOOR = intPreferencesKey("gps_speed_floor_kmh") + private val KEY_MOTION_GUARD = stringPreferencesKey("motion_guard_enabled") + private val KEY_MOTION_THRESHOLD = stringPreferencesKey("motion_threshold") } } diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/TrackingService.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/TrackingService.kt index 3c7f3ca..c8f3c54 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/TrackingService.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/TrackingService.kt @@ -10,6 +10,10 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ServiceInfo +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager import android.location.Location import android.location.LocationListener import android.location.LocationManager @@ -53,6 +57,8 @@ data class TrackingState( val currentSpeedKmh: Double? = null, val kcal: Double? = null, val gpsFix: Boolean = false, + /** false = accelerometern säger stilla → spår/distans fryst */ + val isMoving: Boolean = true, ) /** @@ -61,7 +67,7 @@ data class TrackingState( * timern — ingen GPS. UI:t läser [state]; servicen äger sanningen så * spårningen överlever att appen swipas bort från recents. */ -class TrackingService : Service(), LocationListener { +class TrackingService : Service(), LocationListener, SensorEventListener { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var locationManager: LocationManager? = null @@ -76,6 +82,13 @@ class TrackingService : Service(), LocationListener { // Glidande Doppler-fart (m/s) — GPS:ens egen fartmätning, stabilare än positionerna private var dopplerEmaMs = 0.0 private var rejectStreak = 0 + // Rörelsevakt (accelerometer): står man stilla fryses spår/distans/höjd + private var sensorManager: SensorManager? = null + private var motionGuard = true + private var motionThreshold = 0.35 + private val motionWindow = ArrayDeque() + private var isMoving = true + private var lastMotionLogged = true // Glidande medel av höjd för att filtrera GPS-brus innan höjdmeter summeras private val altitudeWindow = ArrayDeque() private var smoothedAltitude: Double? = null @@ -109,8 +122,13 @@ class TrackingService : Service(), LocationListener { accuracyLimitM = intent.getIntExtra(EXTRA_GPS_ACC_LIMIT, 35).toFloat() speedFactor = intent.getDoubleExtra(EXTRA_GPS_SPEED_FACTOR, 2.0) speedFloorMs = intent.getIntExtra(EXTRA_GPS_SPEED_FLOOR, 12) / 3.6 + motionGuard = intent.getBooleanExtra(EXTRA_MOTION_GUARD, true) + motionThreshold = intent.getDoubleExtra(EXTRA_MOTION_THRESHOLD, 0.35) dopplerEmaMs = 0.0 rejectStreak = 0 + motionWindow.clear() + isMoving = true + lastMotionLogged = true _state.value = TrackingState( isActive = true, @@ -143,7 +161,10 @@ class TrackingService : Service(), LocationListener { } startForeground(NOTIFICATION_ID, buildNotification(), type) - if (distanceBased) startGps() + if (distanceBased) { + startGps() + if (motionGuard) startMotionGuard() + } // Tick: tid + kcal + notis. Klockan räknar från att fasen blev ACTIVE. scope.launch { @@ -216,6 +237,44 @@ class TrackingService : Service(), LocationListener { } } + private fun startMotionGuard() { + val sm = getSystemService(Context.SENSOR_SERVICE) as SensorManager + sensorManager = sm + val accel = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + if (accel == null) { + TrackLog.log("rörelsevakt: ingen accelerometer — avstängd") + motionGuard = false + return + } + sm.registerListener(this, accel, SensorManager.SENSOR_DELAY_UI) + TrackLog.log("rörelsevakt: på (tröskel ${motionThreshold} m/s²)") + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return + val x = event.values[0].toDouble() + val y = event.values[1].toDouble() + val z = event.values[2].toDouble() + // Avvikelse från vilomagnituden (gravitationen) — steg ger tydliga utslag + val deviation = kotlin.math.abs(kotlin.math.sqrt(x * x + y * y + z * z) - 9.81) + motionWindow.addLast(deviation) + if (motionWindow.size > 20) motionWindow.removeFirst() // ~4 s vid UI-takt + if (motionWindow.size < 8) return + + val avg = motionWindow.average() + val moving = avg > motionThreshold + if (moving != isMoving) { + isMoving = moving + _state.value = _state.value.copy(isMoving = moving) + if (moving != lastMotionLogged) { + TrackLog.log("rörelsevakt: ${if (moving) "i rörelse" else "stilla"} (nivå ${"%.2f".format(avg)})") + lastMotionLogged = moving + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit + /** Gå från GPS-sökning till aktiv (klockan börjar ticka). */ private fun activate(reason: String) { val s = _state.value @@ -274,6 +333,13 @@ class TrackingService : Service(), LocationListener { } rejectStreak = 0 + // Rörelsevakten: stilla ⇒ uppdatera bara kartpositionen, frys resten. + // GPS-drift när man står still blir annars fejkdistans/-spår/-höjdmeter. + if (motionGuard && !isMoving) { + lastLocation = location + return + } + TrackLog.log( "${location.provider}: fix acc=${"%.0f".format(acc)} m" + " alt=${if (location.hasAltitude()) "%.0f".format(location.altitude) else "?"}" + @@ -321,6 +387,7 @@ class TrackingService : Service(), LocationListener { } private fun stopTracking() { + sensorManager?.unregisterListener(this) val s = _state.value TrackLog.log( "stopp: ${s.elapsedSeconds}s, ${"%.0f".format(s.distanceMeters)} m, " + @@ -333,6 +400,7 @@ class TrackingService : Service(), LocationListener { } override fun onDestroy() { + sensorManager?.unregisterListener(this) locationManager?.removeUpdates(this) if (_state.value.isActive) _state.value = _state.value.copy(isActive = false) scope.cancel() @@ -393,6 +461,8 @@ class TrackingService : Service(), LocationListener { const val EXTRA_GPS_ACC_LIMIT = "gpsAccLimit" const val EXTRA_GPS_SPEED_FACTOR = "gpsSpeedFactor" const val EXTRA_GPS_SPEED_FLOOR = "gpsSpeedFloor" + const val EXTRA_MOTION_GUARD = "motionGuard" + const val EXTRA_MOTION_THRESHOLD = "motionThreshold" private val _state = MutableStateFlow(TrackingState()) /** Läses av UI:t — servicen äger sanningen. */ @@ -405,6 +475,8 @@ class TrackingService : Service(), LocationListener { gpsAccuracyLimitM: Int = 35, gpsSpeedFactor: Double = 2.0, gpsSpeedFloorKmh: Int = 12, + motionGuardEnabled: Boolean = true, + motionThreshold: Double = 0.35, ) { val intent = Intent(context, TrackingService::class.java).apply { putExtra(EXTRA_TYPE_ID, type.id) @@ -417,6 +489,8 @@ class TrackingService : Service(), LocationListener { putExtra(EXTRA_GPS_ACC_LIMIT, gpsAccuracyLimitM) putExtra(EXTRA_GPS_SPEED_FACTOR, gpsSpeedFactor) putExtra(EXTRA_GPS_SPEED_FLOOR, gpsSpeedFloorKmh) + putExtra(EXTRA_MOTION_GUARD, motionGuardEnabled) + putExtra(EXTRA_MOTION_THRESHOLD, motionThreshold) } context.startForegroundService(intent) } diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt index bf7c789..5789d6d 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/settings/SettingsScreen.kt @@ -82,6 +82,10 @@ class SettingsViewModel( fun adjustGpsSpeedFloor(delta: Int) = viewModelScope.launch { store.setGpsSpeedFloor(settings.value.gpsSpeedFloorKmh + delta) } + fun setMotionGuard(value: Boolean) = viewModelScope.launch { store.setMotionGuardEnabled(value) } + fun adjustMotionThreshold(delta: Double) = viewModelScope.launch { + store.setMotionThreshold(settings.value.motionThreshold + delta) + } fun setAutoRelogin(value: Boolean) = viewModelScope.launch { store.setAutoRelogin(value) @@ -184,8 +188,35 @@ fun SettingsScreen( onMinus = { viewModel.adjustGpsSpeedFloor(-2) }, onPlus = { viewModel.adjustGpsSpeedFloor(2) }, ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Rörelsevakt (accelerometer)", style = MaterialTheme.typography.bodyLarge) + Text( + "Fryser spår och distans när telefonen känner att du står stilla.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + androidx.compose.material3.Switch( + checked = settings.motionGuardEnabled, + onCheckedChange = viewModel::setMotionGuard, + ) + } + if (settings.motionGuardEnabled) { + StepperRow( + label = "Rörelsekänslighet", + value = "${settings.motionThreshold} m/s²", + onMinus = { viewModel.adjustMotionThreshold(-0.05) }, + onPlus = { viewModel.adjustMotionThreshold(0.05) }, + ) + } Text( - "Gäller från nästa startade aktivitet. Standard: 35 m / 2,0× / 12 km/h.", + "Gäller från nästa startade aktivitet. Standard: 35 m / 2,0× / 12 km/h / 0,35 m/s².", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt index 9679246..c7d402d 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/track/TrackScreen.kt @@ -117,6 +117,8 @@ class TrackViewModel( gpsAccuracyLimitM = cfg.gpsAccuracyLimitM, gpsSpeedFactor = cfg.gpsSpeedFactor, gpsSpeedFloorKmh = cfg.gpsSpeedFloorKmh, + motionGuardEnabled = cfg.motionGuardEnabled, + motionThreshold = cfg.motionThreshold, ) } } @@ -425,7 +427,15 @@ private fun LiveContent( } else "—", Modifier.weight(1f), ) - StatBox("GPS", if (tracking.gpsFix) "OK" else "söker…", Modifier.weight(1f)) + StatBox( + "GPS", + when { + !tracking.gpsFix -> "söker…" + !tracking.isMoving -> "stilla" + else -> "OK" + }, + Modifier.weight(1f), + ) } } Button(