377 lines
11 KiB
Dart
377 lines
11 KiB
Dart
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<bool> _permissionStatusController = StreamController<bool>.broadcast();
|
|
|
|
/// Get the permission status stream
|
|
Stream<bool> get permissionStatusStream => _permissionStatusController.stream;
|
|
|
|
/// Dispose the stream controller
|
|
void dispose() {
|
|
_permissionStatusController.close();
|
|
}
|
|
|
|
/// Initialize notification service
|
|
Future<void> 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<bool> 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<IOSFlutterLocalNotificationsPlugin>();
|
|
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<bool> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> cancelOngoingFocusNotification() async {
|
|
await cancel(2); // Cancel notification with ID 2
|
|
}
|
|
|
|
/// Check if notifications are supported on this platform
|
|
bool get isSupported => !kIsWeb;
|
|
}
|