diff --git a/README.md b/README.md index a036839e..10f46767 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ notificationTitle | `String` | The title of the notification triggered whe notificationBody | `String` | The body of the notification. enableNotificationOnKill | `bool` | Whether to show a notification when application is killed to warn the user that the alarm he set may not ring. Enabled by default. androidFullScreenIntent | `bool` | Whether to turn screen on when android alarm notification is triggered. Enabled by default. +notificationActionSettings | `NotificationActionSettings` | Settings for notification action buttons (only stop at the moment). Note that if `notificationTitle` and `notificationBody` are both empty, iOS will not show the notification and Android will show an empty notification. @@ -181,7 +182,6 @@ We welcome contributions to this plugin! If you would like to make a change or a These are some features that I have in mind that could be useful: - [Android] Reschedule alarms after device reboot. -- Notification actions: stop and snooze. - Use `ffigen` and `jnigen` binding generators to call native code more efficiently instead of using method channels. - Stop alarm sound when notification is dismissed. diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt index ca8f3a63..dad6c315 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmPlugin.kt @@ -14,6 +14,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import androidx.annotation.NonNull +import java.util.Date import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall @@ -64,19 +65,19 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler { override fun onReceive(context: Context?, intent: Intent?) { Log.d("AlarmPlugin", "Received alarm notification action") val action = intent?.getStringExtra(AlarmReceiver.EXTRA_ALARM_ACTION) - val alarmId = intent?.getIntExtra("id", -1) ?: -1 + val id = intent?.getIntExtra("id", -1) ?: -1 + when (action) { NotificationHandler.ACTION_STOP -> { - Log.d("AlarmPlugin", "Notification action: STOP, id: $alarmId") - stopAlarm(alarmId) - } - NotificationHandler.ACTION_SNOOZE -> { - Log.d("AlarmPlugin", "Notification action: SNOOZE, id: $alarmId") - if (context == null || alarmId == -1) { - Log.e("AlarmPlugin", "Snooze alarm error") - return + Log.d("AlarmPlugin", "Notification action: STOP, id: $id") + stopAlarm(id) + if (context != null) { + AlarmStorage(context!!).unsaveAlarm(id) + eventSink?.success(mapOf( + "id" to id, + "method" to "stop" + )) } - snoozeAlarm(context, alarmId, 9 * 60) } } } @@ -254,12 +255,6 @@ class AlarmPlugin: FlutterPlugin, MethodCallHandler { notifOnKillEnabled = false } - fun snoozeAlarm(context: Context, id: Int, snoozeInSeconds: Int) { - Log.d("AlarmPlugin", "Snoozing alarm with id: $id") - - // get alarm intent with id, stop it and reschedule with extras and snooze time - } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { methodChannel.setMethodCallHandler(null) eventChannel.setStreamHandler(null) diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt index 64680164..8f9b1885 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/alarm/AlarmService.kt @@ -45,17 +45,16 @@ class AlarmService : Service() { return START_NOT_STICKY } + // Only Android 12 and above ? val action = intent.action - Log.d("AlarmService", "Received action: $action") + Log.d("AlarmService", "-> Received action: $action") val id = intent.getIntExtra("id", 0) if (action == "STOP_ALARM" && id != 0) { stopAlarm(id) return START_NOT_STICKY } else if (action == NotificationHandler.ACTION_STOP) { Log.d("AlarmService", "Action button clicked: STOP") - return START_NOT_STICKY - } else if (action == NotificationHandler.ACTION_SNOOZE) { - Log.d("AlarmService", "Action button clicked: SNOOZE") + stopAlarm(id) return START_NOT_STICKY } @@ -97,7 +96,10 @@ class AlarmService : Service() { return START_NOT_STICKY // Return on security exception } - AlarmPlugin.eventSink?.success(mapOf("id" to id)) + AlarmPlugin.eventSink?.success(mapOf( + "id" to id, + "method" to "ring" + )) if (volume >= 0.0 && volume <= 1.0) { volumeService?.setVolume(volume, showSystemUI) diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationActionSettings.kt b/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationActionSettings.kt index 8c117e04..81b34fae 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationActionSettings.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/models/NotificationActionSettings.kt @@ -4,10 +4,7 @@ import com.google.gson.Gson data class NotificationActionSettings( val hasStopButton: Boolean = false, - val hasSnoozeButton: Boolean = false, - val stopButtonText: String = "Stop", - val snoozeButtonText: String = "Snooze", - val snoozeDurationInSeconds: Int = 9 * 60 + val stopButtonText: String = "Stop" ) { companion object { fun fromJson(json: Map): NotificationActionSettings { diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt index 2b1cc5a9..59ff14e6 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/AlarmStorage.kt @@ -27,9 +27,12 @@ class AlarmStorage(context: Context) { fun unsaveAlarm(id: Int) { val key = "$PREFIX$id" + Log.d("AlarmStorage", "Unsaving alarm with id: $id and key: $key") val editor = prefs.edit() editor.remove(key) + Log.d("AlarmStorage", "Removed alarm with key: $key from preferences") editor.apply() + Log.d("AlarmStorage", "Changes applied to preferences") } diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt index a3eb48cc..e361900c 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/services/NotificationService.kt @@ -15,7 +15,6 @@ class NotificationHandler(private val context: Context) { private const val CHANNEL_ID = "alarm_plugin_channel" private const val CHANNEL_NAME = "Alarm Notification" const val ACTION_STOP = "com.gdelataillade.alarm.ACTION_STOP" - const val ACTION_SNOOZE = "com.gdelataillade.alarm.ACTION_SNOOZE" } init { @@ -37,6 +36,7 @@ class NotificationHandler(private val context: Context) { } } + // TODO: Add wake lock with loop param fun buildNotification( title: String, body: String, @@ -58,17 +58,6 @@ class NotificationHandler(private val context: Context) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) - val snoozeIntent = Intent(context, AlarmReceiver::class.java).apply { - action = ACTION_SNOOZE - putExtra("id", alarmId) - } - val snoozePendingIntent = PendingIntent.getBroadcast( - context, - 1, - snoozeIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(appIconResId) .setContentTitle(title) @@ -89,9 +78,6 @@ class NotificationHandler(private val context: Context) { if (it.hasStopButton) { notificationBuilder.addAction(0, it.stopButtonText, stopPendingIntent) } - if (it.hasSnoozeButton) { - notificationBuilder.addAction(0, it.snoozeButtonText, snoozePendingIntent) - } } return notificationBuilder.build() diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 364c0688..a06ba510 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -386,7 +386,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarm.alarmExample; + PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarmExample; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -522,7 +522,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarm.alarmExample; + PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarmExample; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -550,7 +550,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarm.alarmExample; + PRODUCT_BUNDLE_IDENTIFIER = com.gdelataillade.alarmExample; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; diff --git a/example/lib/screens/edit_alarm.dart b/example/lib/screens/edit_alarm.dart index cd608b22..3f8089b6 100644 --- a/example/lib/screens/edit_alarm.dart +++ b/example/lib/screens/edit_alarm.dart @@ -100,9 +100,6 @@ class _ExampleAlarmEditScreenState extends State { enableNotificationOnKill: Platform.isIOS, notificationActionSettings: const NotificationActionSettings( hasStopButton: true, - stopButtonText: 'STOP !', - snoozeButtonText: 'SNOOZE !', - snoozeDurationInSeconds: 5, ), ); return alarmSettings; diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 9bfb4992..d27378aa 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -4,9 +4,9 @@ import 'package:alarm/alarm.dart'; import 'package:alarm_example/screens/edit_alarm.dart'; import 'package:alarm_example/screens/ring.dart'; import 'package:alarm_example/screens/shortcut_button.dart'; +import 'package:alarm_example/services/permission.dart'; import 'package:alarm_example/widgets/tile.dart'; import 'package:flutter/material.dart'; -import 'package:permission_handler/permission_handler.dart'; class ExampleAlarmHomeScreen extends StatefulWidget { const ExampleAlarmHomeScreen({super.key}); @@ -18,17 +18,21 @@ class ExampleAlarmHomeScreen extends StatefulWidget { class _ExampleAlarmHomeScreenState extends State { late List alarms; - static StreamSubscription? subscription; + static StreamSubscription? ringSubscription; + static StreamSubscription? updateSubscription; @override void initState() { super.initState(); if (Alarm.android) { - checkAndroidNotificationPermission(); - checkAndroidScheduleExactAlarmPermission(); + AlarmPermissions.checkAndroidNotificationPermission(); + AlarmPermissions.checkAndroidScheduleExactAlarmPermission(); } loadAlarms(); - subscription ??= Alarm.ringStream.stream.listen(navigateToRingScreen); + ringSubscription ??= Alarm.ringStream.stream.listen(navigateToRingScreen); + updateSubscription ??= Alarm.updateStream.stream.listen((_) { + loadAlarms(); + }); } void loadAlarms() { @@ -67,43 +71,10 @@ class _ExampleAlarmHomeScreenState extends State { if (res != null && res == true) loadAlarms(); } - Future checkAndroidNotificationPermission() async { - final status = await Permission.notification.status; - if (status.isDenied) { - alarmPrint('Requesting notification permission...'); - final res = await Permission.notification.request(); - alarmPrint( - 'Notification permission ${res.isGranted ? '' : 'not '}granted', - ); - } - } - - Future checkAndroidExternalStoragePermission() async { - final status = await Permission.storage.status; - if (status.isDenied) { - alarmPrint('Requesting external storage permission...'); - final res = await Permission.storage.request(); - alarmPrint( - 'External storage permission ${res.isGranted ? '' : 'not'} granted', - ); - } - } - - Future checkAndroidScheduleExactAlarmPermission() async { - final status = await Permission.scheduleExactAlarm.status; - alarmPrint('Schedule exact alarm permission: $status.'); - if (status.isDenied) { - alarmPrint('Requesting schedule exact alarm permission...'); - final res = await Permission.scheduleExactAlarm.request(); - alarmPrint( - 'Schedule exact alarm permission ${res.isGranted ? '' : 'not'} granted', - ); - } - } - @override void dispose() { - subscription?.cancel(); + ringSubscription?.cancel(); + updateSubscription?.cancel(); super.dispose(); } diff --git a/example/lib/screens/shortcut_button.dart b/example/lib/screens/shortcut_button.dart index 70adf6ba..bccbdb2a 100644 --- a/example/lib/screens/shortcut_button.dart +++ b/example/lib/screens/shortcut_button.dart @@ -42,10 +42,6 @@ class _ExampleAlarmHomeShortcutButtonState enableNotificationOnKill: Platform.isIOS, notificationActionSettings: const NotificationActionSettings( hasStopButton: true, - hasSnoozeButton: true, - stopButtonText: 'STOP', - snoozeButtonText: 'SNOOZE', - snoozeDurationInSeconds: 10, ), ); diff --git a/example/lib/services/permission.dart b/example/lib/services/permission.dart new file mode 100644 index 00000000..0627ba8d --- /dev/null +++ b/example/lib/services/permission.dart @@ -0,0 +1,38 @@ +import 'package:alarm/alarm.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AlarmPermissions { + static Future checkAndroidNotificationPermission() async { + final status = await Permission.notification.status; + if (status.isDenied) { + alarmPrint('Requesting notification permission...'); + final res = await Permission.notification.request(); + alarmPrint( + 'Notification permission ${res.isGranted ? '' : 'not '}granted', + ); + } + } + + static Future checkAndroidExternalStoragePermission() async { + final status = await Permission.storage.status; + if (status.isDenied) { + alarmPrint('Requesting external storage permission...'); + final res = await Permission.storage.request(); + alarmPrint( + 'External storage permission ${res.isGranted ? '' : 'not'} granted', + ); + } + } + + static Future checkAndroidScheduleExactAlarmPermission() async { + final status = await Permission.scheduleExactAlarm.status; + alarmPrint('Schedule exact alarm permission: $status.'); + if (status.isDenied) { + alarmPrint('Requesting schedule exact alarm permission...'); + final res = await Permission.scheduleExactAlarm.request(); + alarmPrint( + 'Schedule exact alarm permission ${res.isGranted ? '' : 'not'} granted', + ); + } + } +} diff --git a/ios/Classes/SwiftAlarmPlugin.swift b/ios/Classes/SwiftAlarmPlugin.swift index 3dc48894..29ec773a 100644 --- a/ios/Classes/SwiftAlarmPlugin.swift +++ b/ios/Classes/SwiftAlarmPlugin.swift @@ -77,14 +77,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { self.stopAlarm(id: id, cancelNotif: true, result: { _ in }) } NSLog("SwiftAlarmPlugin: stopAlarmFromNotification...") - // channel.invokeMethod("alarmStoppedFromNotification", arguments: ["id": id]) - } - - func snoozeAlarmFromNotification(id: Int, snoozeDurationInSeconds: Int) { - safeModifyResources { - self.stopAlarm(id: id, cancelNotif: true, result: { _ in }) - } - // channel.invokeMethod("alarmSnoozedFromNotification", arguments: ["id": id, "snoozeDurationInSeconds": snoozeDurationInSeconds]) + channel.invokeMethod("alarmStoppedFromNotification", arguments: ["id": id]) } func safeModifyResources(_ modificationBlock: @escaping () -> Void) { diff --git a/ios/Classes/models/NotificationActionSettings.swift b/ios/Classes/models/NotificationActionSettings.swift index 305590c5..47aefea6 100644 --- a/ios/Classes/models/NotificationActionSettings.swift +++ b/ios/Classes/models/NotificationActionSettings.swift @@ -2,28 +2,19 @@ import Foundation struct NotificationActionSettings: Codable { var hasStopButton: Bool = false - var hasSnoozeButton: Bool = false var stopButtonText: String = "Stop" - var snoozeButtonText: String = "Snooze" - var snoozeDurationInSeconds: Int = 9 * 60 static func fromJson(json: [String: Any]) -> NotificationActionSettings { return NotificationActionSettings( hasStopButton: json["hasStopButton"] as? Bool ?? false, - hasSnoozeButton: json["hasSnoozeButton"] as? Bool ?? false, - stopButtonText: json["stopButtonText"] as? String ?? "Stop", - snoozeButtonText: json["snoozeButtonText"] as? String ?? "Snooze", - snoozeDurationInSeconds: json["snoozeDurationInSeconds"] as? Int ?? 9 * 60 + stopButtonText: json["stopButtonText"] as? String ?? "Stop" ) } static func toJson(notificationActionSettings: NotificationActionSettings) -> [String: Any] { return [ "hasStopButton": notificationActionSettings.hasStopButton, - "hasSnoozeButton": notificationActionSettings.hasSnoozeButton, - "stopButtonText": notificationActionSettings.stopButtonText, - "snoozeButtonText": notificationActionSettings.snoozeButtonText, - "snoozeDurationInSeconds": notificationActionSettings.snoozeDurationInSeconds + "stopButtonText": notificationActionSettings.stopButtonText ] } } \ No newline at end of file diff --git a/ios/Classes/services/NotificationManager.swift b/ios/Classes/services/NotificationManager.swift index 7161962a..c0cbf1d9 100644 --- a/ios/Classes/services/NotificationManager.swift +++ b/ios/Classes/services/NotificationManager.swift @@ -8,7 +8,7 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { super.init() } - private func setupNotificationActions(hasStopButton: Bool, hasSnoozeButton: Bool, stopButtonText: String, snoozeButtonText: String) { + private func setupNotificationActions(hasStopButton: Bool, stopButtonText: String) { var actions: [UNNotificationAction] = [] if hasStopButton { @@ -16,11 +16,6 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { actions.append(stopAction) } - if hasSnoozeButton { - let snoozeAction = UNNotificationAction(identifier: "SNOOZE_ACTION", title: snoozeButtonText, options: []) - actions.append(snoozeAction) - } - let category = UNNotificationCategory(identifier: "ALARM_CATEGORY", actions: actions, intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([category]) @@ -38,14 +33,14 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { return } - self.setupNotificationActions(hasStopButton: actionSettings.hasStopButton, hasSnoozeButton: actionSettings.hasSnoozeButton, stopButtonText: actionSettings.stopButtonText, snoozeButtonText: actionSettings.snoozeButtonText) + self.setupNotificationActions(hasStopButton: actionSettings.hasStopButton, stopButtonText: actionSettings.stopButtonText) let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = nil content.categoryIdentifier = "ALARM_CATEGORY" - content.userInfo = ["id": id, "snoozeDurationInSeconds": actionSettings.snoozeDurationInSeconds] // Include the id as an Integer in userInfo + content.userInfo = ["id": id] // Include the id as an Integer in userInfo let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(delayInSeconds), repeats: false) let request = UNNotificationRequest(identifier: "alarm-\(id)", content: content, trigger: trigger) @@ -66,12 +61,6 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { case "STOP_ACTION": NSLog("Stop action triggered for notification: \(notification.request.identifier)") SwiftAlarmPlugin.shared.stopAlarmFromNotification(id: id) - - case "SNOOZE_ACTION": - guard let snoozeDurationInSeconds = notification.request.content.userInfo["snoozeDurationInSeconds"] as? Int else { return } - NSLog("Snooze action triggered for notification: \(notification.request.identifier)") - SwiftAlarmPlugin.shared.snoozeAlarmFromNotification(id: id, snoozeDurationInSeconds: snoozeDurationInSeconds) - default: break } diff --git a/lib/alarm.dart b/lib/alarm.dart index d5d6f67d..9ddc3c47 100644 --- a/lib/alarm.dart +++ b/lib/alarm.dart @@ -24,6 +24,9 @@ class Alarm { /// Whether it's Android device. static bool get android => defaultTargetPlatform == TargetPlatform.android; + /// Stream of the alarm updates. + static final updateStream = StreamController(); + /// Stream of the ringing status. static final ringStream = StreamController(); @@ -79,18 +82,10 @@ class Alarm { await AlarmStorage.saveAlarm(alarmSettings); - if (iOS) { - return IOSAlarm.setAlarm( - alarmSettings, - () => ringStream.add(alarmSettings), - ); - } - if (android) { - return AndroidAlarm.set( - alarmSettings, - () => ringStream.add(alarmSettings), - ); - } + if (iOS) return IOSAlarm.setAlarm(alarmSettings); + if (android) return AndroidAlarm.set(alarmSettings); + + updateStream.add(alarmSettings.id); return false; } @@ -142,6 +137,7 @@ class Alarm { /// Stops alarm. static Future stop(int id) async { await AlarmStorage.unsaveAlarm(id); + updateStream.add(id); return iOS ? await IOSAlarm.stopAlarm(id) : await AndroidAlarm.stop(id); } @@ -177,4 +173,11 @@ class Alarm { /// Returns all the alarms. static List getAlarms() => AlarmStorage.getSavedAlarms(); + + /// Reloads the shared preferences instance in the case modifications + /// were made in the native code, after a notification action. + static Future reload(int id) async { + await AlarmStorage.prefs.reload(); + updateStream.add(id); + } } diff --git a/lib/model/notification_action_settings.dart b/lib/model/notification_action_settings.dart index a1689ed0..9f695ade 100644 --- a/lib/model/notification_action_settings.dart +++ b/lib/model/notification_action_settings.dart @@ -5,48 +5,30 @@ import 'package:flutter/widgets.dart'; /// Model for notification action settings. class NotificationActionSettings { /// Constructs an instance of `NotificationActionSettings`. + /// + /// Open PR if you want more features. const NotificationActionSettings({ this.hasStopButton = false, - this.hasSnoozeButton = false, this.stopButtonText = 'Stop', - this.snoozeButtonText = 'Snooze', - this.snoozeDurationInSeconds = 9 * 60, }); /// Constructs an instance of `NotificationActionSettings` from a JSON object. factory NotificationActionSettings.fromJson(Map json) => NotificationActionSettings( hasStopButton: json['hasStopButton'] as bool? ?? false, - hasSnoozeButton: json['hasSnoozeButton'] as bool? ?? false, - snoozeDurationInSeconds: - json['snoozeDurationInSeconds'] as int? ?? 9 * 60, + stopButtonText: json['stopButtonText'] as String? ?? 'Stop', ); /// Whether to show the stop button. final bool hasStopButton; - /// Whether to show the snooze button. - final bool hasSnoozeButton; - /// The text to display on the stop button. Defaults to 'Stop'. final String stopButtonText; - /// The text to display on the snooze button. Defaults to 'Snooze'. - final String snoozeButtonText; - - /// The snooze duration in seconds. Defaults to 9 minutes. - final int snoozeDurationInSeconds; - - /// Whether the notification action buttons are enabled. - bool get enabled => hasStopButton || hasSnoozeButton; - /// Converts the `NotificationActionSettings` instance to a JSON object. Map toJson() => { 'hasStopButton': hasStopButton, - 'hasSnoozeButton': hasSnoozeButton, 'stopButtonText': stopButtonText, - 'snoozeButtonText': snoozeButtonText, - 'snoozeDurationInSeconds': snoozeDurationInSeconds, }; @override @@ -55,21 +37,14 @@ class NotificationActionSettings { other is NotificationActionSettings && runtimeType == other.runtimeType && hasStopButton == other.hasStopButton && - hasSnoozeButton == other.hasSnoozeButton && - stopButtonText == other.stopButtonText && - snoozeButtonText == other.snoozeButtonText && - snoozeDurationInSeconds == other.snoozeDurationInSeconds; + stopButtonText == other.stopButtonText; @override int get hashCode => Object.hash( hasStopButton, - hasSnoozeButton, stopButtonText, - snoozeButtonText, - snoozeDurationInSeconds, ); @override - String toString() => - '''NotificationActionSettings(hasStopButton: $hasStopButton, hasSnoozeButton: $hasSnoozeButton, stopButtonText: $stopButtonText, snoozeButtonText: $snoozeButtonText, snoozeDurationInSeconds: $snoozeDurationInSeconds)'''; + String toString() => 'NotificationActionSettings: ${toJson()}'; } diff --git a/lib/src/android_alarm.dart b/lib/src/android_alarm.dart index 2bd321ba..692913f2 100644 --- a/lib/src/android_alarm.dart +++ b/lib/src/android_alarm.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:alarm/alarm.dart'; -import 'package:alarm/service/alarm_storage.dart'; import 'package:alarm/utils/alarm_exception.dart'; import 'package:flutter/services.dart'; @@ -13,22 +12,28 @@ class AndroidAlarm { static const eventChannel = EventChannel('com.gdelataillade.alarm/events'); /// Whether there are other alarms set. - static bool get hasOtherAlarms => AlarmStorage.getSavedAlarms().length > 1; + static bool get hasOtherAlarms => Alarm.getAlarms().length > 1; - /// Starts listening to the alarm events. - static void init() { - // listenToAlarmEvents(); - } + /// Starts listening to the native alarm events. + static void init() => listenToNativeEvents(); /// Listens to the alarm events. - static void listenToAlarmEvents() { + static void listenToNativeEvents() { eventChannel.receiveBroadcastStream().listen( - (dynamic event) { + (dynamic event) async { try { final eventMap = Map.from(event as Map); - final id = eventMap['id'] as int; - final settings = Alarm.getAlarm(id); - if (settings != null) Alarm.ringStream.add(settings); + final id = eventMap['id'] as int?; + final method = eventMap['method'] as String?; + if (id == null || method == null) return; + + switch (method) { + case 'stop': + await Alarm.reload(id); + case 'ring': + final settings = Alarm.getAlarm(id); + if (settings != null) Alarm.ringStream.add(settings); + } } catch (e) { alarmPrint('Error receiving alarm events: $e'); } @@ -40,10 +45,7 @@ class AndroidAlarm { } /// Schedules a native alarm with given [settings] with its notification. - static Future set( - AlarmSettings settings, - void Function()? onRing, - ) async { + static Future set(AlarmSettings settings) async { try { await methodChannel.invokeMethod( 'setAlarm', diff --git a/lib/src/ios_alarm.dart b/lib/src/ios_alarm.dart index 7f1cfb9c..2a750bd2 100644 --- a/lib/src/ios_alarm.dart +++ b/lib/src/ios_alarm.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:alarm/alarm.dart'; import 'package:alarm/utils/alarm_exception.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; @@ -22,62 +21,26 @@ class IOSAlarm { /// Handles incoming method calls from the native platform. static Future handleMethodCall(MethodCall call) async { - debugPrint('handleMethodCall: ${call.method}'); - // switch (call.method) { - // case 'alarmStoppedFromNotification': - // print('call.arguments: ${call.arguments}'); - // final arguments = call.arguments as Map; - // final id = arguments['id'] as int; - // // await handleAlarmStoppedFromNotification(id); - // case 'alarmSnoozedFromNotification': - // print('call.arguments: ${call.arguments}'); - // final arguments = call.arguments as Map; - // final id = arguments['id'] as int; - // final snoozeDurationInSeconds = - // arguments['snoozeDurationInSeconds'] as int; - // // await handleAlarmSnoozedFromNotification(id, snoozeDurationInSeconds); - // default: - // throw MissingPluginException('not implemented'); - // } - } - - /// Handles the alarm stopped from notification event. - static Future handleAlarmStoppedFromNotification(int id) async { - // AlarmStorage.unsaveAlarm(id); - // disposeAlarm(id); - await Alarm.stop(id); - alarmPrint('Alarm with id $id was stopped from notification'); - } - - /// Handles the alarm snoozed from notification event. - static Future handleAlarmSnoozedFromNotification( - int id, - int snoozeDurationInSeconds, - ) async { - final alarm = Alarm.getAlarm(id); - if (alarm == null) { - alarmPrint('Alarm with id $id was not found. Snooze failed.'); - return; - } + print('handleMethodCall: ${call.method}'); - await Alarm.stop(id); + final id = call.arguments['id'] as int?; + if (id == null) return; - final newAlarm = alarm.copyWith( - dateTime: DateTime.now().add(Duration(seconds: snoozeDurationInSeconds)), - ); - await Alarm.set(alarmSettings: newAlarm); + await Alarm.reload(id); - alarmPrint('Alarm with id $id was snoozed for ${snoozeDurationInSeconds}s'); + switch (call.method) { + case 'alarmStoppedFromNotification': + Alarm.updateStream.add(id); + default: + throw MissingPluginException('not implemented'); + } } /// Calls the native function `setAlarm` and listens to alarm ring state. /// /// Also set periodic timer and listens for app state changes to trigger /// the alarm ring callback at the right time. - static Future setAlarm( - AlarmSettings settings, - void Function()? onRing, - ) async { + static Future setAlarm(AlarmSettings settings) async { final id = settings.id; try { final res = await methodChannel.invokeMethod( @@ -97,7 +60,11 @@ class IOSAlarm { } if (timers[id] != null && timers[id]!.isActive) timers[id]!.cancel(); - timers[id] = periodicTimer(onRing, settings.dateTime, id); + timers[id] = periodicTimer( + () => Alarm.ringStream.add(settings), + settings.dateTime, + id, + ); listenAppStateChange( id: id, @@ -109,10 +76,14 @@ class IOSAlarm { if (isRinging) { disposeAlarm(id); - onRing?.call(); + Alarm.ringStream.add(settings); } else { if (timers[id] != null && timers[id]!.isActive) timers[id]!.cancel(); - timers[id] = periodicTimer(onRing, settings.dateTime, id); + timers[id] = periodicTimer( + () => Alarm.ringStream.add(settings), + settings.dateTime, + id, + ); } }, );