fix(notification): harden local alerts

This commit is contained in:
JiWoong Sul
2025-12-03 14:48:21 +09:00
parent 3ff9e5f837
commit e4c5fa7356
3 changed files with 100 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
import Flutter
import UIKit
import UserNotifications
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -7,7 +8,16 @@ import UIKit
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}
}

View File

@@ -117,11 +117,15 @@ class NotificationService {
enableVibration: true,
);
await _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(androidChannel);
try {
await _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(androidChannel);
} catch (e) {
AppLogger.debug('안드로이드 채널 생성 실패: $e');
}
}
/// 알림 권한 요청
@@ -214,6 +218,46 @@ class NotificationService {
return true;
}
/// 정확 알람 권한 가능 여부 확인 (Android 12+)
Future<bool> canScheduleExactAlarms() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
return true;
}
try {
final androidImplementation = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final canExact = await androidImplementation
?.canScheduleExactNotifications();
return canExact ?? true;
} catch (e) {
AppLogger.debug('정확 알람 권한 확인 실패: $e');
return false;
}
}
/// 정확 알람 권한 요청 (Android 12+ 설정 화면 이동)
Future<bool> requestExactAlarmsPermission() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
return true;
}
try {
final androidImplementation = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final granted = await androidImplementation
?.requestExactAlarmsPermission();
return granted ?? false;
} catch (e) {
AppLogger.debug('정확 알람 권한 요청 실패: $e');
return false;
}
}
// 알림 탭 콜백
static void Function(NotificationResponse)? onNotificationTap;
@@ -238,7 +282,7 @@ class NotificationService {
}
final location = await _ensureLocalTimezone();
final minutesToWait = delayMinutes ?? 90 + Random().nextInt(31);
final minutesToWait = max(delayMinutes ?? 90 + Random().nextInt(31), 1);
final scheduledTime = tz.TZDateTime.now(
location,
).add(Duration(minutes: minutesToWait));
@@ -343,17 +387,9 @@ class NotificationService {
return AndroidScheduleMode.exactAllowWhileIdle;
}
try {
final androidImplementation = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final canExact = await androidImplementation
?.canScheduleExactNotifications();
if (canExact == false) {
return AndroidScheduleMode.inexactAllowWhileIdle;
}
} catch (_) {
final canExact = await canScheduleExactAlarms();
if (!canExact) {
AppLogger.debug('정확 알람 권한 없음 → 근사 모드로 예약');
return AndroidScheduleMode.inexactAllowWhileIdle;
}

View File

@@ -6,6 +6,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/constants/app_typography.dart';
import '../../providers/settings_provider.dart';
import '../../providers/notification_provider.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@@ -174,6 +175,29 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
);
},
),
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android)
FutureBuilder<bool>(
future: ref
.read(notificationServiceProvider)
.canScheduleExactAlarms(),
builder: (context, snapshot) {
final canExact = snapshot.data;
// 권한이 이미 허용된 경우 UI 생략
if (canExact == true) {
return const SizedBox.shrink();
}
return _buildPermissionTile(
icon: Icons.alarm,
title: '정확 알람 권한',
subtitle: '정확한 예약 알림을 위해 필요합니다',
isGranted: canExact ?? false,
onRequest: _requestExactAlarmPermission,
isDark: isDark,
);
},
),
], isDark),
// 알림 설정
@@ -407,6 +431,19 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
}
}
Future<void> _requestExactAlarmPermission() async {
final notificationService = ref.read(notificationServiceProvider);
final granted = await notificationService.requestExactAlarmsPermission();
if (!mounted) return;
setState(() {});
if (!granted) {
_showPermissionDialog('정확 알람');
}
}
void _showPermissionDialog(String permissionName) {
final isDark = Theme.of(context).brightness == Brightness.dark;