Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Use triggerTime to determine if alarm is ringing #283

Merged
merged 13 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 5.0.1
* Fixes a bug where `isRinging` might return FALSE immediately after alarm starts to ring.
* Handles alarm events on the platform side, increasing efficiency.

## 5.0.0
* **BREAKING**: Old alarms (alarms created pre v5) will be deleted.
* BREAKING: Some API parameters have been renamed, this update requires a small amount of refactoring for users. No features have been removed.
Expand Down
6 changes: 3 additions & 3 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63LD84R3KS;
DEVELOPMENT_TEAM = WQ65PJ26MP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -514,7 +514,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63LD84R3KS;
DEVELOPMENT_TEAM = WQ65PJ26MP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -542,7 +542,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63LD84R3KS;
DEVELOPMENT_TEAM = WQ65PJ26MP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
8 changes: 4 additions & 4 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
version: "6.3.2"
url_launcher_linux:
dependency: transitive
description:
Expand All @@ -408,10 +408,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
Expand Down
4 changes: 0 additions & 4 deletions ios/Classes/AlarmPlugin.h

This file was deleted.

15 changes: 0 additions & 15 deletions ios/Classes/AlarmPlugin.m

This file was deleted.

12 changes: 10 additions & 2 deletions ios/Classes/SwiftAlarmPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
static let backgroundTaskIdentifier: String = "com.gdelataillade.fetch"

private static var api: AlarmApiImpl? = nil

