import 'dart:async'; import 'dart:developer'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:workmanager/workmanager.dart'; import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event.dart'; import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_params.dart'; import '../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart'; import '../api/webuntis/queries/authenticate/authenticate.dart'; import '../api/webuntis/queries/get_holidays/get_holidays.dart'; import '../api/webuntis/queries/get_holidays/get_holidays_response.dart'; import '../api/webuntis/queries/get_rooms/get_rooms.dart'; import '../api/webuntis/queries/get_rooms/get_rooms_response.dart'; import '../api/webuntis/queries/get_subjects/get_subjects.dart'; import '../api/webuntis/queries/get_subjects/get_subjects_response.dart'; import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units.dart'; import '../api/webuntis/queries/get_timegrid_units/get_timegrid_units_response.dart'; import '../api/webuntis/queries/get_timetable/get_timetable.dart'; import '../api/webuntis/queries/get_timetable/get_timetable_params.dart'; import '../model/account_data.dart'; import '../widget_data/widget_data_mapper.dart'; import '../widget_data/widget_publisher.dart'; import '../widget_data/widget_sync.dart'; /// Periodic widget refresh in a background Dart isolate. Native HTTP would /// mean reimplementing WebUntis JSON-RPC (auth, session-timeout retry -8520, /// payload quirks) twice — Dart isolate keeps that logic in one place. class WidgetBackgroundTask { static const String periodicTaskName = 'eu.mhsl.marianum.widget.refresh'; static const String oneOffTaskName = 'eu.mhsl.marianum.widget.refresh.once'; static const Duration periodicFrequency = Duration(minutes: 30); static Future initialize() async { await Workmanager().initialize(_callbackDispatcher); await Workmanager().registerPeriodicTask( periodicTaskName, periodicTaskName, frequency: periodicFrequency, constraints: Constraints(networkType: NetworkType.connected), existingWorkPolicy: ExistingPeriodicWorkPolicy.keep, backoffPolicy: BackoffPolicy.linear, backoffPolicyDelay: const Duration(minutes: 5), ); } static Future requestImmediateRefresh() async { await Workmanager().registerOneOffTask( '$oneOffTaskName-${DateTime.now().millisecondsSinceEpoch}', oneOffTaskName, constraints: Constraints(networkType: NetworkType.connected), existingWorkPolicy: ExistingWorkPolicy.append, ); } static Future cancelAll() async { await Workmanager().cancelAll(); } } @pragma('vm:entry-point') void _callbackDispatcher() { Workmanager().executeTask((task, inputData) async { try { WidgetsFlutterBinding.ensureInitialized(); await AccountData().waitForPopulation(); if (!AccountData().isPopulated()) { log('[widget-bg] not logged in, skipping refresh'); await WidgetSync.setLoggedIn(false); await WidgetSync.triggerUpdate(); return true; } await _refresh(); return true; } on Exception catch (e, s) { log('[widget-bg] refresh failed: $e', stackTrace: s); // false → Workmanager retries with backoff. Native side keeps the // last good snapshot so the user still sees something. return false; } }); } Future _refresh() async { await WidgetSync.ensureInitialized(); await Authenticate.createSession(); final now = WidgetPublisher.widgetNow(); final dateFormat = DateFormat('yyyyMMdd'); // 14-day window so the week-widget rolls forward into next Monday's // lessons on Friday evening. final weekStart = _startOfWeek(now); final weekEndExclusive = weekStart.add(const Duration(days: 14)); final session = await Authenticate.getSession(); final timetable = await GetTimetable( GetTimetableParams( options: GetTimetableParamsOptions( element: GetTimetableParamsOptionsElement( id: session.personId, type: session.personType, keyType: GetTimetableParamsOptionsElementKeyType.id, ), startDate: int.parse(dateFormat.format(weekStart)), endDate: int.parse( dateFormat.format(weekEndExclusive.subtract(const Duration(days: 1))), ), teacherFields: GetTimetableParamsOptionsFields.all, subjectFields: GetTimetableParamsOptionsFields.all, roomFields: GetTimetableParamsOptionsFields.all, klasseFields: GetTimetableParamsOptionsFields.all, ), ), ).run(); // Reference data — failures fall through to null in the mapper rather // than aborting the whole refresh. final subjects = await _runOrNull(() => GetSubjects().run()); final rooms = await _runOrNull(() => GetRooms().run()); final holidays = await _runOrNull(() => GetHolidays().run()); final timegrid = await _runOrNull( () => GetTimegridUnits().run(), ); final customEvents = await _runOrNull( () => GetCustomTimetableEvent( GetCustomTimetableEventParams(AccountData().getUserSecret()), ).run(), ); final lessons = timetable.result; final connectDouble = await WidgetSync.getConnectDoubleLessons(); final dayData = WidgetDataMapper.buildDayData( now: now, lessons: lessons, subjects: subjects, rooms: rooms, holidays: holidays, timegrid: timegrid, customEvents: customEvents, connectDoubleLessons: connectDouble, ); final weekData = WidgetDataMapper.buildWeekData( now: now, lessons: lessons, subjects: subjects, rooms: rooms, holidays: holidays, timegrid: timegrid, customEvents: customEvents, connectDoubleLessons: connectDouble, ); await WidgetSync.writeDayData(dayData); await WidgetSync.writeWeekData(weekData); await WidgetSync.setLoggedIn(true); await WidgetSync.triggerUpdate(); log( '[widget-bg] refreshed: day=${dayData.lessons.length} ' 'week=${weekData.lessons.length}', ); } DateTime _startOfWeek(DateTime reference) { final monday = reference.subtract(Duration(days: reference.weekday - 1)); return DateTime(monday.year, monday.month, monday.day); } Future _runOrNull(Future Function() task) async { try { return await task(); } on Exception catch (e) { log('[widget-bg] reference fetch failed: $e'); return null; } }