TKK_E32231105/lib/services/notification_service.dart

429 lines
13 KiB
Dart

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<void> 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<void> _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<void> _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<void> 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<void> 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<void> 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<void> 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<bool> canScheduleExactNotifications() async {
if (kIsWeb) return false;
await initialize();
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
return await androidPlugin?.canScheduleExactNotifications() ?? false;
}
Future<bool> requestExactAlarmsPermission() async {
if (kIsWeb) return false;
await initialize();
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
return await androidPlugin?.requestExactAlarmsPermission() ?? false;
}
Future<void> cancelAll() async {
if (kIsWeb) return;
await _plugin.cancelAll();
}
Future<List<PendingNotificationRequest>> pendingNotifications() async {
if (kIsWeb) return <PendingNotificationRequest>[];
return _plugin.pendingNotificationRequests();
}
Future<void> showPanenSummaryNotification({
required String jenisPanen,
required Map<String, int> kandangTotals,
}) async {
final normalizedJenis = jenisPanen.trim().toLowerCase();
final total =
kandangTotals.values.fold<int>(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 ?? ''}');
}
}
}