import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:android_intent_plus/android_intent.dart';
import '../../main.dart';
import '../models/calendar_event_model.dart';
import '../models/task_model.dart';
/// **Сервис для работы с локальными уведомлениями**
/// Позволяет отправлять мгновенные и запланированные уведомления.
/// Также обрабатывает разрешения и настройку часового пояса.
class NotificationService {
/// **Экземпляр плагина уведомлений**
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
/// **ID канала для уведомлений (используется в Android)**
static const String _channelId = 'calendar_events_channel';
static const String _channelName = 'Calendar Events';
/// **Инициализация сервиса уведомлений**
/// Должна быть вызвана **один раз** в `main.dart` перед использованием.
Future<void> init() async {
// Инициализация часовых зон для работы с запланированными уведомлениями
tz.initializeTimeZones();
// Запрос разрешений на геолокацию (используется для определения часового пояса)
await _requestLocationPermission();
// Автоматически устанавливаем часовой пояс устройства
await _setTimeZoneAutomatically();
// Запрашиваем разрешения на уведомления
await _requestNotificationPermissions();
// Настройки инициализации для Android
const AndroidInitializationSettings androidInitSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
// Настройки инициализации для iOS
const DarwinInitializationSettings iosInitSettings =
DarwinInitializationSettings();
// Общие настройки для всех платформ
const InitializationSettings initSettings = InitializationSettings(
android: androidInitSettings,
iOS: iosInitSettings,
);
// Инициализируем плагин
await _plugin.initialize(
initSettings,
onDidReceiveNotificationResponse: _onSelectNotification,
);
}
/// **Запрос разрешений на уведомления (Android 13+ и iOS)**
Future<void> _requestNotificationPermissions() async {
final androidSettings =
_plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
final iosSettings =
_plugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>();
if (androidSettings != null) {
final bool? granted = await androidSettings.requestNotificationsPermission();
debugPrint("📢 Android notification permission: ${granted == true ? "Granted" : "Denied"}");
}
if (iosSettings != null) {
final bool? granted = await iosSettings.requestPermissions(
alert: true,
badge: true,
sound: true,
);
debugPrint("📢 iOS notification permission: ${granted == true ? "Granted" : "Denied"}");
}
}
/// **Запрос разрешений на доступ к геолокации**
/// Нужно для определения точного часового пояса.
Future<void> _requestLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever) {
debugPrint("🚫 Location permission permanently denied. Timezone detection may not work.");
} else if (permission == LocationPermission.always || permission == LocationPermission.whileInUse) {
debugPrint("✅ Location permission granted.");
}
}
/// **Автоматически определяет часовой пояс устройства**
Future<void> _setTimeZoneAutomatically() async {
final Duration offset = _getSystemTimeZoneOffset();
final String timeZoneName = _offsetToTimeZoneName(offset);
debugPrint("📍 Auto-detected system timezone: $timeZoneName");
if (tz.timeZoneDatabase.locations.containsKey(timeZoneName)) {
tz.setLocalLocation(tz.getLocation(timeZoneName));
debugPrint("✅ Timezone set to: $timeZoneName");
} else {
debugPrint("⚠ Timezone not found, using UTC.");
tz.setLocalLocation(tz.getLocation('UTC'));
}
}
/// **Возвращает смещение текущего часового пояса от UTC**
Duration _getSystemTimeZoneOffset() {
return DateTime.now().timeZoneOffset;
}
/// **Конвертирует смещение UTC в название часового пояса**
/// Используется, если `timezone` сам не определяет корректный часовой пояс.
String _offsetToTimeZoneName(Duration offset) {
final int hours = offset.inHours;
final int minutes = offset.inMinutes.remainder(60);
final String sign = hours >= 0 ? '+' : '-';
final String formatted = '${sign}${hours.abs().toString().padLeft(2, '0')}:${minutes.abs().toString().padLeft(2, '0')}';
// 🕒 Сопоставление часовых поясов по смещению UTC
return {
'-12:00': 'Etc/GMT+12',
'-11:00': 'Pacific/Midway',
'-10:00': 'Pacific/Honolulu',
'-09:30': 'Pacific/Marquesas',
'-09:00': 'America/Anchorage',
'-08:00': 'America/Los_Angeles',
'-07:00': 'America/Denver',
'-06:00': 'America/Chicago',
'-05:00': 'America/New_York',
'-04:00': 'America/Caracas',
'-03:30': 'America/St_Johns',
'-03:00': 'America/Argentina/Buenos_Aires',
'-02:00': 'Atlantic/South_Georgia',
'-01:00': 'Atlantic/Azores',
'+00:00': 'UTC',
'+01:00': 'Europe/London',
'+02:00': 'Europe/Berlin',
'+03:00': 'Europe/Moscow',
'+03:30': 'Asia/Tehran',
'+04:00': 'Asia/Dubai',
'+04:30': 'Asia/Kabul',
'+05:00': 'Asia/Tashkent',
'+05:30': 'Asia/Kolkata',
'+05:45': 'Asia/Kathmandu',
'+06:00': 'Asia/Dhaka',
'+06:30': 'Asia/Yangon',
'+07:00': 'Asia/Bangkok',
'+08:00': 'Asia/Shanghai',
'+09:00': 'Asia/Tokyo',
'+09:30': 'Australia/Darwin',
'+10:00': 'Australia/Sydney',
'+10:30': 'Australia/Lord_Howe',
'+11:00': 'Pacific/Noumea',
'+12:00': 'Pacific/Fiji',
'+12:45': 'Pacific/Chatham',
'+13:00': 'Pacific/Tongatapu',
'+14:00': 'Pacific/Kiritimati',
}[formatted] ?? 'UTC';
}
/// **Показать мгновенное уведомление**
Future<void> showInstantNotification(String title, String body) async {
const NotificationDetails platformChannelSpecifics = NotificationDetails(
android: AndroidNotificationDetails(_channelId, _channelName,
importance: Importance.max, priority: Priority.high),
iOS: DarwinNotificationDetails(),
);
await _plugin.show(0, title, body, platformChannelSpecifics);
}
/// **Запланировать уведомление на определённое время**
Future<void> scheduleNotification(CalendarEventModel event) async {
await _setTimeZoneAutomatically();
final notificationId = event.hashCode;
final now = tz.TZDateTime.now(tz.local);
final scheduledDate = tz.TZDateTime.from(event.date, tz.local);
if (scheduledDate.isBefore(now)) {
debugPrint("⚠ Ошибка: Время уведомления в прошлом. Пропускаем.");
return;
}
const platformChannelSpecifics = NotificationDetails(
android: AndroidNotificationDetails(_channelId, _channelName,
importance: Importance.max, priority: Priority.high),
iOS: DarwinNotificationDetails(),
);
await _plugin.zonedSchedule(
notificationId,
event.getTitleForCalendar().isNotEmpty ? event.getTitleForCalendar() : "Напоминание",
event.jsonData.toString().isNotEmpty ? event.jsonData.toString() : "У вас запланировано событие",
scheduledDate,
platformChannelSpecifics,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
androidScheduleMode: AndroidScheduleMode.alarmClock,
payload: event.jsonData.toString(),
);
debugPrint("✅ Уведомление запланировано: ID=$notificationId, Время=$scheduledDate");
}
/// **Отменить запланированное уведомление**
Future<void> cancelNotification(CalendarEventModel event) async {
await _plugin.cancel(event.hashCode);
}
/// **Обработчик нажатия на уведомление**
void _onSelectNotification(NotificationResponse details) {
debugPrint('📩 Уведомление нажато, payload: ${details.payload}');
}
/// **Тестирование уведомлений** (запланировать уведомление через 10 секунд)
Future<void> testScheduleNotification() async {
final event = CalendarEventModel(
id: "test_id",
date: DateTime.now().add(const Duration(seconds: 10)),
jsonData: TaskModel(
reminderTime: DateTime.now().add(const Duration(seconds: 20)),
title: "TEST TASK",
id: uuid.v4(),
subTasks: [],
).toJson(),
eventType: EventType.task,
);
debugPrint("🚀 Тестовое уведомление запланировано на ${event.date}");
await scheduleNotification(event);
}
/// **Отключение оптимизации батареи (чтобы уведомления работали в фоне)**
Future<void> requestIgnoreBatteryOptimizations() async {
try {
const intent = AndroidIntent(action: 'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS');
await intent.launch();
} catch (e) {
debugPrint("⚠ Ошибка при запуске настроек батареи: $e");
}
}
}
Comments