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
|
|
@ -450,4 +450,37 @@ void main() {
|
|||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
group('a ride indoors (D66)', () {
|
||||
test('waits for no GPS and warns of none, and shows no distance without '
|
||||
'a sensor', () {
|
||||
final live = liveOf(
|
||||
Recording(activity: Activity.indoorCycling, startedAt: at(0)),
|
||||
at(600),
|
||||
);
|
||||
expect(usesGps(Activity.indoorCycling), isFalse);
|
||||
expect(live.waitingForGps, isFalse);
|
||||
expect(live.gpsSilent, isFalse);
|
||||
expect(live.distanceMetres, isNull);
|
||||
expect(live.movingTime, const Duration(seconds: 600));
|
||||
});
|
||||
|
||||
test("shows the distance a wheel's sensor gives", () {
|
||||
final live = liveOf(
|
||||
Recording(
|
||||
activity: Activity.indoorCycling,
|
||||
startedAt: at(0),
|
||||
speedSensors: [LinkMark(time: at(0), state: LinkState.connected)],
|
||||
wheelMm: 2000,
|
||||
wheel: [
|
||||
Reading(time: at(0), value: 0),
|
||||
Reading(time: at(100), value: 500),
|
||||
],
|
||||
),
|
||||
at(100),
|
||||
);
|
||||
expect(live.distanceMetres, 1000);
|
||||
expect(live.bikeSensors, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -800,6 +800,48 @@ void main() {
|
|||
expect((await open()).state.rower, rower);
|
||||
});
|
||||
|
||||
test('a ride indoors asks for no location, starts without GPS or a route, '
|
||||
'and waits for no sensor (D66)', () async {
|
||||
final bloc = await open();
|
||||
bloc.add(const RecordingEventStartRequested(Activity.indoorCycling));
|
||||
await bloc.stream.firstWhere((s) => s.phase == RecordingPhase.recording);
|
||||
|
||||
// The activity's permission is what lets Android keep it running
|
||||
// where nothing else gives the service a type.
|
||||
expect(platform.asked.single, {
|
||||
RecorderPermission.activity,
|
||||
RecorderPermission.notifications,
|
||||
});
|
||||
expect(platform.started.single.autoPause, isNull);
|
||||
expect(platform.started.single.route, isNull);
|
||||
});
|
||||
|
||||
test("a ride indoors takes the bike's sensors, as a ride outdoors does "
|
||||
'(D66)', () async {
|
||||
final bloc = await open();
|
||||
bloc.add(
|
||||
const RecordingEventDeviceChosen(DeviceKind.powerMeter, rower),
|
||||
);
|
||||
await bloc.stream.firstWhere((s) => s.powerMeter == rower);
|
||||
bloc.add(const RecordingEventStartRequested(Activity.indoorCycling));
|
||||
await bloc.stream.firstWhere((s) => s.phase == RecordingPhase.recording);
|
||||
|
||||
expect(platform.started.single.powerMeter, rower.address);
|
||||
});
|
||||
|
||||
test(
|
||||
'without the activity permission a ride indoors starts not (D66)',
|
||||
() async {
|
||||
final bloc = await open();
|
||||
platform.allows = {RecorderPermission.notifications};
|
||||
bloc.add(const RecordingEventStartRequested(Activity.indoorCycling));
|
||||
await bloc.stream.firstWhere((s) => s.activityRefused);
|
||||
|
||||
expect(platform.started, isEmpty);
|
||||
expect(bloc.state.phase, RecordingPhase.idle);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'without a rower, or without nearby devices, a row starts not',
|
||||
() async {
|
||||
|
|
|
|||
|
|
@ -518,4 +518,28 @@ void main() {
|
|||
contains('<name>Run</name>'),
|
||||
);
|
||||
});
|
||||
|
||||
test('a ride indoors goes out as indoor cycling, and as a ride (D66)', () {
|
||||
final ride = Recording(
|
||||
activity: Activity.indoorCycling,
|
||||
startedAt: start,
|
||||
heartRates: [
|
||||
for (var s = 0; s <= 60; s++) HeartRate(time: at(s), bpm: 140),
|
||||
],
|
||||
endedAt: at(60),
|
||||
);
|
||||
final (rode, series) = recordedWorkout(ride);
|
||||
final track = exportTrackOf(rode, series);
|
||||
final tcx = XmlDocument.parse(tcxOf(rode, track));
|
||||
expect(
|
||||
tcx.findAllElements('Activity').single.getAttribute('Sport'),
|
||||
'Biking',
|
||||
);
|
||||
final sport = [
|
||||
for (final message in readFit(fitOf(rode, track)))
|
||||
if (message.global == 18) message.fields,
|
||||
].single;
|
||||
expect(sport[5], 2);
|
||||
expect(sport[6], 6);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue