Skip to content

Commit

Permalink
Handle time zone change when app goes foreground
Browse files Browse the repository at this point in the history
  • Loading branch information
gdelataillade committed Apr 3, 2024
1 parent 734046d commit b06b99e
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class AlarmService : Service() {
}

val assetAudioPath = intent?.getStringExtra("assetAudioPath")
val loopAudio = intent?.getBooleanExtra("loopAudio", true)
val loopAudio = intent?.getBooleanExtra("loopAudio", true) ?: true
val vibrate = intent?.getBooleanExtra("vibrate", true)
val volume = intent?.getDoubleExtra("volume", -1.0) ?: -1.0
val fadeDuration = intent?.getDoubleExtra("fadeDuration", 0.0)
Expand Down Expand Up @@ -141,6 +141,7 @@ class AlarmService : Service() {
audioService?.cleanUp()
vibrationService?.stopVibrating()
volumeService?.restorePreviousVolume(showSystemUI)
volumeService?.abandonAudioFocus()

stopForeground(true)

Expand Down
2 changes: 1 addition & 1 deletion example/lib/screens/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class _ExampleAlarmHomeScreenState extends State<ExampleAlarmHomeScreen> {

void loadAlarms() {
setState(() {
alarms = Alarm.getAlarms();
alarms = Alarm.alarms;
alarms.sort((a, b) => a.dateTime.isBefore(b.dateTime) ? 0 : 1);
});
}
Expand Down
91 changes: 9 additions & 82 deletions ios/Classes/SwiftAlarmPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,97 +379,24 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
}
}
}

checkTimeZoneChange()
}

