fix(train): a recording followed whichever starts first, the app listening or the service
On a fresh install (GrapheneOS) a walk recorded no GPS on the screen, which waited for GPS throughout though the file was whole. The app listens to the recorder's lines as soon as it asked for the start, and Android starts the service a moment later: listening first, it got nothing of the file, and the service then wrote the header straight to the file, past what hands each line on. The reader takes the first line it gets as the header, found none, and dropped every line after. The service now writes its header through write() as every line, and the app takes the header from the file's first line where lines came without one (RecordingReader.recover, on the next second), reading the lines kept since after it, so either order and either recorder version is followed. The fake recorder can now begin late, as the service does. On foot, auto-pause by steps paused after 5 s and went on only with a step where the phone has no step detector or one switched off (GrapheneOS' Sensors): GPS speed decides there, and until the first step and once the detector has been silent 30 s. A minute without any fix, the recording screen asks whether location is on and precise. ADR 0030, D65. Whats-New: A recording follows its GPS again on a phone where it waited for GPS throughout. Whats-New: Auto-pause on foot goes by GPS speed where the phone counts no steps. Whats-New: After a minute without GPS, the recording screen asks whether location is on and precise. Whats-New-De: Eine Aufzeichnung folgt ihrem GPS wieder auf Handys, auf denen sie die ganze Zeit auf GPS wartete. Whats-New-De: Die automatische Pause zu Fuß richtet sich nach der GPS-Geschwindigkeit, wo das Handy keine Schritte zählt. Whats-New-De: Nach einer Minute ohne GPS fragt die Aufzeichnung, ob der Standort an und genau ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
bcbefe38a3
commit
d305725bb5
3 changed files with 252 additions and 21 deletions
|
|
@ -4,7 +4,9 @@ package app.halkyon.train
|
|||
// service, which runs while the app may not: steps on foot, confirmed by GPS;
|
||||
// GPS speed on a bike; GPS speed on foot where steps aren't allowed (D19);
|
||||
// the rower's strokes (D28); on a bike the wheel's speed while its sensor
|
||||
// sends, GPS's once it has been silent 5 s (D51).
|
||||
// sends, GPS's once it has been silent 5 s (D51). On foot by steps, GPS speed
|
||||
// decides while the step detector hasn't sent yet or has been silent 30 s
|
||||
// (D65): a detector switched off (GrapheneOS' Sensors) or missing sends none.
|
||||
// Times in milliseconds; each call returns true where it paused, false where
|
||||
// it went on, null where nothing changed.
|
||||
|
||||
|
|
@ -30,6 +32,9 @@ class AutoPause(private val signal: Signal) {
|
|||
|
||||
private var byHand = false
|
||||
private var lastStep: Long? = null
|
||||
|
||||
/** The last step the detector counted, by hand or not. */
|
||||
private var lastCounted: Long? = null
|
||||
private var slowSince: Long? = null
|
||||
private var lastFixAt: Long? = null
|
||||
private var lastFixFast = false
|
||||
|
|
@ -37,7 +42,10 @@ class AutoPause(private val signal: Signal) {
|
|||
|
||||
/** A step at [at]: two close together go on again (a lone one is a jostle). */
|
||||
fun step(at: Long): Boolean? {
|
||||
if (signal != Signal.STEPS || byHand) return null
|
||||
if (signal != Signal.STEPS) return null
|
||||
lastCounted = at
|
||||
slowSince = null
|
||||
if (byHand) return null
|
||||
val before = lastStep
|
||||
lastStep = at
|
||||
if (paused && before != null && at - before <= STEPS_TO_GO_ON_MS) {
|
||||
|
|
@ -66,7 +74,7 @@ class AutoPause(private val signal: Signal) {
|
|||
// Faster than walking: moving, whatever the steps say (a phone in a
|
||||
// bag counts fewer).
|
||||
lastFixFast = kmh > 5
|
||||
return null
|
||||
return if (stepless(at)) decide(at, kmh) else null
|
||||
}
|
||||
// The wheel decides while it sends (D51).
|
||||
if (signal == Signal.BIKE_SPEED && lastWheelAt?.let { at - it <= WHEEL_FRESH_MS } == true) return null
|
||||
|
|
@ -100,13 +108,14 @@ class AutoPause(private val signal: Signal) {
|
|||
fun tick(at: Long): Boolean? {
|
||||
if (byHand || paused) return null
|
||||
val still =
|
||||
when (signal) {
|
||||
Signal.STROKES -> {
|
||||
when {
|
||||
signal == Signal.STEPS && stepless(at) -> slowSince?.let { at - it >= STILL_MS } ?: false
|
||||
signal == Signal.STROKES -> {
|
||||
// Starting counts as a stroke.
|
||||
val last = lastStep ?: at.also { lastStep = it }
|
||||
at - last >= STILL_MS
|
||||
}
|
||||
Signal.STEPS -> {
|
||||
signal == Signal.STEPS -> {
|
||||
// Starting counts as a step.
|
||||
val last = lastStep ?: at.also { lastStep = it }
|
||||
val fixFresh = lastFixAt?.let { at - it <= FRESH_FIX_MS } ?: false
|
||||
|
|
@ -121,6 +130,9 @@ class AutoPause(private val signal: Signal) {
|
|||
return null
|
||||
}
|
||||
|
||||
/** Whether no step came yet, or none for [STEPLESS_MS]: then GPS speed decides on foot (D65). */
|
||||
private fun stepless(at: Long): Boolean = lastCounted?.let { at - it >= STEPLESS_MS } ?: true
|
||||
|
||||
/** Paused, or gone on, by hand at [at]: left alone while paused, starting afresh after. */
|
||||
fun byHand(paused: Boolean, at: Long = 0) {
|
||||
byHand = paused
|
||||
|
|
@ -136,5 +148,6 @@ class AutoPause(private val signal: Signal) {
|
|||
const val STEPS_TO_GO_ON_MS = 2_000L
|
||||
const val FRESH_FIX_MS = 3_000L
|
||||
const val WHEEL_FRESH_MS = 5_000L
|
||||
const val STEPLESS_MS = 30_000L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,11 +143,16 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
/** Shows [notice] (JSON) in the recording's notification. */
|
||||
fun show(notice: String) = running?.present(JSONObject(notice))
|
||||
|
||||
/** Every line of the file so far, then each new one as it is written. */
|
||||
/**
|
||||
* Every line of the file so far, then each new one as it is written.
|
||||
* Called before the service has begun (the app listens as soon as it
|
||||
* asked for the start, D65), it hands nothing: the header and every line
|
||||
* after come through [lines], as [write] hands each one.
|
||||
*/
|
||||
fun replay(to: (String) -> Unit) {
|
||||
running?.let { service ->
|
||||
service.writer?.flush()
|
||||
File(service.path).forEachLine(action = to)
|
||||
runCatching { File(service.path).forEachLine(action = to) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -284,15 +289,19 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
linked = listOf(strapAt, rowerAt, loopAt, speedSensorAt, powerMeterAt).any { it != null },
|
||||
),
|
||||
)
|
||||
running = this
|
||||
val now = System.currentTimeMillis()
|
||||
writer = File(path).apply { parentFile?.mkdirs() }.bufferedWriter().also {
|
||||
it.append("halkyon-train-recording 1 $activity $now\n")
|
||||
}
|
||||
writer = File(path).apply { parentFile?.mkdirs() }.bufferedWriter()
|
||||
running = this
|
||||
// Through write, as every line: an app that listened before the service
|
||||
// began gets the header too, which it reads everything after by (D65).
|
||||
write("halkyon-train-recording 1 $activity $now")
|
||||
val detects =
|
||||
getSystemService(SensorManager::class.java).getDefaultSensor(Sensor.TYPE_STEP_DETECTOR) != null
|
||||
autoPause =
|
||||
when (autoPauseBy) {
|
||||
// Steps where they may be counted, GPS speed where not (D19).
|
||||
"steps" -> AutoPause(if (counted) AutoPause.Signal.STEPS else AutoPause.Signal.FOOT_SPEED)
|
||||
// Steps where they may be counted and the phone has a step detector,
|
||||
// GPS speed where not (D19, D65).
|
||||
"steps" -> AutoPause(if (counted && detects) AutoPause.Signal.STEPS else AutoPause.Signal.FOOT_SPEED)
|
||||
"bike" -> AutoPause(AutoPause.Signal.BIKE_SPEED)
|
||||
"strokes" -> AutoPause(AutoPause.Signal.STROKES)
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -49,6 +49,24 @@ class AutoPauseTest {
|
|||
assertEquals(emptyMap<Int, Boolean>(), changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on foot a step detector that never sends leaves it to GPS speed from the start`() {
|
||||
// GrapheneOS with Sensors off: steps allowed, none ever counted.
|
||||
val walking = onFoot(steps = emptyList(), until = 40, fixes = (0..40).associateWith { 4.0 })
|
||||
val standing = onFoot(steps = emptyList(), until = 10, fixes = (0..10).associateWith { 1.0 })
|
||||
|
||||
assertEquals(emptyMap<Int, Boolean>(), walking)
|
||||
assertEquals(mapOf(5 to true), standing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on foot a step detector gone silent 30 s while GPS moves leaves it to GPS speed`() {
|
||||
val changes = onFoot(steps = (0..10).toList(), until = 50, fixes = (0..50).associateWith { 4.0 })
|
||||
|
||||
// Paused 5 s after the last step, going on by GPS 30 s after it.
|
||||
assertEquals(mapOf(15 to true, 40 to false), changes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `by bike it pauses below 3 kmh for five seconds and goes on above 5 kmh`() {
|
||||
val changes = byBike(listOf(20.0, 18.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 4.0, 4.5, 6.0, 15.0))
|
||||
|
|
|
|||
Loading…
Reference in a new issue