wake-alarm/phone_app/lib/main.dart
Krzysztof kuhy Rudnicki af2a9b416b Phase 4/3: the phone's Dart half — ring screen, challenges, room proofs
Dart side of the alarm (items 1, 4, 3). main.dart had no init seam at all;
it now has ensureInitialized(), a navigatorKey and a /ring route.

- challenge.dart: all five kinds ported from _challenges.py so both halves
  behave identically. kDismissCodeLength/kDismissFlashSeconds are duplicated
  from _constants.py, not synced — noted in the file, since Dart cannot read
  Python.
- alarm_scheduler.dart: the Dart side of the single MethodChannel.
- ring_screen.dart: room proof first, then a challenge, then dismiss. Back is
  a no-op (PopScope) so the alarm cannot be escaped.
- room_proof.dart: NFC or QR satisfies on its own; steps NEVER do alone —
  the phone is stationary while you walk to it, so steps only accrue after
  pickup and can be faked by shaking it in bed. Steps count only together
  with elapsed time, as the fallback when both others fail.
- home_screen: arms the app's own alarm instead of asking the user to confirm
  in the Clock app (item 1's "no user interaction"). SET_ALARM stays as
  redundancy with SKIP_UI=true.

Re-arm has three hooks and needs all three: startup, BOOT_COMPLETED, and
dismiss. The plan assumed LogStore.changes would cover this; that stream does
not exist in crdt_sync_dart (verified — the library has no Stream at all), so
the picker's save is only one hook and a restart would otherwise arm nothing.

Fixed a real gap the API 36 AVD exposed: POST_NOTIFICATIONS was never
requested at runtime, so a fresh install sat at importance=NONE and the
full-screen intent was rejected outright — the alarm would ring with no
dismiss screen. MainActivity now requests it, and canUseFullScreenIntent()
also checks areNotificationsEnabled() so the check gates rather than warns.

Verified on the API 36 AVD: screen Asleep -> Awake, AlarmActivity became the
top resumed activity over the lockscreen, service isForeground=true
types=0x2 (mediaPlayback) category=alarm, no crash. flutter test: 54 pass.
dart analyze: clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
2026-08-27 21:22:07 +02:00

48 lines
1.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:wake_alarm_sync/screens/home_screen.dart';
import 'package:wake_alarm_sync/screens/ring_screen.dart';
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
import 'package:wake_alarm_sync/ui/theme.dart';
/// Route the Kotlin `AlarmActivity` hands to the engine as its initial
/// route.
const String kRingRoute = '/ring';
/// Navigator key, so the platform side can drive navigation without a context.
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
/// Entry point.
///
/// `ensureInitialized` is required before any MethodChannel call: the
/// startup re-arm below runs before the first frame.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// The log store has no change stream, so the picker's save is the only other
// arm hook. Without this, restarting the app without touching the picker
// would leave nothing armed at all.
await AlarmScheduler().rearm();
runApp(const WakeAlarmSyncApp());
}
/// Root application widget.
class WakeAlarmSyncApp extends StatelessWidget {
/// Creates a [WakeAlarmSyncApp].
const WakeAlarmSyncApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wake Alarm Sync',
theme: buildAppTheme(),
navigatorKey: navigatorKey,
// AlarmActivity sets its initial route to kRingRoute, so the ring screen
// is what comes up over the lockscreen; the launcher still gets home.
routes: <String, WidgetBuilder>{
'/': (_) => const HomeScreen(),
kRingRoute: (_) => const RingScreen(),
},
);
}
}