diff --git a/mobile/lib/services/notification_service.dart b/mobile/lib/services/notification_service.dart index 86cabfe014..a8b3288678 100644 --- a/mobile/lib/services/notification_service.dart +++ b/mobile/lib/services/notification_service.dart @@ -1,9 +1,12 @@ import 'dart:io'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import "package:flutter_timezone/flutter_timezone.dart"; import "package:logging/logging.dart"; import "package:photos/services/sync/remote_sync_service.dart"; import "package:shared_preferences/shared_preferences.dart"; +import 'package:timezone/data/latest_10y.dart' as tzdb; +import "package:timezone/timezone.dart" as tz; class NotificationService { static final NotificationService instance = @@ -24,11 +27,14 @@ class NotificationService { _preferences = preferences; } + bool timezoneInitialized = false; + Future initialize( void Function( NotificationResponse notificationResponse, ) onNotificationTapped, ) async { + await initTimezones(); const androidSettings = AndroidInitializationSettings('notification_icon'); const iosSettings = DarwinInitializationSettings( requestAlertPermission: false, @@ -59,6 +65,14 @@ class NotificationService { } } + Future initTimezones() async { + if (timezoneInitialized) return; + tzdb.initializeTimeZones(); + final String currentTimeZone = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(currentTimeZone)); + timezoneInitialized = true; + } + Future requestPermissions() async { bool? result; if (Platform.isIOS) { @@ -127,4 +141,82 @@ class NotificationService { payload: payload, ); } + + Future scheduleNotification( + String title, + String message, { + required int id, + String channelID = "io.ente.photos", + String channelName = "ente", + String payload = "ente://home", + required DateTime dateTime, + }) async { + _logger.info( + "Scheduling notification with: $title, $message, $channelID, $channelName, $payload", + ); + await initTimezones(); + if (!hasGrantedPermissions()) { + _logger.warning("Notification permissions not granted"); + await requestPermissions(); + if (!hasGrantedPermissions()) { + _logger.severe("Failed to get notification permissions"); + return; + } + } else { + _logger.info("Notification permissions already granted"); + } + final androidSpecs = AndroidNotificationDetails( + channelID, + channelName, + channelDescription: 'ente alerts', + importance: Importance.max, + priority: Priority.high, + category: AndroidNotificationCategory.reminder, + showWhen: false, + ); + final iosSpecs = DarwinNotificationDetails(threadIdentifier: channelID); + final platformChannelSpecs = + NotificationDetails(android: androidSpecs, iOS: iosSpecs); + final scheduledDate = tz.TZDateTime.local( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); + // final tz.TZDateTime scheduledDate = tz.TZDateTime.now(tz.local).add(delay); + await _notificationsPlugin.zonedSchedule( + id, + title, + message, + scheduledDate, + platformChannelSpecs, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.wallClockTime, + androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, + payload: payload, + ); + _logger.info( + "Scheduled notification with: $title, $message, $channelID, $channelName, $payload", + ); + } + + Future clearAllScheduledNotifications() async { + _logger.info("Clearing all scheduled notifications"); + final pending = await _notificationsPlugin.pendingNotificationRequests(); + if (pending.isEmpty) { + _logger.info("No pending notifications to clear"); + return; + } + for (final request in pending) { + _logger.info("Clearing notification with id: ${request.id}"); + await _notificationsPlugin.cancel(request.id); + } + } + + Future pendingNotifications() async { + final pending = await _notificationsPlugin.pendingNotificationRequests(); + return pending.length; + } } diff --git a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart index 1a78f7c851..e97cc5b3de 100644 --- a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart @@ -12,6 +12,7 @@ import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; import 'package:photos/services/machine_learning/ml_service.dart'; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; +import "package:photos/services/notification_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; @@ -66,6 +67,90 @@ class _MLDebugSectionWidgetState extends State { logger.info("Building ML Debug section options"); return Column( children: [ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Now notification", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.showNotification( + "On this day", + "Look back on X memories 🌄", + channelID: "memoryID", + channelName: "onThisDay", + payload: "memoryID", + ); + } catch (e, s) { + logger.warning('test notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Soon notification", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.scheduleNotification( + "On this day", + "Look back on X memories 🌄", + id: 1, + channelID: "memoryID", + channelName: "On this day", + payload: "memoryID", + dateTime: DateTime.now().add(const Duration(seconds: 5)), + ); + showShortToast(context, 'Done'); + } catch (e, s) { + logger.warning('soon exact notification failed ', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Show pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + final amount = + await NotificationService.instance.pendingNotifications(); + showShortToast(context, '$amount pending notifications'); + } catch (e) { + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Clear pending notifications", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await NotificationService.instance.clearAllScheduledNotifications(); + showShortToast(context, 'Done'); + } catch (e) { + await showGenericErrorDialog(context: context, error: e); + } + }, + ), sectionOptionSpacing, MenuItemWidget( captionedTextWidget: FutureBuilder(