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,
+    );
   }
 }