fix(app): the Pomodoro timer holds up with the app gone, asleep or edited
A review of the timer found:
- Stop in the notification with the app swiped away took the timer
away there, but the app showed it again at its next start. The
service now leaves a mark the app takes when it starts, and the timer
it kept is forgotten.
- Pause, Resume and Skip did nothing with the app gone: since Android
12 a service may not start an activity from a notification's button.
The notification is posted again with buttons that open the app.
- A timer kept while the app was closed was shown as it was before it
caught up, so its ended phase alerted a second time. It is now caught
up before it first shows.
- Dart's timers sleep with the device, so a phase that ended while the
phone or laptop slept went unnoticed until the app came back. While a
phase runs the timer looks every 15 seconds, and on Android the
alarm's receiver wakes it too.
- On Linux, changes that came at once overlapped on the bus (an
indicator registered twice, notifications left over, a ticking label
never stopped); they now go one at a time. The indicator registers
again when GNOME's watcher restarts, and with a watcher that only
turns up later.
- Changing the running profile could put the timer outside its cycle
("Focus 6 of 4", the Live Update's bar past its end); where it is
stays within the new cycle, and a phase waiting at its full length
takes the new length. A countdown past its end shows 00:00.
- Writes of the kept timer go one at a time, and a closed timer arms no
more timers.
- The alert no longer renames the timer's notification channel, and a
German hint said the alert came at the device's end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
6ba229be2c
commit
a822763df8
3 changed files with 420 additions and 65 deletions
|
|
@ -144,4 +144,56 @@ void main() {
|
|||
expect(bloc.state.settings.profiles, hasLength(2));
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer kept while the app was closed shows caught up, never as it '
|
||||
'was', () {
|
||||
fakeAsync((async) {
|
||||
withClock(async.getClock(start), () {
|
||||
final settings = SettingsRepository(FakeSettingsGateway());
|
||||
final store = MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(
|
||||
PomodoroProfile.classic,
|
||||
).started(start.subtract(const Duration(hours: 1))),
|
||||
);
|
||||
final bloc = PomodoroBloc(settings, store);
|
||||
final states = <PomodoroState>[];
|
||||
bloc.stream.listen(states.add);
|
||||
bloc.add(const PomodoroEventStarted());
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(
|
||||
states.where((state) => state.run?.isRunning ?? false),
|
||||
isEmpty,
|
||||
);
|
||||
expect(states.last.run?.phase, PomodoroPhase.shortBreak);
|
||||
bloc.close();
|
||||
settings.dispose();
|
||||
async.flushMicrotasks();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('a timer stopped outside the app while it was gone is no more', () {
|
||||
fakeAsync((async) {
|
||||
withClock(async.getClock(start), () {
|
||||
final settings = SettingsRepository(FakeSettingsGateway());
|
||||
final store = MemoryPomodoroRunStore(
|
||||
PomodoroRun.ready(PomodoroProfile.classic).started(start),
|
||||
);
|
||||
final bloc = PomodoroBloc(
|
||||
settings,
|
||||
store,
|
||||
stoppedOutside: () async => true,
|
||||
)..add(const PomodoroEventStarted());
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(bloc.state.loaded, isTrue);
|
||||
expect(bloc.state.run, isNull);
|
||||
expect(store.kept, isNull);
|
||||
bloc.close();
|
||||
settings.dispose();
|
||||
async.flushMicrotasks();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,21 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('changes that come at once still make one notification', () async {
|
||||
final run = PomodoroRun.ready(PomodoroProfile.classic);
|
||||
|
||||
await Future.wait([
|
||||
platform.show(notice(run.started(DateTime.now().toUtc()))),
|
||||
platform.show(notice(run)),
|
||||
]);
|
||||
|
||||
expect(
|
||||
notifications.notified.map((call) => (call[1] as DBusUint32).value),
|
||||
[0, 1],
|
||||
reason: 'the second replaces the first',
|
||||
);
|
||||
});
|
||||
|
||||
test('hands the buttons pressed to the app', () async {
|
||||
final pressed = platform.actions.first;
|
||||
await platform.show(notice(PomodoroRun.ready(PomodoroProfile.classic)));
|
||||
|
|
@ -260,6 +275,26 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('the item registers again with a watcher that restarts', () async {
|
||||
var watcher = _Watcher();
|
||||
var shell = DBusClient(address);
|
||||
await shell.requestName('org.kde.StatusNotifierWatcher');
|
||||
await shell.registerObject(watcher);
|
||||
final run = PomodoroRun.ready(PomodoroProfile.classic);
|
||||
await platform.show(notice(run));
|
||||
expect(watcher.registered, hasLength(1));
|
||||
|
||||
await shell.close();
|
||||
watcher = _Watcher();
|
||||
shell = DBusClient(address);
|
||||
await shell.requestName('org.kde.StatusNotifierWatcher');
|
||||
await shell.registerObject(watcher);
|
||||
addTearDown(shell.close);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
expect(watcher.registered, hasLength(1));
|
||||
});
|
||||
|
||||
test('an icon of the timer is drawn for the top bar', () {
|
||||
final icon = timerIcon(24);
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,55 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('a profile changed under way keeps where the timer is within its '
|
||||
'cycle', () {
|
||||
var run = PomodoroRun.ready(PomodoroProfile.fiftyTwoSeventeen);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
run = run.next(now);
|
||||
}
|
||||
final withLongBreaks = run.withProfile(
|
||||
PomodoroProfile.fiftyTwoSeventeen.copyWith(
|
||||
longBreakMinutes: 30,
|
||||
longBreakAfter: 4,
|
||||
),
|
||||
);
|
||||
|
||||
expect(withLongBreaks.phase, PomodoroPhase.focus);
|
||||
expect(
|
||||
withLongBreaks.indexInCycle,
|
||||
lessThan(withLongBreaks.cycle.length),
|
||||
);
|
||||
expect(withLongBreaks.focusDone, lessThan(4));
|
||||
final cycleLength = withLongBreaks.cycle.fold(
|
||||
Duration.zero,
|
||||
(sum, phase) => sum + withLongBreaks.lengthIn(phase),
|
||||
);
|
||||
expect(
|
||||
withLongBreaks.elapsedInCycleAt(now),
|
||||
lessThanOrEqualTo(cycleLength),
|
||||
);
|
||||
});
|
||||
|
||||
test('a phase waiting at its full length takes the length of the changed '
|
||||
'profile; one under way keeps its end', () {
|
||||
final waiting = PomodoroRun.ready(classic);
|
||||
expect(
|
||||
waiting.withProfile(classic.copyWith(focusMinutes: 40)).leftAt(now),
|
||||
const Duration(minutes: 40),
|
||||
);
|
||||
|
||||
final running = waiting.started(now);
|
||||
expect(
|
||||
running.withProfile(classic.copyWith(focusMinutes: 40)).endsAt,
|
||||
running.endsAt,
|
||||
);
|
||||
});
|
||||
|
||||
test('a countdown past its end shows no time left', () {
|
||||
expect(formatPomodoroLeft(const Duration(seconds: -13)), '00:00');
|
||||
expect(formatPomodoroLeft(const Duration(minutes: 61)), '1:01:00');
|
||||
});
|
||||
|
||||
test('survives being kept and read back', () {
|
||||
final run = PomodoroRun.ready(classic).next(now).started(now);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue