import 'dart:async'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; import 'dart:io' show Platform; /// Notification Service - Handles local notifications class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); bool _initialized = false; /// Stream controller for permission status changes final StreamController _permissionStatusController = StreamController.broadcast(); /// Get the permission status stream Stream get permissionStatusStream => _permissionStatusController.stream; /// Dispose the stream controller void dispose() { _permissionStatusController.close(); } /// Initialize notification service Future initialize() async { if (_initialized) return; // Skip initialization on web platform if (kIsWeb) { if (kDebugMode) { print('Notifications not supported on web platform'); } return; } try { // Android initialization settings const androidSettings = AndroidInitializationSettings( '@drawable/ic_notification', ); // iOS initialization settings const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, ); await _notifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationTapped, ); _initialized = true; // Start listening for permission changes await listenForPermissionChanges(); // Check initial permission status await hasPermission(); if (kDebugMode) { print('Notification service initialized successfully'); } } catch (e) { if (kDebugMode) { print('Failed to initialize notifications: $e'); } } } /// Handle notification tap void _onNotificationTapped(NotificationResponse response) { if (kDebugMode) { print('Notification tapped: ${response.payload}'); } } /// Request notification permissions (iOS and Android 13+) Future requestPermissions() async { if (kIsWeb) return false; try { bool isGranted = false; // Check if we're on Android or iOS if (Platform.isAndroid) { // Android 13+ requires runtime permission final status = await Permission.notification.request(); isGranted = status.isGranted; if (kDebugMode) { print('Android notification permission status: $status'); } } else if (Platform.isIOS) { // iOS permission request final iosImplementation = _notifications.resolvePlatformSpecificImplementation(); if (iosImplementation != null) { final result = await iosImplementation.requestPermissions(alert: true, badge: true, sound: true); isGranted = result ?? false; if (kDebugMode) { print('iOS notification permission result: $result'); } } else { isGranted = true; // Assume granted if we can't request } } else { isGranted = true; // Assume granted for other platforms } // Update the permission status stream _permissionStatusController.add(isGranted); return isGranted; } catch (e) { if (kDebugMode) { print('Failed to request permissions: $e'); } _permissionStatusController.add(false); return false; } } /// Check if notification permission is granted Future hasPermission() async { if (kIsWeb) return false; try { bool isGranted = false; if (Platform.isAndroid) { final status = await Permission.notification.status; isGranted = status.isGranted; } else if (Platform.isIOS) { // For iOS, we assume granted after initial request isGranted = true; } else { isGranted = true; // Assume granted for other platforms } // Update the permission status stream _permissionStatusController.add(isGranted); return isGranted; } catch (e) { if (kDebugMode) { print('Failed to check permission status: $e'); } _permissionStatusController.add(false); return false; } } /// Listen for permission status changes Future listenForPermissionChanges() async { // Permission status changes listening is not supported in current permission_handler version // This method is kept for future implementation if (kDebugMode) { print('Permission status changes listening is not supported'); } } /// Show focus session completed notification Future showFocusCompletedNotification({ required int minutes, required int distractionCount, String? title, String? body, }) async { if (kIsWeb || !_initialized) return; try { const androidDetails = AndroidNotificationDetails( 'focus_completed', 'Focus Session Completed', channelDescription: 'Notifications for completed focus sessions', importance: Importance.high, priority: Priority.high, enableVibration: true, playSound: true, icon: '@drawable/ic_notification', ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); // Use provided title/body or fall back to English final notificationTitle = title ?? '🎉 Focus session complete!'; final notificationBody = body ?? (distractionCount == 0 ? 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'} without distractions!' : 'You focused for $minutes ${minutes == 1 ? 'minute' : 'minutes'}. Great effort!'); await _notifications.show( 0, // Notification ID notificationTitle, notificationBody, notificationDetails, payload: 'focus_completed', ); if (kDebugMode) { print('Notification shown: $notificationTitle - $notificationBody'); } } catch (e) { if (kDebugMode) { print('Failed to show notification: $e'); } } } /// Show reminder notification (optional feature for future) Future showReminderNotification({required String message}) async { if (kIsWeb || !_initialized) return; try { const androidDetails = AndroidNotificationDetails( 'reminders', 'Focus Reminders', channelDescription: 'Gentle reminders to focus', importance: Importance.defaultImportance, priority: Priority.defaultPriority, icon: '@drawable/ic_notification', ); const iosDetails = DarwinNotificationDetails(); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); await _notifications.show( 1, // Different ID from completion notifications '💚 FocusBuddy', message, notificationDetails, payload: 'reminder', ); } catch (e) { if (kDebugMode) { print('Failed to show reminder: $e'); } } } /// Cancel all notifications Future cancelAll() async { if (kIsWeb || !_initialized) return; try { await _notifications.cancelAll(); } catch (e) { if (kDebugMode) { print('Failed to cancel notifications: $e'); } } } /// Cancel a specific notification by ID Future cancel(int id) async { if (kIsWeb || !_initialized) return; try { await _notifications.cancel(id); } catch (e) { if (kDebugMode) { print('Failed to cancel notification $id: $e'); } } } /// Show ongoing focus session notification (for background) /// This notification stays visible while the timer is running Future showOngoingFocusNotification({ required int remainingMinutes, required int remainingSeconds, String? title, String? timeRemainingText, }) async { if (kIsWeb || !_initialized) return; try { // Format time display for fallback final timeStr = '${remainingMinutes.toString().padLeft(2, '0')}:${(remainingSeconds % 60).toString().padLeft(2, '0')}'; const androidDetails = AndroidNotificationDetails( 'focus_timer', 'Focus Timer', channelDescription: 'Shows ongoing focus session timer', importance: Importance.low, priority: Priority.low, ongoing: true, // Makes notification persistent autoCancel: false, showWhen: false, enableVibration: false, playSound: false, // Show in status bar showProgress: false, icon: '@drawable/ic_notification', ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: false, presentSound: false, ); const notificationDetails = NotificationDetails( android: androidDetails, iOS: iosDetails, ); // Use provided text or fall back to English final notificationTitle = title ?? '⏱️ Focus session in progress'; final notificationBody = timeRemainingText ?? '$timeStr remaining'; await _notifications.show( 2, // Use ID 2 for ongoing notifications notificationTitle, notificationBody, notificationDetails, payload: 'focus_ongoing', ); } catch (e) { if (kDebugMode) { print('Failed to show ongoing notification: $e'); } } } /// Update ongoing notification with new time Future updateOngoingFocusNotification({ required int remainingMinutes, required int remainingSeconds, String? title, String? timeRemainingText, }) async { // On Android, showing the same notification ID updates it await showOngoingFocusNotification( remainingMinutes: remainingMinutes, remainingSeconds: remainingSeconds, title: title, timeRemainingText: timeRemainingText, ); } /// Cancel ongoing focus notification Future cancelOngoingFocusNotification() async { await cancel(2); // Cancel notification with ID 2 } /// Check if notifications are supported on this platform bool get isSupported => !kIsWeb; }