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:
Tizian 「ティツィアーン」 2026-11-05 16:47:23 +01:00
commit 3bd49920db
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 275 additions and 52 deletions

View file

@ -15,6 +15,7 @@
"activityHiking": "Wandern",
"activityCycling": "Radfahren",
"activityStrength": "Krafttraining",
"activityIndoorCycling": "Radfahren drinnen",
"activityIndoorRowing": "Rudern drinnen",
"activityOther": "Anderes Training",
"recordAutoPause": "Von selbst pausieren",
@ -28,6 +29,7 @@
"recordingNextStep": "Danach: {step}",
"recordingStepTapLap": "Bis du auf Runde tippst",
"recordLocationRefused": "Train braucht deinen Standort, um das aufzuzeichnen. Du kannst ihn in den Einstellungen des Systems erlauben.",
"recordActivityRefused": "Train braucht die Berechtigung für körperliche Aktivität, um eine Fahrt drinnen aufzuzeichnen. Du kannst sie in den Systemeinstellungen erlauben.",
"recordingWaitingForGps": "Warte auf GPS",
"recordingGpsSilent": "Seit einer Minute kein GPS. Ist der Standort an und darf Train deinen genauen Standort verwenden?",
"recordingWaitingForGpsAccuracy": "Warte auf GPS · ±{metres} m",
@ -55,7 +57,7 @@
"recordingCutOffMessage": "Eine Aufzeichnung von {start} wurde abgebrochen. Bis {end} speichern oder verwerfen?",
"recordingNotificationChannel": "Aufzeichnung",
"recordingNotificationPaused": "{activity} · pausiert",
"workoutName": "{when, select, morning_strength{Krafttraining am Morgen} afternoon_strength{Krafttraining am Nachmittag} evening_strength{Krafttraining am Abend} night_strength{Krafttraining in der Nacht} morning_running{Morgenlauf} morning_walking{Morgenspaziergang} morning_hiking{Morgenwanderung} morning_cycling{Morgenradfahrt} morning_indoorRowing{Rudern am Morgen} afternoon_running{Nachmittagslauf} afternoon_walking{Nachmittagsspaziergang} afternoon_hiking{Nachmittagswanderung} afternoon_cycling{Nachmittagsradfahrt} afternoon_indoorRowing{Rudern am Nachmittag} evening_running{Abendlauf} evening_walking{Abendspaziergang} evening_hiking{Abendwanderung} evening_cycling{Abendradfahrt} evening_indoorRowing{Rudern am Abend} night_running{Nachtlauf} night_walking{Nachtspaziergang} night_hiking{Nachtwanderung} night_cycling{Nachtradfahrt} night_indoorRowing{Rudern in der Nacht} other{Training}}",
"workoutName": "{when, select, morning_strength{Krafttraining am Morgen} afternoon_strength{Krafttraining am Nachmittag} evening_strength{Krafttraining am Abend} night_strength{Krafttraining in der Nacht} morning_running{Morgenlauf} morning_walking{Morgenspaziergang} morning_hiking{Morgenwanderung} morning_cycling{Morgenradfahrt} morning_indoorCycling{Radfahren drinnen am Morgen} afternoon_indoorCycling{Radfahren drinnen am Nachmittag} evening_indoorCycling{Radfahren drinnen am Abend} night_indoorCycling{Radfahren drinnen in der Nacht} morning_indoorRowing{Rudern am Morgen} afternoon_running{Nachmittagslauf} afternoon_walking{Nachmittagsspaziergang} afternoon_hiking{Nachmittagswanderung} afternoon_cycling{Nachmittagsradfahrt} afternoon_indoorRowing{Rudern am Nachmittag} evening_running{Abendlauf} evening_walking{Abendspaziergang} evening_hiking{Abendwanderung} evening_cycling{Abendradfahrt} evening_indoorRowing{Rudern am Abend} night_running{Nachtlauf} night_walking{Nachtspaziergang} night_hiking{Nachtwanderung} night_cycling{Nachtradfahrt} night_indoorRowing{Rudern in der Nacht} other{Training}}",
"workoutLocked": "Gesperrtes Training",
"workoutLockedHint": "Dieses Gerät hat den Schlüssel nicht, mit dem es versiegelt ist.",
"workoutEdit": "Bearbeiten",

