Skip to content

Commit

Permalink
Error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
orkun1675 committed Nov 24, 2024
1 parent ffac2d3 commit 2b686db
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 95 deletions.
15 changes: 8 additions & 7 deletions example/lib/screens/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@ class _ExampleAlarmHomeScreenState extends State<ExampleAlarmHomeScreen> {
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<void> 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;
});
}

Expand All @@ -53,7 +54,7 @@ class _ExampleAlarmHomeScreenState extends State<ExampleAlarmHomeScreen> {
ExampleAlarmRingScreen(alarmSettings: alarmSettings),
),
);
loadAlarms();
unawaited(loadAlarms());
}

Future<void> navigateToAlarmScreen(AlarmSettings? settings) async {
Expand All @@ -71,7 +72,7 @@ class _ExampleAlarmHomeScreenState extends State<ExampleAlarmHomeScreen> {
},
);

if (res != null && res == true) loadAlarms();
if (res != null && res == true) unawaited(loadAlarms());
}

Future<void> launchReadmeUrl() async {
Expand Down
64 changes: 42 additions & 22 deletions lib/alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void> checkAlarm() async {
final alarms = getAlarms();
final alarms = await getAlarms();

if (iOS) await stopAll();

Expand All @@ -74,7 +73,9 @@ class Alarm {
static Future<bool> 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);
Expand All @@ -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}',
);
}
}
Expand All @@ -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<void> setWarningNotificationOnKill(
String title,
String body,
) async {
if (iOS) await IOSAlarm.setWarningNotificationOnKill(title, body);
if (android) await AndroidAlarm.setWarningNotificationOnKill(title, body);
}

/// Stops alarm.
Expand All @@ -145,7 +163,7 @@ class Alarm {

/// Stops all the alarms.
static Future<void> stopAll() async {
final alarms = getAlarms();
final alarms = await getAlarms();

for (final alarm in alarms) {
await stop(alarm.id);
Expand All @@ -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<bool> hasAlarm() => AlarmStorage.hasAlarm();

/// Returns alarm by given id. Returns null if not found.
static AlarmSettings? getAlarm(int id) {
final alarms = getAlarms();
static Future<AlarmSettings?> getAlarm(int id) async {
final alarms = await getAlarms();

for (final alarm in alarms) {
if (alarm.id == id) return alarm;
Expand All @@ -176,12 +194,14 @@ class Alarm {
}

/// Returns all the alarms.
static List<AlarmSettings> getAlarms() => AlarmStorage.getSavedAlarms();
static Future<List<AlarmSettings>> getAlarms() =>
AlarmStorage.getSavedAlarms();

/// Reloads the shared preferences instance in the case modifications
/// were made in the native code, after a notification action.
static Future<void> 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);
}
}
38 changes: 9 additions & 29 deletions lib/service/alarm_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<FGBGType> fgbgSubscription;

/// Shared preferences instance.
static late SharedPreferences prefs;

/// Initializes shared preferences instance.
static Future<void> 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<void> saveAlarm(AlarmSettings alarmSettings) => prefs.setString(
static Future<void> saveAlarm(AlarmSettings alarmSettings) =>
SharedPreferencesAsync().setString(
'$prefix${alarmSettings.id}',
json.encode(alarmSettings.toJson()),
);

/// Removes alarm from local storage.
static Future<void> unsaveAlarm(int id) => prefs.remove('$prefix$id');
static Future<void> unsaveAlarm(int id) =>
SharedPreferencesAsync().remove('$prefix$id');

/// Whether at least one alarm is set.
static bool hasAlarm() {
final keys = prefs.getKeys();
static Future<bool> hasAlarm() async {
final keys = await SharedPreferencesAsync().getKeys();

for (final key in keys) {
if (key.startsWith(prefix)) return true;
Expand All @@ -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<AlarmSettings> getSavedAlarms() {
static Future<List<AlarmSettings>> getSavedAlarms() async {
final alarms = <AlarmSettings>[];
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<String, dynamic>),
);
Expand All @@ -76,9 +61,4 @@ class AlarmStorage {

return alarms;
}

/// Dispose the fgbg subscription to avoid memory leaks.
static void dispose() {
fgbgSubscription.cancel();
}
}
4 changes: 2 additions & 2 deletions lib/src/alarm_trigger_api_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class AlarmTriggerApiImpl extends AlarmTriggerApi {
}

@override
void alarmRang(int alarmId) {
final settings = Alarm.getAlarm(alarmId);
Future<void> alarmRang(int alarmId) async {
final settings = await Alarm.getAlarm(alarmId);
if (settings == null) {
return;
}
Expand Down
46 changes: 24 additions & 22 deletions lib/src/android_alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> get hasOtherAlarms =>
Alarm.getAlarms().then((alarms) => alarms.length > 1);

/// Schedules a native alarm with given [settings] with its notification.
static Future<bool> 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<void>);

alarmPrint(
'''Alarm with id ${settings.id} scheduled at ${settings.dateTime}''',
Expand All @@ -30,10 +30,12 @@ class AndroidAlarm {
/// can stop playing and dispose.
static Future<bool> stop(int id) async {
try {
await _api.stopAlarm(alarmId: id);
if (!hasOtherAlarms) await disableWarningNotificationOnKill();
await _api
.stopAlarm(alarmId: id)
.catchError(AlarmExceptionHandlers.catchError<void>);
if (!(await hasOtherAlarms)) await disableWarningNotificationOnKill();
return true;
} catch (e) {
} on AlarmException catch (e) {
alarmPrint('Failed to stop alarm: $e');
return false;
}
Expand All @@ -42,27 +44,27 @@ class AndroidAlarm {
/// Checks whether an alarm or any alarm (if id is null) is ringing.
static Future<bool> isRinging([int? id]) async {
try {
final res = await _api.isRinging(alarmId: id);
final res = await _api
.isRinging(alarmId: id)
.catchError(AlarmExceptionHandlers.catchError<bool>);
return res;
} catch (e) {
} on AlarmException catch (e) {
alarmPrint('Failed to check if alarm is ringing: $e');
return false;
}
}

/// Sets the native notification on app kill title and body.
static Future<void> setWarningNotificationOnKill(String title, String body) =>
_api.setWarningNotificationOnKill(
title: title,
body: body,
);
_api
.setWarningNotificationOnKill(
title: title,
body: body,
)
.catchError(AlarmExceptionHandlers.catchError<void>);

/// Disable the notification on kill service.
static Future<void> disableWarningNotificationOnKill() async {
try {
await _api.disableWarningNotificationOnKill();
} catch (e) {
throw AlarmException('NotificationOnKillService error: $e');
}
}
static Future<void> disableWarningNotificationOnKill() => _api
.disableWarningNotificationOnKill()
.catchError(AlarmExceptionHandlers.catchError<void>);
}
Loading

0 comments on commit 2b686db

Please sign in to comment.