diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cebab8e..08655bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add support for fading the alarm volume using a staircase function. * Fixes a bug where `isRinging` might return FALSE immediately after alarm starts to ring. * Handles alarm events on the platform side, increasing efficiency. +* Handles `stopAll` on the platform side for improved reliability. ## 4.1.1 * [Android] Show app on lock screen when alarm rings. diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt b/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt index af27ea2f..d2c614ec 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/api/AlarmApiImpl.kt @@ -58,6 +58,16 @@ class AlarmApiImpl(private val context: Context) : AlarmApi { } } + override fun stopAll() { + for (alarm in AlarmStorage(context).getSavedAlarms()) { + stopAlarm(alarm.id.toLong()) + } + val alarmIdsCopy = alarmIds.toList() + for (alarmId in alarmIdsCopy) { + stopAlarm(alarmId.toLong()) + } + } + override fun isRinging(alarmId: Long?): Boolean { val ringingAlarmIds = AlarmService.ringingAlarmIds if (alarmId == null) { diff --git a/android/src/main/kotlin/com/gdelataillade/alarm/generated/FlutterBindings.g.kt b/android/src/main/kotlin/com/gdelataillade/alarm/generated/FlutterBindings.g.kt index 2dc9e283..5138d6df 100644 --- a/android/src/main/kotlin/com/gdelataillade/alarm/generated/FlutterBindings.g.kt +++ b/android/src/main/kotlin/com/gdelataillade/alarm/generated/FlutterBindings.g.kt @@ -251,6 +251,7 @@ private open class FlutterBindingsPigeonCodec : StandardMessageCodec() { interface AlarmApi { fun setAlarm(alarmSettings: AlarmSettingsWire) fun stopAlarm(alarmId: Long) + fun stopAll() fun isRinging(alarmId: Long?): Boolean fun setWarningNotificationOnKill(title: String, body: String) fun disableWarningNotificationOnKill() @@ -300,6 +301,22 @@ interface AlarmApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.alarm.AlarmApi.stopAll$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.stopAll() + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.alarm.AlarmApi.isRinging$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 422f6a5e..38693c7d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,6 +4,8 @@ PODS: - Flutter (1.0.0) - flutter_fgbg (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - permission_handler_apple (9.3.0): - Flutter - shared_preferences_foundation (0.0.1): @@ -16,6 +18,7 @@ DEPENDENCIES: - alarm (from `.symlinks/plugins/alarm/ios`) - Flutter (from `Flutter`) - flutter_fgbg (from `.symlinks/plugins/flutter_fgbg/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -27,6 +30,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_fgbg: :path: ".symlinks/plugins/flutter_fgbg/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" shared_preferences_foundation: @@ -38,6 +43,7 @@ SPEC CHECKSUMS: alarm: 6c1f6a9688f94cd6bf8f104c67cc26e78c9d8d13 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_fgbg: 31c0d1140a131daea2d342121808f6aa0dcd879d + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index 97358cd3..0b0f56cc 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -131,6 +131,16 @@ class _ExampleAlarmHomeScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ExampleAlarmHomeShortcutButton(refreshAlarms: loadAlarms), + const FloatingActionButton( + onPressed: Alarm.stopAll, + backgroundColor: Colors.red, + heroTag: null, + child: Text( + 'STOP ALL', + textScaler: TextScaler.linear(0.9), + textAlign: TextAlign.center, + ), + ), FloatingActionButton( onPressed: () => navigateToAlarmScreen(null), child: const Icon(Icons.alarm_add_rounded, size: 33), diff --git a/example/lib/screens/shortcut_button.dart b/example/lib/screens/shortcut_button.dart index cc017fd1..20435574 100644 --- a/example/lib/screens/shortcut_button.dart +++ b/example/lib/screens/shortcut_button.dart @@ -60,9 +60,13 @@ class _ExampleAlarmHomeShortcutButtonState }, child: FloatingActionButton( onPressed: () => onPressButton(0), - backgroundColor: Colors.red, + backgroundColor: Colors.green[700], heroTag: null, - child: const Text('RING NOW', textAlign: TextAlign.center), + child: const Text( + 'RING NOW', + textScaler: TextScaler.linear(0.9), + textAlign: TextAlign.center, + ), ), ), if (showMenu) diff --git a/ios/Classes/api/AlarmApiImpl.swift b/ios/Classes/api/AlarmApiImpl.swift index 55f9912f..403e278c 100644 --- a/ios/Classes/api/AlarmApiImpl.swift +++ b/ios/Classes/api/AlarmApiImpl.swift @@ -93,6 +93,42 @@ public class AlarmApiImpl: NSObject, AlarmApi { func stopAlarm(alarmId: Int64) throws { self.stopAlarmInternal(id: Int(truncatingIfNeeded: alarmId), cancelNotif: true) } + + func stopAll() throws { + NotificationManager.shared.removeAllNotifications() + + self.mixOtherAudios() + + self.vibratingAlarms.removeAll() + + if let previousVolume = self.previousVolume { + self.setVolume(volume: previousVolume, enable: false) + } + + let alarmIds = self.alarms.keys + + for (_, alarm) in self.alarms { + alarm.timer?.invalidate() + alarm.task?.cancel() + alarm.audioPlayer?.stop() + alarm.volumeEnforcementTimer?.invalidate() + } + self.alarms.removeAll() + + self.stopSilentSound() + self.stopNotificationOnKillService() + + for (id) in alarmIds { + // Inform the Flutter plugin that the alarm was stopped + SwiftAlarmPlugin.alarmTriggerApi?.alarmStopped(alarmId: Int64(id), completion: { result in + if case .success = result { + NSLog("[SwiftAlarmPlugin] Alarm stopped notification for \(id) was processed successfully by Flutter.") + } else { + NSLog("[SwiftAlarmPlugin] Alarm stopped notification for \(id) encountered error in Flutter.") + } + }) + } + } func isRinging(alarmId: Int64?) throws -> Bool { if let alarmId = alarmId { diff --git a/ios/Classes/generated/FlutterBindings.g.swift b/ios/Classes/generated/FlutterBindings.g.swift index f214450c..6cc6eddc 100644 --- a/ios/Classes/generated/FlutterBindings.g.swift +++ b/ios/Classes/generated/FlutterBindings.g.swift @@ -144,7 +144,6 @@ struct VolumeSettingsWire { var volumeEnforced: Bool - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> VolumeSettingsWire? { let volume: Double? = nilOrValue(pigeonVar_list[0]) @@ -175,7 +174,6 @@ struct VolumeFadeStepWire { var volume: Double - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> VolumeFadeStepWire? { let timeMillis = pigeonVar_list[0] as! Int64 @@ -290,6 +288,7 @@ class FlutterBindingsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendab protocol AlarmApi { func setAlarm(alarmSettings: AlarmSettingsWire) throws func stopAlarm(alarmId: Int64) throws + func stopAll() throws func isRinging(alarmId: Int64?) throws -> Bool func setWarningNotificationOnKill(title: String, body: String) throws func disableWarningNotificationOnKill() throws @@ -331,6 +330,19 @@ class AlarmApiSetup { } else { stopAlarmChannel.setMessageHandler(nil) } + let stopAllChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.alarm.AlarmApi.stopAll\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopAllChannel.setMessageHandler { _, reply in + do { + try api.stopAll() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + stopAllChannel.setMessageHandler(nil) + } let isRingingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.alarm.AlarmApi.isRinging\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isRingingChannel.setMessageHandler { message, reply in diff --git a/ios/Classes/services/NotificationManager.swift b/ios/Classes/services/NotificationManager.swift index 54978afe..aa697120 100644 --- a/ios/Classes/services/NotificationManager.swift +++ b/ios/Classes/services/NotificationManager.swift @@ -74,6 +74,11 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { let notificationIdentifier = "alarm-\(id)" UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationIdentifier]) } + + func removeAllNotifications() { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + } func handleAction(withIdentifier identifier: String?, for notification: UNNotification) { guard let identifier = identifier else { return } diff --git a/lib/alarm.dart b/lib/alarm.dart index 031a16a1..540a5558 100644 --- a/lib/alarm.dart +++ b/lib/alarm.dart @@ -152,8 +152,12 @@ class Alarm { static Future stopAll() async { final alarms = await getAlarms(); + iOS ? await IOSAlarm.stopAll() : await AndroidAlarm.stopAll(); + + await AlarmStorage.unsaveAll(); + for (final alarm in alarms) { - await stop(alarm.id); + updateStream.add(alarm.id); } } diff --git a/lib/service/alarm_storage.dart b/lib/service/alarm_storage.dart index 621c9749..11cf1034 100644 --- a/lib/service/alarm_storage.dart +++ b/lib/service/alarm_storage.dart @@ -64,6 +64,18 @@ class AlarmStorage { await _prefs.remove('$prefix$id'); } + /// Removes all alarms from local storage. + static Future unsaveAll() async { + await _waitUntilInitialized(); + + final keys = _prefs.getKeys(); + for (final key in keys) { + if (key.startsWith(prefix)) { + await _prefs.remove(key); + } + } + } + /// Whether at least one alarm is set. static Future hasAlarm() async { await _waitUntilInitialized(); diff --git a/lib/src/android_alarm.dart b/lib/src/android_alarm.dart index 8810ecef..9b521f71 100644 --- a/lib/src/android_alarm.dart +++ b/lib/src/android_alarm.dart @@ -41,6 +41,11 @@ class AndroidAlarm { } } + /// Calls the native `stopAll` function. + static Future stopAll() async { + return _api.stopAll().catchError(AlarmExceptionHandlers.catchError); + } + /// Checks whether an alarm or any alarm (if id is null) is ringing. static Future isRinging([int? id]) async { try { diff --git a/lib/src/generated/platform_bindings.g.dart b/lib/src/generated/platform_bindings.g.dart index 9e4203b2..66bee744 100644 --- a/lib/src/generated/platform_bindings.g.dart +++ b/lib/src/generated/platform_bindings.g.dart @@ -314,6 +314,30 @@ class AlarmApi { } } + Future stopAll() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.alarm.AlarmApi.stopAll$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + Future isRinging({required int? alarmId}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.alarm.AlarmApi.isRinging$pigeonVar_messageChannelSuffix'; diff --git a/lib/src/ios_alarm.dart b/lib/src/ios_alarm.dart index dee230ca..3f7b7e0e 100644 --- a/lib/src/ios_alarm.dart +++ b/lib/src/ios_alarm.dart @@ -31,7 +31,6 @@ class IOSAlarm { return true; } - /// Disposes timer and FGBG subscription /// and calls the native `stopAlarm` function. static Future stopAlarm(int id) async { try { @@ -46,6 +45,11 @@ class IOSAlarm { } } + /// Calls the native `stopAll` function. + static Future stopAll() async { + return _api.stopAll().catchError(AlarmExceptionHandlers.catchError); + } + /// Checks whether an alarm or any alarm (if id is null) is ringing. static Future isRinging([int? id]) async { try { diff --git a/pigeons/alarm_api.dart b/pigeons/alarm_api.dart index 634a8700..38e626b7 100644 --- a/pigeons/alarm_api.dart +++ b/pigeons/alarm_api.dart @@ -103,6 +103,8 @@ abstract class AlarmApi { void stopAlarm({required int alarmId}); + void stopAll(); + bool isRinging({required int? alarmId}); void setWarningNotificationOnKill({