fix: remove deprecated Flutter notification API parameters and unnecessary imports

This commit is contained in:
Wizznu 2026-04-10 20:03:25 +07:00
parent 789f80ffe0
commit fb7e3216f1
1 changed files with 674 additions and 0 deletions

View File

@ -0,0 +1,674 @@
import 'dart:convert';
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
/// 🔔 NOTIFICATION SERVICE v2.0 - WITH BACKGROUND SUPPORT
///
/// Fitur Utama:
/// Exact alarm scheduling untuk precision timing
/// Background notifications (muncul bahkan saat app minimized!)
/// Retry logic untuk reliability
/// Multiple notification channels (watering, alert, info)
/// Full Firebase Cloud Messaging support
/// Proper permission handling
class NotificationService {
NotificationService._();
static final NotificationService instance = NotificationService._();
static const String topicName = 'apsgo_notifications';
static const String wateringChannelId = 'apsgo_watering_channel';
static const String alertChannelId = 'apsgo_alert_channel';
static const String infoChannelId = 'apsgo_info_channel';
static const int _maxAndroidNotificationId = 2147483647;
final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
int _notificationCounter = 0;
/// ==================== INITIALIZATION ====================
/// Initialize notification service dengan complete setup
Future<void> initialize() async {
if (_isInitialized || kIsWeb) return;
debugPrint('🔔 Initializing Notification Service v2.0...');
// Setup timezones
tz.initializeTimeZones();
try {
tz.setLocalLocation(tz.getLocation('Asia/Jakarta'));
} catch (_) {
tz.setLocalLocation(tz.UTC);
}
// Android & iOS initialization
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,
);
// Initialize dengan background response support
await _plugin.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationResponse,
onDidReceiveBackgroundNotificationResponse: _onNotificationResponse,
);
// Setup channels & permissions
await _setupNotificationChannels();
await _requestAllPermissions();
// Firebase setup
try {
await FirebaseMessaging.instance.subscribeToTopic(topicName);
debugPrint('✅ Subscribed to Firebase topic: $topicName');
} catch (e) {
debugPrint('⚠️ Firebase subscription failed: $e');
}
_setupFCMHandlers();
_isInitialized = true;
debugPrint('✅ Notification Service v2.0 ready!');
}
/// Setup notification channels
Future<void> _setupNotificationChannels() async {
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin == null) return;
try {
// Watering channel - PRIORITY TINGGI
const wateringChannel = AndroidNotificationChannel(
wateringChannelId,
'ApsGo Penyiraman',
description: 'Notifikasi penyiraman - selalu muncul!',
importance: Importance.max,
playSound: true,
enableLights: true,
enableVibration: true,
);
// Alert channel
const alertChannel = AndroidNotificationChannel(
alertChannelId,
'ApsGo Alert',
description: 'Alert dan warning dari sistem',
importance: Importance.high,
playSound: true,
enableLights: true,
enableVibration: true,
);
// Info channel
const infoChannel = AndroidNotificationChannel(
infoChannelId,
'ApsGo Informasi',
description: 'Informasi sistem ApsGo',
importance: Importance.low,
playSound: false,
enableLights: false,
enableVibration: false,
);
await Future.wait([
androidPlugin.createNotificationChannel(wateringChannel),
androidPlugin.createNotificationChannel(alertChannel),
androidPlugin.createNotificationChannel(infoChannel),
]);
debugPrint('✅ Notification channels created');
} catch (e) {
debugPrint('❌ Error creating channels: $e');
}
}
/// Request all permissions
Future<void> _requestAllPermissions() async {
try {
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
// Notifications permission
await androidPlugin?.requestNotificationsPermission();
// Exact alarms permission
final canScheduleExact =
await androidPlugin?.canScheduleExactNotifications() ?? false;
if (!canScheduleExact) {
await androidPlugin?.requestExactAlarmsPermission();
}
// iOS permissions
await _plugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>()
?.requestPermissions(alert: true, badge: true, sound: true);
// Firebase permissions
await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
debugPrint('✅ All permissions requested');
} catch (e) {
debugPrint('⚠️ Permission error: $e');
}
}
/// Setup FCM handlers
void _setupFCMHandlers() {
FirebaseMessaging.onMessage.listen((message) {
debugPrint('📨 FCM onMessage: ${message.messageId}');
_showRemoteMessage(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((message) {
debugPrint('📨 FCM onMessageOpenedApp: ${message.messageId}');
});
}
/// ==================== NOTIFICATION DISPLAY ====================
/// Build watering notification details (HIGH PRIORITY)
NotificationDetails _buildWateringNotificationDetails() {
return NotificationDetails(
android: AndroidNotificationDetails(
wateringChannelId,
'ApsGo Penyiraman',
channelDescription: 'Notifikasi penyiraman',
importance: Importance.max,
priority: Priority.high,
playSound: true,
enableVibration: true,
enableLights: true,
icon: '@mipmap/ic_launcher',
largeIcon: const DrawableResourceAndroidBitmap('@mipmap/ic_launcher'),
autoCancel: true,
showWhen: true,
fullScreenIntent: true,
tag: 'watering',
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
categoryIdentifier: 'watering',
),
);
}
/// Build alert notification details
NotificationDetails _buildAlertNotificationDetails() {
return NotificationDetails(
android: AndroidNotificationDetails(
alertChannelId,
'ApsGo Alert',
channelDescription: 'Alert dan warning',
importance: Importance.high,
priority: Priority.high,
playSound: true,
enableVibration: true,
enableLights: true,
icon: '@mipmap/ic_launcher',
autoCancel: true,
showWhen: true,
),
iOS: const DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
);
}
/// Build basic notification details
NotificationDetails _buildBasicNotificationDetails() {
return NotificationDetails(
android: AndroidNotificationDetails(
infoChannelId,
'ApsGo Informasi',
channelDescription: 'Informasi sistem',
importance: Importance.low,
priority: Priority.low,
playSound: false,
enableVibration: false,
icon: '@mipmap/ic_launcher',
autoCancel: true,
),
iOS: const DarwinNotificationDetails(
presentAlert: false,
presentBadge: true,
presentSound: false,
),
);
}
/// Show instant notification
Future<void> showInstantNotification({
required String title,
required String body,
String? payload,
bool isWatering = false,
}) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
try {
final details =
isWatering
? _buildWateringNotificationDetails()
: _buildBasicNotificationDetails();
await _plugin.show(_nextId(), title, body, details, payload: payload);
debugPrint('✅ Notification: "$title"');
} catch (e) {
debugPrint('❌ Error: $e');
}
}
/// Show watering notification (PENTING UNTUK PENYIRAMAN!)
Future<void> showWateringNotification({
required String title,
required String body,
Map<String, dynamic>? payload,
}) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
try {
await _plugin.show(
_nextId(),
title,
body,
_buildWateringNotificationDetails(),
payload: payload == null ? null : jsonEncode(payload),
);
debugPrint('✅ Watering notification: "$title" - "$body"');
} catch (e) {
debugPrint('❌ Error showing watering notification: $e');
}
}
/// Show alert notification
Future<void> showAlertNotification({
required String title,
required String body,
String? payload,
}) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
try {
await _plugin.show(
_nextId(),
title,
body,
_buildAlertNotificationDetails(),
payload: payload,
);
debugPrint('✅ Alert notification: "$title"');
} catch (e) {
debugPrint('❌ Error: $e');
}
}
/// Show mode changed notification
Future<void> showModeChangedNotification({
required String modeLabel,
required bool enabled,
}) async {
final body =
enabled
? 'Mode diubah menjadi penyiraman $modeLabel.'
: 'Mode penyiraman $modeLabel dinonaktifkan.';
await showInstantNotification(
title: 'Mode Penyiraman',
body: body,
payload: jsonEncode({'type': 'mode_change', 'mode': modeLabel}),
);
}
/// ==================== SCHEDULING ====================
/// Schedule notification dengan retry logic
Future<void> scheduleNotification({
required String title,
required String body,
required Duration delay,
String? payload,
int retryCount = 3,
}) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
for (int attempt = 1; attempt <= retryCount; attempt++) {
try {
final scheduledDate = tz.TZDateTime.now(tz.local).add(delay);
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final canScheduleExact =
await androidPlugin?.canScheduleExactNotifications() ?? false;
final scheduleMode =
canScheduleExact
? AndroidScheduleMode.exactAllowWhileIdle
: AndroidScheduleMode.inexactAllowWhileIdle;
await _plugin.zonedSchedule(
_nextId(),
title,
body,
scheduledDate,
_buildWateringNotificationDetails(),
androidScheduleMode: scheduleMode,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
payload: payload,
);
debugPrint('✅ Scheduled: "$title" in ${delay.inSeconds}s');
return;
} catch (e) {
if (attempt < retryCount) {
await Future.delayed(Duration(milliseconds: 500 * attempt));
} else {
debugPrint('❌ Failed after $retryCount attempts: $e');
}
}
}
}
/// Schedule daily notification
Future<void> scheduleDailyNotificationAtTime({
required String scheduleKey,
required String title,
required String body,
required int hour,
required int minute,
String? payload,
}) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
try {
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));
}
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final canScheduleExact =
await androidPlugin?.canScheduleExactNotifications() ?? false;
final scheduleMode =
canScheduleExact
? AndroidScheduleMode.exactAllowWhileIdle
: AndroidScheduleMode.inexactAllowWhileIdle;
await _plugin.zonedSchedule(
_stableIdFromKey(scheduleKey),
title,
body,
scheduled,
_buildWateringNotificationDetails(),
androidScheduleMode: scheduleMode,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
matchDateTimeComponents: DateTimeComponents.time,
payload: payload,
);
debugPrint(
'✅ Daily scheduled at $hour:${minute.toString().padLeft(2, '0')}',
);
} catch (e) {
debugPrint('❌ Error: $e');
}
}
/// Cancel notification
Future<void> cancelScheduledNotification(String scheduleKey) async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
try {
await _plugin.cancel(_stableIdFromKey(scheduleKey));
debugPrint('✅ Cancelled: $scheduleKey');
} catch (e) {
debugPrint('⚠️ Error: $e');
}
}
/// Sync schedule reminders from Firebase
Future<void> syncScheduleRemindersFromFirebase() async {
if (kIsWeb) return;
if (!_isInitialized) await initialize();
debugPrint('📡 Syncing schedules from Firebase...');
final db = FirebaseDatabase.instance.ref();
await _syncFromKontrolNode(db.child('kontrol'), 'kontrol');
await _syncFromKontrolNode(db.child('kontrol_1'), 'kontrol_1');
}
Future<void> _syncFromKontrolNode(
DatabaseReference ref,
String nodeName,
) async {
try {
final snapshot = await ref.get();
if (!snapshot.exists || snapshot.value is! Map) {
return;
}
final data = Map<String, dynamic>.from(snapshot.value as Map);
final waktuEnabled = data['waktu'] == true;
final scheduleKeys =
data.keys.where((key) => key.startsWith('jadwal_')).toList()..sort();
for (final key in scheduleKeys) {
final scheduleKey = '$nodeName:$key';
final scheduleDataRaw = data[key];
if (scheduleDataRaw is! Map) {
await cancelScheduledNotification(scheduleKey);
continue;
}
final scheduleData = Map<String, dynamic>.from(scheduleDataRaw);
final aktif = scheduleData['aktif'] != false;
final waktu = (scheduleData['waktu'] ?? '').toString();
final isValidTime = RegExp(
r'^([01]\d|2[0-3]):[0-5]\d$',
).hasMatch(waktu);
if (!waktuEnabled || !aktif || !isValidTime) {
await cancelScheduledNotification(scheduleKey);
continue;
}
final parts = waktu.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
await scheduleDailyNotificationAtTime(
scheduleKey: scheduleKey,
title: 'ApsGo - Jadwal Penyiraman',
body: 'Penyiraman akan dimulai pada jam $waktu',
hour: hour,
minute: minute,
payload: jsonEncode({
'type': 'local_schedule',
'key': key,
'time': waktu,
}),
);
}
} catch (e) {
debugPrint('❌ Error syncing $nodeName: $e');
}
}
/// ==================== FIREBASE & FCM ====================
Future<String?> getFcmToken() async {
if (kIsWeb) return null;
if (!_isInitialized) await initialize();
try {
final token = await FirebaseMessaging.instance.getToken();
return token;
} catch (e) {
debugPrint('❌ FCM token error: $e');
return null;
}
}
Future<void> _showRemoteMessage(RemoteMessage message) async {
final notification = message.notification;
final title = notification?.title ?? 'ApsGo';
final body = notification?.body ?? '';
if (body.isEmpty) return;
await showInstantNotification(
title: title,
body: body,
payload: jsonEncode(message.data),
isWatering: body.toLowerCase().contains('penyiraman'),
);
}
/// ==================== UTILITIES ====================
Future<List<PendingNotificationRequest>> pendingNotifications() async {
if (kIsWeb) return <PendingNotificationRequest>[];
if (!_isInitialized) await initialize();
try {
return await _plugin.pendingNotificationRequests();
} catch (e) {
debugPrint('⚠️ Error: $e');
return [];
}
}
Future<void> cancelAll() async {
if (kIsWeb) return;
try {
await _plugin.cancelAll();
debugPrint('✅ All notifications cancelled');
} catch (e) {
debugPrint('⚠️ Error: $e');
}
}
Future<bool> canScheduleExactNotifications() async {
if (kIsWeb) return false;
if (!_isInitialized) await initialize();
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
return await androidPlugin?.canScheduleExactNotifications() ?? false;
}
Future<bool> requestExactAlarmsPermission() async {
if (kIsWeb) return false;
if (!_isInitialized) await initialize();
final androidPlugin =
_plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
try {
return await androidPlugin?.requestExactAlarmsPermission() ?? false;
} on PlatformException {
return false;
}
}
// ==================== PRIVATE HELPERS ====================
int _nextId() {
_notificationCounter = (_notificationCounter + 1) % 100000;
final nowPart = DateTime.now().millisecondsSinceEpoch % 2000000000;
final id = nowPart + _notificationCounter;
return id > _maxAndroidNotificationId ? id - _maxAndroidNotificationId : id;
}
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;
}
static void _onNotificationResponse(NotificationResponse response) {
final payload = response.payload;
if (payload != null) {
debugPrint('🔔 Notification response payload: $payload');
}
}
}