From eb8afa4c28f510dda640728c2bc21cb196dfa0e5 Mon Sep 17 00:00:00 2001 From: Adrasteon <73344919+AdrasteonDev@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:41:47 +0100 Subject: [PATCH] Option for using persistent notifications (#107) * Add an option to receive persistent notifications * Add notification methods to NotificationService * Reschedule notification when the daily entry status change * Reschedule notification when the user open the app if there is a daily entry --- CHANGELOG.md | 3 +- CONTRIBUTORS.md | 1 + lib/bindings/initial_binding.dart | 5 + lib/controllers/daily_entry_controller.dart | 9 + lib/lang/en.dart | 1 + lib/lang/fr.dart | 1 + .../widgets/switch_notifications.dart | 192 +++++++----------- lib/utils/notification_service.dart | 182 ++++++++++++++++- 8 files changed, 269 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf53470..64d7f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -## v1.5.3 - 09/2023 +## v1.5.3 - 10/2023 +- Added option to receive persistent notifications - Added option to change calendar days color for colorblind users - Added option to disable date filter in experimental file picker - Added reverse filter order button in experimental file picker diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ce72cf3..15377bc 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -7,6 +7,7 @@ - Bagas Wastu (@bagaswastu) - Harry Schiller (@waitingwittykitty) - David Coker (@daoxve) +- Adrasteon (@AdrasteonDev) ## Testing & Feedback - Augusto Vesco diff --git a/lib/bindings/initial_binding.dart b/lib/bindings/initial_binding.dart index 54dff5c..7e25d98 100644 --- a/lib/bindings/initial_binding.dart +++ b/lib/bindings/initial_binding.dart @@ -1,10 +1,15 @@ import 'package:get/get.dart'; import '../controllers/lang_controller.dart'; +import '../utils/notification_service.dart'; class InitialBinding extends Bindings { @override void dependencies() { Get.put<LanguageController>(LanguageController()); + Get.put<NotificationService>( + NotificationService(), + permanent: true, + ); } } diff --git a/lib/controllers/daily_entry_controller.dart b/lib/controllers/daily_entry_controller.dart index 152aa77..21e50f6 100644 --- a/lib/controllers/daily_entry_controller.dart +++ b/lib/controllers/daily_entry_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import '../utils/date_format_utils.dart'; +import '../utils/notification_service.dart'; import '../utils/shared_preferences_util.dart'; class DailyEntryController extends GetxController { @@ -11,11 +12,15 @@ class DailyEntryController extends GetxController { } final dailyEntry = SharedPrefsUtil.getBool('dailyEntry')?.obs ?? false.obs; + final NotificationService notificationService = Get.find(); void updateDaily({bool value = true}) { SharedPrefsUtil.putBool('dailyEntry', value); dailyEntry.value = value; dailyEntry.refresh(); + + // Remove the existing notification and schedule it again + notificationService.rescheduleNotification(DateTime.now()); } void _checkTodayEntry() { @@ -28,5 +33,9 @@ class DailyEntryController extends GetxController { dailyEntry.value = false; dailyEntry.refresh(); } + + // Remove the existing notification and schedule it again if there is a daily entry + if(dailyEntry.value) + notificationService.rescheduleNotification(DateTime.now()); } } diff --git a/lib/lang/en.dart b/lib/lang/en.dart index be58f69..87566cc 100644 --- a/lib/lang/en.dart +++ b/lib/lang/en.dart @@ -62,6 +62,7 @@ const Map<String, String> en = { 'notifications': 'Notifications', 'enableNotifications': 'Enable Notifications', 'scheduleTime': 'Schedule Time', + 'usePersistentNotifications': 'Persistent notifications', 'test': 'Test', 'notificationTitle': 'Heyy!', 'notificationBody': 'Do not forget to record 1 second of your day 👀', diff --git a/lib/lang/fr.dart b/lib/lang/fr.dart index 19cac5e..0bb895a 100644 --- a/lib/lang/fr.dart +++ b/lib/lang/fr.dart @@ -62,6 +62,7 @@ const Map<String, String> fr = { 'notifications': 'Notifications', 'enableNotifications': 'Activer les notifications', 'scheduleTime': 'Horaire', + 'usePersistentNotifications': 'Notifications persistantes', 'test': 'Test', 'notificationTitle': 'Hé !', 'notificationBody': "N'oubliez pas d'enregistrer une seconde de votre journée. 👀", diff --git a/lib/pages/home/notification/widgets/switch_notifications.dart b/lib/pages/home/notification/widgets/switch_notifications.dart index a40dcc6..7e1adf1 100644 --- a/lib/pages/home/notification/widgets/switch_notifications.dart +++ b/lib/pages/home/notification/widgets/switch_notifications.dart @@ -1,17 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:timezone/data/latest.dart' as tz; -import 'package:timezone/timezone.dart' as tz; +import '../../../../controllers/daily_entry_controller.dart'; import '../../../../routes/app_pages.dart'; import '../../../../utils/constants.dart'; import '../../../../utils/notification_service.dart'; -import '../../../../utils/shared_preferences_util.dart'; import '../../../../utils/theme.dart'; -import '../../../../utils/utils.dart'; class SwitchNotificationsComponent extends StatefulWidget { @override @@ -21,36 +15,19 @@ class SwitchNotificationsComponent extends StatefulWidget { class _SwitchNotificationsComponentState extends State<SwitchNotificationsComponent> { - final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - - final int notificationId = 1; - late bool isSwitchToggled; + late bool isNotificationSwitchToggled; TimeOfDay scheduledTimeOfDay = const TimeOfDay(hour: 20, minute: 00); + late bool isPersistentSwitchToggled; + final NotificationService notificationService = Get.find(); @override void initState() { super.initState(); - isSwitchToggled = NotificationService().isNotificationActivated(); + isNotificationSwitchToggled = notificationService.isNotificationActivated(); + isPersistentSwitchToggled = notificationService.isPersistentNotificationActivated(); // Sets the default values for scheduled time - getScheduledTime(); - - /// Initializing notification settings - tz.initializeTimeZones(); - - const AndroidInitializationSettings androidInitializationSettings = - AndroidInitializationSettings('@mipmap/ic_launcher'); - - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings(); - - const InitializationSettings initializationSettings = - InitializationSettings( - android: androidInitializationSettings, - iOS: iosInitializationSettings, - ); - - flutterLocalNotificationsPlugin.initialize(initializationSettings); + scheduledTimeOfDay = notificationService.getScheduledTime(); } @override @@ -58,69 +35,6 @@ class _SwitchNotificationsComponentState super.dispose(); } - final platformNotificationDetails = const NotificationDetails( - android: AndroidNotificationDetails( - 'channel id', - 'channel name', - channelDescription: 'channel description', - ), - ); - - // Checks for the scheduled time and sets it to a value in shared prefs - void getScheduledTime() { - final int hour = SharedPrefsUtil.getInt('scheduledTimeHour') ?? 20; - final int minute = SharedPrefsUtil.getInt('scheduledTimeMinute') ?? 00; - scheduledTimeOfDay = TimeOfDay(hour: hour, minute: minute); - } - - Future<void> scheduleNotification() async { - final now = DateTime.now(); - - // sets the scheduled time in DateTime format - final String setTime = DateTime( - now.year, - now.month, - now.day, - scheduledTimeOfDay.hour, - scheduledTimeOfDay.minute, - ).toString(); - - Utils.logInfo('[NOTIFICATIONS] - Scheduled with setTime=$setTime'); - - /// Schedule notification - await flutterLocalNotificationsPlugin.zonedSchedule( - notificationId, - 'notificationTitle'.tr, - 'notificationBody'.tr, - tz.TZDateTime.parse(tz.local, setTime), - platformNotificationDetails, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, - // Allow notification to be shown daily - matchDateTimeComponents: DateTimeComponents.time, - ); - } - - Future<void> showTestNotification() async { - await flutterLocalNotificationsPlugin.show( - notificationId, - 'test'.tr, - 'test'.tr, - platformNotificationDetails, - ); - - // Feedback to the user that the notification was called - await Fluttertoast.showToast( - msg: 'done'.tr, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.CENTER, - backgroundColor: AppColors.dark, - textColor: Colors.white, - fontSize: 16.0, - ); - } - @override Widget build(BuildContext context) { return Column( @@ -139,31 +53,23 @@ class _SwitchNotificationsComponentState ), ), Switch( - value: isSwitchToggled, + value: isNotificationSwitchToggled, onChanged: (value) async { if (value) { - Utils.logInfo( - '[NOTIFICATIONS] - Notifications were enabled', - ); + await notificationService.turnOnNotifications(); - /// Schedule notification if switch in ON - await Utils.requestPermission(Permission.notification); - await scheduleNotification(); - } else { - Utils.logInfo( - '[NOTIFICATIONS] - Notifications were disabled', + await notificationService.scheduleNotification( + scheduledTimeOfDay.hour, + scheduledTimeOfDay.minute, + DateTime.now() ); - - /// Cancel notification if switch is OFF - flutterLocalNotificationsPlugin.cancelAll(); + } else { + await notificationService.turnOffNotifications(); } - /// Save notification on SharedPrefs - NotificationService().switchNotification(); - /// Update switch value setState(() { - isSwitchToggled = !isSwitchToggled; + isNotificationSwitchToggled = !isNotificationSwitchToggled; }); }, activeTrackColor: AppColors.mainColor.withOpacity(0.4), @@ -227,23 +133,25 @@ class _SwitchNotificationsComponentState if (newTimeOfDay == null) return; // Enable notification if it's disabled - if (!isSwitchToggled) { - print('here'); - await Utils.requestPermission(Permission.notification); - NotificationService().switchNotification(); + if (!isNotificationSwitchToggled) { + await notificationService.turnOnNotifications(); setState(() { - isSwitchToggled = true; + isNotificationSwitchToggled = true; }); } - SharedPrefsUtil.putInt('scheduledTimeHour', newTimeOfDay.hour); - SharedPrefsUtil.putInt('scheduledTimeMinute', newTimeOfDay.minute); + notificationService.setScheduledTime(newTimeOfDay.hour, + newTimeOfDay.minute); setState(() { scheduledTimeOfDay = newTimeOfDay; }); - await scheduleNotification(); + await notificationService.scheduleNotification( + scheduledTimeOfDay.hour, + scheduledTimeOfDay.minute, + DateTime.now() + ); }, child: Container( padding: @@ -268,6 +176,54 @@ class _SwitchNotificationsComponentState ), ), const Divider(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'usePersistentNotifications'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.width * 0.045, + ), + ), + Switch( + value: isPersistentSwitchToggled, + onChanged: (value) async { + if (value) { + notificationService.activatePersistentNotifications(); + } else { + notificationService.deactivatePersistentNotifications(); + } + + /// Schedule notification if switch in ON + if(isNotificationSwitchToggled && !isNotificationSwitchToggled){ + await notificationService.turnOnNotifications(); + setState(() { + isNotificationSwitchToggled = true; + }); + } + + if(isNotificationSwitchToggled){ + await notificationService.scheduleNotification( + scheduledTimeOfDay.hour, + scheduledTimeOfDay.minute, + DateTime.now() + ); + } + + /// Update switch value + setState(() { + isPersistentSwitchToggled = !isPersistentSwitchToggled; + }); + }, + activeTrackColor: AppColors.mainColor.withOpacity(0.4), + activeColor: AppColors.mainColor, + ), + ], + ), + ), + const Divider(), ], ); } diff --git a/lib/utils/notification_service.dart b/lib/utils/notification_service.dart index c026382..0c502fa 100644 --- a/lib/utils/notification_service.dart +++ b/lib/utils/notification_service.dart @@ -1,15 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +import 'constants.dart'; import 'shared_preferences_util.dart'; +import 'utils.dart'; class NotificationService { - final _key = 'activatedNotification'; + final _notificationKey = 'activatedNotification'; + final _persistentKey = 'persistentNotification'; + final _hourKey = 'scheduledTimeHour'; + final _minuteKey = 'scheduledTimeMinute'; + final int _notificationId = 1; + final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + late NotificationDetails _platformNotificationDetails; + final NotificationDetails _platformNonPersistentNotificationDetails = const NotificationDetails( + android: AndroidNotificationDetails( + 'channel id', + 'channel name', + channelDescription: 'channel description', + ongoing: false, + autoCancel: true + ), + ); + final NotificationDetails _platformPersistentNotificationDetails = const NotificationDetails( + android: AndroidNotificationDetails( + 'channel id', + 'channel name', + channelDescription: 'channel description', + ongoing: true, + autoCancel: false + ), + ); + + NotificationService(){ + if(isPersistentNotificationActivated()) + _platformNotificationDetails = _platformPersistentNotificationDetails; + else + _platformNotificationDetails = _platformNonPersistentNotificationDetails; + + /// Initializing notification settings + tz.initializeTimeZones(); + + const AndroidInitializationSettings androidInitializationSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + const DarwinInitializationSettings iosInitializationSettings = + DarwinInitializationSettings(); + + const InitializationSettings initializationSettings = + InitializationSettings( + android: androidInitializationSettings, + iOS: iosInitializationSettings, + ); + + _flutterLocalNotificationsPlugin.initialize(initializationSettings); + } // Notification is deactivated by default - bool isNotificationActivated() => SharedPrefsUtil.getBool(_key) ?? false; + bool isNotificationActivated() => SharedPrefsUtil.getBool(_notificationKey) ?? false; + bool isPersistentNotificationActivated() => SharedPrefsUtil.getBool(_persistentKey) ?? false; + + // Checks for the scheduled time and sets it to a value in shared prefs + TimeOfDay getScheduledTime() { + final int hour = SharedPrefsUtil.getInt(_hourKey) ?? 20; + final int minute = SharedPrefsUtil.getInt(_minuteKey) ?? 00; + return TimeOfDay(hour: hour, minute: minute); + } + + void _switchNotification() { + SharedPrefsUtil.putBool(_notificationKey, !isNotificationActivated()); + } + + void _switchPersistentNotification() { + SharedPrefsUtil.putBool(_persistentKey, !isPersistentNotificationActivated()); + } + + void setScheduledTime(int hour, int minute) { + SharedPrefsUtil.putInt(_hourKey, hour); + SharedPrefsUtil.putInt(_minuteKey, minute); + } + + Future<void> turnOnNotifications() async { + Utils.logInfo( + '[NOTIFICATIONS] - Notifications were enabled', + ); + + /// Schedule notification if switch in ON + await Utils.requestPermission(Permission.notification); + + /// Save notification on SharedPrefs + _switchNotification(); + } + + Future<void> turnOffNotifications() async { + Utils.logInfo( + '[NOTIFICATIONS] - Notifications were disabled', + ); + + /// Cancel notification if switch is OFF + _flutterLocalNotificationsPlugin.cancelAll(); + + /// Save notification on SharedPrefs + _switchNotification(); + } + + Future<void> activatePersistentNotifications() async { + Utils.logInfo( + '[NOTIFICATIONS] - Persistent notifications were enabled', + ); + _platformNotificationDetails = _platformPersistentNotificationDetails; + + /// Save notification on SharedPrefs + _switchPersistentNotification(); + } + + Future<void> deactivatePersistentNotifications() async { + Utils.logInfo( + '[NOTIFICATIONS] - Persistent notifications were disabled', + ); + _platformNotificationDetails = _platformNonPersistentNotificationDetails; + + /// Save notification on SharedPrefs + _switchPersistentNotification(); + } + + Future<void> scheduleNotification(int hour, int minute, DateTime day) async { + _flutterLocalNotificationsPlugin.cancelAll(); + + // sets the scheduled time in DateTime format + final String setTime = DateTime( + day.year, + day.month, + day.day, + hour, + minute, + ).toString(); + + Utils.logInfo('[NOTIFICATIONS] - Scheduled with setTime=$setTime'); + + /// Schedule notification + await _flutterLocalNotificationsPlugin.zonedSchedule( + _notificationId, + 'notificationTitle'.tr, + 'notificationBody'.tr, + tz.TZDateTime.parse(tz.local, setTime), + _platformNotificationDetails, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + // Allow notification to be shown daily + matchDateTimeComponents: DateTimeComponents.time, + ); + } + + Future<void> rescheduleNotification(DateTime day) async { + final TimeOfDay timeOfDay = getScheduledTime(); + await scheduleNotification(timeOfDay.hour, timeOfDay.minute, day); + } - Future<bool> _saveNotification(bool isActivated) => - SharedPrefsUtil.putBool(_key, isActivated); + Future<void> showTestNotification() async { + await _flutterLocalNotificationsPlugin.show( + _notificationId, + 'test'.tr, + 'test'.tr, + _platformNotificationDetails, + ); - void switchNotification() { - _saveNotification(!isNotificationActivated()); + // Feedback to the user that the notification was called + await Fluttertoast.showToast( + msg: 'done'.tr, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + backgroundColor: AppColors.dark, + textColor: Colors.white, + fontSize: 16.0, + ); } }