added base homescreen-widget setup, working on Android, iOS in progress
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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<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();
|
||||
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<GetSubjectsResponse>(() => GetSubjects().run());
|
||||
final rooms = await _runOrNull<GetRoomsResponse>(() => GetRooms().run());
|
||||
final holidays = await _runOrNull<GetHolidaysResponse>(() => GetHolidays().run());
|
||||
final timegrid = await _runOrNull<GetTimegridUnitsResponse>(
|
||||
() => GetTimegridUnits().run(),
|
||||
);
|
||||
final customEvents = await _runOrNull<GetCustomTimetableEventResponse>(
|
||||
() => 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<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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user