mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:07:03 +01:00
phone: read the native sick-day cache back on the Sick days screen
A release build gives no way to see the SharedPreferences cache that AlarmReceiver reads at ring time (no run-as, no log of its contents), so "the Dart UI shows it" could not prove "the alarm will stay silent". - SickDays.store/dates log the sorted set under WakeAlarmSickDays. - AlarmChannel getSickDays returns the cached set (null if unreadable). - The Sick days screen reads it on open (before the sync rewrites it) and after every sync: "Alarm cache: <dates>" / "none" / "unreadable". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
This commit is contained in:
parent
29e64fb7fa
commit
4f149e67a4
@ -83,6 +83,8 @@ object AlarmChannel {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
// Read-back for the picker: what AlarmReceiver will see.
|
||||
"getSickDays" -> result.success(SickDays.dates(context))
|
||||
"hasNfc" -> result.success(NfcReader.isAvailable(context))
|
||||
// Raw observations only; home_presence.dart decides what they mean.
|
||||
// Lives here rather than in MainActivity's override (as NfcReader
|
||||
|
||||
@ -28,6 +28,24 @@ object SickDays {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
|
||||
.putStringSet(KEY_DATES, isoDates.toSet())
|
||||
.apply()
|
||||
Log.i(TAG, "native cache stored: ${isoDates.sorted()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* The cached dates, sorted, for the picker's read-back line (the only
|
||||
* way to see this cache on a release build); null when unreadable,
|
||||
* which the ring path treats as "not a sick day".
|
||||
*/
|
||||
fun dates(context: Context): List<String>? {
|
||||
val dates = try {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getStringSet(KEY_DATES, emptySet())?.sorted()
|
||||
} catch (e: RuntimeException) {
|
||||
Log.w(TAG, "sick-day cache unreadable", e)
|
||||
null
|
||||
}
|
||||
Log.i(TAG, "native cache read: $dates")
|
||||
return dates
|
||||
}
|
||||
|
||||
/** Whether today (local calendar day) is in the cached set. */
|
||||
|
||||
@ -14,6 +14,7 @@ class SickDaysBody extends StatelessWidget {
|
||||
const SickDaysBody({
|
||||
required this.upcoming,
|
||||
required this.status,
|
||||
required this.cache,
|
||||
required this.refused,
|
||||
required this.calendar,
|
||||
super.key,
|
||||
@ -28,6 +29,9 @@ class SickDaysBody extends StatelessWidget {
|
||||
/// The status line; may be a raw `Error: …`.
|
||||
final String status;
|
||||
|
||||
/// The native ring-path cache, read back (`Alarm cache: …`).
|
||||
final String cache;
|
||||
|
||||
/// Whether [status] is a refused tap, drawn like an error.
|
||||
final bool refused;
|
||||
|
||||
@ -68,6 +72,10 @@ class SickDaysBody extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (cache.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(cache, style: TextStyle(color: muted)),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
calendar,
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wake_alarm_sync/screens/day_calendar.dart';
|
||||
import 'package:wake_alarm_sync/screens/sick_days_body.dart';
|
||||
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
|
||||
import 'package:wake_alarm_sync/services/free_day_pool.dart';
|
||||
import 'package:wake_alarm_sync/services/free_day_sync.dart';
|
||||
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
||||
@ -27,6 +28,7 @@ class SickDaysScreen extends StatefulWidget {
|
||||
this.openFreePool,
|
||||
this.freeSyncer,
|
||||
this.now,
|
||||
this.scheduler,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -45,6 +47,9 @@ class SickDaysScreen extends StatefulWidget {
|
||||
/// The reference date, for tests.
|
||||
final DateTime Function()? now;
|
||||
|
||||
/// Reads the native cache back; defaults to the real channel.
|
||||
final AlarmScheduler? scheduler;
|
||||
|
||||
@override
|
||||
State<SickDaysScreen> createState() => _SickDaysScreenState();
|
||||
}
|
||||
@ -59,6 +64,9 @@ class _SickDaysScreenState extends State<SickDaysScreen> {
|
||||
String _status = '';
|
||||
bool _refused = false;
|
||||
|
||||
/// What the native ring-path cache holds, read back over the channel.
|
||||
String _cache = '';
|
||||
|
||||
late DateTime _month = DateTime(_today.year, _today.month);
|
||||
|
||||
late final SickDaySync _syncer = widget.syncer ?? SickDaySync();
|
||||
@ -83,10 +91,32 @@ class _SickDaysScreenState extends State<SickDaysScreen> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Read before syncing too: the sync rewrites the cache, so only this
|
||||
// read shows what earlier marks left there.
|
||||
await _readCache();
|
||||
await _loadFree();
|
||||
await _sync();
|
||||
}
|
||||
|
||||
/// Shows the native cache `AlarmReceiver` reads, not the Dart pool.
|
||||
/// Unreadable is never shown as "none": the ring treats it as no sick day.
|
||||
Future<void> _readCache() async {
|
||||
List<String>? dates;
|
||||
try {
|
||||
dates = await (widget.scheduler ?? AlarmScheduler()).getSickDays();
|
||||
} on Object {
|
||||
dates = null;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_cache = 'Alarm cache: ${switch (dates) {
|
||||
null => 'unreadable',
|
||||
[] => 'none',
|
||||
final d => d.join(', '),
|
||||
}}';
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens and syncs the free-day pool, so a free day booked on the PC is
|
||||
/// refused here too. Its status is not shown on this screen.
|
||||
Future<void> _loadFree() async {
|
||||
@ -108,6 +138,7 @@ class _SickDaysScreenState extends State<SickDaysScreen> {
|
||||
_status = 'Syncing…';
|
||||
});
|
||||
final message = await _syncer.sync(pool);
|
||||
await _readCache();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
@ -172,6 +203,7 @@ class _SickDaysScreenState extends State<SickDaysScreen> {
|
||||
if (!day.isBefore(_today)) day,
|
||||
],
|
||||
status: _status,
|
||||
cache: _cache,
|
||||
refused: _refused,
|
||||
calendar: DayCalendar(
|
||||
month: _month,
|
||||
|
||||
@ -137,6 +137,11 @@ class AlarmScheduler {
|
||||
Future<void> setSickDays(List<String> isoDates) => _channel
|
||||
.invokeMethod<void>('setSickDays', <String, Object>{'dates': isoDates});
|
||||
|
||||
/// Reads the native sick-day cache back, sorted: what `AlarmReceiver`
|
||||
/// will read at ring time. Null when the native side cannot read it.
|
||||
Future<List<String>?> getSickDays() =>
|
||||
_channel.invokeListMethod<String>('getSickDays');
|
||||
|
||||
/// Starts the ring immediately, without waiting for the alarm instant.
|
||||
///
|
||||
/// Exists so the foreground-service and full-screen-intent path can be
|
||||
|
||||
@ -24,9 +24,11 @@ void main() {
|
||||
late FreeDayPool free;
|
||||
late int syncs;
|
||||
late List<List<String>> caches;
|
||||
late bool cacheUnreadable;
|
||||
|
||||
setUp(() async {
|
||||
caches = [];
|
||||
cacheUnreadable = false;
|
||||
const channel = MethodChannel(AlarmScheduler.channelName);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
@ -35,6 +37,9 @@ void main() {
|
||||
((call.arguments as Map)['dates'] as List).cast<String>(),
|
||||
);
|
||||
}
|
||||
if (call.method == 'getSickDays' && !cacheUnreadable) {
|
||||
return caches.isEmpty ? <String>[] : caches.last;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
scheduler = AlarmScheduler(channel: channel);
|
||||
@ -73,6 +78,7 @@ void main() {
|
||||
scheduler: scheduler,
|
||||
),
|
||||
now: () => _today,
|
||||
scheduler: scheduler,
|
||||
),
|
||||
);
|
||||
|
||||
@ -116,6 +122,22 @@ void main() {
|
||||
expect(syncs, 3);
|
||||
});
|
||||
|
||||
testWidgets('the native cache is read back after a mark', (tester) async {
|
||||
await open(tester);
|
||||
expect(find.text('Alarm cache: none'), findsOneWidget);
|
||||
await tapDay(tester, DateTime(2026, 10, 18));
|
||||
await tapDay(tester, DateTime(2026, 10, 17));
|
||||
expect(find.text('Alarm cache: 2026-10-17, 2026-10-18'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an unreadable native cache never reads as none', (
|
||||
tester,
|
||||
) async {
|
||||
cacheUnreadable = true;
|
||||
await open(tester);
|
||||
expect(find.text('Alarm cache: unreadable'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a future day is marked in the sick colour', (tester) async {
|
||||
await open(tester);
|
||||
await tapDay(tester, DateTime(2026, 10, 20));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user