fix(notification): harden local alerts
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@@ -7,7 +8,16 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(
|
||||||
|
_ center: UNUserNotificationCenter,
|
||||||
|
willPresent notification: UNNotification,
|
||||||
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
|
) {
|
||||||
|
completionHandler([.banner, .sound, .badge])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,11 +117,15 @@ class NotificationService {
|
|||||||
enableVibration: true,
|
enableVibration: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
await _notifications
|
await _notifications
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
AndroidFlutterLocalNotificationsPlugin
|
AndroidFlutterLocalNotificationsPlugin
|
||||||
>()
|
>()
|
||||||
?.createNotificationChannel(androidChannel);
|
?.createNotificationChannel(androidChannel);
|
||||||
|
} catch (e) {
|
||||||
|
AppLogger.debug('안드로이드 채널 생성 실패: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 알림 권한 요청
|
/// 알림 권한 요청
|
||||||
@@ -214,6 +218,46 @@ class NotificationService {
|
|||||||
return true;
|
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;
|
static void Function(NotificationResponse)? onNotificationTap;
|
||||||
|
|
||||||
@@ -238,7 +282,7 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final location = await _ensureLocalTimezone();
|
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(
|
final scheduledTime = tz.TZDateTime.now(
|
||||||
location,
|
location,
|
||||||
).add(Duration(minutes: minutesToWait));
|
).add(Duration(minutes: minutesToWait));
|
||||||
@@ -343,17 +387,9 @@ class NotificationService {
|
|||||||
return AndroidScheduleMode.exactAllowWhileIdle;
|
return AndroidScheduleMode.exactAllowWhileIdle;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
final canExact = await canScheduleExactAlarms();
|
||||||
final androidImplementation = _notifications
|
if (!canExact) {
|
||||||
.resolvePlatformSpecificImplementation<
|
AppLogger.debug('정확 알람 권한 없음 → 근사 모드로 예약');
|
||||||
AndroidFlutterLocalNotificationsPlugin
|
|
||||||
>();
|
|
||||||
final canExact = await androidImplementation
|
|
||||||
?.canScheduleExactNotifications();
|
|
||||||
if (canExact == false) {
|
|
||||||
return AndroidScheduleMode.inexactAllowWhileIdle;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
return AndroidScheduleMode.inexactAllowWhileIdle;
|
return AndroidScheduleMode.inexactAllowWhileIdle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:permission_handler/permission_handler.dart';
|
|||||||
import '../../../core/constants/app_colors.dart';
|
import '../../../core/constants/app_colors.dart';
|
||||||
import '../../../core/constants/app_typography.dart';
|
import '../../../core/constants/app_typography.dart';
|
||||||
import '../../providers/settings_provider.dart';
|
import '../../providers/settings_provider.dart';
|
||||||
|
import '../../providers/notification_provider.dart';
|
||||||
|
|
||||||
class SettingsScreen extends ConsumerStatefulWidget {
|
class SettingsScreen extends ConsumerStatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
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),
|
], 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) {
|
void _showPermissionDialog(String permissionName) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user