mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 21:33:58 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
The between-set break never finished once the phone's screen was off. Three independent causes (worked through on-device on 2026-09-12; left uncommitted awaiting the go-ahead, committed here as-is plus one pin bump): 1. The countdown was not wall-clock: `_breakRemaining--` on a Timer.periodic, which Android throttles and Doze suspends in a backgrounded isolate, so the counter stalled and _onBreakFinished() never fired. Now a BreakClock deadline on the wall clock. 2. An expired break was silently dropped on restore (`if (remaining > 0)` only) — now cued and logged. 3. No notification infrastructure at all: no package, no POST_NOTIFICATIONS / FOREGROUND_SERVICE / WAKE_LOCK. Now: flutter_foreground_task runs the countdown in a service isolate that survives BACK and the screen lock; flutter_local_notifications shows an ongoing "Workout A · break m:ss / Next: …" notification with −1 rep / Done actions and a "Break over" alert on its own channel (raw break_end sound, vibration). Button presses go through a persisted BreakIntent queue the screen drains on resume, so a press with the app killed is not lost. Finish and Reset stop the service; a startup reaper handles the crash case. Verified on the phone with the screen off and locked: service alive and ticking while Dozing, both notifications on the lock screen, channel importance 5 with the break_end sound. Not verifiable from adb (receiver is RECEIVER_NOT_EXPORTED): an actual action-button tap, and a true recents swipe — see stronglift_replacement/TODO-workout-app-v2.md. Also fixed en route: the break-end sound had never played in any widget test (AssetSource needs path_provider, which nothing faked, and the failure was swallowed by catchError); the queue's prune wrote back a stale list. This commit additionally: - takes the freshness gate's bump flutter_local_notifications 22.3.1; - restores web/package.json to HEAD (the tree held a stale pre-bump copy); - gives scripts/check_flutter_coverage.sh a FLUTTER_TEST_CONCURRENCY knob: the default one-isolate-per-core run peaks over 4 GiB and is OOM-killed under the resource cap. Default behaviour unchanged. Gate: flutter analyze clean, 511 tests, 3187/3187 lines (100%), pre-commit green. Not re-installed on the phone for the 22.3.1 pin: no device attached at commit time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7vgacLhb4WJjxuVj2QLEP
105 lines
3.0 KiB
Dart
105 lines
3.0 KiB
Dart
/// A workout action the user triggered from the status-bar notification.
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:developer';
|
|
|
|
/// The three things a notification button can ask the workout screen to do.
|
|
enum BreakIntentKind {
|
|
/// Record the target set as completed at its full target reps.
|
|
done,
|
|
|
|
/// Knock one rep off an already-recorded set.
|
|
minusRep,
|
|
|
|
/// End the running rest period early.
|
|
skipBreak;
|
|
|
|
/// Parses [name] back into a kind, or null when it is not one of these.
|
|
static BreakIntentKind? tryParse(String name) {
|
|
for (final kind in BreakIntentKind.values) {
|
|
if (kind.name == name) return kind;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// One durable, self-describing button press.
|
|
///
|
|
/// Self-describing matters: the foreground service resolves which set a press
|
|
/// refers to *at press time* and writes those indices down. Re-deriving "the
|
|
/// most recent set" when the queue is drained would resolve against state the
|
|
/// user never saw, which is how a decrement lands on the wrong set.
|
|
class BreakIntent {
|
|
/// Creates an intent.
|
|
const BreakIntent({
|
|
required this.seq,
|
|
required this.kind,
|
|
required this.exIdx,
|
|
required this.setIdx,
|
|
required this.tsMs,
|
|
});
|
|
|
|
/// Rebuilds an intent from [json], or returns null if it is not one.
|
|
///
|
|
/// Total rather than throwing: a malformed entry must never wedge the drain
|
|
/// loop, and the caller logs each rejection with its raw text.
|
|
static BreakIntent? tryDecode(String json) {
|
|
final Object? decoded;
|
|
try {
|
|
decoded = jsonDecode(json);
|
|
} on FormatException catch (error) {
|
|
log(
|
|
'BreakIntent: a queued notification press is unreadable and will be '
|
|
'skipped — the press is lost. $error. Raw text: $json',
|
|
level: 900,
|
|
);
|
|
return null;
|
|
}
|
|
if (decoded is! Map<String, dynamic>) return null;
|
|
final seq = decoded['seq'];
|
|
final kindName = decoded['kind'];
|
|
final exIdx = decoded['exIdx'];
|
|
final setIdx = decoded['setIdx'];
|
|
final tsMs = decoded['tsMs'];
|
|
if (seq is! int || kindName is! String || exIdx is! int) return null;
|
|
if (setIdx is! int || tsMs is! int) return null;
|
|
final kind = BreakIntentKind.tryParse(kindName);
|
|
if (kind == null) return null;
|
|
return BreakIntent(
|
|
seq: seq,
|
|
kind: kind,
|
|
exIdx: exIdx,
|
|
setIdx: setIdx,
|
|
tsMs: tsMs,
|
|
);
|
|
}
|
|
|
|
/// Monotonic sequence number, allocated by the service isolate.
|
|
final int seq;
|
|
|
|
/// What the user asked for.
|
|
final BreakIntentKind kind;
|
|
|
|
/// Exercise this press referred to, or -1 when it referred to no set.
|
|
final int exIdx;
|
|
|
|
/// Set this press referred to, or -1 when it referred to no set.
|
|
final int setIdx;
|
|
|
|
/// When the button was pressed, epoch milliseconds.
|
|
final int tsMs;
|
|
|
|
/// Serializes this intent for the durable queue.
|
|
String encode() => jsonEncode({
|
|
'seq': seq,
|
|
'kind': kind.name,
|
|
'exIdx': exIdx,
|
|
'setIdx': setIdx,
|
|
'tsMs': tsMs,
|
|
});
|
|
|
|
@override
|
|
String toString() => 'BreakIntent(#$seq ${kind.name} [$exIdx][$setIdx])';
|
|
}
|