diff --git a/README.md b/README.md index 7a050b31..9e296056 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ void main() { callbackDispatcher, // The top level function, aka callbackDispatcher isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks ); - Workmanager().registerOneOffTask("task-identifier", "simpleTask"); + Workmanager().registerOneOffTask("task-identifier", "simpleTask"); // Android and 'iOS Processing task' (see options below) + if (Platform.isIOS) { + Workmanager().registerAppRefreshTask(initialDelay: const Duration(minutes: 15), frequency: const Duration(minutes: 60)); // iOS Only + } runApp(MyApp()); } ``` @@ -90,11 +93,12 @@ Refer to the example app for a successful, retrying and a failed task. # iOS specific setup and note -iOS supports **One off tasks** with a few basic constraints: +iOS supports **One off tasks** with a few basic constraints +(Note on iOS this is for long-running Processing tasks run every 1-2 days): ```dart Workmanager().registerOneOffTask( - "task-identifier", + "task-identifier", // Ignored on iOS simpleTaskKey, // Ignored on iOS initialDelay: Duration(minutes: 30), constraints: Constraints( @@ -103,7 +107,16 @@ Workmanager().registerOneOffTask( // require external power requiresCharging: true, ), - inputData: ... // fully supported + inputData: ... // Android Only ? +); +``` + +Tasks registered this way will appear in the callback dispatcher using as `Workmanager.iOSBackgroundProcessingTask`. + +```dart +Workmanager().registerAppRefreshTask( // iOS only + initialDelay: const Duration(minutes: 15), // 'Suggested' initial delay, won't start sooner than this. Default: Duration.zero + frequency: const Duration(minutes: 60) // 'Suggested' frequency to run after initial execution. If Duration.zero, will not repeat execution. Defalut: Duration.zero ); ``` diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 6233ff4b..257dc2e3 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -11,6 +11,12 @@ extension String { public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { static let identifier = "be.tramckrijte.workmanager" + + // note: this must also be in info.plist - see key: BGTaskSchedulerPermittedIdentifiers + static let defaultBGAppRefreshTaskIdentifier = "workmanager.background.refresh.task" + + // gets set when task is scheduled. If value is 0.0 it doesn't Repeat + static var bgRefreshTaskFrequency = 0.0 private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? @@ -34,6 +40,15 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { case requiresCharging } } + + struct RegisterAppRefreshTask { + static let name = "\(RegisterAppRefreshTask.self)".lowercasingFirst + enum Arguments: String { + case initialDelaySeconds + case refreshFrequencySeconds + } + } + struct CancelAllTasks { static let name = "\(CancelAllTasks.self)".lowercasingFirst enum Arguments: String { @@ -75,6 +90,51 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { operationQueue.addOperation(operation) } + @available(iOS 13.0, *) + private static func scheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier) + + request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshTaskFrequency * 60) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + print("Could not schedule app refresh: \(error)") + } + } + + @available(iOS 13.0, *) + private static func handleAppRefresh(task: BGAppRefreshTask) { + print("handleAppRefresh()", task.identifier) + + if (bgRefreshTaskFrequency > 0) { + // Schedule a new refresh task. + scheduleAppRefresh() + } + + let operationQueue = OperationQueue() + + // Create an operation that performs the main part of the background task. + let operation = BackgroundTaskOperation( + task.identifier, + flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback + ) + + // Provide the background task with an expiration handler that cancels the operation. + task.expirationHandler = { + operation.cancel() + } + + // Inform the system that the background task is complete + // when the operation completes. + operation.completionBlock = { + task.setTaskCompleted(success: !operation.isCancelled) + } + + // Start the operation. + operationQueue.addOperation(operation) + } + @objc public static func registerTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { @@ -86,6 +146,11 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { handleBGProcessingTask(task) } } + + // just using the static identifier for now. + BGTaskScheduler.shared.register(forTaskWithIdentifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier, using: nil) { task in + handleAppRefresh(task: task as! BGAppRefreshTask) + } } } } @@ -124,24 +189,49 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { UserDefaultsHelper.storeIsDebug(isInDebug) result(true) - case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): - if !validateCallbackHandle() { - result( - FlutterError( - code: "1", - message: "You have not properly initialized the Flutter WorkManager Package. " + - "You should ensure you have called the 'initialize' function first! " + - "Example: \n" + - "\n" + - "`Workmanager().initialize(\n" + - " callbackDispatcher,\n" + - " )`" + - "\n" + - "\n" + - "The `callbackDispatcher` is a top level function. See example in repository.", - details: nil - ) + case (ForegroundMethodChannel.Methods.RegisterAppRefreshTask.name, let .some(arguments)): + print("ForegroundMethodChannel.Methods.RegisterAppRefreshTask") + if !SwiftWorkmanagerPlugin.validateCallbackHandle(result: result) { + return + } + + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.RegisterAppRefreshTask.self + guard let initialDelaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return + } + + guard let refreshFrequencySeconds = + arguments[method.Arguments.refreshFrequencySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return + } + + // save this, can't store in task + SwiftWorkmanagerPlugin.bgRefreshTaskFrequency = Double(refreshFrequencySeconds) + + let request = BGAppRefreshTaskRequest( + identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier ) + + request.earliestBeginDate = Date(timeIntervalSinceNow: Double(initialDelaySeconds)) + + do { + try BGTaskScheduler.shared.submit(request) + result(true) + } catch { + result(WMPError.bgTaskSchedulingFailed(error).asFlutterError) + } + + return + } else { + result(WMPError.unhandledMethod(call.method).asFlutterError) + } + + case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): + if !SwiftWorkmanagerPlugin.validateCallbackHandle(result: result) { return } @@ -208,8 +298,27 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } } - private func validateCallbackHandle() -> Bool { - return UserDefaultsHelper.getStoredCallbackHandle() != nil + private static func validateCallbackHandle(result: @escaping FlutterResult) -> Bool { + let valid = UserDefaultsHelper.getStoredCallbackHandle() != nil + if (!valid) { + result( + FlutterError( + code: "1", + message: "You have not properly initialized the Flutter WorkManager Package. " + + "You should ensure you have called the 'initialize' function first! " + + "Example: \n" + + "\n" + + "`Workmanager().initialize(\n" + + " callbackDispatcher,\n" + + " )`" + + "\n" + + "\n" + + "The `callbackDispatcher` is a top level function. See example in repository.", + details: nil + ) + ) + } + return valid } } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index d258a94c..4bd8dca5 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -156,7 +156,7 @@ class Workmanager { /// Schedule a one off task /// A [uniqueName] is required so only one task can be registered. /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] - /// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list + /// The [inputData] is the input data for task. Android Only. Valid value types are: int, bool, double, String and their list Future registerOneOffTask( /// Only supported on Android. final String uniqueName, @@ -201,6 +201,27 @@ class Workmanager { ), ); + /// Schedule an iOS BGAppRefreshTask + Future registerAppRefreshTask({ + + /// Configures an initial delay. + /// + /// The delay configured here is not guaranteed. The underlying system may + /// decide to schedule the task a lot later. + final Duration initialDelay = Duration.zero, + + /// Frequency to repeat task after initial execution. Duration.zero does not repeat. + final Duration frequency = Duration.zero, + }) async => + await _foregroundChannel.invokeMethod( + "registerAppRefreshTask", + JsonMapperHelper.toRegisterAppRefreshMethodArgument( + isInDebugMode: _isInDebugMode, + initialDelay: initialDelay, + frequency: frequency + ), + ); + /// Schedules a periodic task that will run every provided [frequency]. /// A [uniqueName] is required so only one task can be registered. /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] @@ -314,6 +335,19 @@ class JsonMapperHelper { }; } + @visibleForTesting + static Map toRegisterAppRefreshMethodArgument({ + final bool isInDebugMode = false, + final Duration? initialDelay, + final Duration? frequency + }) { + return { + "isInDebugMode": isInDebugMode, + "initialDelaySeconds": initialDelay?.inSeconds, + "refreshFrequencySeconds": frequency?.inSeconds + }; + } + @visibleForTesting static Map toInitializeMethodArgument({ required final bool isInDebugMode,