// If time zone changed, reschedule all alarms
private func checkTimeZoneChange() {
let currentTimeZone = TimeZone.current.identifier

NSLog("SwiftAlarmPlugin: Background check !")
NSLog("SwiftAlarmPlugin: Current time zone: \(currentTimeZone)")

NSLog("SwiftAlarmPlugin: alarms number: \(SwiftAlarmPlugin.alarms.count)")
SwiftAlarmPlugin.alarms.forEach { (id, alarmConfig) in
NSLog("SwiftAlarmPlugin: Alarm id: \(id)")
NSLog("SwiftAlarmPlugin: Alarm time zone: \(alarmConfig.timeZone)")
NSLog("SwiftAlarmPlugin: Current time zone: \(currentTimeZone)")
if alarmConfig.timeZone != currentTimeZone {
// Detected a time zone change, now recalculate the alarm trigger time
let timeDifference = calculateTimeDifference(from: alarmConfig.timeZone, to: currentTimeZone)
// Safely unwrap triggerTime before using it
if let triggerTime = alarmConfig.triggerTime,
let newTriggerTime = adjustAlarmTime(triggerTime, by: timeDifference) {
NSLog("SwiftAlarmPlugin: Time zone change detected for alarm with id \(id)")
NSLog("SwiftAlarmPlugin: Current GMT date time: \(NSDate())")
NSLog("SwiftAlarmPlugin: Seconds from GMT : \(TimeZone.current.secondsFromGMT()) seconds")
NSLog("SwiftAlarmPlugin: Old trigger time : \(triggerTime)")
NSLog("SwiftAlarmPlugin: New trigger time : \(newTriggerTime)")

let delayInSeconds = Int(floor(newTriggerTime.timeIntervalSinceNow)) - TimeZone.current.secondsFromGMT()
NSLog("SwiftAlarmPlugin: New trigger time is \(delayInSeconds) seconds from now, rescheduling alarm")

// If the new trigger time is in the past
if delayInSeconds < 0 {
NSLog("SwiftAlarmPlugin: New trigger time is in the past, triggering alarm immediately")
NotificationManager.shared.triggerNotification(id: String(id), title: alarmConfig.notificationTitle, body: alarmConfig.notificationBody) { error in
if let error = error {
NSLog("[SwiftAlarmPlugin] Error triggering notification: \(error.localizedDescription)")
}
}
SwiftAlarmPlugin.alarms[id]?.timer?.invalidate()
self.handleAlarmAfterDelay(id: id)
guard let alarm = SwiftAlarmPlugin.alarms[id] else { return }
alarm.audioPlayer?.stop()
alarm.audioPlayer?.play()
} else {
NotificationManager.shared.scheduleNotification(id: String(id), delayInSeconds: delayInSeconds, title: alarmConfig.notificationTitle, body: alarmConfig.notificationBody) { error in
if let error = error {
NSLog("[SwiftAlarmPlugin] Error rescheduling notification: \(error.localizedDescription)")
}
}
DispatchQueue.main.async {
guard let alarm = SwiftAlarmPlugin.alarms[id] else { return }

// Stop the old timer
alarm.timer?.invalidate()
alarm.timer = nil

// Start a new timer with the updated delay
alarm.audioPlayer?.stop()

if let deviceCurrentTime = alarm.audioPlayer?.deviceCurrentTime {
let newPlayTime = deviceCurrentTime + TimeInterval(delayInSeconds)
alarm.audioPlayer?.play(atTime: newPlayTime)
}

alarm.timer = Timer.scheduledTimer(timeInterval: TimeInterval(delayInSeconds), target: self, selector: #selector(self.executeTask(_:)), userInfo: id, repeats: false)

NSLog("SwiftAlarmPlugin: Rescheduled alarm with id \(id) to trigger at \(newTriggerTime)")
}
}
let currentTimeZoneIdentifier = TimeZone.current.identifier

// Also warn the Flutter side to update its local storage with the new trigger time
DispatchQueue.main.async {
SwiftAlarmPlugin.channel?.invokeMethod("onAlarmRescheduled", arguments: ["id": id, "newTriggerTime": delayInSeconds])
}
}
SwiftAlarmPlugin.alarms.forEach { id, alarmConfig in
guard let originalTriggerTime = alarmConfig.triggerTime else { return }
let originalTimeZoneIdentifier = alarmConfig.timeZone

if currentTimeZoneIdentifier != originalTimeZoneIdentifier {
// just show notification with custom title and body
// title: hey user, we detected a time zone change
// descr: tap to reopen the app and reschedule automatically your alarms
}
}
}

private func calculateTimeDifference(from oldTimeZoneIdentifier: String, to newTimeZoneIdentifier: String) -> TimeInterval {
let oldTimeZone = TimeZone(identifier: oldTimeZoneIdentifier)!
let newTimeZone = TimeZone(identifier: newTimeZoneIdentifier)!
let difference = TimeInterval(newTimeZone.secondsFromGMT() - oldTimeZone.secondsFromGMT())
return difference
}

private func adjustAlarmTime(_ originalTime: Date, by timeDifference: TimeInterval) -> Date? {
return originalTime.addingTimeInterval(timeDifference)
}

private func stopNotificationOnKillService() {
safeModifyResources {
if SwiftAlarmPlugin.alarms.isEmpty && self.observerAdded {
Expand Down
63 changes: 56 additions & 7 deletions lib/alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:alarm/src/ios_alarm.dart';
import 'package:alarm/utils/alarm_exception.dart';
import 'package:alarm/utils/extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart';

/// Custom print function designed for Alarm plugin.
DebugPrintCallback alarmPrint = debugPrintThrottled;
Expand All @@ -21,9 +22,15 @@ class Alarm {
/// Whether it's Android device.
static bool get android => defaultTargetPlatform == TargetPlatform.android;

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

/// Stream of the ringing status.
static final ringStream = StreamController<AlarmSettings>();

/// Stream of the foreground/background status.
static StreamSubscription<FGBGType>? fgbgSubscription;

/// Initializes Alarm services.
///
/// Also calls [checkAlarm] that will reschedule alarms that were set before
Expand All @@ -39,16 +46,17 @@ class Alarm {
await AlarmStorage.init();

await checkAlarm();
await checkTimeZoneChange();
}

/// Checks if some alarms were set on previous session.
/// If it's the case then reschedules them.
static Future<void> checkAlarm() async {
final alarms = AlarmStorage.getSavedAlarms();
final savedAlarms = alarms;

if (iOS) await stopAll();

for (final alarm in alarms) {
for (final alarm in savedAlarms) {
final now = DateTime.now();
if (alarm.dateTime.isAfter(now)) {
await set(alarmSettings: alarm);
Expand All @@ -66,7 +74,7 @@ class Alarm {
static Future<bool> set({required AlarmSettings alarmSettings}) async {
alarmSettingsValidation(alarmSettings);

for (final alarm in Alarm.getAlarms()) {
for (final alarm in alarms) {
if (alarm.id == alarmSettings.id ||
alarm.dateTime.isSameSecond(alarmSettings.dateTime)) {
await Alarm.stop(alarm.id);
Expand All @@ -90,6 +98,44 @@ class Alarm {
return false;
}

/// Checks if the time zone has changed and reschedules the alarms if needed.
/// If new time zone is in the past, alarm is lost.
static Future<void> checkTimeZoneChange() async {
final now = DateTime.now();
// print(
// 'Current time zone offset (${now.timeZoneName}): ${now.timeZoneOffset.inHours} hours');

final lastTimeZoneOffsetSaved = AlarmStorage.getLastSavedTimeZoneOffset();
if (lastTimeZoneOffsetSaved == null) {
// print('No time zone saved. Saving current time zone...');
await AlarmStorage.saveTimeZone();
return;
}
// print('Last time zone saved: $lastTimeZoneOffsetSaved hours');

final difference = now.timeZoneOffset.inHours - lastTimeZoneOffsetSaved;

if (difference == 0) return;

alarmPrint(
'Time zone changed detected. New time zone is ${now.timeZoneName}.',
);
alarmPrint(
'Difference of ${difference}h. Rescheduling ${alarms.length} alarm(s)...',
);

await AlarmStorage.saveTimeZone();

for (final alarm in alarms) {
final newDateTime = alarm.dateTime.subtract(Duration(hours: difference));
final newAlarm = alarm.copyWith(dateTime: newDateTime);
alarmPrint(
'Rescheduling alarm ${alarm.id} from ${alarm.dateTime} to $newDateTime',
);
await set(alarmSettings: newAlarm);
}
}

/// Validates [alarmSettings] fields.
static void alarmSettingsValidation(AlarmSettings alarmSettings) {
if (alarmSettings.id == 0 || alarmSettings.id == -1) {
Expand Down Expand Up @@ -144,8 +190,6 @@ class Alarm {

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

for (final alarm in alarms) {
await stop(alarm.id);
}
Expand All @@ -161,8 +205,6 @@ class Alarm {

/// Returns alarm by given id. Returns null if not found.
static AlarmSettings? getAlarm(int id) {
final alarms = AlarmStorage.getSavedAlarms();

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

/// Returns all the alarms.
@Deprecated('This method will be removed soon, use `alarms` getter instead.')
static List<AlarmSettings> getAlarms() => AlarmStorage.getSavedAlarms();

/// Disposes the Alarm service when it's no longer needed.
static void dispose() {
fgbgSubscription?.cancel();
ringStream.close();
}
}
9 changes: 9 additions & 0 deletions lib/service/alarm_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,13 @@ class AlarmStorage {
static String getNotificationOnAppKillBody() =>
prefs.getString(notificationOnAppKillBody) ??
'You killed the app. Please reopen so your alarms can be rescheduled.';

/// Saves current time zone offset.
static Future<void> saveTimeZone() => prefs.setInt(
'timeZoneOffset',
DateTime.now().timeZoneOffset.inHours,
);

/// Returns last saved time zone offset.
static int? getLastSavedTimeZoneOffset() => prefs.getInt('timeZoneOffset');
}

0 comments on commit b06b99e

Please sign in to comment.