import 'dart:async'; import 'dart:developer'; import 'package:flutter/widgets.dart'; import 'package:workmanager/workmanager.dart'; import '../api/marianumconnect/marianumconnect_endpoint.dart'; import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays.dart'; import '../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart'; import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms.dart'; import '../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart'; import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects.dart'; import '../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart'; import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid.dart'; import '../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart'; import '../api/marianumconnect/queries/timetable_get_week/timetable_get_week.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 '../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. The Marianum-Connect /// dio singleton + bearer interceptor handle login/refresh transparently — /// we only need to pin the endpoint to whatever the user picked in the /// in-app settings before issuing calls. 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(); // The background isolate doesn't go through main.dart's BlocBuilder, so we // re-apply the endpoint the foreground last persisted. Without this the // dio singleton would fall back to its hardcoded live default even when // the user picked beta/custom in the in-app settings. final mcBaseUrl = await WidgetSync.getMarianumConnectBaseUrl(); if (mcBaseUrl != null && mcBaseUrl.isNotEmpty) { MarianumConnectEndpoint.update(mcBaseUrl); } final now = WidgetPublisher.widgetNow(); // 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 timetable = await TimetableGetWeek().run( from: weekStart, until: weekEndExclusive.subtract(const Duration(days: 1)), ); // Reference data — failures fall through to null in the mapper rather // than aborting the whole refresh. final subjects = await _runOrNull( () => TimetableGetSubjects().run(), ); final rooms = await _runOrNull( () => TimetableGetRooms().run(), ); final holidays = await _runOrNull( () => TimetableGetHolidays().run(), ); final timegrid = await _runOrNull( () => TimetableGetTimegrid().run(), ); final customEvents = await _runOrNull( () => GetCustomTimetableEvent( GetCustomTimetableEventParams(AccountData().getUserSecret()), ).run(), ); final lessons = timetable.entries; 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; } }