mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 18:33:57 +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
74 lines
2.7 KiB
Dart
74 lines
2.7 KiB
Dart
/// Wall-clock arithmetic for a rest period between sets.
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
/// An immutable rest period, described by when it *ends* rather than by how
|
|
/// many ticks are left.
|
|
///
|
|
/// The break countdown used to be a plain `int` decremented once per
|
|
/// `Timer.periodic` tick. Android throttles — and in Doze suspends — timers in
|
|
/// a backgrounded isolate, so that counter stalled, never reached zero, and the
|
|
/// break-end cue never fired. Deriving the remaining time from a stored
|
|
/// deadline makes a stalled timer merely *stale*: the next tick, whenever it
|
|
/// lands, reports the truth.
|
|
@immutable
|
|
class BreakClock {
|
|
/// Creates a clock for a break of [durationSecs] ending at [endTime].
|
|
const BreakClock({required this.endTime, required this.durationSecs});
|
|
|
|
/// Creates a clock for a break of [durationSecs] starting at [start].
|
|
factory BreakClock.startingAt(DateTime start, int durationSecs) => BreakClock(
|
|
endTime: start.add(Duration(seconds: durationSecs)),
|
|
durationSecs: durationSecs,
|
|
);
|
|
|
|
/// When the rest period is over.
|
|
final DateTime endTime;
|
|
|
|
/// The rest period's full length in seconds, for the progress bar.
|
|
final int durationSecs;
|
|
|
|
/// When the rest period began.
|
|
DateTime get startTime => endTime.subtract(Duration(seconds: durationSecs));
|
|
|
|
/// Whole seconds left at [now], rounded up and floored at zero.
|
|
///
|
|
/// Rounded up so a break of 180 s reads "180" for its whole first second,
|
|
/// matching what the decrementing counter used to show.
|
|
int remainingSecsAt(DateTime now) {
|
|
final us = endTime.difference(now).inMicroseconds;
|
|
if (us <= 0) return 0;
|
|
return (us + Duration.microsecondsPerSecond - 1) ~/
|
|
Duration.microsecondsPerSecond;
|
|
}
|
|
|
|
/// True once [now] has reached [endTime].
|
|
bool expiredAt(DateTime now) => !now.isBefore(endTime);
|
|
|
|
/// Whole seconds since the break ended; zero while it is still running.
|
|
int secondsOverdueAt(DateTime now) {
|
|
final overdue = now.difference(endTime).inSeconds;
|
|
return overdue > 0 ? overdue : 0;
|
|
}
|
|
|
|
/// Returns this break re-cut to [secs], keeping the moment it started.
|
|
///
|
|
/// This is the 3-min ↔ 5-min switch that happens when the user decrements
|
|
/// reps on the set that earned the rest: the clock does not restart, it just
|
|
/// ends sooner or later than it was going to.
|
|
BreakClock withDuration(int secs) => BreakClock.startingAt(startTime, secs);
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
other is BreakClock &&
|
|
other.endTime == endTime &&
|
|
other.durationSecs == durationSecs;
|
|
|
|
@override
|
|
int get hashCode => Object.hash(endTime, durationSecs);
|
|
|
|
@override
|
|
String toString() => 'BreakClock($endTime, ${durationSecs}s)';
|
|
}
|