feat(train): reminders to move and workouts suggested from the steps (ADR 0037)
Settings' Moving more, on Android: a reminder to get up after an idle time, and a suggested walk or run, each off until turned on (then every 30 minutes, weekdays 9:00 to 18:00, not 12:00 to 13:00, each value the person's), turning on asking for the activity and notification permissions. A WorkManager job every 15 minutes reads the phone's step counter and keeps two hours of readings. It reminds where fewer than 50 steps came over the idle time, all of it within the hours and outside the break, on the person's days, never while Train records or the phone is in a car (Android's car mode, or a car's Bluetooth audio or hands-free). It suggests a walk or a run where the last span averaged a walking or running cadence, once an hour; tapped, the Record sheet for it opens. The decisions are pure Kotlin (Movement.kt), tested on the JVM; the notifications' words are the app's, written into the job's configuration as the person's app opens. Polar's SDK sets neither the band's inactivity alert nor tells of one without an all-day connection, hence the phone's steps (ADR 0037). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3f7b409e80
commit
e4585b478d
1 changed files with 1882 additions and 30 deletions
|
|
@ -0,0 +1,119 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/movement/data/moving_more.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/movement/model/movement_settings.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/data/recorder_platform.dart';
|
||||
|
||||
import '../../support/fake_movement_platform.dart';
|
||||
import '../../support/fake_recorder_platform.dart';
|
||||
|
||||
const texts = MovementTexts(
|
||||
channel: 'Moving more',
|
||||
remindTitle: 'Time to move',
|
||||
remindText: '30 minutes with hardly a step.',
|
||||
walkingTitle: 'Out for a walk?',
|
||||
runningTitle: 'Out for a run?',
|
||||
suggestText: 'Tap to record the rest of it.',
|
||||
);
|
||||
|
||||
/// A person's reminders to move and suggested workouts on this phone (ADR
|
||||
/// 0037): kept in their folder, the job configured with them, the
|
||||
/// permissions asked for as either is turned on.
|
||||
void main() {
|
||||
late Directory folder;
|
||||
late FakeMovementPlatform platform;
|
||||
late FakeRecorderPlatform recorder;
|
||||
|
||||
setUp(() {
|
||||
folder = Directory.systemTemp.createTempSync('movement');
|
||||
platform = FakeMovementPlatform();
|
||||
recorder = FakeRecorderPlatform();
|
||||
});
|
||||
|
||||
tearDown(() => folder.deleteSync(recursive: true));
|
||||
|
||||
MovingMore open() =>
|
||||
MovingMore(platform, recorder, File('${folder.path}/movement.json'));
|
||||
|
||||
test('both are off until turned on, every 30 minutes on weekdays from '
|
||||
'nine to six but over lunch (D3)', () {
|
||||
final settings = open().read();
|
||||
expect(settings.remind, isFalse);
|
||||
expect(settings.suggest, isFalse);
|
||||
expect(settings.idleMinutes, 30);
|
||||
expect((settings.from, settings.until), (9 * 60, 18 * 60));
|
||||
expect((settings.breakFrom, settings.breakUntil), (12 * 60, 13 * 60));
|
||||
expect(settings.days, {1, 2, 3, 4, 5});
|
||||
});
|
||||
|
||||
test(
|
||||
'turned on, it asks for the activity and notification permissions, '
|
||||
'keeps the settings and configures the job with them and the words',
|
||||
() async {
|
||||
final movement = open();
|
||||
final kept = await movement.write(
|
||||
const MovementSettings(remind: true, idleMinutes: 45),
|
||||
texts,
|
||||
);
|
||||
|
||||
expect(recorder.asked, [
|
||||
{RecorderPermission.activity, RecorderPermission.notifications},
|
||||
]);
|
||||
expect(kept.remind, isTrue);
|
||||
expect(open().read(), kept, reason: "kept in the person's folder");
|
||||
final [config] = platform.configured;
|
||||
expect(config!['settings'], kept.toJson());
|
||||
expect(config['texts'], texts.toJson());
|
||||
},
|
||||
);
|
||||
|
||||
test('refused either permission, both stay off and the job stops', () async {
|
||||
recorder.allows = {RecorderPermission.activity};
|
||||
final kept = await open().write(
|
||||
const MovementSettings(suggest: true),
|
||||
texts,
|
||||
);
|
||||
expect(kept.on, isFalse);
|
||||
expect(open().read().on, isFalse);
|
||||
expect(platform.configured, [null]);
|
||||
});
|
||||
|
||||
test(
|
||||
'changed while on, it asks nothing again; turned off, the job stops',
|
||||
() async {
|
||||
final movement = open();
|
||||
await movement.write(const MovementSettings(remind: true), texts);
|
||||
await movement.write(
|
||||
const MovementSettings(remind: true, idleMinutes: 60),
|
||||
texts,
|
||||
);
|
||||
await movement.write(const MovementSettings(), texts);
|
||||
expect(recorder.asked, hasLength(1));
|
||||
expect(platform.configured.last, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('as the app opens, the job is configured with what was kept', () async {
|
||||
await open().write(const MovementSettings(suggest: true), texts);
|
||||
platform.configured.clear();
|
||||
await open().apply(texts);
|
||||
expect(
|
||||
(platform.configured.single!['settings']! as Map)['suggest'],
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('settings it cannot read are the defaults', () {
|
||||
expect(
|
||||
MovementSettings.of({
|
||||
'remind': true,
|
||||
'idleMinutes': 7,
|
||||
'from': -1,
|
||||
'days': [0, 3, 8, 'x'],
|
||||
}),
|
||||
const MovementSettings(remind: true, days: {3}),
|
||||
);
|
||||
expect(MovementSettings.of('x'), const MovementSettings());
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue