diff --git a/README.md b/README.md index 28fbdc1f..474e0ed8 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This Flutter plugin provides a simple and easy-to-use interface for setting and [See screenshot 1](https://github.com/gdelataillade/alarm/assets/32983806/2ad67186-1f22-462c-b953-fe55a35fea09) -2) Still in Xcode, open your Info.plist and add the key `Permitted background task scheduler identifiers`, with the item `com.transistorsoft.fetch` inside. +2) Still in Xcode, open your Info.plist and add the key `Permitted background task scheduler identifiers`, with the item `com.gdelataillade.fetch` inside. [See screenshot 2](https://github.com/gdelataillade/alarm/assets/32983806/a7ab90be-4518-48a7-a21d-d2844b4b036c) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 44a9f601..14de2046 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,8 +3,6 @@ PODS: - Flutter - audio_session (0.0.1): - Flutter - - background_fetch (1.2.1): - - Flutter - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) @@ -28,7 +26,6 @@ PODS: DEPENDENCIES: - alarm (from `.symlinks/plugins/alarm/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_fgbg (from `.symlinks/plugins/flutter_fgbg/ios`) @@ -44,8 +41,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/alarm/ios" audio_session: :path: ".symlinks/plugins/audio_session/ios" - background_fetch: - :path: ".symlinks/plugins/background_fetch/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -68,7 +63,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: alarm: 6c1f6a9688f94cd6bf8f104c67cc26e78c9d8d13 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 - background_fetch: 896944864b038d2837fc750d470e9841e1e6a363 device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_fgbg: 31c0d1140a131daea2d342121808f6aa0dcd879d diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 6a8eb3e2..15ababe4 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Flutter import UserNotifications +import alarm @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -11,6 +12,7 @@ import UserNotifications if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } + SwiftAlarmPlugin.registerBackgroundTasks() GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 9e4279ec..c1b3145f 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -4,7 +4,7 @@ BGTaskSchedulerPermittedIdentifiers - com.transistorsoft.fetch + com.gdelataillade.fetch CADisableMinimumFrameDurationOnPhone diff --git a/example/lib/screens/home.dart b/example/lib/screens/home.dart index bb0ca7b5..416dc1fa 100644 --- a/example/lib/screens/home.dart +++ b/example/lib/screens/home.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:alarm/alarm.dart'; -import 'package:alarm/service/background_fetch.dart'; import 'package:alarm_example/screens/edit_alarm.dart'; import 'package:alarm_example/screens/ring.dart'; import 'package:alarm_example/screens/shortcut_button.dart'; @@ -72,24 +71,7 @@ class _ExampleAlarmHomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('alarm 2.0.0-dev.4'), - actions: [ - IconButton( - onPressed: () { - final checksDt = AlarmBackgroundFetch.fetches.map((e) { - return '${e.year}-${e.month}-${e.day} ${e.hour}h${e.minute}'; - }); - final content = - "Background App Checks done this session:\n$checksDt"; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(content)), - ); - }, - icon: const Icon(Icons.info), - ), - ], - ), + appBar: AppBar(title: const Text('alarm 2.0.0-dev.4')), body: SafeArea( child: alarms.isNotEmpty ? ListView.separated( diff --git a/example/pubspec.lock b/example/pubspec.lock index 7333847a..e7dadc11 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -40,14 +40,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.16" - background_fetch: - dependency: transitive - description: - name: background_fetch - sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4 - url: "https://pub.dev" - source: hosted - version: "1.2.1" boolean_selector: dependency: transitive description: diff --git a/help/INSTALL-ANDROID.md b/help/INSTALL-ANDROID.md new file mode 100644 index 00000000..e1f85953 --- /dev/null +++ b/help/INSTALL-ANDROID.md @@ -0,0 +1 @@ +# Android Setup \ No newline at end of file diff --git a/help/INSTALL-IOS.md b/help/INSTALL-IOS.md new file mode 100644 index 00000000..4ba6a067 --- /dev/null +++ b/help/INSTALL-IOS.md @@ -0,0 +1 @@ +# iOS Setup \ No newline at end of file diff --git a/ios/Classes/SwiftAlarmPlugin.swift b/ios/Classes/SwiftAlarmPlugin.swift index e436c656..9a157ca1 100644 --- a/ios/Classes/SwiftAlarmPlugin.swift +++ b/ios/Classes/SwiftAlarmPlugin.swift @@ -3,6 +3,7 @@ import UIKit import AVFoundation import AudioToolbox import MediaPlayer +import BackgroundTasks public class SwiftAlarmPlugin: NSObject, FlutterPlugin { #if targetEnvironment(simulator) @@ -12,6 +13,8 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { #endif private var registrar: FlutterPluginRegistrar! + static let sharedInstance = SwiftAlarmPlugin() + static let backgroundTaskIdentifier: String = "com.gdelataillade.fetch" public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "com.gdelataillade/alarm", binaryMessenger: registrar.messenger()) @@ -50,10 +53,6 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { let args = call.arguments as! Dictionary let id = args["id"] as! Int self.audioCurrentTime(id: id, result: result) - } else if call.method == "backgroundCheck" { - let args = call.arguments as! Dictionary - let ids = args["ids"] as! [Int] - self.backgroundCheck(ids: ids, result: result) } else { DispatchQueue.main.sync { result(FlutterMethodNotImplemented) @@ -149,6 +148,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { DispatchQueue.main.async { self.timers[id] = Timer.scheduledTimer(timeInterval: delayInSeconds, target: self, selector: #selector(self.executeTask(_:)), userInfo: id, repeats: false) + SwiftAlarmPlugin.scheduleAppRefresh() } result(true) @@ -280,6 +280,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { self.playSilent = false self.silentAudioPlayer?.stop() NotificationCenter.default.removeObserver(self) + SwiftAlarmPlugin.cancelBackgroundTasks() } } @@ -316,14 +317,17 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { } } - private func backgroundCheck(ids: [Int], result: FlutterResult) { - self.mixOtherAudios() + // Runs when a background fetch event is triggered + private func backgroundFetch() { + NSLog("SwiftAlarmPlugin: -> Background fetch !") - let isPlaying = self.silentAudioPlayer?.isPlaying ?? false + self.mixOtherAudios() self.silentAudioPlayer?.pause() self.silentAudioPlayer?.play() + let ids = Array(self.audioPlayers.keys) + for id in ids { NSLog("SwiftAlarmPlugin: Background check alarm with id \(id)") if let audioPlayer = self.audioPlayers[id] { @@ -339,8 +343,6 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { self.timers[id] = Timer.scheduledTimer(timeInterval: delayInSeconds, target: self, selector: #selector(self.executeTask(_:)), userInfo: id, repeats: false) } } - - result(isPlaying) } private func stopNotificationOnKillService() { @@ -349,6 +351,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { observerAdded = false } } + @objc func applicationWillTerminate(_ notification: Notification) { let content = UNMutableNotificationContent() content.title = notificationTitleOnKill @@ -382,4 +385,48 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin { NSLog("SwiftAlarmPlugin: Error setting up audio session with option duckOthers: \(error.localizedDescription)") } } -} + + // Runs from AppDelegate when the app is launched + static public func registerBackgroundTasks() { + NSLog("SwiftAlarmPlugin: -> registerBackgroundTasks !") + + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in + self.scheduleAppRefresh() + sharedInstance.backgroundFetch() + task.setTaskCompleted(success: true) + } + } else { + NSLog("SwiftAlarmPlugin: BGTaskScheduler not available for your version of iOS lower than 13.0") + } + } + + // Enables background fetch + static func scheduleAppRefresh() { + NSLog("SwiftAlarmPlugin: -> scheduleAppRefresh !") + + if #available(iOS 13.0, *) { + let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier) + + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) + do { + try BGTaskScheduler.shared.submit(request) + } catch { + NSLog("SwiftAlarmPlugin: Could not schedule app refresh: \(error)") + } + } else { + NSLog("SwiftAlarmPlugin: BGTaskScheduler not available for your version of iOS lower than 13.0") + } + } + + // Disable background fetch + static func cancelBackgroundTasks() { + NSLog("SwiftAlarmPlugin: -> cancelBackgroundTasks !") + + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundTaskIdentifier) + } else { + NSLog("SwiftAlarmPlugin: BGTaskScheduler not available for your version of iOS lower than 13.0") + } + } +} \ No newline at end of file diff --git a/lib/alarm.dart b/lib/alarm.dart index f8a2923c..52f02112 100644 --- a/lib/alarm.dart +++ b/lib/alarm.dart @@ -4,7 +4,6 @@ export 'package:alarm/model/alarm_settings.dart'; import 'dart:async'; import 'package:alarm/model/alarm_settings.dart'; -import 'package:alarm/service/background_fetch.dart'; import 'package:alarm/src/ios_alarm.dart'; import 'package:alarm/src/android_alarm.dart'; import 'package:alarm/service/notification.dart'; @@ -103,8 +102,6 @@ class Alarm { } if (iOS) { - AlarmBackgroundFetch.set(); - return IOSAlarm.setAlarm( alarmSettings.id, alarmSettings.dateTime, @@ -153,8 +150,6 @@ class Alarm { AlarmNotification.instance.cancel(id); - if (iOS && !hasAlarm()) AlarmBackgroundFetch.stop(); - return iOS ? await IOSAlarm.stopAlarm(id) : await AndroidAlarm.stop(id); } diff --git a/lib/service/background_fetch.dart b/lib/service/background_fetch.dart deleted file mode 100644 index a000aa41..00000000 --- a/lib/service/background_fetch.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:async'; - -import 'package:alarm/alarm.dart'; -import 'package:alarm/service/notification.dart'; -import 'package:alarm/src/ios_alarm.dart'; -import 'package:background_fetch/background_fetch.dart'; - -/// The purpose of this class is to trigger a background fetch event -/// every 15 minutes to reschedule all the alarms to make sure the alarms -/// are still active. -class AlarmBackgroundFetch { - static List fetches = []; - static bool isActive = false; - - static BackgroundFetchConfig config = BackgroundFetchConfig( - minimumFetchInterval: 15, - stopOnTerminate: false, - enableHeadless: true, - startOnBoot: true, - requiresBatteryNotLow: false, - requiresCharging: false, - requiresStorageNotLow: false, - requiresDeviceIdle: false, - requiredNetworkType: NetworkType.NONE, - ); - - static Future set() async { - if (isActive) return; - - final res = await BackgroundFetch.configure(config, callback); - - if (res == BackgroundFetch.STATUS_AVAILABLE) { - isActive = true; - alarmPrint("Background fetch activated with minimum interval: 15min."); - } else { - alarmPrint("Background fetch not available. Probably disabled by user."); - } - } - - static Future stop() async { - if (!isActive) return; - isActive = false; - - await BackgroundFetch.stop(); - - alarmPrint( - "Background fetch stopped. ${fetches.length} fetches during this session.", - ); - } - - static Future callback(String taskId) async { - alarmPrint("Background fetch event received. Starting background check..."); - - try { - final res = await IOSAlarm.backgroundCheck(); - - final now = DateTime.now(); - AlarmNotification.instance.scheduleAlarmNotif( - id: now.millisecond + now.second, - dateTime: now.add(const Duration(seconds: 3)), - title: "Alarm background check", - body: - "${now.hour}h${now.minute}: Silent audio player was ${res ? '' : 'not '}playing", - ); - - fetches.add(DateTime.now()); - - alarmPrint("Finishing task with success..."); - } catch (e) { - alarmPrint("Error during background check: $e"); - } - - BackgroundFetch.finish(taskId); - } -} diff --git a/lib/src/ios_alarm.dart b/lib/src/ios_alarm.dart index 5e9c8eac..cf8a1967 100644 --- a/lib/src/ios_alarm.dart +++ b/lib/src/ios_alarm.dart @@ -114,20 +114,6 @@ class IOSAlarm { return pos2 > pos1; } - /// Checks if alarm audio players are still active during the background check. - static Future backgroundCheck() async { - final alarms = Alarm.getAlarms(); - if (alarms.isEmpty) return true; - - final alarmIds = alarms.map((e) => e.id).toList(); - - return await methodChannel.invokeMethod( - 'backgroundCheck', - {'ids': alarmIds}, - ) ?? - false; - } - /// 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({ diff --git a/pubspec.yaml b/pubspec.yaml index 3c84b802..a980c35a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,6 @@ environment: dependencies: android_alarm_manager_plus: ^3.0.1 - background_fetch: ^1.2.1 flutter: sdk: flutter flutter_fgbg: ^0.3.0