Defects fixed: - D1: Fix notification ID range collision (report reminders 700k→2M+) - D2: Streak risk now checks both dzikir pagi & petang - D3: _cancelPrayerPending no longer kills non-prayer notifications - D4: Push notifications carry deeplink in payload for proper routing - D5: Add reconfigureTimeZoneIfNeeded() for TZ change detection - D6: Defer launch notification routing until widget tree is ready Gaps closed: - G1: Add streak risk + weekly summary toggles to settings UI - G2: Verified boot reschedule already in place (flutter_local_notifications v21) - G3: Remove unused mirrorAdzanToInbox field and legacy cleanup calls - G4: Add notif_push_opened analytics tracking - G5: Add notif_settings_changed analytics tracking Enhancements: - O1: Rich notification with Sudah Sholat action button on report reminders - O2: Permission check on app resume via WidgetsBindingObserver (30s throttle) - O2b: Fix stretched notification icon (white crescent moon vector drawable) - O3: Expired inbox cleanup in background sync - O4: Haptic feedback on notification bell quick actions Bump version 1.0.8+9 → 1.1.0+10
121 lines
4.0 KiB
Dart
121 lines
4.0 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:workmanager/workmanager.dart';
|
|
|
|
import '../local/hive_boxes.dart';
|
|
import '../local/models/app_settings.dart';
|
|
import 'myquran_sholat_service.dart';
|
|
import 'notification_inbox_service.dart';
|
|
import 'notification_orchestrator_service.dart';
|
|
import 'notification_service.dart';
|
|
|
|
class BackgroundSyncService {
|
|
BackgroundSyncService._();
|
|
static final BackgroundSyncService instance = BackgroundSyncService._();
|
|
|
|
static const String periodicTaskName = 'jamshalat_periodic_sync';
|
|
static const String periodicUniqueName = 'jamshalat_periodic_sync_unique';
|
|
|
|
Future<void> init() async {
|
|
await Workmanager().initialize(_workmanagerCallbackDispatcher);
|
|
}
|
|
|
|
Future<void> registerPeriodicSync() async {
|
|
await Workmanager().registerPeriodicTask(
|
|
periodicUniqueName,
|
|
periodicTaskName,
|
|
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
|
frequency: const Duration(hours: 6),
|
|
initialDelay: const Duration(minutes: 15),
|
|
constraints: Constraints(
|
|
networkType: NetworkType.connected,
|
|
),
|
|
backoffPolicy: BackoffPolicy.exponential,
|
|
backoffPolicyDelay: const Duration(minutes: 10),
|
|
);
|
|
}
|
|
|
|
static Future<void> runSyncPass() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await initHive();
|
|
|
|
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = settingsBox.get('default') ?? AppSettings();
|
|
final cityId = _resolveCityId(settings);
|
|
|
|
await NotificationInboxService.instance.removeExpired();
|
|
|
|
final schedulesByDate = await _buildWindowSchedules(cityId);
|
|
if (schedulesByDate.isNotEmpty) {
|
|
await NotificationService.instance.syncPrayerNotifications(
|
|
cityId: cityId,
|
|
adhanEnabled: settings.adhanEnabled,
|
|
iqamahOffset: settings.iqamahOffset,
|
|
schedulesByDate: schedulesByDate,
|
|
reportReminderEnabled: settings.shalatReportReminderEnabled,
|
|
reportReminderDelayMinutes: settings.shalatReportReminderDelayMinutes,
|
|
reportReminderRepeatCount: settings.shalatReportReminderRepeatCount,
|
|
reportReminderRepeatIntervalMinutes:
|
|
settings.shalatReportReminderRepeatIntervalMinutes,
|
|
);
|
|
}
|
|
|
|
await NotificationService.instance.syncHabitNotifications(
|
|
settings: settings,
|
|
);
|
|
await NotificationOrchestratorService.instance.runPassivePass(
|
|
settings: settings,
|
|
);
|
|
}
|
|
|
|
static Future<Map<String, Map<String, String>>> _buildWindowSchedules(
|
|
String cityId) async {
|
|
final now = DateTime.now();
|
|
final startDate = DateTime(now.year, now.month, now.day);
|
|
final endDate = startDate.add(const Duration(days: 35));
|
|
|
|
final monthKeys = <String>{
|
|
DateFormat('yyyy-MM').format(startDate),
|
|
DateFormat('yyyy-MM').format(endDate),
|
|
};
|
|
|
|
final schedulesByDate = <String, Map<String, String>>{};
|
|
for (final monthKey in monthKeys) {
|
|
final monthly = await MyQuranSholatService.instance
|
|
.getMonthlySchedule(cityId, monthKey);
|
|
for (final entry in monthly.entries) {
|
|
final date = DateTime.tryParse(entry.key);
|
|
if (date == null) continue;
|
|
final normalized = DateTime(date.year, date.month, date.day);
|
|
if (normalized.isBefore(startDate) || normalized.isAfter(endDate)) {
|
|
continue;
|
|
}
|
|
schedulesByDate[entry.key] = entry.value;
|
|
}
|
|
}
|
|
return schedulesByDate;
|
|
}
|
|
|
|
static String _resolveCityId(AppSettings settings) {
|
|
final stored = settings.lastCityName ?? '';
|
|
if (stored.contains('|')) {
|
|
return stored.split('|').last;
|
|
}
|
|
return '58a2fc6ed39fd083f55d4182bf88826d';
|
|
}
|
|
}
|
|
|
|
@pragma('vm:entry-point')
|
|
void _workmanagerCallbackDispatcher() {
|
|
Workmanager().executeTask((task, inputData) async {
|
|
if (task == BackgroundSyncService.periodicTaskName) {
|
|
await BackgroundSyncService.runSyncPass();
|
|
return true;
|
|
}
|
|
return true;
|
|
});
|
|
}
|