Rörelsevakt: accelerometern fryser spåret när man står stilla
All checks were successful
release / build-release (push) Successful in 4m19s
All checks were successful
release / build-release (push) Successful in 4m19s
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Double>()
|
||||
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<Double>()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user