Skip to content

Commit

Permalink
[iOS] Call AlarmTriggerApi for alarm events and use triggerTime when …
Browse files Browse the repository at this point in the history
…evaluating isRinging
  • Loading branch information
orkun1675 committed Dec 8, 2024
1 parent 907b809 commit 7caa021
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 109 deletions.
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

0 comments on commit 7caa021

Please sign in to comment.