174 lines
6.4 KiB
Dart
174 lines
6.4 KiB
Dart
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<void> 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<void> requestImmediateRefresh() async {
|
|
await Workmanager().registerOneOffTask(
|
|
'$oneOffTaskName-${DateTime.now().millisecondsSinceEpoch}',
|
|
oneOffTaskName,
|
|
constraints: Constraints(networkType: NetworkType.connected),
|
|
existingWorkPolicy: ExistingWorkPolicy.append,
|
|
);
|
|
}
|
|
|
|
static Future<void> 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<void> _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<TimetableGetSubjectsResponse>(
|
|
() => TimetableGetSubjects().run(),
|
|
);
|
|
final rooms = await _runOrNull<TimetableGetRoomsResponse>(
|
|
() => TimetableGetRooms().run(),
|
|
);
|
|
final holidays = await _runOrNull<TimetableGetHolidaysResponse>(
|
|
() => TimetableGetHolidays().run(),
|
|
);
|
|
final timegrid = await _runOrNull<TimetableGetTimegridResponse>(
|
|
() => TimetableGetTimegrid().run(),
|
|
);
|
|
final customEvents = await _runOrNull<GetCustomTimetableEventResponse>(
|
|
() => 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<T?> _runOrNull<T>(Future<T> Function() task) async {
|
|
try {
|
|
return await task();
|
|
} on Exception catch (e) {
|
|
log('[widget-bg] reference fetch failed: $e');
|
|
return null;
|
|
}
|
|
}
|