public static func register(with registrar: FlutterPluginRegistrar) {
static var alarmTriggerApi: AlarmTriggerApi? = nil

public static func register(with registrar: any FlutterPluginRegistrar) {
self.api = AlarmApiImpl(registrar: registrar)
AlarmApiSetup.setUp(binaryMessenger: registrar.messenger(), api: self.api)
NSLog("[SwiftAlarmPlugin] AlarmApi initialized.")
self.alarmTriggerApi = AlarmTriggerApi(binaryMessenger: registrar.messenger())
NSLog("[SwiftAlarmPlugin] AlarmTriggerApi initialized.")
}

public func detachFromEngine(for registrar: any FlutterPluginRegistrar) {
SwiftAlarmPlugin.alarmTriggerApi = nil
NSLog("[SwiftAlarmPlugin] AlarmTriggerApi detached.")
}

public func applicationWillTerminate(_ application: UIApplication) {
Expand Down
38 changes: 32 additions & 6 deletions ios/Classes/api/AlarmApiImpl.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AVFoundation
import Flutter
import MediaPlayer

public class AlarmApiImpl: NSObject, AlarmApi {
Expand Down Expand Up @@ -62,6 +63,7 @@ public class AlarmApiImpl: NSObject, AlarmApi {
let currentTime = audioPlayer.deviceCurrentTime
let time = currentTime + delayInSeconds
let dateTime = Date().addingTimeInterval(delayInSeconds)
self.alarms[id]?.triggerTime = dateTime

if alarmSettings.loopAudio {
audioPlayer.numberOfLoops = -1
Expand All @@ -77,7 +79,6 @@ public class AlarmApiImpl: NSObject, AlarmApi {
audioPlayer.play(atTime: time + 0.5)

self.alarms[id]?.audioPlayer = audioPlayer
self.alarms[id]?.triggerTime = dateTime
self.alarms[id]?.task = DispatchWorkItem(block: {
self.handleAlarmAfterDelay(id: id)
})
Expand All @@ -96,9 +97,7 @@ public class AlarmApiImpl: NSObject, AlarmApi {
func isRinging(alarmId: Int64?) throws -> Bool {
if let alarmId = alarmId {
let id = Int(truncatingIfNeeded: alarmId)
let isPlaying = self.alarms[id]?.audioPlayer?.isPlaying ?? false
let currentTime = self.alarms[id]?.audioPlayer?.currentTime ?? 0.0
return isPlaying && currentTime > 0
return self.alarms[id]?.triggerTime?.timeIntervalSinceNow ?? 1.0 <= 0.0
} else {
return self.isAnyAlarmRinging()
}
Expand Down Expand Up @@ -226,15 +225,24 @@ public class AlarmApiImpl: NSObject, AlarmApi {

private func isAnyAlarmRinging() -> Bool {
for (_, alarmConfig) in self.alarms {
if let audioPlayer = alarmConfig.audioPlayer, audioPlayer.isPlaying, audioPlayer.currentTime > 0 {
if alarmConfig.triggerTime?.timeIntervalSinceNow ?? 1.0 <= 0.0 {
return true
}
}
return false
}

private func isAnyAlarmRingingExcept(id: Int) -> Bool {
for (alarmId, alarmConfig) in self.alarms {
if alarmId != id && alarmConfig.triggerTime?.timeIntervalSinceNow ?? 1.0 <= 0.0 {
return true
}
}
return false
}

private func handleAlarmAfterDelay(id: Int) {
if self.isAnyAlarmRinging() {
if self.isAnyAlarmRingingExcept(id: id) {
NSLog("[SwiftAlarmPlugin] Ignoring alarm with id \(id) because another alarm is already ringing.")
self.unsaveAlarm(id: id)
return
Expand All @@ -249,6 +257,15 @@ public class AlarmApiImpl: NSObject, AlarmApi {
if !audioPlayer.isPlaying || audioPlayer.currentTime == 0.0 {
audioPlayer.play()
}

// Inform the Flutter plugin that the alarm rang
SwiftAlarmPlugin.alarmTriggerApi?.alarmRang(alarmId: Int64(id), completion: { result in
if case .success = result {
NSLog("[SwiftAlarmPlugin] Alarm rang notification for \(id) was processed successfully by Flutter.")
} else {
NSLog("[SwiftAlarmPlugin] Alarm rang notification for \(id) encountered error in Flutter.")
}
})

if alarm.settings.vibrate {
self.vibratingAlarms.insert(id)
Expand Down Expand Up @@ -375,6 +392,15 @@ public class AlarmApiImpl: NSObject, AlarmApi {

self.stopSilentSound()
self.stopNotificationOnKillService()

// 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.")
}
})
}

private func stopSilentSound() {
Expand Down
72 changes: 0 additions & 72 deletions lib/src/ios_alarm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ 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';

/// Uses method channel to interact with the native platform.
class IOSAlarm {
static final AlarmApi _api = AlarmApi();

/// Map of alarm timers.
static Map<int, Timer?> timers = {};

/// Map of foreground/background subscriptions.
static Map<int, StreamSubscription<FGBGType>?> fgbgSubscriptions = {};

/// Calls the native function `setAlarm` and listens to alarm ring state.
///
/// Also set periodic timer and listens for app state changes to trigger
Expand All @@ -35,42 +28,12 @@ class IOSAlarm {
rethrow;
}

if (timers[id] != null && timers[id]!.isActive) timers[id]!.cancel();
timers[id] = periodicTimer(
() => Alarm.ringStream.add(settings),
settings.dateTime,
id,
);

listenAppStateChange(
id: id,
onBackground: () => disposeTimer(id),
onForeground: () async {
if (fgbgSubscriptions[id] == null) return;

final alarmIsRinging = await isRinging(id);

if (alarmIsRinging) {
disposeAlarm(id);
Alarm.ringStream.add(settings);
} else {
if (timers[id] != null && timers[id]!.isActive) timers[id]!.cancel();
timers[id] = periodicTimer(
() => Alarm.ringStream.add(settings),
settings.dateTime,
id,
);
}
},
);

return true;
}

/// Disposes timer and FGBG subscription
/// and calls the native `stopAlarm` function.
static Future<bool> stopAlarm(int id) async {
disposeAlarm(id);
try {
await _api
.stopAlarm(alarmId: id)
Expand All @@ -96,44 +59,9 @@ class IOSAlarm {
}
}

/// Listens when app goes foreground so we can check if alarm is ringing.
/// When app goes background, periodical timer will be disposed.
static void listenAppStateChange({
required int id,
required void Function() onForeground,
required void Function() onBackground,
}) {
fgbgSubscriptions[id] = FGBGEvents.instance.stream.listen((event) {
if (event == FGBGType.foreground) onForeground();
if (event == FGBGType.background) onBackground();
});
}

/// Checks periodically if alarm is ringing, as long as app is in foreground.
static Timer periodicTimer(void Function()? onRing, DateTime dt, int id) {
return Timer.periodic(const Duration(milliseconds: 200), (_) {
if (DateTime.now().isBefore(dt)) return;
disposeAlarm(id);
onRing?.call();
});
}

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

/// Disposes alarm timer.
static void disposeTimer(int id) {
timers[id]?.cancel();
timers.removeWhere((key, value) => key == id);
}

/// Disposes alarm timer and FGBG subscription.
static void disposeAlarm(int id) {
disposeTimer(id);
fgbgSubscriptions[id]?.cancel();
fgbgSubscriptions.removeWhere((key, value) => key == id);
}
}
6 changes: 3 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: alarm
description: A simple Flutter alarm manager plugin for both iOS and Android.
version: 4.1.1
version: 5.0.1
homepage: https://github.com/gdelataillade/alarm

environment:
Expand All @@ -21,7 +21,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
json_serializable: ^6.9.0
pigeon: ^22.6.3
pigeon: ^22.7.0
very_good_analysis: ^6.0.0

flutter:
Expand All @@ -35,4 +35,4 @@ flutter:
package: com.gdelataillade.alarm.alarm
pluginClass: AlarmPlugin
ios:
pluginClass: AlarmPlugin
pluginClass: SwiftAlarmPlugin