feat(train): record a ride indoors, without GPS (ADR 0030, D66)
A home trainer's or an exercise bike's ride had no place: Cycling waits for GPS and pauses by speed. Cycling indoors needs no GPS, route or device: the time, then whichever strap, speed and cadence sensor or power meter there is. It goes to Health Connect as stationary biking, and another app's stationary ride comes in as one. With no GPS, steps or device the recorder has no foreground service type, which Android 14 on doesn't allow, so it asks for the physical activity permission and doesn't start without it. Whats-New: Record a ride on a home trainer or exercise bike with Cycling indoors, without GPS. Whats-New-De: Zeichne eine Fahrt auf dem Heimtrainer mit Radfahren drinnen auf, ganz ohne GPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
34a693f365
commit
3bd49920db
3 changed files with 275 additions and 52 deletions
|
|
@ -49,6 +49,7 @@ object HealthRecords {
|
|||
"walking" -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING
|
||||
"hiking" -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING
|
||||
"cycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING
|
||||
"indoorCycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY
|
||||
"indoorRowing" -> ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE
|
||||
"strength" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
|
||||
else -> ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT
|
||||
|
|
@ -57,7 +58,7 @@ object HealthRecords {
|
|||
/**
|
||||
* Train's activity for another app's exercise type; null for one Train
|
||||
* doesn't record, which isn't brought in. A treadmill's run is a run, a
|
||||
* stationary bike's ride a ride.
|
||||
* stationary bike's ride a ride indoors (D66).
|
||||
*/
|
||||
fun activityOf(exercise: Int): String? =
|
||||
when (exercise) {
|
||||
|
|
@ -66,9 +67,8 @@ object HealthRecords {
|
|||
-> "running"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_WALKING -> "walking"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_HIKING -> "hiking"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY,
|
||||
-> "cycling"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_BIKING -> "cycling"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY -> "indoorCycling"
|
||||
ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE -> "indoorRowing"
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,8 +258,9 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
private fun begin(intent: Intent) {
|
||||
path = intent.getStringExtra(EXTRA_PATH) ?: return stopSelf()
|
||||
val activity = intent.getStringExtra(EXTRA_ACTIVITY) ?: return stopSelf()
|
||||
// No GPS on the rower, whatever location allows.
|
||||
val located = activity != "indoorRowing" && granted(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
// No GPS indoors, whatever location allows.
|
||||
val indoors = activity == "indoorRowing" || activity == "indoorCycling"
|
||||
val located = !indoors && granted(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
val counted =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || granted(Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
// The strap and the rower where they were chosen and connecting is
|
||||
|
|
@ -285,7 +286,9 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
notification(),
|
||||
types(
|
||||
located,
|
||||
stepping,
|
||||
// A ride indoors may have no GPS, steps or device (D66); the app
|
||||
// asked for the permission that allows this type.
|
||||
stepping || (activity == "indoorCycling" && counted),
|
||||
linked = listOf(strapAt, rowerAt, loopAt, speedSensorAt, powerMeterAt).any { it != null },
|
||||
),
|
||||
)
|
||||
|
|
@ -600,7 +603,8 @@ class RecorderService : Service(), LocationListener, SensorEventListener {
|
|||
|
||||
/**
|
||||
* The foreground service's types: location with GPS, health with the step
|
||||
* detector, connectedDevice with the strap or the rower (Android 14 on asks).
|
||||
* detector or on a ride indoors, connectedDevice with the strap or the rower
|
||||
* (Android 14 on asks).
|
||||
*/
|
||||
private fun types(located: Boolean, stepping: Boolean, linked: Boolean): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return 0
|
||||
|
|
|
|||
|
|
@ -135,10 +135,11 @@ class HealthRecordsTest {
|
|||
@Test
|
||||
fun `other apps' exercise types map onto Train's activities, or none`() {
|
||||
assertEquals("running", HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_TREADMILL))
|
||||
assertEquals("cycling", HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY))
|
||||
assertEquals("cycling", HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_BIKING))
|
||||
assertEquals("indoorCycling", HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY))
|
||||
assertEquals("indoorRowing", HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE))
|
||||
assertNull(HealthRecords.activityOf(ExerciseSessionRecord.EXERCISE_TYPE_YOGA))
|
||||
for (activity in listOf("running", "walking", "hiking", "cycling", "indoorRowing")) {
|
||||
for (activity in listOf("running", "walking", "hiking", "cycling", "indoorCycling", "indoorRowing")) {
|
||||
assertEquals(activity, HealthRecords.activityOf(HealthRecords.exerciseOf(activity)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue