From 2b686db318ea834dbcb0e4a9fc1625b1fd8a9062 Mon Sep 17 00:00:00 2001 From: Orkun Duman Date: Sat, 23 Nov 2024 20:13:11 -0400 Subject: [PATCH] Error handling --- example/lib/screens/home.dart | 15 +++---- lib/alarm.dart | 64 +++++++++++++++++++---------- lib/service/alarm_storage.dart | 38 ++++------------- lib/src/alarm_trigger_api_impl.dart | 4 +- lib/src/android_alarm.dart | 46 +++++++++++---------- lib/src/ios_alarm.dart | 29 ++++++++----- lib/utils/alarm_exception.dart | 15 +++++-- lib/utils/alarm_handler.dart | 59 ++++++++++++++++++++++++++ pigeons/alarm_api.dart | 1 + 9 files changed, 176 insertions(+), 95 deletions(-) create mode 100644 lib/utils/alarm_handler.dart diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index bfd89bc9..5d28028a 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -31,17 +31,18 @@ class _ExampleAlarmHomeScreenState extends State { if (Alarm.android) { AlarmPermissions.checkAndroidScheduleExactAlarmPermission(); } - loadAlarms(); + unawaited(loadAlarms()); ringSubscription ??= Alarm.ringStream.stream.listen(navigateToRingScreen); updateSubscription ??= Alarm.updateStream.stream.listen((_) { - loadAlarms(); + unawaited(loadAlarms()); }); } - void loadAlarms() { + Future loadAlarms() async { + final updatedAlarms = await Alarm.getAlarms(); + updatedAlarms.sort((a, b) => a.dateTime.isBefore(b.dateTime) ? 0 : 1); setState(() { - alarms = Alarm.getAlarms(); - alarms.sort((a, b) => a.dateTime.isBefore(b.dateTime) ? 0 : 1); + alarms = updatedAlarms; }); } @@ -53,7 +54,7 @@ class _ExampleAlarmHomeScreenState extends State { ExampleAlarmRingScreen(alarmSettings: alarmSettings), ), ); - loadAlarms(); + unawaited(loadAlarms()); } Future navigateToAlarmScreen(AlarmSettings? settings) async { @@ -71,7 +72,7 @@ class _ExampleAlarmHomeScreenState extends State { }, ); - if (res != null && res == true) loadAlarms(); + if (res != null && res == true) unawaited(loadAlarms()); } Future launchReadmeUrl() async { diff --git a/lib/alarm.dart b/lib/alarm.dart index 68e81b6f..f76c6319 100644 --- a/lib/alarm.dart +++ b/lib/alarm.dart @@ -6,6 +6,7 @@ import 'package:alarm/model/alarm_settings.dart'; import 'package:alarm/service/alarm_storage.dart'; import 'package:alarm/src/alarm_trigger_api_impl.dart'; import 'package:alarm/src/android_alarm.dart'; +import 'package:alarm/src/generated/platform_bindings.g.dart'; import 'package:alarm/src/ios_alarm.dart'; import 'package:alarm/utils/alarm_exception.dart'; import 'package:alarm/utils/extensions.dart'; @@ -44,15 +45,13 @@ class Alarm { AlarmTriggerApiImpl.ensureInitialized(); - await AlarmStorage.init(); - await checkAlarm(); } /// Checks if some alarms were set on previous session. /// If it's the case then reschedules them. static Future checkAlarm() async { - final alarms = getAlarms(); + final alarms = await getAlarms(); if (iOS) await stopAll(); @@ -74,7 +73,9 @@ class Alarm { static Future set({required AlarmSettings alarmSettings}) async { alarmSettingsValidation(alarmSettings); - for (final alarm in getAlarms()) { + final alarms = await getAlarms(); + + for (final alarm in alarms) { if (alarm.id == alarmSettings.id || alarm.dateTime.isSameSecond(alarmSettings.dateTime)) { await Alarm.stop(alarm.id); @@ -83,40 +84,54 @@ class Alarm { await AlarmStorage.saveAlarm(alarmSettings); - if (iOS) return IOSAlarm.setAlarm(alarmSettings); - if (android) return AndroidAlarm.set(alarmSettings); + final success = iOS + ? await IOSAlarm.setAlarm(alarmSettings) + : await AndroidAlarm.set(alarmSettings); - updateStream.add(alarmSettings.id); + if (success) { + updateStream.add(alarmSettings.id); + } - return false; + return success; } /// Validates [alarmSettings] fields. static void alarmSettingsValidation(AlarmSettings alarmSettings) { if (alarmSettings.id == 0 || alarmSettings.id == -1) { throw AlarmException( - 'Alarm id cannot be 0 or -1. Provided: ${alarmSettings.id}', + AlarmErrorCode.invalidArguments, + message: 'Alarm id cannot be 0 or -1. Provided: ${alarmSettings.id}', ); } if (alarmSettings.id > 2147483647) { throw AlarmException( - '''Alarm id cannot be set larger than Int max value (2147483647). Provided: ${alarmSettings.id}''', + AlarmErrorCode.invalidArguments, + message: + 'Alarm id cannot be set larger than Int max value (2147483647). ' + 'Provided: ${alarmSettings.id}', ); } if (alarmSettings.id < -2147483648) { throw AlarmException( - '''Alarm id cannot be set smaller than Int min value (-2147483648). Provided: ${alarmSettings.id}''', + AlarmErrorCode.invalidArguments, + message: + 'Alarm id cannot be set smaller than Int min value (-2147483648). ' + 'Provided: ${alarmSettings.id}', ); } if (alarmSettings.volume != null && (alarmSettings.volume! < 0 || alarmSettings.volume! > 1)) { throw AlarmException( - 'Volume must be between 0 and 1. Provided: ${alarmSettings.volume}', + AlarmErrorCode.invalidArguments, + message: 'Volume must be between 0 and 1. ' + 'Provided: ${alarmSettings.volume}', ); } if (alarmSettings.fadeDuration < 0) { throw AlarmException( - '''Fade duration must be positive. Provided: ${alarmSettings.fadeDuration}''', + AlarmErrorCode.invalidArguments, + message: 'Fade duration must be positive. ' + 'Provided: ${alarmSettings.fadeDuration}', ); } } @@ -130,9 +145,12 @@ class Alarm { /// /// [body] default value is `You killed the app. /// Please reopen so your alarm can ring.` - static void setWarningNotificationOnKill(String title, String body) { - if (iOS) IOSAlarm.setWarningNotificationOnKill(title, body); - if (android) AndroidAlarm.setWarningNotificationOnKill(title, body); + static Future setWarningNotificationOnKill( + String title, + String body, + ) async { + if (iOS) await IOSAlarm.setWarningNotificationOnKill(title, body); + if (android) await AndroidAlarm.setWarningNotificationOnKill(title, body); } /// Stops alarm. @@ -145,7 +163,7 @@ class Alarm { /// Stops all the alarms. static Future stopAll() async { - final alarms = getAlarms(); + final alarms = await getAlarms(); for (final alarm in alarms) { await stop(alarm.id); @@ -161,11 +179,11 @@ class Alarm { iOS ? await IOSAlarm.isRinging(id) : await AndroidAlarm.isRinging(id); /// Whether an alarm is set. - static bool hasAlarm() => AlarmStorage.hasAlarm(); + static Future hasAlarm() => AlarmStorage.hasAlarm(); /// Returns alarm by given id. Returns null if not found. - static AlarmSettings? getAlarm(int id) { - final alarms = getAlarms(); + static Future getAlarm(int id) async { + final alarms = await getAlarms(); for (final alarm in alarms) { if (alarm.id == id) return alarm; @@ -176,12 +194,14 @@ class Alarm { } /// Returns all the alarms. - static List getAlarms() => AlarmStorage.getSavedAlarms(); + static Future> 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(); + // TODO(orkun1675): Remove this function and publish stream updates for + // alarm start/stop events. updateStream.add(id); } } diff --git a/lib/service/alarm_storage.dart b/lib/service/alarm_storage.dart index 2d9a6e0a..3b744ec3 100644 --- a/lib/service/alarm_storage.dart +++ b/lib/service/alarm_storage.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:alarm/model/alarm_settings.dart'; -import 'package:flutter_fgbg/flutter_fgbg.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Class that handles the local storage of the alarm info. @@ -22,35 +21,21 @@ class AlarmStorage { /// notification on app kill body. static const notificationOnAppKillBody = 'notificationOnAppKillBody'; - /// Stream subscription to listen to foreground/background events. - static late StreamSubscription fgbgSubscription; - - /// Shared preferences instance. - static late SharedPreferences prefs; - - /// Initializes shared preferences instance. - static Future init() async { - prefs = await SharedPreferences.getInstance(); - - /// Reloads the shared preferences instance in the case modifications - /// were made in the native code, after a notification action. - fgbgSubscription = - FGBGEvents.instance.stream.listen((event) => prefs.reload()); - } - /// Saves alarm info in local storage so we can restore it later /// in the case app is terminated. - static Future saveAlarm(AlarmSettings alarmSettings) => prefs.setString( + static Future saveAlarm(AlarmSettings alarmSettings) => + SharedPreferencesAsync().setString( '$prefix${alarmSettings.id}', json.encode(alarmSettings.toJson()), ); /// Removes alarm from local storage. - static Future unsaveAlarm(int id) => prefs.remove('$prefix$id'); + static Future unsaveAlarm(int id) => + SharedPreferencesAsync().remove('$prefix$id'); /// Whether at least one alarm is set. - static bool hasAlarm() { - final keys = prefs.getKeys(); + static Future hasAlarm() async { + final keys = await SharedPreferencesAsync().getKeys(); for (final key in keys) { if (key.startsWith(prefix)) return true; @@ -61,13 +46,13 @@ class AlarmStorage { /// Returns all alarms info from local storage in the case app is terminated /// and we need to restore previously scheduled alarms. - static List getSavedAlarms() { + static Future> getSavedAlarms() async { final alarms = []; - final keys = prefs.getKeys(); + final keys = await SharedPreferencesAsync().getKeys(); for (final key in keys) { if (key.startsWith(prefix)) { - final res = prefs.getString(key); + final res = await SharedPreferencesAsync().getString(key); alarms.add( AlarmSettings.fromJson(json.decode(res!) as Map), ); @@ -76,9 +61,4 @@ class AlarmStorage { return alarms; } - - /// Dispose the fgbg subscription to avoid memory leaks. - static void dispose() { - fgbgSubscription.cancel(); - } } diff --git a/lib/src/alarm_trigger_api_impl.dart b/lib/src/alarm_trigger_api_impl.dart index 02a6a182..8f3575f7 100644 --- a/lib/src/alarm_trigger_api_impl.dart +++ b/lib/src/alarm_trigger_api_impl.dart @@ -17,8 +17,8 @@ class AlarmTriggerApiImpl extends AlarmTriggerApi { } @override - void alarmRang(int alarmId) { - final settings = Alarm.getAlarm(alarmId); + Future alarmRang(int alarmId) async { + final settings = await Alarm.getAlarm(alarmId); if (settings == null) { return; } diff --git a/lib/src/android_alarm.dart b/lib/src/android_alarm.dart index 25473cdf..8810ecef 100644 --- a/lib/src/android_alarm.dart +++ b/lib/src/android_alarm.dart @@ -3,21 +3,21 @@ import 'dart:async'; import 'package:alarm/alarm.dart'; import 'package:alarm/src/generated/platform_bindings.g.dart'; import 'package:alarm/utils/alarm_exception.dart'; +import 'package:alarm/utils/alarm_handler.dart'; /// Uses method channel to interact with the native platform. class AndroidAlarm { static final AlarmApi _api = AlarmApi(); /// Whether there are other alarms set. - static bool get hasOtherAlarms => Alarm.getAlarms().length > 1; + static Future get hasOtherAlarms => + Alarm.getAlarms().then((alarms) => alarms.length > 1); /// Schedules a native alarm with given [settings] with its notification. static Future set(AlarmSettings settings) async { - try { - await _api.setAlarm(alarmSettings: settings.toWire()); - } catch (e) { - throw AlarmException('AndroidAlarm.setAlarm error: $e'); - } + await _api + .setAlarm(alarmSettings: settings.toWire()) + .catchError(AlarmExceptionHandlers.catchError); alarmPrint( '''Alarm with id ${settings.id} scheduled at ${settings.dateTime}''', @@ -30,10 +30,12 @@ class AndroidAlarm { /// can stop playing and dispose. static Future stop(int id) async { try { - await _api.stopAlarm(alarmId: id); - if (!hasOtherAlarms) await disableWarningNotificationOnKill(); + await _api + .stopAlarm(alarmId: id) + .catchError(AlarmExceptionHandlers.catchError); + if (!(await hasOtherAlarms)) await disableWarningNotificationOnKill(); return true; - } catch (e) { + } on AlarmException catch (e) { alarmPrint('Failed to stop alarm: $e'); return false; } @@ -42,9 +44,11 @@ class AndroidAlarm { /// Checks whether an alarm or any alarm (if id is null) is ringing. static Future isRinging([int? id]) async { try { - final res = await _api.isRinging(alarmId: id); + final res = await _api + .isRinging(alarmId: id) + .catchError(AlarmExceptionHandlers.catchError); return res; - } catch (e) { + } on AlarmException catch (e) { alarmPrint('Failed to check if alarm is ringing: $e'); return false; } @@ -52,17 +56,15 @@ class AndroidAlarm { /// Sets the native notification on app kill title and body. static Future setWarningNotificationOnKill(String title, String body) => - _api.setWarningNotificationOnKill( - title: title, - body: body, - ); + _api + .setWarningNotificationOnKill( + title: title, + body: body, + ) + .catchError(AlarmExceptionHandlers.catchError); /// Disable the notification on kill service. - static Future disableWarningNotificationOnKill() async { - try { - await _api.disableWarningNotificationOnKill(); - } catch (e) { - throw AlarmException('NotificationOnKillService error: $e'); - } - } + static Future disableWarningNotificationOnKill() => _api + .disableWarningNotificationOnKill() + .catchError(AlarmExceptionHandlers.catchError); } diff --git a/lib/src/ios_alarm.dart b/lib/src/ios_alarm.dart index b4553ed2..b3a26f4e 100644 --- a/lib/src/ios_alarm.dart +++ b/lib/src/ios_alarm.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:alarm/alarm.dart'; import 'package:alarm/src/generated/platform_bindings.g.dart'; import 'package:alarm/utils/alarm_exception.dart'; +import 'package:alarm/utils/alarm_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; @@ -23,13 +24,15 @@ class IOSAlarm { static Future setAlarm(AlarmSettings settings) async { final id = settings.id; try { - await _api.setAlarm(alarmSettings: settings.toWire()); + await _api + .setAlarm(alarmSettings: settings.toWire()) + .catchError(AlarmExceptionHandlers.catchError); alarmPrint( 'Alarm with id $id scheduled successfully at ${settings.dateTime}', ); - } catch (e) { + } on AlarmException catch (_) { await Alarm.stop(id); - throw AlarmException(e.toString()); + rethrow; } if (timers[id] != null && timers[id]!.isActive) timers[id]!.cancel(); @@ -69,21 +72,25 @@ class IOSAlarm { static Future stopAlarm(int id) async { disposeAlarm(id); try { - await _api.stopAlarm(alarmId: id); + await _api + .stopAlarm(alarmId: id) + .catchError(AlarmExceptionHandlers.catchError); alarmPrint('Alarm with id $id stopped'); return true; - } catch (e) { - alarmPrint('Failed to stop alarm $id.'); + } on AlarmException catch (e) { + alarmPrint('Failed to stop alarm $id. $e'); + return false; } - return false; } /// Checks whether an alarm or any alarm (if id is null) is ringing. static Future isRinging([int? id]) async { try { - final res = await _api.isRinging(alarmId: id); + final res = await _api + .isRinging(alarmId: id) + .catchError(AlarmExceptionHandlers.catchError); return res; - } catch (e) { + } on AlarmException catch (e) { debugPrint('Error checking if alarm is ringing: $e'); return false; } @@ -113,7 +120,9 @@ class IOSAlarm { /// Sets the native notification on app kill title and body. static Future setWarningNotificationOnKill(String title, String body) => - _api.setWarningNotificationOnKill(title: title, body: body); + _api + .setWarningNotificationOnKill(title: title, body: body) + .catchError(AlarmExceptionHandlers.catchError); /// Disposes alarm timer. static void disposeTimer(int id) { diff --git a/lib/utils/alarm_exception.dart b/lib/utils/alarm_exception.dart index 12d919d6..63a9992a 100644 --- a/lib/utils/alarm_exception.dart +++ b/lib/utils/alarm_exception.dart @@ -1,11 +1,20 @@ +import 'package:alarm/src/generated/platform_bindings.g.dart'; + /// Custom exception for the alarm. class AlarmException implements Exception { /// Creates an [AlarmException] with the given error [message]. - const AlarmException(this.message); + const AlarmException(this.code, {this.message, this.stacktrace}); + + /// The type/category of error. + final AlarmErrorCode code; /// Exception message. - final String message; + final String? message; + + /// The Stacktrace when the exception occured. + final String? stacktrace; @override - String toString() => message; + String toString() => + '${code.name}: $message${stacktrace != null ? '\n$stacktrace' : ''}'; } diff --git a/lib/utils/alarm_handler.dart b/lib/utils/alarm_handler.dart new file mode 100644 index 00000000..c3e95664 --- /dev/null +++ b/lib/utils/alarm_handler.dart @@ -0,0 +1,59 @@ +import 'package:alarm/src/generated/platform_bindings.g.dart'; +import 'package:alarm/utils/alarm_exception.dart'; +import 'package:flutter/services.dart'; + +/// Handlers for parsing runtime exceptions as an AlarmException. +extension AlarmExceptionHandlers on AlarmException { + /// Wraps a PlatformException within an AlarmException. + static AlarmException fromPlatformException(PlatformException ex) { + return AlarmException( + ex.code == 'channel-error' + ? AlarmErrorCode.channelError + : AlarmErrorCode.values.firstWhere( + (e) => e.index == (int.tryParse(ex.code) ?? 0), + orElse: () => AlarmErrorCode.unknown, + ), + message: ex.message, + stacktrace: ex.stacktrace, + ); + } + + /// Wraps a Exception within an AlarmException. + static AlarmException fromException( + Exception ex, [ + StackTrace? stacktrace, + ]) { + return AlarmException( + AlarmErrorCode.unknown, + message: ex.toString(), + stacktrace: stacktrace?.toString() ?? StackTrace.current.toString(), + ); + } + + /// Wraps a dynamic error within an AlarmException. + static AlarmException fromError( + dynamic error, [ + StackTrace? stacktrace, + ]) { + if (error is AlarmException) { + return error; + } + if (error is PlatformException) { + return fromPlatformException(error); + } + if (error is Exception) { + return fromException(error, stacktrace); + } + return AlarmException( + AlarmErrorCode.unknown, + message: error.toString(), + stacktrace: stacktrace?.toString() ?? StackTrace.current.toString(), + ); + } + + /// Utility method that can be used for wrapping errors thrown by Futures + /// in an AlarmException. + static T catchError(dynamic error, StackTrace stacktrace) { + throw fromError(error, stacktrace); + } +} diff --git a/pigeons/alarm_api.dart b/pigeons/alarm_api.dart index 85e32aa9..1504571b 100644 --- a/pigeons/alarm_api.dart +++ b/pigeons/alarm_api.dart @@ -204,6 +204,7 @@ abstract class AlarmApi { // become available. See: https://github.com/flutter/flutter/issues/66711 @FlutterApi() abstract class AlarmTriggerApi { + @async void alarmRang(int alarmId); @async