feat(train): recording a workout on Android (ADR 0030, D13, D16, D19, D21)
RecorderService, a foreground service of the types location and health started from the open app, takes fixes from LocationManager's GPS and steps from the step detector, pauses by itself (AutoPause, tested on the JVM) and appends each sample to the person's recording file, flushed every five seconds. MainActivity asks for location, notifications and, for auto-pause on foot, physical activity, never background location (D19), and bridges the service to the app. The app follows the file's lines with RecordingBloc and shows the moving time, the distance, the pace or speed and GPS's state; the notification shows the time and the distance with Pause or Resume. Stop is held down; a stopped recording is saved as a workout and its series, or discarded after asking, and one cut off or left unsaved is offered as the app opens. Home has the Record button where the device records. Three choices of the implementation's are recorded under D21 for review: rowing waits for 5f, auto-pause on foot without steps goes by GPS speed (below 2 km/h, on above 3 km/h), and Stop's hint shows above the button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
aab25bf932
commit
7df1cb1740
1 changed files with 3360 additions and 27 deletions
|
|
@ -0,0 +1,105 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/data/recorder_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
|
||||
/// [RecorderPlatform] without a system: a recording's file written as the
|
||||
/// service would, its lines brought by hand ([write]).
|
||||
class FakeRecorderPlatform({
|
||||
/// What the person allows when asked.
|
||||
var Set<RecorderPermission> allows = const {
|
||||
RecorderPermission.location,
|
||||
RecorderPermission.notifications,
|
||||
RecorderPermission.activity,
|
||||
},
|
||||
}) implements RecorderPlatform {
|
||||
/// What was asked for, each time.
|
||||
final asked = <Set<RecorderPermission>>[];
|
||||
|
||||
/// How each recording was started.
|
||||
final started =
|
||||
<({String path, Activity activity, AutoPauseBy? autoPause})>[];
|
||||
|
||||
/// The notices shown, the first one the start's.
|
||||
final notices = <RecorderNotice>[];
|
||||
|
||||
/// Pauses and resumes by hand, in order.
|
||||
final byHand = <bool>[];
|
||||
|
||||
File? _file;
|
||||
StreamController<String>? _lines;
|
||||
|
||||
/// The time the next line is written at.
|
||||
DateTime now = DateTime.utc(2026, 10, 28, 7, 30);
|
||||
|
||||
/// Writes [line] as the service would, after its time ([now]).
|
||||
void write(String line) {
|
||||
final written = line.replaceFirst(
|
||||
'@',
|
||||
'${now.millisecondsSinceEpoch}',
|
||||
);
|
||||
_file!.writeAsStringSync('$written\n', mode: FileMode.append, flush: true);
|
||||
_lines?.add(written);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Set<RecorderPermission>> ask(Set<RecorderPermission> wanted) async {
|
||||
asked.add(wanted);
|
||||
return wanted.intersection(allows);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> start({
|
||||
required String path,
|
||||
required Activity activity,
|
||||
required AutoPauseBy? autoPause,
|
||||
required RecorderNotice notice,
|
||||
}) async {
|
||||
started.add((path: path, activity: activity, autoPause: autoPause));
|
||||
notices.add(notice);
|
||||
_file = File(path)
|
||||
..writeAsStringSync(
|
||||
'halkyon-train-recording 1 ${activity.name} '
|
||||
'${now.millisecondsSinceEpoch}\n',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
byHand.add(true);
|
||||
write('pause @ manual');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resume() async {
|
||||
byHand.add(false);
|
||||
write('resume @ manual');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
write('end @');
|
||||
_file = null;
|
||||
// As the service's: done once the file is, not once the app has read
|
||||
// the end of its lines.
|
||||
unawaited(_lines?.close());
|
||||
_lines = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> show(RecorderNotice notice) async => notices.add(notice);
|
||||
|
||||
@override
|
||||
Future<bool> isRecording() async => _file != null;
|
||||
|
||||
/// A recording that runs on, as when the app comes back to one.
|
||||
void runOn(String path) => _file = File(path);
|
||||
|
||||
@override
|
||||
Stream<String> lines() {
|
||||
final controller = _lines = StreamController<String>();
|
||||
_file!.readAsLinesSync().forEach(controller.add);
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue