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
10 changed files with 252 additions and 21 deletions
|
|
@ -29,6 +29,7 @@
|
|||
"recordingStepTapLap": "Bis du auf Runde tippst",
|
||||
"recordLocationRefused": "Train braucht deinen Standort, um das aufzuzeichnen. Du kannst ihn in den Einstellungen des Systems 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",
|
||||
"recordingGpsAccuracy": "GPS ±{metres} m",
|
||||
"recordingMovingTime": "Zeit in Bewegung",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
"recordingStepTapLap": "Until you tap Lap",
|
||||
"recordLocationRefused": "Train needs your location to record this. 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": {
|
||||
"description": "No fix at all for a minute while recording: location off, or allowed only approximately."
|
||||
},
|
||||
"recordingWaitingForGpsAccuracy": "Waiting for GPS · ±{metres} m",
|
||||
"@recordingWaitingForGpsAccuracy": {
|
||||
"description": "Before a fix precise enough: how far off the last one may be.",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,12 @@ abstract class AppLocalizations {
|
|||
/// **'Waiting for GPS'**
|
||||
String get recordingWaitingForGps;
|
||||
|
||||
/// No fix at all for a minute while recording: location off, or allowed only approximately.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No GPS for a minute. Is location on, and is Train allowed your precise location?'**
|
||||
String get recordingGpsSilent;
|
||||
|
||||
/// Before a fix precise enough: how far off the last one may be.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get recordingWaitingForGps => 'Warte auf GPS';
|
||||
|
||||
@override
|
||||
String get recordingGpsSilent =>
|
||||
'Seit einer Minute kein GPS. Ist der Standort an und darf Train deinen genauen Standort verwenden?';
|
||||
|
||||
@override
|
||||
String recordingWaitingForGpsAccuracy(int metres) {
|
||||
return 'Warte auf GPS · ±$metres m';
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get recordingWaitingForGps => 'Waiting for GPS';
|
||||
|
||||
@override
|
||||
String get recordingGpsSilent =>
|
||||
'No GPS for a minute. Is location on, and is Train allowed your precise location?';
|
||||
|
||||
@override
|
||||
String recordingWaitingForGpsAccuracy(int metres) {
|
||||
return 'Waiting for GPS · ±$metres m';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
|
|
@ -113,7 +114,10 @@ class RecordingBloc(
|
|||
_refresh(emit);
|
||||
}
|
||||
});
|
||||
on<RecordingEventTicked>((event, emit) => _refresh(emit));
|
||||
on<RecordingEventTicked>((event, emit) {
|
||||
_recoverHeader();
|
||||
_refresh(emit);
|
||||
});
|
||||
}
|
||||
|
||||
final RecorderNotice Function(LiveRecording live) _notice = notice;
|
||||
|
|
@ -390,6 +394,34 @@ class RecordingBloc(
|
|||
}
|
||||
}
|
||||
|
||||
/// Takes the header from the file where the lines came without it: the
|
||||
/// recorder began after the app listened, and wrote its header before
|
||||
/// handing lines on (D65). The file has it once the recorder flushed, within
|
||||
/// seconds.
|
||||
void _recoverHeader() {
|
||||
if (!_lines.headless || !_file.existsSync()) {
|
||||
return;
|
||||
}
|
||||
final RandomAccessFile opened;
|
||||
try {
|
||||
opened = _file.openSync();
|
||||
} on FileSystemException {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// The header is far shorter: its magic, a version, an activity, a time.
|
||||
final start = utf8.decode(opened.readSync(256), allowMalformed: true);
|
||||
final end = start.indexOf('\n');
|
||||
if (end >= 0) {
|
||||
_lines.recover(start.substring(0, end));
|
||||
}
|
||||
} on FileSystemException {
|
||||
return;
|
||||
} finally {
|
||||
opened.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
/// What of a notice is worth telling: the time counts on by itself.
|
||||
static (String, String, bool) _keyOf(RecorderNotice notice) =>
|
||||
(notice.title, notice.text, notice.moving == null);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ bool usesGps(Activity activity) => activity != Activity.indoorRowing;
|
|||
/// strap and a rower send about once a second.
|
||||
const freshHeartRate = Duration(seconds: 5);
|
||||
|
||||
/// How long without any fix before the screen asks whether location is on
|
||||
/// and precise (D65): a phone finds its first fix outdoors within seconds to
|
||||
/// half a minute.
|
||||
const gpsSilentAfter = Duration(minutes: 1);
|
||||
|
||||
/// The split per 500 m over the rower's last [window] up to [now], from its
|
||||
/// metres; null without metres enough.
|
||||
Duration? _splitOf(
|
||||
|
|
@ -101,6 +106,9 @@ LiveRecording liveOf(Recording recording, DateTime now) {
|
|||
},
|
||||
waitingForGps:
|
||||
gps && !recording.fixes.any((fix) => fix.accuracyCm <= maxAccuracyCm),
|
||||
gpsSilent:
|
||||
gps &&
|
||||
now.difference(lastFix?.time ?? recording.startedAt) >= gpsSilentAfter,
|
||||
paused: paused,
|
||||
autoPaused: paused && last!.auto,
|
||||
strap: strap,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,11 @@ final class RecordingReader() {
|
|||
/// Whether its first line was read.
|
||||
var _started = false;
|
||||
|
||||
/// The lines taken in while the header is missing, kept for [recover]:
|
||||
/// an app listening before the recorder began may have been handed none
|
||||
/// (ADR 0030, D65).
|
||||
final _headless = <String>[];
|
||||
|
||||
final _fixes = <Fix>[];
|
||||
final _heartRates = <HeartRate>[];
|
||||
final _steps = <DateTime>[];
|
||||
|
|
@ -94,9 +99,13 @@ final class RecordingReader() {
|
|||
if (!_started) {
|
||||
_started = true;
|
||||
_header = _headerOf(line);
|
||||
if (_header == null) {
|
||||
_headless.add(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_header == null) {
|
||||
_headless.add(line);
|
||||
return;
|
||||
}
|
||||
final fields = line.split(' ');
|
||||
|
|
@ -224,6 +233,32 @@ final class RecordingReader() {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether lines came but no header before them, which [recover] may
|
||||
/// take from the file.
|
||||
bool get headless => _header == null && _headless.isNotEmpty;
|
||||
|
||||
/// Takes [header], the file's first line, where the lines came without it
|
||||
/// (D65), and reads the lines kept since as following it; nothing where it
|
||||
/// has its header, or [header] is none it reads.
|
||||
void recover(String header) {
|
||||
if (_header != null) {
|
||||
return;
|
||||
}
|
||||
final read = _headerOf(header);
|
||||
if (read == null) {
|
||||
return;
|
||||
}
|
||||
_header = read;
|
||||
final kept = [..._headless];
|
||||
_headless.clear();
|
||||
for (final line in kept) {
|
||||
// A header it couldn't read came first; the file's own is the one.
|
||||
if (!line.startsWith(recordingFileMagic)) {
|
||||
add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The recording read so far: null where the header is missing, of a
|
||||
/// newer version or names an activity this app doesn't know.
|
||||
Recording? get recording {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ final class const LiveRecording({
|
|||
/// Whether it waits for a fix precise enough (D20) to count distance.
|
||||
final bool waitingForGps = false,
|
||||
|
||||
/// Whether no fix at all came for a minute, from Start or since the last
|
||||
/// (D65): location off, or allowed only approximately.
|
||||
final bool gpsSilent = false,
|
||||
|
||||
/// Whether it is paused now, and whether Train paused it by itself.
|
||||
final bool paused = false,
|
||||
final bool autoPaused = false,
|
||||
|
|
@ -138,6 +142,7 @@ final class const LiveRecording({
|
|||
kmh,
|
||||
accuracyMetres,
|
||||
waitingForGps,
|
||||
gpsSilent,
|
||||
paused,
|
||||
autoPaused,
|
||||
strap,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ class const _Figures({required final LiveRecording live})
|
|||
LiveRecording(strap: LinkState.lost, :final loop)
|
||||
when loop != LinkState.connected =>
|
||||
(l10n.recordingStrapLost, true),
|
||||
LiveRecording(gpsSilent: true) => (l10n.recordingGpsSilent, true),
|
||||
_ => (gps, false),
|
||||
};
|
||||
// Scrolled where a plan's and a route's panels leave too little room.
|
||||
|
|
|
|||
Loading…
Reference in a new issue