import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; class NotificationService { NotificationService._(); static final NotificationService instance = NotificationService._(); static const String _channelId = 'telurku_panen_channel'; static const String _channelName = 'Panen TelurKu'; static const String _channelDescription = 'Notifikasi panen dan debug TelurKu'; final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin(); static const int _maxAndroidNotificationId = 2147483647; bool _isInitialized = false; int _notificationCounter = 0; int _stableIdFromKey(String key) { var hash = 0; for (final codeUnit in key.codeUnits) { hash = 0x1fffffff & (hash + codeUnit); hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); hash ^= (hash >> 6); } hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); hash ^= (hash >> 11); hash = 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); return hash % _maxAndroidNotificationId; } Future initialize() async { if (_isInitialized) return; if (kIsWeb) return; try { tz.initializeTimeZones(); final localTimeZone = await FlutterTimezone.getLocalTimezone(); tz.setLocalLocation(tz.getLocation(localTimeZone)); const androidSettings = AndroidInitializationSettings( '@mipmap/ic_launcher', ); const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, defaultPresentAlert: true, defaultPresentBadge: true, defaultPresentSound: true, ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await _plugin.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationResponse, ); await _setupNotificationChannel(); await _requestPermissions(); _isInitialized = true; if (kDebugMode) { print('✅ NotificationService initialized dengan channel panen'); } } catch (e) { if (kDebugMode) { print('❌ NotificationService init failed: $e'); } rethrow; } } Future _setupNotificationChannel() async { try { final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); const AndroidNotificationChannel channel = AndroidNotificationChannel( _channelId, _channelName, description: _channelDescription, importance: Importance.max, playSound: true, enableLights: true, enableVibration: true, ); await androidPlugin?.createNotificationChannel(channel); if (kDebugMode) { print('✅ Notification channel telurku_panen_channel created'); } } catch (e) { if (kDebugMode) { print('⚠️ Error setting notification channel: $e'); } } } Future _requestPermissions() async { try { final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); final notifPermission = await androidPlugin?.requestNotificationsPermission() ?? false; if (kDebugMode) { print( notifPermission ? '✅ Notification permission granted' : '⚠️ Notification permission not granted', ); } final exactAlarmPermission = await androidPlugin?.requestExactAlarmsPermission() ?? false; if (kDebugMode) { print(exactAlarmPermission ? '✅ Exact alarm permission granted' : '⚠️ Exact alarm permission not granted'); } await _plugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions(alert: true, badge: true, sound: true); } catch (e) { if (kDebugMode) { print('⚠️ Error requesting permissions: $e'); } } } NotificationDetails _buildNotificationDetails() { return const NotificationDetails( android: AndroidNotificationDetails( _channelId, _channelName, channelDescription: _channelDescription, importance: Importance.max, priority: Priority.high, playSound: true, enableVibration: true, icon: '@mipmap/ic_launcher', ), iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ), ); } int _nextId() { // Android mewajibkan ID notifikasi dalam rentang signed 32-bit. _notificationCounter = (_notificationCounter + 1) % 100000; final nowPart = DateTime.now().millisecondsSinceEpoch % 2000000000; final id = nowPart + _notificationCounter; return id > _maxAndroidNotificationId ? id - _maxAndroidNotificationId : id; } Future showInstantNotification({ required String title, required String body, String? payload, }) async { if (kIsWeb) return; await initialize(); await _plugin.show( _nextId(), title, body, _buildNotificationDetails(), payload: payload, ); } Future scheduleNotification({ required String title, required String body, required Duration delay, bool prioritizeBackgroundDelivery = false, String? payload, }) async { if (kIsWeb) return; await initialize(); final scheduledDate = tz.TZDateTime.now(tz.local).add(delay); final notificationId = _nextId(); final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); final canScheduleExact = await androidPlugin?.canScheduleExactNotifications() ?? true; if (!canScheduleExact) { final permissionGranted = await androidPlugin?.requestExactAlarmsPermission() ?? false; if (kDebugMode) { print( permissionGranted ? '✅ Exact alarm permission granted' : '⚠️ Exact alarm permission not granted, using inexact fallback', ); } } final useExactSchedule = await androidPlugin?.canScheduleExactNotifications() ?? true; final preferredAndroidScheduleMode = prioritizeBackgroundDelivery && useExactSchedule ? AndroidScheduleMode.alarmClock : (useExactSchedule ? AndroidScheduleMode.exactAllowWhileIdle : AndroidScheduleMode.inexactAllowWhileIdle); try { await _plugin.zonedSchedule( notificationId, title, body, scheduledDate, _buildNotificationDetails(), androidScheduleMode: preferredAndroidScheduleMode, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, payload: payload, ); } on PlatformException catch (e) { if (e.code != 'exact_alarms_not_permitted') { rethrow; } if (kDebugMode) { print('⚠️ Exact alarm tidak diizinkan, fallback ke inexact alarm.'); } await _plugin.zonedSchedule( notificationId, title, body, scheduledDate, _buildNotificationDetails(), androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, payload: payload, ); } } Future scheduleDailyNotificationAtTime({ required String scheduleKey, required String title, required String body, required int hour, required int minute, String? payload, }) async { if (kIsWeb) return; await initialize(); final scheduledDate = _nextTimeAt(hour, minute); final notificationId = _stableIdFromKey(scheduleKey); final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); final canScheduleExactBeforeRequest = await androidPlugin?.canScheduleExactNotifications() ?? true; if (!canScheduleExactBeforeRequest) { await androidPlugin?.requestExactAlarmsPermission(); } final canScheduleExact = await androidPlugin?.canScheduleExactNotifications() ?? true; final preferredMode = canScheduleExact ? AndroidScheduleMode.alarmClock : AndroidScheduleMode.inexactAllowWhileIdle; if (kDebugMode) { print( '⏰ Daily reminder [$scheduleKey] -> ${scheduledDate.toLocal()} (mode: $preferredMode)'); } try { await _plugin.zonedSchedule( notificationId, title, body, scheduledDate, _buildNotificationDetails(), androidScheduleMode: preferredMode, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: DateTimeComponents.time, payload: payload, ); } on PlatformException catch (e) { if (e.code != 'exact_alarms_not_permitted') { if (kDebugMode) { print('⚠️ Daily reminder primary mode failed: $e'); } } try { await _plugin.zonedSchedule( notificationId, title, body, scheduledDate, _buildNotificationDetails(), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: DateTimeComponents.time, payload: payload, ); } on PlatformException { await _plugin.zonedSchedule( notificationId, title, body, scheduledDate, _buildNotificationDetails(), androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, matchDateTimeComponents: DateTimeComponents.time, payload: payload, ); } } } Future cancelScheduledNotification(String scheduleKey) async { if (kIsWeb) return; try { final notificationId = _stableIdFromKey(scheduleKey); await _plugin.cancel(notificationId); if (kDebugMode) { print( '✅ Cancelled scheduled notification: $scheduleKey (ID: $notificationId)'); } } catch (e) { if (kDebugMode) { print('⚠️ Error cancelling notification $scheduleKey: $e'); } } } tz.TZDateTime _nextTimeAt(int hour, int minute) { final now = tz.TZDateTime.now(tz.local); var scheduled = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute); if (scheduled.isBefore(now)) { scheduled = scheduled.add(const Duration(days: 1)); } return scheduled; } Future canScheduleExactNotifications() async { if (kIsWeb) return false; await initialize(); final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); return await androidPlugin?.canScheduleExactNotifications() ?? false; } Future requestExactAlarmsPermission() async { if (kIsWeb) return false; await initialize(); final androidPlugin = _plugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); return await androidPlugin?.requestExactAlarmsPermission() ?? false; } Future cancelAll() async { if (kIsWeb) return; await _plugin.cancelAll(); } Future> pendingNotifications() async { if (kIsWeb) return []; return _plugin.pendingNotificationRequests(); } Future showPanenSummaryNotification({ required String jenisPanen, required Map kandangTotals, }) async { final normalizedJenis = jenisPanen.trim().toLowerCase(); final total = kandangTotals.values.fold(0, (sum, value) => sum + value); final detailText = kandangTotals.entries .map((entry) => '${entry.key} = ${entry.value}') .join(', '); final title = 'Panen ${_capitalize(normalizedJenis)}'; final body = 'Panen ${normalizedJenis.isEmpty ? jenisPanen : normalizedJenis} telah dilakukan, didapatkan total telur $total${detailText.isNotEmpty ? ', $detailText' : ''}'; await showInstantNotification( title: title, body: body, payload: 'panen:$normalizedJenis', ); } static String _capitalize(String value) { if (value.isEmpty) return value; return value[0].toUpperCase() + value.substring(1); } static void _onNotificationResponse(NotificationResponse response) { if (kDebugMode) { print('🔔 Notification tapped: ${response.payload ?? ''}'); } } }