View file

@ -21,6 +21,7 @@
"activityHiking": "Hiking",
"activityCycling": "Cycling",
"activityStrength": "Strength training",
"activityIndoorCycling": "Cycling indoors",
"activityIndoorRowing": "Rowing indoors",
"activityOther": "Other workout",
"recordAutoPause": "Pause by itself",
@ -54,6 +55,7 @@
},
"recordingStepTapLap": "Until you tap Lap",
"recordLocationRefused": "Train needs your location to record this. You can allow it in the system's settings.",
"recordActivityRefused": "Train needs the physical activity permission to record a ride indoors. You can allow it in the system's settings.",
"recordingWaitingForGps": "Waiting for GPS",
"recordingGpsSilent": "No GPS for a minute. Is location on, and is Train allowed your precise location?",
"@recordingGpsSilent": {
@ -156,7 +158,7 @@
}
}
},
"workoutName": "{when, select, morning_strength{Morning strength} afternoon_strength{Afternoon strength} evening_strength{Evening strength} night_strength{Night strength} morning_running{Morning run} morning_walking{Morning walk} morning_hiking{Morning hike} morning_cycling{Morning ride} morning_indoorRowing{Morning row} afternoon_running{Afternoon run} afternoon_walking{Afternoon walk} afternoon_hiking{Afternoon hike} afternoon_cycling{Afternoon ride} afternoon_indoorRowing{Afternoon row} evening_running{Evening run} evening_walking{Evening walk} evening_hiking{Evening hike} evening_cycling{Evening ride} evening_indoorRowing{Evening row} night_running{Night run} night_walking{Night walk} night_hiking{Night hike} night_cycling{Night ride} night_indoorRowing{Night row} other{Workout}}",
"workoutName": "{when, select, morning_strength{Morning strength} afternoon_strength{Afternoon strength} evening_strength{Evening strength} night_strength{Night strength} morning_running{Morning run} morning_walking{Morning walk} morning_hiking{Morning hike} morning_cycling{Morning ride} morning_indoorCycling{Morning indoor ride} afternoon_indoorCycling{Afternoon indoor ride} evening_indoorCycling{Evening indoor ride} night_indoorCycling{Night indoor ride} morning_indoorRowing{Morning row} afternoon_running{Afternoon run} afternoon_walking{Afternoon walk} afternoon_hiking{Afternoon hike} afternoon_cycling{Afternoon ride} afternoon_indoorRowing{Afternoon row} evening_running{Evening run} evening_walking{Evening walk} evening_hiking{Evening hike} evening_cycling{Evening ride} evening_indoorRowing{Evening row} night_running{Night run} night_walking{Night walk} night_hiking{Night hike} night_cycling{Night ride} night_indoorRowing{Night row} other{Workout}}",
"@workoutName": {
"description": "A workout's name where the person gave none: the time of day it started (morning 5–12, afternoon 12–17, evening 17–22, night) and its activity, as Strava names them.",
"placeholders": {

View file

@ -190,6 +190,12 @@ abstract class AppLocalizations {
/// **'Strength training'**
String get activityStrength;
/// No description provided for @activityIndoorCycling.
///
/// In en, this message translates to:
/// **'Cycling indoors'**
String get activityIndoorCycling;
/// No description provided for @activityIndoorRowing.
///
/// In en, this message translates to:
@ -268,6 +274,12 @@ abstract class AppLocalizations {
/// **'Train needs your location to record this. You can allow it in the system\'s settings.'**
String get recordLocationRefused;
/// No description provided for @recordActivityRefused.
///
/// In en, this message translates to:
/// **'Train needs the physical activity permission to record a ride indoors. You can allow it in the system\'s settings.'**
String get recordActivityRefused;
/// No description provided for @recordingWaitingForGps.
///
/// In en, this message translates to:
@ -433,7 +445,7 @@ abstract class AppLocalizations {
/// A workout's name where the person gave none: the time of day it started (morning 5–12, afternoon 12–17, evening 17–22, night) and its activity, as Strava names them.
///
/// In en, this message translates to:
/// **'{when, select, morning_strength{Morning strength} afternoon_strength{Afternoon strength} evening_strength{Evening strength} night_strength{Night strength} morning_running{Morning run} morning_walking{Morning walk} morning_hiking{Morning hike} morning_cycling{Morning ride} morning_indoorRowing{Morning row} afternoon_running{Afternoon run} afternoon_walking{Afternoon walk} afternoon_hiking{Afternoon hike} afternoon_cycling{Afternoon ride} afternoon_indoorRowing{Afternoon row} evening_running{Evening run} evening_walking{Evening walk} evening_hiking{Evening hike} evening_cycling{Evening ride} evening_indoorRowing{Evening row} night_running{Night run} night_walking{Night walk} night_hiking{Night hike} night_cycling{Night ride} night_indoorRowing{Night row} other{Workout}}'**
/// **'{when, select, morning_strength{Morning strength} afternoon_strength{Afternoon strength} evening_strength{Evening strength} night_strength{Night strength} morning_running{Morning run} morning_walking{Morning walk} morning_hiking{Morning hike} morning_cycling{Morning ride} morning_indoorCycling{Morning indoor ride} afternoon_indoorCycling{Afternoon indoor ride} evening_indoorCycling{Evening indoor ride} night_indoorCycling{Night indoor ride} morning_indoorRowing{Morning row} afternoon_running{Afternoon run} afternoon_walking{Afternoon walk} afternoon_hiking{Afternoon hike} afternoon_cycling{Afternoon ride} afternoon_indoorRowing{Afternoon row} evening_running{Evening run} evening_walking{Evening walk} evening_hiking{Evening hike} evening_cycling{Evening ride} evening_indoorRowing{Evening row} night_running{Night run} night_walking{Night walk} night_hiking{Night hike} night_cycling{Night ride} night_indoorRowing{Night row} other{Workout}}'**
String workoutName(String when);
/// No description provided for @workoutLocked.

View file

@ -57,6 +57,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get activityStrength => 'Krafttraining';
@override
String get activityIndoorCycling => 'Radfahren drinnen';
@override
String get activityIndoorRowing => 'Rudern drinnen';
@ -102,6 +105,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get recordLocationRefused =>
'Train braucht deinen Standort, um das aufzuzeichnen. Du kannst ihn in den Einstellungen des Systems erlauben.';
@override
String get recordActivityRefused =>
'Train braucht die Berechtigung für körperliche Aktivität, um eine Fahrt drinnen aufzuzeichnen. Du kannst sie in den Systemeinstellungen erlauben.';
@override
String get recordingWaitingForGps => 'Warte auf GPS';
@ -214,6 +221,10 @@ class AppLocalizationsDe extends AppLocalizations {
'morning_walking': 'Morgenspaziergang',
'morning_hiking': 'Morgenwanderung',
'morning_cycling': 'Morgenradfahrt',
'morning_indoorCycling': 'Radfahren drinnen am Morgen',
'afternoon_indoorCycling': 'Radfahren drinnen am Nachmittag',
'evening_indoorCycling': 'Radfahren drinnen am Abend',
'night_indoorCycling': 'Radfahren drinnen in der Nacht',
'morning_indoorRowing': 'Rudern am Morgen',
'afternoon_running': 'Nachmittagslauf',
'afternoon_walking': 'Nachmittagsspaziergang',

View file

@ -56,6 +56,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get activityStrength => 'Strength training';
@override
String get activityIndoorCycling => 'Cycling indoors';
@override
String get activityIndoorRowing => 'Rowing indoors';
@ -101,6 +104,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get recordLocationRefused =>
'Train needs your location to record this. You can allow it in the system\'s settings.';
@override
String get recordActivityRefused =>
'Train needs the physical activity permission to record a ride indoors. You can allow it in the system\'s settings.';
@override
String get recordingWaitingForGps => 'Waiting for GPS';
@ -212,6 +219,10 @@ class AppLocalizationsEn extends AppLocalizations {
'morning_walking': 'Morning walk',
'morning_hiking': 'Morning hike',
'morning_cycling': 'Morning ride',
'morning_indoorCycling': 'Morning indoor ride',
'afternoon_indoorCycling': 'Afternoon indoor ride',
'evening_indoorCycling': 'Evening indoor ride',
'night_indoorCycling': 'Night indoor ride',
'morning_indoorRowing': 'Morning row',
'afternoon_running': 'Afternoon run',
'afternoon_walking': 'Afternoon walk',