From b74dc9328061554215d8c30599ec9eb31ccfc4d8 Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Wed, 26 Oct 2022 23:09:01 +0200 Subject: [PATCH 01/26] fix:Update Workmanager iOS because no callback in Background on iOS real device, added 30sec BGAppRefresh, Updated example #396 --- example/ios/.swiftlint.yml | 8 + example/ios/Runner.xcodeproj/project.pbxproj | 8 +- example/ios/Runner/AppDelegate.swift | 10 +- example/ios/Runner/Info.plist | 1 + example/lib/main.dart | 125 ++++++-- ios/Classes/BackgroundWorker.swift | 6 + ios/Classes/DebugNotificationHelper.swift | 47 ++- ios/Classes/SwiftWorkmanagerPlugin.swift | 284 +++++++++++++++---- ios/Classes/WorkmanagerPlugin.h | 8 + ios/Classes/WorkmanagerPlugin.m | 8 +- lib/src/workmanager.dart | 9 +- 11 files changed, 429 insertions(+), 85 deletions(-) diff --git a/example/ios/.swiftlint.yml b/example/ios/.swiftlint.yml index 340696c0..e17d492e 100644 --- a/example/ios/.swiftlint.yml +++ b/example/ios/.swiftlint.yml @@ -1,6 +1,14 @@ excluded: - example/ios/Pods + - example/ios/Classes/SwiftWorkmanagerPlugin.swift - Pods included: - .symlinks/plugins/workmanager/ios +line_length: + warning: 200 + error: 250 +function_body_length: + warning: 300 + error: 500 + diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ce9f9d2e..879761ae 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -231,7 +231,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 6KRGLYTFWP; + DevelopmentTeam = 6Y9WT2BQR9; LastSwiftMigration = 1110; }; 9EA9C43226E8F58700E77F3E = { @@ -503,7 +503,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 79BMQESM94; + DEVELOPMENT_TEAM = 6Y9WT2BQR9; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -640,7 +640,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6KRGLYTFWP; + DEVELOPMENT_TEAM = 6Y9WT2BQR9; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -669,7 +669,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 79BMQESM94; + DEVELOPMENT_TEAM = 6Y9WT2BQR9; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 4b404a65..63415d86 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -19,7 +19,14 @@ import workmanager // This will make other plugins available during a background operation. GeneratedPluginRegistrant.register(with: registry) } - + //All launch handlers must be registered before application finishes launching + //set these identifiers in info.plist + //example + //BGTaskSchedulerPermittedIdentifiers + // + // be.tramckrijte.workmanagerExample.taskId + // + // WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask") @@ -27,6 +34,7 @@ import workmanager WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodicTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask") + WorkmanagerPlugin.registerPeriodicTask( withIdentifier: "app.workmanagerExample.iOSBackgroundAppRefresh") return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 718d8540..42839d01 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -11,6 +11,7 @@ be.tramckrijte.workmanagerExample.simpleDelayedTask be.tramckrijte.workmanagerExample.simplePeriodicTask be.tramckrijte.workmanagerExample.simplePeriodic1HourTask + app.workmanagerExample.iOSBackgroundAppRefresh CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/example/lib/main.dart b/example/lib/main.dart index a009ce0c..03a06473 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,6 +17,8 @@ const simplePeriodicTask = "be.tramckrijte.workmanagerExample.simplePeriodicTask"; const simplePeriodic1HourTask = "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask"; +const iOSBackgroundAppRefresh = + "app.workmanagerExample.iOSBackgroundAppRefresh"; @pragma( 'vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ @@ -56,11 +58,18 @@ void callbackDispatcher() { print("The iOS background fetch was triggered"); Directory? tempDir = await getTemporaryDirectory(); String? tempPath = tempDir.path; + sleep(Duration(seconds: 55)); print( "You can access other plugins in the background, for example Directory.getTemporaryDirectory(): $tempPath"); break; + case Workmanager.iOSBackgroundAppRefresh: + //maximum duration 29seconds - App could perhaps killed by iOS when it takes a longer time than 30 seconds for BGAppRefresh included native work + print("The iOSBackgroundAppRefresh was triggered"); + sleep(Duration(seconds: 11)); // sleep as sample + // test on debugger - pause debugger in xcode and enter in terminal: + // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] + break; } - return Future.value(true); }); } @@ -71,6 +80,8 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + bool workmanagerInitialized = false; + @override Widget build(BuildContext context) { return MaterialApp( @@ -91,10 +102,13 @@ class _MyAppState extends State { ElevatedButton( child: Text("Start the Flutter background service"), onPressed: () { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } }, ), SizedBox(height: 16), @@ -104,6 +118,13 @@ class _MyAppState extends State { ElevatedButton( child: Text("Register OneOff Task"), onPressed: () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } Workmanager().registerOneOffTask( simpleTaskKey, simpleTaskKey, @@ -118,31 +139,50 @@ class _MyAppState extends State { }, ), ElevatedButton( - child: Text("Register rescheduled Task"), - onPressed: () { - Workmanager().registerOneOffTask( - rescheduledTaskKey, - rescheduledTaskKey, - inputData: { - 'key': Random().nextInt(64000), - }, - ); - }, - ), + child: Text("Register rescheduled Task"), + onPressed: () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } + Workmanager().registerOneOffTask( + rescheduledTaskKey, + rescheduledTaskKey, + inputData: { + 'key': Random().nextInt(64000), + }, + ); + }), ElevatedButton( - child: Text("Register failed Task"), - onPressed: () { - Workmanager().registerOneOffTask( - failedTaskKey, - failedTaskKey, - ); - }, - ), + child: Text("Register failed Task"), + onPressed: () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } + Workmanager().registerOneOffTask( + failedTaskKey, + failedTaskKey, + ); + }), //This task runs once //This wait at least 10 seconds before running ElevatedButton( child: Text("Register Delayed OneOff Task"), onPressed: () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } Workmanager().registerOneOffTask( simpleDelayedTask, simpleDelayedTask, @@ -157,6 +197,13 @@ class _MyAppState extends State { child: Text("Register Periodic Task (Android)"), onPressed: Platform.isAndroid ? () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } Workmanager().registerPeriodicTask( simplePeriodicTask, simplePeriodicTask, @@ -164,12 +211,42 @@ class _MyAppState extends State { ); } : null), + //This task runs periodically dependening on iOS - there is no safe timing - see Apple doc + //Since we have not provided a frequency it will be the default 2 minutes + //register name in info.plist BGTaskSchedulerPermittedIdentifiers + //register name in iOS - Appdelegate + ElevatedButton( + child: + Text("Register Periodic Backgound App Refresh (iOS)"), + onPressed: Platform.isIOS + ? () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } + Workmanager().registerPeriodicTask( + iOSBackgroundAppRefresh, + iOSBackgroundAppRefresh, + initialDelay: Duration(seconds: 10), //ignored + ); + } + : null), //This task runs periodically //It will run about every hour ElevatedButton( child: Text("Register 1 hour Periodic Task (Android)"), onPressed: Platform.isAndroid ? () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } Workmanager().registerPeriodicTask( simplePeriodicTask, simplePeriodic1HourTask, diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index 376d5f58..a19935f1 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -8,20 +8,26 @@ import Foundation enum BackgroundMode { + case backgroundAppRefresh(identifier: String) case backgroundFetch case backgroundTask(identifier: String) var flutterThreadlabelPrefix: String { switch self { + case .backgroundAppRefresh: + return "\(SwiftWorkmanagerPlugin.identifier).BackgroundAppRefresh" case .backgroundFetch: return "\(SwiftWorkmanagerPlugin.identifier).BackgroundFetch" case .backgroundTask: return "\(SwiftWorkmanagerPlugin.identifier).BGTaskScheduler" + } } var onResultSendArguments: [String: String] { switch self { + case .backgroundAppRefresh: + return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundAppRefresh"] case .backgroundFetch: return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSPerformFetch"] case .backgroundTask(let identifier): diff --git a/ios/Classes/DebugNotificationHelper.swift b/ios/Classes/DebugNotificationHelper.swift index 66ea0a9a..39acb0bf 100644 --- a/ios/Classes/DebugNotificationHelper.swift +++ b/ios/Classes/DebugNotificationHelper.swift @@ -23,10 +23,10 @@ class DebugNotificationHelper { let message = """ Starting Dart/Flutter with following params: - • callbackHandle: '\(callBackHandle)' • callBackName: '\(callbackInfo.callbackName ?? "not found")' • callbackClassName: '\(callbackInfo.callbackClassName ?? "not found")' • callbackLibraryPath: '\(callbackInfo.callbackLibraryPath ?? "not found")' + • callbackHandle: '\(callBackHandle)' """ DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, title: startDate.formatted(), @@ -39,7 +39,7 @@ class DebugNotificationHelper { elapsedTime: TimeInterval) { let message = """ - Perform fetch completed: + Perform backgroundworkerfetch completed: • Elapsed time: \(elapsedTime.formatToSeconds()) • Result: UIBackgroundFetchResult.\(result) """ @@ -48,6 +48,49 @@ class DebugNotificationHelper { body: message, icon: result == .newData ? .success : .failure) } + + func showStartBGRefreshNotification(startDate: Date, + callBackHandle: Int64, + callbackInfo: FlutterCallbackInformation + ) { + let message = + """ + Starting Dart/Flutter BGAppRefresh with following params: + • callBackName: '\(callbackInfo.callbackName ?? "not found")' + • callbackClassName: '\(callbackInfo.callbackClassName ?? "not found")' + • callbackLibraryPath: '\(callbackInfo.callbackLibraryPath ?? "not found")' + • callbackHandle: '\(callBackHandle)' + """ + DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, + title: startDate.formatted(), + body: message, + icon: .startWork) + } + + func showCompletedBGRefreshNotification(completedDate: Date, + result: UIBackgroundFetchResult, + elapsedTime: TimeInterval) { + let message = + """ + Perform BGRefresh completed: + • Elapsed time: \(elapsedTime.formatToSeconds()) + • Result: UIBackgroundFetchResult.\(result) + """ + DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, + title: completedDate.formatted(), + body: message, + icon: result == .newData ? .success : .failure) + } + + ///Show a notification for iOS Debugging + func showDebugNotification (completedDate: Date, + content: String + ) { + DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, + title: completedDate.formatted(), + body: content, + icon: .success) + } // MARK: - Private helper functions diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 6233ff4b..9ee51b58 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -11,12 +11,12 @@ extension String { public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { static let identifier = "be.tramckrijte.workmanager" - + private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - + private struct ForegroundMethodChannel { static let channelName = "\(SwiftWorkmanagerPlugin.identifier)/foreground_channel_work_manager" - + struct Methods { struct Initialize { static let name = "\(Initialize.self)".lowercasingFirst @@ -34,6 +34,13 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { case requiresCharging } } + struct RegisterPeriodicTask { + static let name = "\(RegisterPeriodicTask.self)".lowercasingFirst + enum Arguments: String { + case uniqueName + case initialDelaySeconds + } + } struct CancelAllTasks { static let name = "\(CancelAllTasks.self)".lowercasingFirst enum Arguments: String { @@ -48,36 +55,83 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } } - + + //Handlers @available(iOS 13.0, *) private static func handleBGProcessingTask(_ task: BGProcessingTask) { let operationQueue = OperationQueue() - + // Create an operation that performs the main part of the background task let operation = BackgroundTaskOperation( task.identifier, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback ) - + // Provide an expiration handler for the background task // 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 + @available(iOS 13.0, *) + public static func handleAppRefresh(task: BGAppRefreshTask) { + guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), + let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + else { + logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") + return + } + + let taskSessionStart = Date() + let taskSessionIdentifier = UUID() + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) + debugHelper.showStartBGRefreshNotification( + startDate: taskSessionStart, + callBackHandle: callbackHandle, + callbackInfo: flutterCallbackInformation + ) + ///TODO get seconds + scheduleAppRefresh(withIdentifier: task.identifier,earliestbeginInSeconds: 120) + let semaphore = DispatchSemaphore(value: 0) + + DispatchQueue.main.async { + let worker = BackgroundWorker(mode: .backgroundAppRefresh(identifier: self.identifier), + flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) + + worker.performBackgroundRequest { _ in + semaphore.signal() + } + } + //timeout after 29seconds ,max execution time is 30seconds + //-> 1 second for dispatching and other stuff (Flutter Messenger etc) + let dispatchResult = semaphore.wait(timeout:DispatchTime.now()+29) + + print("handleAppRefresh \(dispatchResult)") + debugHelper.showCompletedBGRefreshNotification( + completedDate: Date(), + result: dispatchResult == .timedOut ? .failed : .newData, + elapsedTime: Date().timeIntervalSince(taskSessionStart)) + + } + + ///register names for BGProcessingTask called by workmanger.m + ///you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown @objc - public static func registerTask(withIdentifier identifier: String) { + public static func registerBackgroundProcessingTask(taskIdentifier identifier: String){ if #available(iOS 13.0, *) { + print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, using: nil @@ -88,17 +142,104 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } } + + @objc + public static func registerAppRefreshTask(withIdentifier identifier: String) { + if #available(iOS 13.0, *) { + print("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") + + BGTaskScheduler.shared.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + if let task = task as? BGAppRefreshTask{ + handleAppRefresh(task: task) + } + }}} + + @objc + public static func registerBackgroundProcessingTaskScheduler(withIdentifier identifier: String, + earliestBeginInSeconds begin:Double, + requiresNetworkConnectivity:Bool, + requiresExternalPower:Bool) { + if #available(iOS 13.0, *) { + print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(identifier)") + scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) + + //set notificationhandler on app did enter background + //NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil + // ) { (notification) in + // //schedule scheduleBackgroundProcessingTask + // scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestbeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) + // } + } + + } + + + + @objc + public static func registerAppRefreshTaskScheduler(withIdentifier identifier: String, earliestbeginInSeconds begin:Double) { + if #available(iOS 13.0, *) { + print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") + //schedule on app did enter background + NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil + ) { (notification) in + //schedule apprefresh + scheduleAppRefresh(withIdentifier: identifier,earliestbeginInSeconds: begin) + } + } + } + + static func callback(_: UIBackgroundFetchResult){ + } + + @objc + @available(iOS 13.0, *) + private static func scheduleAppRefresh(withIdentifier identifier: String, earliestbeginInSeconds begin:Double) { + + let request = BGAppRefreshTaskRequest( + identifier: identifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: begin) + do { + try BGTaskScheduler.shared.submit(request) + print("scheduleAppRefresh workmanager (re)scheduled app refresh \(identifier)") + } catch { + print("Couldn't schedule app refresh \(error.localizedDescription)") + return + + } + } + + @objc + @available(iOS 13.0, *) + private static func scheduleBackgroundProcessingTask(withIdentifier identifier: String, earliestBeginInSeconds begin:Double, + requiresNetworkConnectivity:Bool, + requiresExternalPower:Bool + ) { + let request = BGProcessingTaskRequest( + identifier: identifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: begin) + request.requiresNetworkConnectivity = requiresNetworkConnectivity + request.requiresExternalPower = requiresExternalPower + do { + try BGTaskScheduler.shared.submit(request) + print("Requested BackgroundProcessingTask \(identifier)") + } catch { + print("Couldn't schedule app BackgroundProcessingTask identifier:\(identifier) error:\(error.localizedDescription)") + } + } } // MARK: - FlutterPlugin conformance extension SwiftWorkmanagerPlugin: FlutterPlugin { - + @objc public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { flutterPluginRegistrantCallback = callback } - + public static func register(with registrar: FlutterPluginRegistrar) { let foregroundMethodChannel = FlutterMethodChannel( name: ForegroundMethodChannel.channelName, @@ -108,9 +249,13 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: foregroundMethodChannel) registrar.addApplicationDelegate(instance) } - + + //added to .swiftlint.yml following lines + //because error on Xcode build Function body should span 40 lines or less excluding comments and whitespace + //function_body_length: + //warning: 300 + //error: 500 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch (call.method, call.arguments as? [AnyHashable: Any]) { case (ForegroundMethodChannel.Methods.Initialize.name, let .some(arguments)): let method = ForegroundMethodChannel.Methods.Initialize.self @@ -119,35 +264,84 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { result(WMPError.invalidParameters.asFlutterError) return } - + UserDefaultsHelper.storeCallbackHandle(handle) UserDefaultsHelper.storeIsDebug(isInDebug) result(true) - + return + //register bgAppRefreshTask for less than 30 seconds backgroundtime + case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): + print("Registering Periodic Task background (BGAppRefreshTask)") + if !validateCallbackHandle() { + result( + FlutterError( + code: "1", + message: "RegisterPeriodicTask - 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 + } + + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self + + guard let identifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { + result(WMPError.invalidParameters.asFlutterError) + return + } + guard let initialDelaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return + } + //task will scheduled when app goes to background + SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler(withIdentifier:identifier, earliestbeginInSeconds: Double(initialDelaySeconds)) + print("Registered \(identifier)") + result(true) + return; + + } + result(FlutterError(code: "99", message: "Not registered", details: "iOS Version lower than 13.0")) + return + + //register processingtask for more than 30 seconds backgroundtime case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): + print("Registering OneOffTask (BackgroundProcessingTask)") 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.", + "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 } - + + if #available(iOS 13.0, *) { let method = ForegroundMethodChannel.Methods.RegisterOneOffTask.self - guard let initialDelaySeconds = + guard let delaySeconds = arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { result(WMPError.invalidParameters.asFlutterError) return @@ -157,40 +351,28 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { result(WMPError.invalidParameters.asFlutterError) return } - let request = BGProcessingTaskRequest( - identifier: identifier - ) let requiresCharging = arguments[method.Arguments.requiresCharging.rawValue] as? Bool ?? false - - var requiresNetworkConnectivity = false + var requiresNetwork = false if let networkTypeInput = arguments[method.Arguments.networkType.rawValue] as? String, let networkType = NetworkType(fromDart: networkTypeInput), networkType == .connected || networkType == .metered { - requiresNetworkConnectivity = true - } - - request.earliestBeginDate = Date(timeIntervalSinceNow: Double(initialDelaySeconds)) - request.requiresExternalPower = requiresCharging - request.requiresNetworkConnectivity = requiresNetworkConnectivity - - do { - try BGTaskScheduler.shared.submit(request) - result(true) - } catch { - result(WMPError.bgTaskSchedulingFailed(error).asFlutterError) + requiresNetwork = true } - + //task will scheduled when app goes to background + SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler(withIdentifier:identifier, earliestBeginInSeconds: Double(delaySeconds), requiresNetworkConnectivity: requiresCharging, requiresExternalPower: requiresNetwork) + + result(true) return } else { result(WMPError.unhandledMethod(call.method).asFlutterError) } - + case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): if #available(iOS 13.0, *) { BGTaskScheduler.shared.cancelAllTaskRequests() } result(true) - + case (ForegroundMethodChannel.Methods.CancelTaskByUniqueName.name, let .some(arguments)): if #available(iOS 13.0, *) { let method = ForegroundMethodChannel.Methods.CancelTaskByUniqueName.self @@ -201,13 +383,13 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) } result(true) - + default: result(WMPError.unhandledMethod(call.method).asFlutterError) return } } - + private func validateCallbackHandle() -> Bool { return UserDefaultsHelper.getStoredCallbackHandle() != nil } @@ -216,7 +398,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { // MARK: - AppDelegate conformance extension SwiftWorkmanagerPlugin { - + override public func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void @@ -225,8 +407,8 @@ extension SwiftWorkmanagerPlugin { mode: .backgroundFetch, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback ) - + return worker.performBackgroundRequest(completionHandler) } - + } diff --git a/ios/Classes/WorkmanagerPlugin.h b/ios/Classes/WorkmanagerPlugin.h index 783130bd..2644b493 100644 --- a/ios/Classes/WorkmanagerPlugin.h +++ b/ios/Classes/WorkmanagerPlugin.h @@ -10,4 +10,12 @@ */ + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier; +/** + * Register a custom task identifier to be iOS Background Task /executed later on. + * @author Lars Huth + * + * @param taskIdentifier The identifier of the custom task. Must be set in info.plist + */ ++ (void)registerPeriodicTaskWithIdentifier:(NSString *) taskIdentifier; + @end diff --git a/ios/Classes/WorkmanagerPlugin.m b/ios/Classes/WorkmanagerPlugin.m index 318baf54..8fad301b 100644 --- a/ios/Classes/WorkmanagerPlugin.m +++ b/ios/Classes/WorkmanagerPlugin.m @@ -18,7 +18,13 @@ + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier { if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerTaskWithIdentifier:taskIdentifier]; + [SwiftWorkmanagerPlugin registerBackgroundProcessingTaskWithTaskIdentifier:taskIdentifier]; + } +} + ++ (void)registerPeriodicTaskWithIdentifier:(NSString *)taskIdentifier{ + if (@available(iOS 13, *)) { + [SwiftWorkmanagerPlugin registerAppRefreshTaskWithIdentifier:taskIdentifier]; } } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 2cfe3e86..55b46d18 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -84,6 +84,9 @@ class Workmanager { /// case Workmanager.iOSBackgroundTask: /// stderr.writeln("The iOS background fetch was triggered"); /// break; + /// case Workmanager.iOSBackgroundAppRefresh: + /// stderr.writeln("The iOS backgroundAppRefresh was triggered"); + /// break; /// } /// /// return Future.value(true); @@ -91,6 +94,7 @@ class Workmanager { /// } /// ``` static const String iOSBackgroundTask = "iOSPerformFetch"; + static const String iOSBackgroundAppRefresh = "iOSBackgroundAppRefresh"; /// Use this constant inside your callbackDispatcher to identify when an iOS Background Processing via BGTaskScheduler occurred. /// @@ -108,7 +112,8 @@ class Workmanager { /// }); /// } /// ``` - @Deprecated('Use custom iOS task names. This property will be removed.') + @Deprecated( + 'Use custom iOS task names. Set keys in info.plist This property will be removed.') static const String iOSBackgroundProcessingTask = "workmanager.background.task"; @@ -206,7 +211,7 @@ class Workmanager { ), ); - /// Schedules a periodic task that will run every provided [frequency]. + /// Schedules a periodic task that will run (if iOS random depending on iOS) 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] /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. From f7c1cef4fe29acfb08c0f598ccdab9b3cc613b81 Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Sun, 6 Nov 2022 00:35:15 +0100 Subject: [PATCH 02/26] added permissionhandler an requests for iOS added alert and MaterialApp to workmanager when no iOS permissions activated --- README.md | 72 ++++++++++-- example/lib/main.dart | 41 ++++++- ios/Classes/CheckAuthorisation.swift | 69 ++++++++++++ ios/Classes/SwiftWorkmanagerPlugin.swift | 137 ++++++++++++----------- ios/Classes/WMPError.swift | 3 + lib/src/options.dart | 16 +++ lib/src/workmanager.dart | 29 +++++ 7 files changed, 286 insertions(+), 81 deletions(-) create mode 100644 ios/Classes/CheckAuthorisation.swift diff --git a/README.md b/README.md index 124dea4e..99211624 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ However, there is an exception for iOS background fetch: `Workmanager.iOSBackgro The `Workmanager().executeTask(...` block supports 3 possible outcomes: 1. `Future.value(true)`: The task is successful. -2. `Future.value(false)`: The task did not complete successfully and needs to be retried. On Android, the retry is done automatically. On iOS (when using BGTaskScheduler), the retry needs to be scheduled manually. +2. `Future.value(false)`: The task did not complete successfully and needs to be retried. On Android, the retry is done + automatically. On iOS (when using BGTaskScheduler), the retry needs to be scheduled manually. 3. `Future.error(...)`: The task failed. On Android, the `BackoffPolicy` will configure how `WorkManager` is going to retry the task. @@ -91,23 +92,70 @@ Refer to the example app for a successful, retrying and a failed task. # iOS specific setup and note +Initialize Workmanager only one once +You can use the background app refresh only on a real device + iOS supports **One off tasks** with a few basic constraints: ```dart -Workmanager().registerOneOffTask( - "task-identifier", - simpleTaskKey, // Ignored on iOS - initialDelay: Duration(minutes: 30), - constraints: Constraints( - // connected or metered mark the task as requiring internet - networkType: NetworkType.connected, - // require external power - requiresCharging: true, - ), - inputData: ... // fully supported +Workmanager +( +).registerOneOffTask +("task-identifier +" +, +simpleTaskKey, // Ignored on iOS +initialDelay: Duration +( +minutes: 30 +) +, +constraints: Constraints +( +// connected or metered mark the task as requiring internet +networkType: NetworkType.connected, +// require external power +requiresCharging: true +, +) +, +inputData: ... // fully supported ); ``` +And alternative supports **PeriodicTask** with maximum 29sec execution time (see example). +Look also +at +Apple's documentation + +```dart + +const iOSBackgroundAppRefresh = + "app.workmanagerExample.iOSBackgroundAppRefresh"; +Workmanager +( +).registerPeriodicTask +( +iOSBackgroundAppRefresh,iOSBackgroundAppRefresh,initialDelay: Duration +( +seconds: 10 +) +, //ignored +); +``` + +Get permissions for iOS BackgroundRefresh - see example + +```dart + if (Platform.isIOS) { +//here you can check whether background refresh is activated in iOS settings +var hasPermissions = await Workmanager() + .checkBackgroundRefreshPermission(); +if (hasPermissions != BackgroundAuthorisationState.available){...} +} + +``` + For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks). # Customisation (Android) diff --git a/example/lib/main.dart b/example/lib/main.dart index 03a06473..5cacb4e4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,7 +7,10 @@ import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; -void main() => runApp(MyApp()); +void main() { + //added MaterialApp for showdialog + runApp(MaterialApp(home: MyApp())); +} const simpleTaskKey = "be.tramckrijte.workmanagerExample.simpleTask"; const rescheduledTaskKey = "be.tramckrijte.workmanagerExample.rescheduledTask"; @@ -101,7 +104,35 @@ class _MyAppState extends State { ), ElevatedButton( child: Text("Start the Flutter background service"), - onPressed: () { + onPressed: () async { + if (Platform.isIOS) { + //here you can check whether background refresh is activated in iOS settings + var hasPermissions = await Workmanager() + .checkBackgroundRefreshPermission(); + if (hasPermissions != + BackgroundAuthorisationState.available) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: new Text("No permissions alert"), + content: new Text( + "no background refresh permissions!!!"), + actions: [ + new TextButton( + child: new Text( + "Status is " + hasPermissions.name), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + return; + } + } if (!workmanagerInitialized) { Workmanager().initialize( callbackDispatcher, @@ -217,9 +248,9 @@ class _MyAppState extends State { //register name in iOS - Appdelegate ElevatedButton( child: - Text("Register Periodic Backgound App Refresh (iOS)"), + Text("Register Periodic Background App Refresh (iOS)"), onPressed: Platform.isIOS - ? () { + ? () async { if (!workmanagerInitialized) { Workmanager().initialize( callbackDispatcher, @@ -227,7 +258,7 @@ class _MyAppState extends State { ); workmanagerInitialized = true; } - Workmanager().registerPeriodicTask( + await Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, initialDelay: Duration(seconds: 10), //ignored diff --git a/ios/Classes/CheckAuthorisation.swift b/ios/Classes/CheckAuthorisation.swift new file mode 100644 index 00000000..bf417e06 --- /dev/null +++ b/ios/Classes/CheckAuthorisation.swift @@ -0,0 +1,69 @@ +// +// CheckAuthorisation.swift +// workmanager +// +// Created by Lars Huth on 03/11/2022. +// +import Foundation + +func checkBackgroundRefreshAuthorisation(result:@escaping FlutterResult) -> BackgroundAuthorisationState{ + switch UIApplication.shared.backgroundRefreshStatus { + case .available: + result(BackgroundAuthorisationState.available.rawValue) + return BackgroundAuthorisationState.available + case .denied: + result(BackgroundAuthorisationState.denied.rawValue) + return BackgroundAuthorisationState.denied + case .restricted: + result(BackgroundAuthorisationState.restricted.rawValue) + return BackgroundAuthorisationState.restricted + default: + result( + FlutterError( + code: "103", + message: "BGAppRefreshTask - You have no iOS background refresh permissions. " + + "\n" + + "BackgroundRefreshStatus is denied\n" + + "\n" + + "Workmanager asked on initialize function for background permissions - when user accepted this you can set a periodic background task", + details: nil + ) + ) + return BackgroundAuthorisationState.unknown + } +} + +func requestBackgroundAuthorisation(){ + //request for authorisation + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) +} + +enum BackgroundAuthorisationState:String +{ + /// iOS Setting Backgroundwork is enabled. + case available + + /// iOS Setting Backgroundwork is disabled in settings. You shoud request for permissions call requestBackgroundAuthorisation only once and respect users choice + case denied + + /// iOS Setting is under parental control etc. Can't be changed by user + case restricted + + /// unknown state + case unknown + + /// Convenience constructor to build a [BackgroundAutorisationState] from a Dart enum. + init?(fromDart: String) { + self.init(rawValue: fromDart.camelCased(with: "_")) + } +} + +private extension String { + func camelCased(with separator: Character) -> String { + return self.lowercased() + .split(separator: separator) + .enumerated() + .map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() } + .joined() + } +} diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 9ee51b58..7c28f34b 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -12,6 +12,7 @@ extension String { public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { static let identifier = "be.tramckrijte.workmanager" + private var _isInitalized = false; private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? private struct ForegroundMethodChannel { @@ -25,6 +26,9 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { case callbackHandle } } + struct CheckBackgroundRefreshPermission { + static let name = "\(CheckBackgroundRefreshPermission.self)".lowercasingFirst + } struct RegisterOneOffTask { static let name = "\(RegisterOneOffTask.self)".lowercasingFirst enum Arguments: String { @@ -87,15 +91,15 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @available(iOS 13.0, *) public static func handleAppRefresh(task: BGAppRefreshTask) { guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), - let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) - else { - logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") - return + let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + else { + logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") + return } - + let taskSessionStart = Date() let taskSessionIdentifier = UUID() - + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) debugHelper.showStartBGRefreshNotification( startDate: taskSessionStart, @@ -105,11 +109,11 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { ///TODO get seconds scheduleAppRefresh(withIdentifier: task.identifier,earliestbeginInSeconds: 120) let semaphore = DispatchSemaphore(value: 0) - + DispatchQueue.main.async { let worker = BackgroundWorker(mode: .backgroundAppRefresh(identifier: self.identifier), flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) - + worker.performBackgroundRequest { _ in semaphore.signal() } @@ -117,7 +121,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { //timeout after 29seconds ,max execution time is 30seconds //-> 1 second for dispatching and other stuff (Flutter Messenger etc) let dispatchResult = semaphore.wait(timeout:DispatchTime.now()+29) - + print("handleAppRefresh \(dispatchResult)") debugHelper.showCompletedBGRefreshNotification( completedDate: Date(), @@ -131,7 +135,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc public static func registerBackgroundProcessingTask(taskIdentifier identifier: String){ if #available(iOS 13.0, *) { - print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") + print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, using: nil @@ -159,24 +163,24 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc public static func registerBackgroundProcessingTaskScheduler(withIdentifier identifier: String, - earliestBeginInSeconds begin:Double, - requiresNetworkConnectivity:Bool, - requiresExternalPower:Bool) { + earliestBeginInSeconds begin:Double, + requiresNetworkConnectivity:Bool, + requiresExternalPower:Bool) { if #available(iOS 13.0, *) { print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(identifier)") scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) - - //set notificationhandler on app did enter background - //NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil - // ) { (notification) in - // //schedule scheduleBackgroundProcessingTask - // scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestbeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) - // } - } - + + //set notificationhandler on app did enter background + //NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil + // ) { (notification) in + // //schedule scheduleBackgroundProcessingTask + // scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestbeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) + // } + } + } - + @objc public static func registerAppRefreshTaskScheduler(withIdentifier identifier: String, earliestbeginInSeconds begin:Double) { @@ -190,7 +194,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } } - + static func callback(_: UIBackgroundFetchResult){ } @@ -255,9 +259,19 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { //function_body_length: //warning: 300 //error: 500 + // swiftlint:disable:next cyclomatic_complexity public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch (call.method, call.arguments as? [AnyHashable: Any]) { case (ForegroundMethodChannel.Methods.Initialize.name, let .some(arguments)): + if _isInitalized { + result(WMPError.workmanagerIsAlreadyInitialized) + return + } + let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result:result) + if (backgroundRefreshAvailable != BackgroundAuthorisationState.available){ + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + return + } let method = ForegroundMethodChannel.Methods.Initialize.self guard let isInDebug = arguments[method.Arguments.isInDebugMode.rawValue] as? Bool, let handle = arguments[method.Arguments.callbackHandle.rawValue] as? Int64 else { @@ -267,34 +281,19 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { UserDefaultsHelper.storeCallbackHandle(handle) UserDefaultsHelper.storeIsDebug(isInDebug) - result(true) + return + case (ForegroundMethodChannel.Methods.CheckBackgroundRefreshPermission.name, .some(_)): + _=checkBackgroundRefreshAuthorisation(result:result) return //register bgAppRefreshTask for less than 30 seconds backgroundtime case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): - print("Registering Periodic Task background (BGAppRefreshTask)") - if !validateCallbackHandle() { - result( - FlutterError( - code: "1", - message: "RegisterPeriodicTask - 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 - ) - ) + print("Registering periodic task in background (BGAppRefreshTask)") + if !validateCallbackHandle(result:result) { return } if #available(iOS 13.0, *) { let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self - guard let identifier = arguments[method.Arguments.uniqueName.rawValue] as? String else { result(WMPError.invalidParameters.asFlutterError) @@ -307,7 +306,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } //task will scheduled when app goes to background SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler(withIdentifier:identifier, earliestbeginInSeconds: Double(initialDelaySeconds)) - print("Registered \(identifier)") + print("Registered PeriodicTask \(identifier)") result(true) return; @@ -318,23 +317,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { //register processingtask for more than 30 seconds backgroundtime case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): print("Registering OneOffTask (BackgroundProcessingTask)") - 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 - ) - ) + if !validateCallbackHandle(result:result) { return } @@ -390,9 +373,35 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } } - private func validateCallbackHandle() -> Bool { - return UserDefaultsHelper.getStoredCallbackHandle() != nil + ///Checks wether getStoredCallbackHandle is set + ///Returns true wenn initilized + ///if false result contains errormessage + private func validateCallbackHandle(result: @escaping FlutterResult) -> Bool { + if UserDefaultsHelper.getStoredCallbackHandle() == nil{ + 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 false; + } + return true; } + + + + } // MARK: - AppDelegate conformance diff --git a/ios/Classes/WMPError.swift b/ios/Classes/WMPError.swift index 7bc2c926..4e908423 100644 --- a/ios/Classes/WMPError.swift +++ b/ios/Classes/WMPError.swift @@ -13,6 +13,7 @@ enum WMPError: Error { case unhandledMethod(_ methodName: String) case unexpectedMethodArguments(_ argumentsDescription: String) case workmanagerNotInitialized + case workmanagerIsAlreadyInitialized case bgTaskSchedulingFailed(_ error: Error) var code: String { @@ -29,6 +30,8 @@ enum WMPError: Error { return "Unhandled method \(methodName)" case .unexpectedMethodArguments(let argumentsDescription): return "Unexpected call arguments \(argumentsDescription)" + case .workmanagerIsAlreadyInitialized: + return "Workmanager was initialized once. It can not initilized a second time" case .bgTaskSchedulingFailed(let error): return """ Scheduling the task using BGTaskScheduler has failed. diff --git a/lib/src/options.dart b/lib/src/options.dart index d341ce68..2585530b 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -64,6 +64,22 @@ enum BackoffPolicy { linear } +/// await Workmanager().checkBackgroundRefreshPermission(); to check these permissions +/// requestBackgroundAuthorisation only once and respect users choice +enum BackgroundAuthorisationState { + /// iOS Setting Backgroundwork is enabled. + available, + + /// iOS Setting Backgroundwork is disabled in settings. You shoud request for permissions call + denied, + + /// iOS Setting is under parental control etc. Can't be changed by user + restricted, + + ///unknown state + unknown +} + /// A specification of the requirements that need to be met before a WorkRequest can run. /// By default, WorkRequests do not have any requirements and can run immediately. /// By adding requirements, you can make sure that work only runs in certain situations - diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 55b46d18..a170d7f1 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -163,6 +163,35 @@ class Workmanager { } } + ///iOS implementation + ///checks whether user or parental control avoids background refresh + /// + Future + checkBackgroundRefreshPermission() async { + try { + var result = await _foregroundChannel.invokeMethod( + 'checkBackgroundRefreshPermission', + JsonMapperHelper.toInitializeMethodArgument( + isInDebugMode: _isInDebugMode, + callbackHandle: + 0), //must provide an argument for switch statement on Swift + ); + switch (result.toString()) { + case 'available': + return BackgroundAuthorisationState.available; + case 'denied': + return BackgroundAuthorisationState.denied; + case 'restricted': + return BackgroundAuthorisationState.restricted; + case 'unknown': + return BackgroundAuthorisationState.unknown; + } + } catch (e) { + print("Could not retrieve BackgroundAuthorisationState " + e.toString()); + } + return BackgroundAuthorisationState.unknown; + } + /// 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] From 1d773de0bdfbc208ce0eb4858a26b81662ee1e39 Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Sun, 6 Nov 2022 00:46:00 +0100 Subject: [PATCH 03/26] fixed errormessage on xcode --- ios/Classes/CheckAuthorisation.swift | 4 ++-- ios/Classes/SwiftWorkmanagerPlugin.swift | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ios/Classes/CheckAuthorisation.swift b/ios/Classes/CheckAuthorisation.swift index bf417e06..8abe11cc 100644 --- a/ios/Classes/CheckAuthorisation.swift +++ b/ios/Classes/CheckAuthorisation.swift @@ -21,9 +21,9 @@ func checkBackgroundRefreshAuthorisation(result:@escaping FlutterResult) -> Back result( FlutterError( code: "103", - message: "BGAppRefreshTask - You have no iOS background refresh permissions. " + + message: "BGAppRefreshTask - You have perhaps no iOS background refresh permissions. " + "\n" + - "BackgroundRefreshStatus is denied\n" + + "BackgroundRefreshStatus is unknown\n" + "\n" + "Workmanager asked on initialize function for background permissions - when user accepted this you can set a periodic background task", details: nil diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 7c28f34b..40c9afd8 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -231,6 +231,8 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { print("Requested BackgroundProcessingTask \(identifier)") } catch { print("Couldn't schedule app BackgroundProcessingTask identifier:\(identifier) error:\(error.localizedDescription)") + print("On BGTaskSchedulerErrorDomain error 1 - please run on real device") + print("On BGTaskSchedulerErrorDomain error 3 - check registered names") } } } From 8419017a488348c5431026aa2cc44056e498072c Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Mon, 7 Nov 2022 20:29:35 +0100 Subject: [PATCH 04/26] feat:Added check for background refresh permissions #441 --- example/ios/.swiftlint.yml | 7 +- example/lib/main.dart | 12 +- ios/Classes/BackgroundWorker.swift | 10 +- ios/Classes/CheckAuthorisation.swift | 17 +- ios/Classes/DebugNotificationHelper.swift | 14 +- ios/Classes/SwiftWorkmanagerPlugin.swift | 379 ++++++++++++---------- ios/Classes/ThumbnailGenerator.swift | 1 - ios/Classes/UserDefaultsHelper.swift | 2 +- ios/Classes/WMPError.swift | 2 - 9 files changed, 227 insertions(+), 217 deletions(-) diff --git a/example/ios/.swiftlint.yml b/example/ios/.swiftlint.yml index e17d492e..954792c1 100644 --- a/example/ios/.swiftlint.yml +++ b/example/ios/.swiftlint.yml @@ -4,11 +4,6 @@ excluded: - Pods included: - .symlinks/plugins/workmanager/ios -line_length: - warning: 200 - error: 250 -function_body_length: - warning: 300 - error: 500 + diff --git a/example/lib/main.dart b/example/lib/main.dart index 5cacb4e4..9dbaef70 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -69,7 +69,7 @@ void callbackDispatcher() { //maximum duration 29seconds - App could perhaps killed by iOS when it takes a longer time than 30 seconds for BGAppRefresh included native work print("The iOSBackgroundAppRefresh was triggered"); sleep(Duration(seconds: 11)); // sleep as sample - // test on debugger - pause debugger in xcode and enter in terminal: + // test on debugger - pause debugger in xcode and enter in terminal ( Connected with real device ) // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] break; } @@ -203,7 +203,7 @@ class _MyAppState extends State { ); }), //This task runs once - //This wait at least 10 seconds before running + //This wait at least 120 seconds before running ElevatedButton( child: Text("Register Delayed OneOff Task"), onPressed: () { @@ -217,12 +217,12 @@ class _MyAppState extends State { Workmanager().registerOneOffTask( simpleDelayedTask, simpleDelayedTask, - initialDelay: Duration(seconds: 10), + initialDelay: Duration(seconds: 120), ); }), SizedBox(height: 8), //This task runs periodically - //It will wait at least 10 seconds before its first launch + //It will wait at least 120 seconds before its first launch //Since we have not provided a frequency it will be the default 15 minutes ElevatedButton( child: Text("Register Periodic Task (Android)"), @@ -238,7 +238,7 @@ class _MyAppState extends State { Workmanager().registerPeriodicTask( simplePeriodicTask, simplePeriodicTask, - initialDelay: Duration(seconds: 10), + initialDelay: Duration(seconds: 120), ); } : null), @@ -261,7 +261,7 @@ class _MyAppState extends State { await Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 10), //ignored + initialDelay: Duration(seconds: 120), //ignored ); } : null), diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index a19935f1..f4af1e7b 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -56,11 +56,11 @@ class BackgroundWorker { @discardableResult func performBackgroundRequest(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), - let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) - else { - logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") - completionHandler(.failed) - return false + let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + else { + logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") + completionHandler(.failed) + return false } let taskSessionStart = Date() diff --git a/ios/Classes/CheckAuthorisation.swift b/ios/Classes/CheckAuthorisation.swift index 8abe11cc..f1b078dd 100644 --- a/ios/Classes/CheckAuthorisation.swift +++ b/ios/Classes/CheckAuthorisation.swift @@ -6,7 +6,7 @@ // import Foundation -func checkBackgroundRefreshAuthorisation(result:@escaping FlutterResult) -> BackgroundAuthorisationState{ +func checkBackgroundRefreshAuthorisation(result: @escaping FlutterResult) -> BackgroundAuthorisationState { switch UIApplication.shared.backgroundRefreshStatus { case .available: result(BackgroundAuthorisationState.available.rawValue) @@ -33,25 +33,24 @@ func checkBackgroundRefreshAuthorisation(result:@escaping FlutterResult) -> Back } } -func requestBackgroundAuthorisation(){ - //request for authorisation +func requestBackgroundAuthorisation() { + // request for authorisation UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) } -enum BackgroundAuthorisationState:String -{ +enum BackgroundAuthorisationState: String { /// iOS Setting Backgroundwork is enabled. case available - + /// iOS Setting Backgroundwork is disabled in settings. You shoud request for permissions call requestBackgroundAuthorisation only once and respect users choice case denied - + /// iOS Setting is under parental control etc. Can't be changed by user case restricted - + /// unknown state case unknown - + /// Convenience constructor to build a [BackgroundAutorisationState] from a Dart enum. init?(fromDart: String) { self.init(rawValue: fromDart.camelCased(with: "_")) diff --git a/ios/Classes/DebugNotificationHelper.swift b/ios/Classes/DebugNotificationHelper.swift index 39acb0bf..458bcf96 100644 --- a/ios/Classes/DebugNotificationHelper.swift +++ b/ios/Classes/DebugNotificationHelper.swift @@ -48,10 +48,10 @@ class DebugNotificationHelper { body: message, icon: result == .newData ? .success : .failure) } - + func showStartBGRefreshNotification(startDate: Date, - callBackHandle: Int64, - callbackInfo: FlutterCallbackInformation + callBackHandle: Int64, + callbackInfo: FlutterCallbackInformation ) { let message = """ @@ -68,8 +68,8 @@ class DebugNotificationHelper { } func showCompletedBGRefreshNotification(completedDate: Date, - result: UIBackgroundFetchResult, - elapsedTime: TimeInterval) { + result: UIBackgroundFetchResult, + elapsedTime: TimeInterval) { let message = """ Perform BGRefresh completed: @@ -82,9 +82,9 @@ class DebugNotificationHelper { icon: result == .newData ? .success : .failure) } - ///Show a notification for iOS Debugging + /// Show a notification for iOS Debugging func showDebugNotification (completedDate: Date, - content: String + content: String ) { DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, title: completedDate.formatted(), diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 40c9afd8..88fc90bb 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -11,13 +11,13 @@ extension String { public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { static let identifier = "be.tramckrijte.workmanager" - - private var _isInitalized = false; + + private var _isInitalized = false private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - + private struct ForegroundMethodChannel { static let channelName = "\(SwiftWorkmanagerPlugin.identifier)/foreground_channel_work_manager" - + struct Methods { struct Initialize { static let name = "\(Initialize.self)".lowercasingFirst @@ -59,34 +59,34 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } } - - //Handlers + + // Handlers @available(iOS 13.0, *) private static func handleBGProcessingTask(_ task: BGProcessingTask) { let operationQueue = OperationQueue() - + // Create an operation that performs the main part of the background task let operation = BackgroundTaskOperation( task.identifier, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback ) - + // Provide an expiration handler for the background task // 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 @available(iOS 13.0, *) public static func handleAppRefresh(task: BGAppRefreshTask) { @@ -96,44 +96,44 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") return } - + let taskSessionStart = Date() let taskSessionIdentifier = UUID() - + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) debugHelper.showStartBGRefreshNotification( startDate: taskSessionStart, callBackHandle: callbackHandle, callbackInfo: flutterCallbackInformation ) - ///TODO get seconds - scheduleAppRefresh(withIdentifier: task.identifier,earliestbeginInSeconds: 120) + /// Could improved _ seconds are ignored + scheduleAppRefresh(withIdentifier: task.identifier, earliestBeginInSeconds: 120) let semaphore = DispatchSemaphore(value: 0) - + DispatchQueue.main.async { let worker = BackgroundWorker(mode: .backgroundAppRefresh(identifier: self.identifier), flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) - + worker.performBackgroundRequest { _ in semaphore.signal() } } - //timeout after 29seconds ,max execution time is 30seconds - //-> 1 second for dispatching and other stuff (Flutter Messenger etc) - let dispatchResult = semaphore.wait(timeout:DispatchTime.now()+29) - + // timeout after 29seconds ,max execution time is 30seconds + // -> 1 second for dispatching and other stuff (Flutter Messenger etc) + let dispatchResult = semaphore.wait(timeout: DispatchTime.now()+29) + print("handleAppRefresh \(dispatchResult)") debugHelper.showCompletedBGRefreshNotification( completedDate: Date(), result: dispatchResult == .timedOut ? .failed : .newData, elapsedTime: Date().timeIntervalSince(taskSessionStart)) - + } - - ///register names for BGProcessingTask called by workmanger.m - ///you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown + + /// register names for BGProcessingTask called by workmanger.m + /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown @objc - public static func registerBackgroundProcessingTask(taskIdentifier identifier: String){ + public static func registerBackgroundProcessingTask(taskIdentifier identifier: String) { if #available(iOS 13.0, *) { print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") BGTaskScheduler.shared.register( @@ -146,62 +146,59 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } } - + @objc public static func registerAppRefreshTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { print("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") - + BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, using: nil ) { task in - if let task = task as? BGAppRefreshTask{ + if let task = task as? BGAppRefreshTask { handleAppRefresh(task: task) } }}} - + @objc public static func registerBackgroundProcessingTaskScheduler(withIdentifier identifier: String, - earliestBeginInSeconds begin:Double, - requiresNetworkConnectivity:Bool, - requiresExternalPower:Bool) { + earliestBeginInSeconds begin: Double, + requiresNetworkConnectivity: Bool, + requiresExternalPower: Bool) { if #available(iOS 13.0, *) { print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(identifier)") - scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) - - //set notificationhandler on app did enter background - //NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil - // ) { (notification) in - // //schedule scheduleBackgroundProcessingTask - // scheduleBackgroundProcessingTask(withIdentifier: identifier, earliestbeginInSeconds: begin, requiresNetworkConnectivity:requiresNetworkConnectivity, requiresExternalPower: requiresExternalPower) - // } + scheduleBackgroundProcessingTask(withIdentifier: identifier, + earliestBeginInSeconds: begin, + requiresNetworkConnectivity: requiresNetworkConnectivity, + requiresExternalPower: requiresExternalPower) } - } - - - + @objc - public static func registerAppRefreshTaskScheduler(withIdentifier identifier: String, earliestbeginInSeconds begin:Double) { - if #available(iOS 13.0, *) { - print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") - //schedule on app did enter background - NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil - ) { (notification) in - //schedule apprefresh - scheduleAppRefresh(withIdentifier: identifier,earliestbeginInSeconds: begin) + public static func registerAppRefreshTaskScheduler( + withIdentifier identifier: String, + earliestBeginInSeconds begin: Double) { + if #available(iOS 13.0, *) { + print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") + // schedule on app did enter background + NotificationCenter.default.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, queue: nil + ) { (_) in + // schedule apprefresh + scheduleAppRefresh(withIdentifier: identifier, earliestBeginInSeconds: begin) + } } } + + static func callback(_: UIBackgroundFetchResult) { } - - static func callback(_: UIBackgroundFetchResult){ - } - + @objc @available(iOS 13.0, *) - private static func scheduleAppRefresh(withIdentifier identifier: String, earliestbeginInSeconds begin:Double) { - + private static func scheduleAppRefresh(withIdentifier identifier: String, earliestBeginInSeconds begin: Double) { + let request = BGAppRefreshTaskRequest( identifier: identifier) request.earliestBeginDate = Date(timeIntervalSinceNow: begin) @@ -211,15 +208,17 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } catch { print("Couldn't schedule app refresh \(error.localizedDescription)") return - + } } - + @objc @available(iOS 13.0, *) - private static func scheduleBackgroundProcessingTask(withIdentifier identifier: String, earliestBeginInSeconds begin:Double, - requiresNetworkConnectivity:Bool, - requiresExternalPower:Bool + private static func scheduleBackgroundProcessingTask( + withIdentifier identifier: String, + earliestBeginInSeconds begin: Double, + requiresNetworkConnectivity: Bool, + requiresExternalPower: Bool ) { let request = BGProcessingTaskRequest( identifier: identifier) @@ -240,12 +239,12 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { // MARK: - FlutterPlugin conformance extension SwiftWorkmanagerPlugin: FlutterPlugin { - + @objc public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { flutterPluginRegistrantCallback = callback } - + public static func register(with registrar: FlutterPluginRegistrar) { let foregroundMethodChannel = FlutterMethodChannel( name: ForegroundMethodChannel.channelName, @@ -255,131 +254,157 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { registrar.addMethodCallDelegate(instance, channel: foregroundMethodChannel) registrar.addApplicationDelegate(instance) } - - //added to .swiftlint.yml following lines - //because error on Xcode build Function body should span 40 lines or less excluding comments and whitespace - //function_body_length: - //warning: 300 - //error: 500 - // swiftlint:disable:next cyclomatic_complexity + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch (call.method, call.arguments as? [AnyHashable: Any]) { case (ForegroundMethodChannel.Methods.Initialize.name, let .some(arguments)): - if _isInitalized { - result(WMPError.workmanagerIsAlreadyInitialized) - return - } - let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result:result) - if (backgroundRefreshAvailable != BackgroundAuthorisationState.available){ - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - return - } - let method = ForegroundMethodChannel.Methods.Initialize.self - guard let isInDebug = arguments[method.Arguments.isInDebugMode.rawValue] as? Bool, - let handle = arguments[method.Arguments.callbackHandle.rawValue] as? Int64 else { - result(WMPError.invalidParameters.asFlutterError) - return - } - - UserDefaultsHelper.storeCallbackHandle(handle) - UserDefaultsHelper.storeIsDebug(isInDebug) + initialize(arguments: arguments, result: result) return - case (ForegroundMethodChannel.Methods.CheckBackgroundRefreshPermission.name, .some(_)): - _=checkBackgroundRefreshAuthorisation(result:result) + case (ForegroundMethodChannel.Methods.CheckBackgroundRefreshPermission.name, .some): + _=checkBackgroundRefreshAuthorisation(result: result) return - //register bgAppRefreshTask for less than 30 seconds backgroundtime case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): - print("Registering periodic task in background (BGAppRefreshTask)") - if !validateCallbackHandle(result:result) { + // register bgAppRefreshTask for less than 30 seconds backgroundtime + registerPeriodicTask(arguments: arguments, result: result) + return + case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): + // register processingtask for more than 30 seconds backgroundtime + registerOneOffTask(arguments: arguments, result: result) + return + case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): + cancelAllTasks(result: result) + return + case (ForegroundMethodChannel.Methods.CancelTaskByUniqueName.name, let .some(arguments)): + cancelTaskByUniqueName(arguments: arguments, result: result) + return + default: + result(WMPError.unhandledMethod(call.method).asFlutterError) + return + } + } + + private func initialize(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + if _isInitalized { + result(WMPError.workmanagerIsAlreadyInitialized) + return + } +#if targetEnvironment(simulator) + print("Workmanager Info: Please run on real device!" + + "No backgroundtask is automatic called in the simulator!!") +#endif + let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result: result) + if backgroundRefreshAvailable != BackgroundAuthorisationState.available { + UIApplication.shared.open(URL( + string: UIApplication.openSettingsURLString)!, + options: [:], + completionHandler: nil) + return + } + let method = ForegroundMethodChannel.Methods.Initialize.self + guard let isInDebug = arguments[method.Arguments.isInDebugMode.rawValue] as? Bool, + let handle = arguments[method.Arguments.callbackHandle.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return + } + UserDefaultsHelper.storeCallbackHandle(handle) + UserDefaultsHelper.storeIsDebug(isInDebug) + } + + private func registerPeriodicTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + print("Registering periodic task in background (BGAppRefreshTask)") + if !validateCallbackHandle(result: result) { + return + } + + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self + guard let identifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { + result(WMPError.invalidParameters.asFlutterError) return } - - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self - guard let identifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - guard let initialDelaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { - result(WMPError.invalidParameters.asFlutterError) - return - } - //task will scheduled when app goes to background - SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler(withIdentifier:identifier, earliestbeginInSeconds: Double(initialDelaySeconds)) - print("Registered PeriodicTask \(identifier)") - result(true) - return; - + guard let initialDelaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return } - result(FlutterError(code: "99", message: "Not registered", details: "iOS Version lower than 13.0")) + // task will scheduled when app goes to background + SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler( + withIdentifier: identifier, + earliestBeginInSeconds: Double(initialDelaySeconds)) + print("Registered PeriodicTask \(identifier) delaySeconds \(initialDelaySeconds)") + result(true) return - - //register processingtask for more than 30 seconds backgroundtime - case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): - print("Registering OneOffTask (BackgroundProcessingTask)") - if !validateCallbackHandle(result:result) { + } else { + result(FlutterError(code: "99", + message: "RegisterPeriodicTask is not registered", + details: "iOS Version lower than 13.0")) + } + } + + private func registerOneOffTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + print("Registering OneOffTask (BackgroundProcessingTask)") + if !validateCallbackHandle(result: result) { + return + } + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.RegisterOneOffTask.self + guard let delaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) return } - - - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisterOneOffTask.self - guard let delaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { - result(WMPError.invalidParameters.asFlutterError) - return - } - guard let identifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - let requiresCharging = arguments[method.Arguments.requiresCharging.rawValue] as? Bool ?? false - var requiresNetwork = false - if let networkTypeInput = arguments[method.Arguments.networkType.rawValue] as? String, - let networkType = NetworkType(fromDart: networkTypeInput), - networkType == .connected || networkType == .metered { - requiresNetwork = true - } - //task will scheduled when app goes to background - SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler(withIdentifier:identifier, earliestBeginInSeconds: Double(delaySeconds), requiresNetworkConnectivity: requiresCharging, requiresExternalPower: requiresNetwork) - - result(true) + guard let identifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { + result(WMPError.invalidParameters.asFlutterError) return - } else { - result(WMPError.unhandledMethod(call.method).asFlutterError) } - - case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): - if #available(iOS 13.0, *) { - BGTaskScheduler.shared.cancelAllTaskRequests() + let requiresCharging = arguments[method.Arguments.requiresCharging.rawValue] as? Bool ?? false + var requiresNetwork = false + if let networkTypeInput = arguments[method.Arguments.networkType.rawValue] as? String, + let networkType = NetworkType(fromDart: networkTypeInput), + networkType == .connected || networkType == .metered { + requiresNetwork = true } + // task will scheduled when app goes to background + SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler( + withIdentifier: identifier, + earliestBeginInSeconds: Double(delaySeconds), + requiresNetworkConnectivity: requiresCharging, + requiresExternalPower: requiresNetwork) result(true) - - case (ForegroundMethodChannel.Methods.CancelTaskByUniqueName.name, let .some(arguments)): - if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.CancelTaskByUniqueName.self - guard let identifier = arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) - } - result(true) - - default: - result(WMPError.unhandledMethod(call.method).asFlutterError) return + } else { + result(FlutterError(code: "99", + message: "RegisterPeriodicTask is not registered", + details: "iOS Version lower than 13.0")) } } - - ///Checks wether getStoredCallbackHandle is set - ///Returns true wenn initilized - ///if false result contains errormessage + + private func cancelAllTasks(result: @escaping FlutterResult) { + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.cancelAllTaskRequests() + } + result(true) + } + + private func cancelTaskByUniqueName(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.CancelTaskByUniqueName.self + guard let identifier = arguments[method.Arguments.uniqueName.rawValue] as? String else { + result(WMPError.invalidParameters.asFlutterError) + return + } + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier) + } + result(true) + } + + /// Checks wether getStoredCallbackHandle is set + /// Returns true wenn initilized + /// if false result contains errormessage private func validateCallbackHandle(result: @escaping FlutterResult) -> Bool { - if UserDefaultsHelper.getStoredCallbackHandle() == nil{ + if UserDefaultsHelper.getStoredCallbackHandle() == nil { result( FlutterError( code: "1", @@ -396,20 +421,16 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { details: nil ) ) - return false; + return false } - return true; + return true } - - - - } // MARK: - AppDelegate conformance extension SwiftWorkmanagerPlugin { - + override public func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void @@ -418,8 +439,6 @@ extension SwiftWorkmanagerPlugin { mode: .backgroundFetch, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback ) - return worker.performBackgroundRequest(completionHandler) } - } diff --git a/ios/Classes/ThumbnailGenerator.swift b/ios/Classes/ThumbnailGenerator.swift index a6c64a40..00592074 100644 --- a/ios/Classes/ThumbnailGenerator.swift +++ b/ios/Classes/ThumbnailGenerator.swift @@ -92,7 +92,6 @@ private extension UIImage { throw ImageError.cannotRepresentAsPNG(self) } try imageData.write(to: fileURL) - return fileURL } diff --git a/ios/Classes/UserDefaultsHelper.swift b/ios/Classes/UserDefaultsHelper.swift index 562b0930..7f2f5a83 100644 --- a/ios/Classes/UserDefaultsHelper.swift +++ b/ios/Classes/UserDefaultsHelper.swift @@ -25,7 +25,7 @@ struct UserDefaultsHelper { // MARK: callbackHandle static func storeCallbackHandle(_ handle: Int64) { - store(handle, key: .callbackHandle) + store(handle, key: .callbackHandle) } static func getStoredCallbackHandle() -> Int64? { diff --git a/ios/Classes/WMPError.swift b/ios/Classes/WMPError.swift index 4e908423..d281aa72 100644 --- a/ios/Classes/WMPError.swift +++ b/ios/Classes/WMPError.swift @@ -35,9 +35,7 @@ enum WMPError: Error { case .bgTaskSchedulingFailed(let error): return """ Scheduling the task using BGTaskScheduler has failed. - This may be due to too many tasks being scheduled but not run. - See the error for details: \(error). """ case .workmanagerNotInitialized: From 3b133fd6435527353eac14a3459aa556f0bfe455 Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Mon, 2 Jan 2023 21:47:31 +0100 Subject: [PATCH 05/26] text to display task event dates (show prefs) added. --- example/lib/main.dart | 72 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9dbaef70..0162b368 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,50 +27,65 @@ const iOSBackgroundAppRefresh = 'vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { + final prefs = await SharedPreferences.getInstance(); switch (task) { case simpleTaskKey: + sleep(Duration(seconds: 22)); // sleep as sample print("$simpleTaskKey was executed. inputData = $inputData"); - final prefs = await SharedPreferences.getInstance(); prefs.setBool("test", true); print("Bool from prefs: ${prefs.getBool("test")}"); + prefs.setString( + "simpleTaskKey", (DateTime.now().toString()) + ' data:$inputData'); break; case rescheduledTaskKey: final key = inputData!['key']!; - final prefs = await SharedPreferences.getInstance(); + prefs.setString("rescheduledTaskKey", DateTime.now().toString()); if (prefs.containsKey('unique-$key')) { print('has been running before, task is successful'); return true; } else { - await prefs.setBool('unique-$key', true); + prefs.setBool('unique-$key', true); print('reschedule task'); return false; } case failedTaskKey: print('failed task'); + prefs.setString("failedTask", DateTime.now().toString()); return Future.error('failed'); case simpleDelayedTask: print("$simpleDelayedTask was executed"); + prefs.setString("simpleDelayedTask", DateTime.now().toString()); break; case simplePeriodicTask: print("$simplePeriodicTask was executed"); + prefs.setString("simplePeriodicTask", DateTime.now().toString()); break; case simplePeriodic1HourTask: print("$simplePeriodic1HourTask was executed"); + prefs.setString("simplePeriodic1HourTask", DateTime.now().toString()); break; case Workmanager.iOSBackgroundTask: print("The iOS background fetch was triggered"); + sleep(Duration(seconds: 34)); // sleep as sample Directory? tempDir = await getTemporaryDirectory(); String? tempPath = tempDir.path; - sleep(Duration(seconds: 55)); print( "You can access other plugins in the background, for example Directory.getTemporaryDirectory(): $tempPath"); + prefs.setString( + Workmanager.iOSBackgroundTask, DateTime.now().toString()); break; case Workmanager.iOSBackgroundAppRefresh: //maximum duration 29seconds - App could perhaps killed by iOS when it takes a longer time than 30 seconds for BGAppRefresh included native work - print("The iOSBackgroundAppRefresh was triggered"); - sleep(Duration(seconds: 11)); // sleep as sample + print("The iOS-BackgroundAppRefresh was triggered"); + sleep(Duration(seconds: 14)); // sleep as sample + prefs.setString( + Workmanager.iOSBackgroundAppRefresh, DateTime.now().toString()); // test on debugger - pause debugger in xcode and enter in terminal ( Connected with real device ) + // pause app and enter in Terminal: // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] + // then resume app + //expire earlier + //e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] break; } return Future.value(true); @@ -82,8 +97,45 @@ class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); } -class _MyAppState extends State { +class _MyAppState extends State with WidgetsBindingObserver { bool workmanagerInitialized = false; + String _prefsString = "empty"; + String _lastResumed = DateTime.now().toString(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.paused) { + //app switched to Background + } + if (state == AppLifecycleState.resumed) { + //app came back to Foreground sett infotext as example for iOS + ///TODO implement Android + final prefs = await SharedPreferences.getInstance(); + var prefKeys = prefs.getKeys(); + var prefsString = ""; + for (var key in prefKeys) { + prefsString += + key.toString() + " : " + prefs.get(key).toString() + "\n"; + } + setState(() { + _lastResumed = DateTime.now().toString(); + _prefsString = prefsString; + }); + } + } @override Widget build(BuildContext context) { @@ -297,6 +349,12 @@ class _MyAppState extends State { print('Cancel all tasks completed'); }, ), + //show entries in prefs on app resume + Text("SharedPrefs Values(executed timestamps):\n" + + _prefsString + + "\n" + + "Last-app resumed at: " + + _lastResumed) ], ), ), From 27726a81942c0edf5a39af1864deb00e3495d681 Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Wed, 11 Jan 2023 21:47:00 +0100 Subject: [PATCH 06/26] fixed workmanager iOS Part fixed BGProcessing fixed inputdata in callback on task clarified timings --- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- example/ios/Runner/AppDelegate.swift | 9 +- example/ios/Runner/Base.lproj/Main.storyboard | 13 +- example/ios/Runner/Info.plist | 1 + example/lib/log_helper.dart | 57 +++ example/lib/main.dart | 304 +++++++++------- ios/Classes/BackgroundTaskOperation.swift | 80 ++++- ios/Classes/BackgroundWorker.swift | 55 ++- ios/Classes/DebugNotificationHelper.swift | 73 ++-- ios/Classes/SwiftWorkmanagerPlugin.swift | 338 ++++++++++++------ ios/Classes/ThumbnailGenerator.swift | 18 +- ios/Classes/WorkmanagerPlugin.h | 11 +- ios/Classes/WorkmanagerPlugin.m | 8 +- lib/src/workmanager.dart | 79 ++-- test/workmanager_test.mocks.dart | 109 ------ 15 files changed, 671 insertions(+), 490 deletions(-) create mode 100644 example/lib/log_helper.dart delete mode 100644 test/workmanager_test.mocks.dart diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 879761ae..49929df0 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -488,7 +488,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_VERSION = 4.2; @@ -572,7 +572,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -623,7 +623,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 63415d86..997d0656 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -27,14 +27,17 @@ import workmanager // be.tramckrijte.workmanagerExample.taskId // // - WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId") + /*WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.failedTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask") WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodicTask") - WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask") - WorkmanagerPlugin.registerPeriodicTask( withIdentifier: "app.workmanagerExample.iOSBackgroundAppRefresh") + WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask")*/ + + //important to register backgroundprocessingtask in Runner/AppDelegate and info.plist + WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "app.workmanagerExample.iOSBackgroundAppRefresh") + WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "app.workmanagerExample.iOSBackgroundProcessingTask") return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard index f3c28516..99fa1f42 100644 --- a/example/ios/Runner/Base.lproj/Main.storyboard +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 42839d01..ccd3f3fb 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -12,6 +12,7 @@ be.tramckrijte.workmanagerExample.simplePeriodicTask be.tramckrijte.workmanagerExample.simplePeriodic1HourTask app.workmanagerExample.iOSBackgroundAppRefresh + app.workmanagerExample.iOSBackgroundProcessingTask CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/example/lib/log_helper.dart b/example/lib/log_helper.dart new file mode 100644 index 00000000..a51c1744 --- /dev/null +++ b/example/lib/log_helper.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +///Helper to write events to a local file, because SharedPrefs doesn't sync datas between isolated +class LogHelper { + static const String _backgroundTaskLogFileName = "iOSBackgroundTask.log"; + + ///Write actual [iOSBackgroundTask] [DateTime] event + static Future LogBGTask({String data = ""}) async { + try { + var logFile = await _getOrCreateFile(_backgroundTaskLogFileName); + if (logFile == null) { + return; + } + + var content = await ReadLogBGTask(); + var sink = logFile.openWrite(); + content += + 'BackgroundTaskRefresh ran on ${DateTime.now()} - Data ${data}\n\n'; + sink.write(content); + await sink.flush(); + await sink.close(); + } catch (e) { + print( + 'Error on LogHelper.LogBGTask $_backgroundTaskLogFileName with exception $e'); + } + } + + ///Read actual iOSBackgroundTask [DateTime] events as [String] + static Future ReadLogBGTask() async { + var logFile = await _getOrCreateFile(_backgroundTaskLogFileName); + if (logFile == null) { + return ("Couldn't open $_backgroundTaskLogFileName"); + } + try { + return await logFile.readAsString(); + } catch (e) { + return "Error:${e}"; + } + } + + static Future _getOrCreateFile(String fileName) async { + Directory appDocDir = await getApplicationDocumentsDirectory(); + String appDocPath = appDocDir.path; + File file = File('$appDocPath/$fileName'); + try { + if (await file.exists()) { + return file; + } + return await file.create(); + } catch (e) { + print('Error on LogHelper._getOrCreateFile $fileName with exception $e'); + return null; + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 0162b368..88185f76 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,10 +3,11 @@ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; +import 'log_helper.dart'; + void main() { //added MaterialApp for showdialog runApp(MaterialApp(home: MyApp())); @@ -20,73 +21,102 @@ const simplePeriodicTask = "be.tramckrijte.workmanagerExample.simplePeriodicTask"; const simplePeriodic1HourTask = "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask"; +//Don'T forget to register these two task in info.plist and AppDelegate.swift (iOS) const iOSBackgroundAppRefresh = "app.workmanagerExample.iOSBackgroundAppRefresh"; +const iOSBackgroundProcessingTask = + "app.workmanagerExample.iOSBackgroundProcessingTask"; @pragma( 'vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { - final prefs = await SharedPreferences.getInstance(); + print("callbackDispatcher for $task called"); + await LogHelper.LogBGTask(data: "callbackDispatcher for $task called"); + final prefs = await SharedPreferences + .getInstance(); //only working on Android ?! isolates on is has incorrect results. switch (task) { case simpleTaskKey: - sleep(Duration(seconds: 22)); // sleep as sample + sleep(Duration(seconds: 12)); // sleep as sample print("$simpleTaskKey was executed. inputData = $inputData"); - prefs.setBool("test", true); - print("Bool from prefs: ${prefs.getBool("test")}"); prefs.setString( "simpleTaskKey", (DateTime.now().toString()) + ' data:$inputData'); + LogHelper.LogBGTask(data: 'simpleTaskKey --> data:$inputData'); break; case rescheduledTaskKey: + if (inputData == null) { + LogHelper.LogBGTask(data: "Rescheduled Task without inputData"); + sleep(Duration(seconds: 2)); + return Future.value(true); + } final key = inputData!['key']!; prefs.setString("rescheduledTaskKey", DateTime.now().toString()); if (prefs.containsKey('unique-$key')) { print('has been running before, task is successful'); return true; } else { - prefs.setBool('unique-$key', true); + prefs.setBool('unique-$key', true); //perhaps not working on iOS print('reschedule task'); - return false; + return Future.value(true); + ; } case failedTaskKey: print('failed task'); prefs.setString("failedTask", DateTime.now().toString()); + LogHelper.LogBGTask(data: 'failedTaskKey --> data:$inputData'); return Future.error('failed'); case simpleDelayedTask: print("$simpleDelayedTask was executed"); prefs.setString("simpleDelayedTask", DateTime.now().toString()); + LogHelper.LogBGTask(data: 'simpleDelayedTaskKey --> data:$inputData'); break; case simplePeriodicTask: print("$simplePeriodicTask was executed"); prefs.setString("simplePeriodicTask", DateTime.now().toString()); + LogHelper.LogBGTask(data: 'simplePeriodicTaskKey --> data:$inputData'); break; case simplePeriodic1HourTask: print("$simplePeriodic1HourTask was executed"); prefs.setString("simplePeriodic1HourTask", DateTime.now().toString()); + LogHelper.LogBGTask( + data: 'simplePeriodic1HourTask --> data:$inputData'); break; - case Workmanager.iOSBackgroundTask: - print("The iOS background fetch was triggered"); - sleep(Duration(seconds: 34)); // sleep as sample - Directory? tempDir = await getTemporaryDirectory(); - String? tempPath = tempDir.path; - print( - "You can access other plugins in the background, for example Directory.getTemporaryDirectory(): $tempPath"); - prefs.setString( - Workmanager.iOSBackgroundTask, DateTime.now().toString()); - break; - case Workmanager.iOSBackgroundAppRefresh: + case Workmanager + .BACKGROUND_APPREFRESH_TASK_NAME: //Fixed value can't change at the moment - see [BackgroundMode.onResultSendArguments] //maximum duration 29seconds - App could perhaps killed by iOS when it takes a longer time than 30 seconds for BGAppRefresh included native work print("The iOS-BackgroundAppRefresh was triggered"); sleep(Duration(seconds: 14)); // sleep as sample - prefs.setString( - Workmanager.iOSBackgroundAppRefresh, DateTime.now().toString()); - // test on debugger - pause debugger in xcode and enter in terminal ( Connected with real device ) + await LogHelper.LogBGTask(data: "iOSBackgroundAppRefresh"); + //iOS SharedPrefs does not work, because they will not updated in isolated + //**** + // prefs.setString(Workmanager.iOSBackgroundAppRefresh, DateTime.now().toString()); + //**** + // test on debugger + // push home-button an let app enter background + // pause debugger in xcode and enter in terminal ( Connected with real device ) // pause app and enter in Terminal: // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] // then resume app - //expire earlier - //e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] break; + case Workmanager + .BACKGROUND_PROCESSING_TASK_NAME: //Fixed value can't change at the moment - see [BackgroundMode.onResultSendArguments] + //here you can run a long running process longer than 30 seconds. It will randomly started by iOS-Operating-System + // test on debugger - pause debugger in xcode and enter in terminal ( Connected with real device ) + // push home-button an let app enter background + // pause app and enter in Terminal: + // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundProcessingTask"] + // then resume app + print("The iOS-BGProcessingTask was triggered"); + await LogHelper.LogBGTask(data: "iOSBackgroundProcessingTask started"); + sleep(Duration(seconds: 210)); // sleep as sample + await LogHelper.LogBGTask( + data: "iOSBackgroundProcessingTask finished (sleep 210 sec)"); + break; + default: + print('callbackhandler: unknown task: $task data:$inputData'); + LogHelper.LogBGTask(data: 'unknown task: $task data:$inputData'); + sleep(Duration(seconds: 5)); + return Future.value(false); } return Future.value(true); }); @@ -121,22 +151,23 @@ class _MyAppState extends State with WidgetsBindingObserver { //app switched to Background } if (state == AppLifecycleState.resumed) { - //app came back to Foreground sett infotext as example for iOS - ///TODO implement Android - final prefs = await SharedPreferences.getInstance(); - var prefKeys = prefs.getKeys(); - var prefsString = ""; - for (var key in prefKeys) { - prefsString += - key.toString() + " : " + prefs.get(key).toString() + "\n"; - } + //app came back to Foreground set infotext in example app setState(() { _lastResumed = DateTime.now().toString(); - _prefsString = prefsString; }); + _updatePrefs(); } } + void _updatePrefs() async { + var prefsString = "BgDatalog" + "\n"; + var log = await LogHelper.ReadLogBGTask(); + prefsString += log; + setState(() { + _prefsString = prefsString; + }); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -188,105 +219,98 @@ class _MyAppState extends State with WidgetsBindingObserver { if (!workmanagerInitialized) { Workmanager().initialize( callbackDispatcher, - isInDebugMode: true, + isInDebugMode: true, //Show notifications on iOS native ); - workmanagerInitialized = true; + setState(() { + workmanagerInitialized = true; + }); } }, ), - SizedBox(height: 16), - + SizedBox(height: 5), + Text( + "Sample tasks to start", + style: Theme.of(context).textTheme.headline5, + ), //This task runs once. //Most likely this will trigger immediately + ///Immedately start a background fetch with 29sec timeout - specification by iOS ElevatedButton( child: Text("Register OneOff Task"), - onPressed: () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - Workmanager().registerOneOffTask( - simpleTaskKey, - simpleTaskKey, - inputData: { - 'int': 1, - 'bool': true, - 'double': 1.0, - 'string': 'string', - 'array': [1, 2, 3], - }, - ); - }, + onPressed: workmanagerInitialized + ? () { + Workmanager().registerOneOffTask( + simpleTaskKey, + //unique Name - must same as in iOS registered Id in info.plist + simpleTaskKey, //ignored on iOS + inputData: { + 'int': 1, + 'bool': true, + 'double': 1.0, + 'string': 'string', + 'array': [1, 2, 3], + 'timeStamp': DateTime.now().toString() + }, + ); + } + : null, ), ElevatedButton( child: Text("Register rescheduled Task"), - onPressed: () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - Workmanager().registerOneOffTask( - rescheduledTaskKey, - rescheduledTaskKey, - inputData: { - 'key': Random().nextInt(64000), - }, - ); - }), + onPressed: workmanagerInitialized + ? () { + Workmanager().registerOneOffTask( + rescheduledTaskKey, + rescheduledTaskKey, + requiresCharging: false, + networkType: NetworkType.not_required, + inputData: { + 'key': Random().nextInt(64000), + 'timeStamp': DateTime.now().toString() + }, + ); + } + : null), ElevatedButton( - child: Text("Register failed Task"), - onPressed: () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - Workmanager().registerOneOffTask( - failedTaskKey, - failedTaskKey, - ); - }), + child: Text("Register failed Task"), + onPressed: workmanagerInitialized + ? () { + Workmanager().registerOneOffTask( + failedTaskKey, + failedTaskKey, + ); + } + : null, + ), //This task runs once //This wait at least 120 seconds before running ElevatedButton( - child: Text("Register Delayed OneOff Task"), - onPressed: () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - Workmanager().registerOneOffTask( - simpleDelayedTask, - simpleDelayedTask, - initialDelay: Duration(seconds: 120), - ); - }), + child: Text("Register Delayed OneOff Task"), + onPressed: workmanagerInitialized + ? () { + if (!workmanagerInitialized) { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, + ); + workmanagerInitialized = true; + } + Workmanager().registerOneOffTask( + simpleDelayedTask, + simpleDelayedTask, + initialDelay: Duration(seconds: 120), + ); + } + : null, + ), SizedBox(height: 8), //This task runs periodically //It will wait at least 120 seconds before its first launch //Since we have not provided a frequency it will be the default 15 minutes ElevatedButton( child: Text("Register Periodic Task (Android)"), - onPressed: Platform.isAndroid + onPressed: Platform.isAndroid && workmanagerInitialized ? () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } Workmanager().registerPeriodicTask( simplePeriodicTask, simplePeriodicTask, @@ -301,7 +325,7 @@ class _MyAppState extends State with WidgetsBindingObserver { ElevatedButton( child: Text("Register Periodic Background App Refresh (iOS)"), - onPressed: Platform.isIOS + onPressed: Platform.isIOS && workmanagerInitialized ? () async { if (!workmanagerInitialized) { Workmanager().initialize( @@ -311,18 +335,17 @@ class _MyAppState extends State with WidgetsBindingObserver { workmanagerInitialized = true; } await Workmanager().registerPeriodicTask( - iOSBackgroundAppRefresh, - iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 120), //ignored - ); + iOSBackgroundAppRefresh, + iOSBackgroundAppRefresh, + initialDelay: Duration(seconds: 120), //ignored + inputData: {} //ignored on iOS + ); } : null), - //This task runs periodically - //It will run about every hour ElevatedButton( - child: Text("Register 1 hour Periodic Task (Android)"), - onPressed: Platform.isAndroid - ? () { + child: Text("Register BackgroundProcessingTask (iOS)"), + onPressed: Platform.isIOS && workmanagerInitialized + ? () async { if (!workmanagerInitialized) { Workmanager().initialize( callbackDispatcher, @@ -330,8 +353,20 @@ class _MyAppState extends State with WidgetsBindingObserver { ); workmanagerInitialized = true; } + await Workmanager() + .registeriOSBackgroundProcessingTask( + iOSBackgroundProcessingTask, + iOSBackgroundProcessingTask); + } + : null), + //This task runs periodically + //It will run about every hour + ElevatedButton( + child: Text("Register 1 hour Periodic Task (Android)"), + onPressed: Platform.isAndroid && workmanagerInitialized + ? () { Workmanager().registerPeriodicTask( - simplePeriodicTask, + simplePeriodic1HourTask, simplePeriodic1HourTask, frequency: Duration(hours: 1), ); @@ -344,17 +379,26 @@ class _MyAppState extends State with WidgetsBindingObserver { ), ElevatedButton( child: Text("Cancel All"), - onPressed: () async { - await Workmanager().cancelAll(); - print('Cancel all tasks completed'); - }, + onPressed: workmanagerInitialized + ? () async { + await Workmanager().cancelAll(); + print('Cancel all tasks completed'); + } + : null, ), //show entries in prefs on app resume - Text("SharedPrefs Values(executed timestamps):\n" + - _prefsString + - "\n" + - "Last-app resumed at: " + - _lastResumed) + GestureDetector( + onTap: () { + _updatePrefs(); + }, + child: SingleChildScrollView( + child: Text( + "Task Values(executed timestamps):\nTap here to update\n" + + _prefsString + + "\n" + + "Last-app resumed at: " + + _lastResumed)), + ), ], ), ), diff --git a/ios/Classes/BackgroundTaskOperation.swift b/ios/Classes/BackgroundTaskOperation.swift index 31837d37..b5f6adbe 100644 --- a/ios/Classes/BackgroundTaskOperation.swift +++ b/ios/Classes/BackgroundTaskOperation.swift @@ -7,28 +7,94 @@ import Foundation +/// Backgroundoperation with maximum 29 sec operation time - specification by iOS +/// Task will killed after 29sec because otherwise iOS will kill the app. class BackgroundTaskOperation: Operation { - private let identifier: String + private let inputData: String private let flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? + private let backgroundMode: BackgroundMode + private let isInDebug: Bool + + private var backgroundWorkerResult: UIBackgroundFetchResult = .noData - init(_ identifier: String, flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?) { + init(_ identifier: String, + inputData: String, + flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?, + backgroundMode: BackgroundMode, + isInDebug: Bool) { self.identifier = identifier + self.inputData = inputData self.flutterPluginRegistrantCallback = flutterPluginRegistrantCallback + self.backgroundMode = backgroundMode + self.isInDebug = isInDebug } + override func main() { - let semaphore = DispatchSemaphore(value: 0) + let taskSessionIdentifier = UUID() + let taskSessionStart = Date() + + if isInDebug { + guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), + let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + else { + logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") + return + } + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) + debugHelper.showStartFetchNotification( + startDate: taskSessionStart, + callBackHandle: callbackHandle, + callbackInfo: flutterCallbackInformation + ) + } + let semaphore = DispatchSemaphore(value: 0) + let worker = BackgroundWorker(mode: self.backgroundMode, + inputData: self.inputData, + flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) DispatchQueue.main.async { - let worker = BackgroundWorker(mode: .backgroundTask(identifier: self.identifier), - flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) - worker.performBackgroundRequest { _ in + worker.performBackgroundRequest { wk in + self.backgroundWorkerResult = wk as UIBackgroundFetchResult; semaphore.signal() } } + switch backgroundMode { + case .backgroundProcessingTask: + semaphore.wait() + + if isInDebug { + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) + let taskSessionCompleter = Date() + let taskDuration = taskSessionCompleter.timeIntervalSince(taskSessionStart) + logInfo("[\(String(describing: self))] \(#function) -> BackgroundTaskOperation.main (\(self.backgroundMode) no timeout) (finished in \(taskDuration.formatToSeconds()))") + debugHelper.showCompletedFetchNotification( + identifier: identifier, + completedDate: taskSessionCompleter, + result: self.backgroundWorkerResult, + elapsedTime: taskDuration + ) + } + break - semaphore.wait() + default: + /// maximum execution time 29 seconds + 1 second flutterstuff (callback etc) + let result = semaphore.wait(timeout: DispatchTime.now() + 29) + + if isInDebug { + let debugHelper = DebugNotificationHelper(taskSessionIdentifier) + let taskSessionCompleter = Date() + let taskDuration = taskSessionCompleter.timeIntervalSince(taskSessionStart) + logInfo("[\(String(describing: self))] \(#function) -> BackgroundTaskOperation.main (\(self.backgroundMode) timeout 29sec)(finished in \(taskDuration.formatToSeconds()))") + debugHelper.showCompletedFetchNotification( + identifier: identifier, + completedDate: taskSessionCompleter, + result: result == DispatchTimeoutResult.success ? self.backgroundWorkerResult : UIBackgroundFetchResult.failed, + elapsedTime: taskDuration + ) + } + } } } diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index f4af1e7b..1ea7a00e 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -8,19 +8,18 @@ import Foundation enum BackgroundMode { - case backgroundAppRefresh(identifier: String) - case backgroundFetch - case backgroundTask(identifier: String) + case backgroundAppRefresh + case backgroundProcessingTask + case backgroundOnOffTask(identifier: String) var flutterThreadlabelPrefix: String { switch self { case .backgroundAppRefresh: - return "\(SwiftWorkmanagerPlugin.identifier).BackgroundAppRefresh" - case .backgroundFetch: - return "\(SwiftWorkmanagerPlugin.identifier).BackgroundFetch" - case .backgroundTask: - return "\(SwiftWorkmanagerPlugin.identifier).BGTaskScheduler" - + return "\(SwiftWorkmanagerPlugin.identifier).BackgroundAppRefreshTask" + case .backgroundProcessingTask: + return "\(SwiftWorkmanagerPlugin.identifier).BackgroundProcessingTask" + case .backgroundOnOffTask: + return "\(SwiftWorkmanagerPlugin.identifier).OnOffTask" } } @@ -28,21 +27,22 @@ enum BackgroundMode { switch self { case .backgroundAppRefresh: return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundAppRefresh"] - case .backgroundFetch: - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSPerformFetch"] - case .backgroundTask(let identifier): + case .backgroundProcessingTask: + return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundProcessingTask"] + case let .backgroundOnOffTask(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] } } } class BackgroundWorker { - let backgroundMode: BackgroundMode + let inputData: String let flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? - init(mode: BackgroundMode, flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?) { - self.backgroundMode = mode + init(mode: BackgroundMode, inputData: String, flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?) { + backgroundMode = mode + self.inputData = inputData self.flutterPluginRegistrantCallback = flutterPluginRegistrantCallback } @@ -64,14 +64,6 @@ class BackgroundWorker { } let taskSessionStart = Date() - let taskSessionIdentifier = UUID() - - let debugHelper = DebugNotificationHelper(taskSessionIdentifier) - debugHelper.showStartFetchNotification( - startDate: taskSessionStart, - callBackHandle: callbackHandle, - callbackInfo: flutterCallbackInformation - ) var flutterEngine: FlutterEngine? = FlutterEngine( name: backgroundMode.flutterThreadlabelPrefix, @@ -96,14 +88,20 @@ class BackgroundWorker { flutterEngine = nil } - backgroundMethodChannel?.setMethodCallHandler { (call, result) in + backgroundMethodChannel?.setMethodCallHandler { call, result in switch call.method { case BackgroundChannel.initialized: - result(true) // Agree to Flutter's method invocation + result(true) // Agree to Flutter's method invocation + var arguments = self.backgroundMode.onResultSendArguments + if self.inputData != ""{ + arguments = arguments.merging(["be.tramckrijte.workmanager.INPUT_DATA": self.inputData]) { current, _ in current } + logDebug("[\(String(describing: self))] \(#function) -> BackgroundWorker.backgroundMethodChannel \(arguments.debugDescription) will called. INPUT_DATA: \(self.inputData)") + + } backgroundMethodChannel?.invokeMethod( BackgroundChannel.onResultSendCommand, - arguments: self.backgroundMode.onResultSendArguments, + arguments:arguments, result: { flutterResult in cleanupFlutterResources() let taskSessionCompleter = Date() @@ -111,11 +109,6 @@ class BackgroundWorker { let taskDuration = taskSessionCompleter.timeIntervalSince(taskSessionStart) logInfo("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration.formatToSeconds()))") - debugHelper.showCompletedFetchNotification( - completedDate: taskSessionCompleter, - result: result, - elapsedTime: taskDuration - ) completionHandler(result) }) default: diff --git a/ios/Classes/DebugNotificationHelper.swift b/ios/Classes/DebugNotificationHelper.swift index 458bcf96..de1ded3d 100644 --- a/ios/Classes/DebugNotificationHelper.swift +++ b/ios/Classes/DebugNotificationHelper.swift @@ -9,82 +9,51 @@ import Foundation import UserNotifications class DebugNotificationHelper { - private let identifier: UUID init(_ identifier: UUID) { self.identifier = identifier } - func showStartFetchNotification(startDate: Date, - callBackHandle: Int64, - callbackInfo: FlutterCallbackInformation + func showStartFetchNotification( + startDate: Date, + callBackHandle: Int64, + callbackInfo: FlutterCallbackInformation ) { let message = """ - Starting Dart/Flutter with following params: - • callBackName: '\(callbackInfo.callbackName ?? "not found")' - • callbackClassName: '\(callbackInfo.callbackClassName ?? "not found")' - • callbackLibraryPath: '\(callbackInfo.callbackLibraryPath ?? "not found")' - • callbackHandle: '\(callBackHandle)' - """ + Starting Dart/Flutter with following params: + • callBackName: '\(callbackInfo.callbackName ?? "not found")' + • callbackClassName: '\(callbackInfo.callbackClassName ?? "not found")' + • callbackLibraryPath: '\(callbackInfo.callbackLibraryPath ?? "not found")' + • callbackHandle: '\(callBackHandle)' + """ DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, title: startDate.formatted(), body: message, icon: .startWork) } - func showCompletedFetchNotification(completedDate: Date, + func showCompletedFetchNotification(identifier: String, + completedDate: Date, result: UIBackgroundFetchResult, elapsedTime: TimeInterval) { let message = """ - Perform backgroundworkerfetch completed: - • Elapsed time: \(elapsedTime.formatToSeconds()) - • Result: UIBackgroundFetchResult.\(result) - """ - DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, - title: completedDate.formatted(), - body: message, - icon: result == .newData ? .success : .failure) - } - - func showStartBGRefreshNotification(startDate: Date, - callBackHandle: Int64, - callbackInfo: FlutterCallbackInformation - ) { - let message = + Perform backgroundworkerfetch completed: + • Identifier: '\(identifier)' + • Elapsed time: \(elapsedTime.formatToSeconds()) + • Result: UIBackgroundFetchResult.\(result) """ - Starting Dart/Flutter BGAppRefresh with following params: - • callBackName: '\(callbackInfo.callbackName ?? "not found")' - • callbackClassName: '\(callbackInfo.callbackClassName ?? "not found")' - • callbackLibraryPath: '\(callbackInfo.callbackLibraryPath ?? "not found")' - • callbackHandle: '\(callBackHandle)' - """ - DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, - title: startDate.formatted(), - body: message, - icon: .startWork) - } - - func showCompletedBGRefreshNotification(completedDate: Date, - result: UIBackgroundFetchResult, - elapsedTime: TimeInterval) { - let message = - """ - Perform BGRefresh completed: - • Elapsed time: \(elapsedTime.formatToSeconds()) - • Result: UIBackgroundFetchResult.\(result) - """ - DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, + DebugNotificationHelper.scheduleNotification(identifier: identifier, title: completedDate.formatted(), body: message, icon: result == .newData ? .success : .failure) } /// Show a notification for iOS Debugging - func showDebugNotification (completedDate: Date, - content: String + func showDebugNotification(completedDate: Date, + content: String ) { DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, title: completedDate.formatted(), @@ -103,7 +72,7 @@ class DebugNotificationHelper { return } - UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { (_, _) in } + UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) { _, _ in } let notificationRequest = createNotificationRequest( identifier: identifier, threadIdentifier: SwiftWorkmanagerPlugin.identifier, @@ -112,7 +81,6 @@ class DebugNotificationHelper { icon: icon ) UNUserNotificationCenter.current().add(notificationRequest, withCompletionHandler: nil) - } private static func createNotificationRequest(identifier: String, @@ -140,5 +108,4 @@ class DebugNotificationHelper { private static var logPrefix: String { return "\(String(describing: SwiftWorkmanagerPlugin.self)) - \(DebugNotificationHelper.self)" } - } diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 88fc90bb..033ee997 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -1,7 +1,7 @@ import BackgroundTasks import Flutter -import UIKit import os +import UIKit extension String { var lowercasingFirst: String { @@ -26,31 +26,48 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { case callbackHandle } } + struct CheckBackgroundRefreshPermission { static let name = "\(CheckBackgroundRefreshPermission.self)".lowercasingFirst } + struct RegisterOneOffTask { static let name = "\(RegisterOneOffTask.self)".lowercasingFirst enum Arguments: String { + case taskName + case uniqueName + case inputData + case initialDelaySeconds + } + } + + struct RegisteriOSBackgroundProcessingTask { + static let name = "\(RegisteriOSBackgroundProcessingTask.self)".lowercasingFirst + enum Arguments: String { + case taskName case uniqueName case initialDelaySeconds case networkType case requiresCharging } } + struct RegisterPeriodicTask { static let name = "\(RegisterPeriodicTask.self)".lowercasingFirst enum Arguments: String { + case taskName case uniqueName case initialDelaySeconds } } + struct CancelAllTasks { static let name = "\(CancelAllTasks.self)".lowercasingFirst enum Arguments: String { case none } } + struct CancelTaskByUniqueName { static let name = "\(CancelTaskByUniqueName.self)".lowercasingFirst enum Arguments: String { @@ -60,15 +77,19 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } - // Handlers + ///Handlers @available(iOS 13.0, *) private static func handleBGProcessingTask(_ task: BGProcessingTask) { + NSLog("Workmanagerplugin handle handleBGProcessingTask") let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task let operation = BackgroundTaskOperation( task.identifier, - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback + inputData: "", //no data + flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, + backgroundMode: .backgroundProcessingTask, + isInDebug: UserDefaultsHelper.getIsDebug() ) // Provide an expiration handler for the background task @@ -90,64 +111,79 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc @available(iOS 13.0, *) public static func handleAppRefresh(task: BGAppRefreshTask) { + NSLog("Workmanagerplugin handle BGAppRefreshTask") guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), - let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + let _ = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)") return } - let taskSessionStart = Date() - let taskSessionIdentifier = UUID() + /// Could improved _ seconds are ignored on refresh //important to reschedule + scheduleAppRefresh(taskIdentifier: task.identifier, earliestBeginInSeconds: 120) - let debugHelper = DebugNotificationHelper(taskSessionIdentifier) - debugHelper.showStartBGRefreshNotification( - startDate: taskSessionStart, - callBackHandle: callbackHandle, - callbackInfo: flutterCallbackInformation + NSLog("Workmanagerplugin handle BGAppRefreshTask") + let operationQueue = OperationQueue() + // Create an operation that performs the main part of the background task + let operation = BackgroundTaskOperation( + task.identifier, + inputData: "", + flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, + backgroundMode: .backgroundAppRefresh, + isInDebug: UserDefaultsHelper.getIsDebug() ) - /// Could improved _ seconds are ignored - scheduleAppRefresh(withIdentifier: task.identifier, earliestBeginInSeconds: 120) - let semaphore = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let worker = BackgroundWorker(mode: .backgroundAppRefresh(identifier: self.identifier), - flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback) - - worker.performBackgroundRequest { _ in - semaphore.signal() - } + // Provide an expiration handler for the background task + // that cancels the operation + task.expirationHandler = { + operation.cancel() } - // timeout after 29seconds ,max execution time is 30seconds - // -> 1 second for dispatching and other stuff (Flutter Messenger etc) - let dispatchResult = semaphore.wait(timeout: DispatchTime.now()+29) - print("handleAppRefresh \(dispatchResult)") - debugHelper.showCompletedBGRefreshNotification( - completedDate: Date(), - result: dispatchResult == .timedOut ? .failed : .newData, - elapsedTime: Date().timeIntervalSince(taskSessionStart)) + // Inform the system that the background task is complete + // when the operation completes + operation.completionBlock = { + NSLog("Workmanagerplugin handle BGAppRefreshTask completed") + task.setTaskCompleted(success: !operation.isCancelled) + } + // Start the operation + operationQueue.addOperation(operation) + // Create an operation that performs the main part of the background task. } - /// register names for BGProcessingTask called by workmanger.m - /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown - @objc - public static func registerBackgroundProcessingTask(taskIdentifier identifier: String) { - if #available(iOS 13.0, *) { - print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") - BGTaskScheduler.shared.register( - forTaskWithIdentifier: identifier, - using: nil - ) { task in - if let task = task as? BGProcessingTask { - handleBGProcessingTask(task) - } - } + /// Initialisation for all Tasks + + @available(iOS 13.0, *) + /// Immedately start a background fetch with 29sec timeout - specification by iOS + public static func startOnOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData:String, delaySeconds: Int64) { + NSLog("Workmanagerplugin immedately startOnOffTask alias iOS backgroundFetch started - timeout after 30sec.") + + let operationQueue = OperationQueue() + // Create an operation that performs the main part of the background task + let operation = BackgroundTaskOperation( + identifier, + inputData: inputData, + flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, + backgroundMode: .backgroundOnOffTask(identifier: identifier), + isInDebug: UserDefaultsHelper.getIsDebug() + ) + + // Inform the system that the task is complete when the operation completes + operation.completionBlock = { + NSLog("Background task ended \(identifier) : ID:\(taskIdentifier).") + UIApplication.shared.endBackgroundTask(taskIdentifier) } + + // Start the operation + operationQueue.addOperation(operation) + // Create an operation that performs the main part of the background task. } + /// @objc + /// First register names for BGAppRefresh + /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown + /// After that you can call [registerAppRefreshTaskScheduler] which schedules task in background public static func registerAppRefreshTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { print("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") @@ -159,46 +195,31 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { if let task = task as? BGAppRefreshTask { handleAppRefresh(task: task) } - }}} - - @objc - public static func registerBackgroundProcessingTaskScheduler(withIdentifier identifier: String, - earliestBeginInSeconds begin: Double, - requiresNetworkConnectivity: Bool, - requiresExternalPower: Bool) { - if #available(iOS 13.0, *) { - print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(identifier)") - scheduleBackgroundProcessingTask(withIdentifier: identifier, - earliestBeginInSeconds: begin, - requiresNetworkConnectivity: requiresNetworkConnectivity, - requiresExternalPower: requiresExternalPower) + } } } + /// App Refresh - called by iOS in background at random time for a max 30 sec task @objc public static func registerAppRefreshTaskScheduler( - withIdentifier identifier: String, + taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { - if #available(iOS 13.0, *) { - print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") - // schedule on app did enter background - NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, queue: nil - ) { (_) in - // schedule apprefresh - scheduleAppRefresh(withIdentifier: identifier, earliestBeginInSeconds: begin) - } + if #available(iOS 13.0, *) { + print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") + // schedule on app did enter background + NotificationCenter.default.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, queue: nil + ) { _ in + // schedule apprefresh + scheduleAppRefresh(taskIdentifier: identifier, earliestBeginInSeconds: begin) } } - - static func callback(_: UIBackgroundFetchResult) { } @objc @available(iOS 13.0, *) - private static func scheduleAppRefresh(withIdentifier identifier: String, earliestBeginInSeconds begin: Double) { - + private static func scheduleAppRefresh(taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { let request = BGAppRefreshTaskRequest( identifier: identifier) request.earliestBeginDate = Date(timeIntervalSinceNow: begin) @@ -208,38 +229,83 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } catch { print("Couldn't schedule app refresh \(error.localizedDescription)") return + } + } + /// First register names for BGProcessingTask called by WorkmangerPlugin.m + /// This happens on registering + /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown + /// After that you can call [registerBackgroundProcessingTaskScheduler] which schedules task in background + @objc + public static func registerBGProcessingTask(withIdentifier identifier: String) { + if #available(iOS 13.0, *) { + print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") + BGTaskScheduler.shared.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + if let task = task as? BGProcessingTask { + handleBGProcessingTask(task) + } + } } } @objc + /// Registers a long running BackgroundProcessingTask - randomly started by iOS when app in background + /// Task will scheduled when app goes to background + public static func registerBackgroundProcessingTaskScheduler(uniqueTaskIdentifier: String, + earliestBeginInSeconds begin: Double, + requiresNetworkConnectivity: Bool, + requiresExternalPower: Bool) { + if #available(iOS 13.0, *) { + // avoid XCode line length issue in notificationcenterobserver maxx 200 chars line length + let network = requiresNetworkConnectivity + let extPower = requiresExternalPower + print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(uniqueTaskIdentifier)") + + NotificationCenter.default.addObserver( + forName: UIApplication.didEnterBackgroundNotification, + object: nil, queue: nil + ) { _ in + // schedule apprefresh + scheduleBackgroundProcessingTask(withIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity: network, requiresExternalPower: extPower) + } + } + } + + @objc + /// Schedules a long running BackgroundProcessingTask - randomly started by iOS when app in background + /// Called by UIApplication.didEnterBackgroundNotification in [registerBackgroundProcessingTaskScheduler] @available(iOS 13.0, *) private static func scheduleBackgroundProcessingTask( - withIdentifier identifier: String, + withIdentifier uniqueTaskIdentifier: String, earliestBeginInSeconds begin: Double, requiresNetworkConnectivity: Bool, requiresExternalPower: Bool ) { let request = BGProcessingTaskRequest( - identifier: identifier) + identifier: uniqueTaskIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: begin) request.requiresNetworkConnectivity = requiresNetworkConnectivity request.requiresExternalPower = requiresExternalPower do { try BGTaskScheduler.shared.submit(request) - print("Requested BackgroundProcessingTask \(identifier)") + print("Requested BackgroundProcessingTask \(uniqueTaskIdentifier)") } catch { - print("Couldn't schedule app BackgroundProcessingTask identifier:\(identifier) error:\(error.localizedDescription)") + print("Couldn't schedule app BackgroundProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") print("On BGTaskSchedulerErrorDomain error 1 - please run on real device") print("On BGTaskSchedulerErrorDomain error 3 - check registered names") } } + + static func callback(_: UIBackgroundFetchResult) { + } } // MARK: - FlutterPlugin conformance extension SwiftWorkmanagerPlugin: FlutterPlugin { - @objc public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { flutterPluginRegistrantCallback = callback @@ -261,16 +327,21 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { initialize(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.CheckBackgroundRefreshPermission.name, .some): - _=checkBackgroundRefreshAuthorisation(result: result) + _ = checkBackgroundRefreshAuthorisation(result: result) return case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): // register bgAppRefreshTask for less than 30 seconds backgroundtime registerPeriodicTask(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): - // register processingtask for more than 30 seconds backgroundtime + // register processingtask for less than 30 seconds backgroundtime + // Task starts immedatly registerOneOffTask(arguments: arguments, result: result) return + case (ForegroundMethodChannel.Methods.RegisteriOSBackgroundProcessingTask.name, let .some(arguments)): + // register long running iOs BGProcessingtask for more than 30 seconds backgroundtime + registerBackgroundProcessingTask(arguments: arguments, result: result) + return case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): cancelAllTasks(result: result) return @@ -288,16 +359,16 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { result(WMPError.workmanagerIsAlreadyInitialized) return } -#if targetEnvironment(simulator) + #if targetEnvironment(simulator) print("Workmanager Info: Please run on real device!" + - "No backgroundtask is automatic called in the simulator!!") -#endif + "No backgroundtask is automatic called in the simulator!!") + #endif let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result: result) if backgroundRefreshAvailable != BackgroundAuthorisationState.available { UIApplication.shared.open(URL( string: UIApplication.openSettingsURLString)!, - options: [:], - completionHandler: nil) + options: [:], + completionHandler: nil) return } let method = ForegroundMethodChannel.Methods.Initialize.self @@ -318,21 +389,18 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { if #available(iOS 13.0, *) { let method = ForegroundMethodChannel.Methods.RegisterPeriodicTask.self - guard let identifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } - guard let initialDelaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + guard let uniqueTaskIdentifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { result(WMPError.invalidParameters.asFlutterError) return } + let initialDelaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 ?? 0 // task will scheduled when app goes to background SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler( - withIdentifier: identifier, + taskIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: Double(initialDelaySeconds)) - print("Registered PeriodicTask \(identifier) delaySeconds \(initialDelaySeconds)") + print("Registered PeriodicTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst) delaySeconds \(initialDelaySeconds)") result(true) return } else { @@ -343,22 +411,65 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } private func registerOneOffTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - print("Registering OneOffTask (BackgroundProcessingTask)") + print("Registering OneOffTask") if !validateCallbackHandle(result: result) { return } if #available(iOS 13.0, *) { let method = ForegroundMethodChannel.Methods.RegisterOneOffTask.self guard let delaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else { + result(WMPError.invalidParameters.asFlutterError) + return + } + guard let uniqueTaskIdentifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { result(WMPError.invalidParameters.asFlutterError) return } - guard let identifier = - arguments[method.Arguments.uniqueName.rawValue] as? String else { + guard let callBackIdentifier = + arguments[method.Arguments.taskName.rawValue] as? String else { result(WMPError.invalidParameters.asFlutterError) return } + var taskIdentifier: UIBackgroundTaskIdentifier = .invalid + let inputData = + arguments[method.Arguments.inputData.rawValue] as? String + + + taskIdentifier = UIApplication.shared.beginBackgroundTask(withName: uniqueTaskIdentifier, expirationHandler: { + // Code to handle if takes way too long + UIApplication.shared.endBackgroundTask(taskIdentifier) + }) + SwiftWorkmanagerPlugin.startOnOffTask(identifier: callBackIdentifier, + taskIdentifier: taskIdentifier, + inputData: inputData ?? "", + delaySeconds: delaySeconds) + result(true) + print("Registered OnOffTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") + return + } else { + result(FlutterError(code: "99", + message: "RegisterPeriodicTask is not registered", + details: "iOS Version lower than 13.0")) + } + } + + private func registerBackgroundProcessingTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + print("Registering backgroundProcessingTask") + if !validateCallbackHandle(result: result) { + return + } + if #available(iOS 13.0, *) { + let method = ForegroundMethodChannel.Methods.RegisteriOSBackgroundProcessingTask.self + guard let uniqueTaskIdentifier = + arguments[method.Arguments.uniqueName.rawValue] as? String else { + result(WMPError.invalidParameters.asFlutterError) + return + } + let delaySeconds = + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Double ?? 0.0 + let requiresCharging = arguments[method.Arguments.requiresCharging.rawValue] as? Bool ?? false var requiresNetwork = false if let networkTypeInput = arguments[method.Arguments.networkType.rawValue] as? String, @@ -366,17 +477,20 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { networkType == .connected || networkType == .metered { requiresNetwork = true } - // task will scheduled when app goes to background + + // task will scheduled by iOS when app goes to background SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler( - withIdentifier: identifier, - earliestBeginInSeconds: Double(delaySeconds), + uniqueTaskIdentifier: uniqueTaskIdentifier, + earliestBeginInSeconds: delaySeconds, requiresNetworkConnectivity: requiresCharging, requiresExternalPower: requiresNetwork) result(true) + print("Registered BackgroundProcessingTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") + return } else { result(FlutterError(code: "99", - message: "RegisterPeriodicTask is not registered", + message: "BackgroundProcessingTask is not registered", details: "iOS Version lower than 13.0")) } } @@ -409,34 +523,34 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { 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.", + "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 false } - return true + return true } } // MARK: - AppDelegate conformance extension SwiftWorkmanagerPlugin { - override public func application( _ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) -> Bool { let worker = BackgroundWorker( - mode: .backgroundFetch, + mode: .backgroundProcessingTask, + inputData: "", flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback ) return worker.performBackgroundRequest(completionHandler) diff --git a/ios/Classes/ThumbnailGenerator.swift b/ios/Classes/ThumbnailGenerator.swift index 00592074..b4cf633e 100644 --- a/ios/Classes/ThumbnailGenerator.swift +++ b/ios/Classes/ThumbnailGenerator.swift @@ -6,11 +6,10 @@ // import Foundation -import UserNotifications import UIKit +import UserNotifications struct ThumbnailGenerator { - enum ThumbnailIcon { case startWork case success @@ -31,6 +30,10 @@ struct ThumbnailGenerator { static func createThumbnail(with icon: ThumbnailIcon) -> UNNotificationAttachment? { let name = "thumbnail" let thumbnailFrame = CGRect(x: 0, y: 0, width: 150, height: 150) + // TODO: use this only on mainthread + // causes [Animation] +[UIView setAnimationsEnabled:] being called from a background thread. + // Performing any operation from a background thread on UIView or a subclass is not supported + // and may result in unexpected and insidious behavior. let thumbnail = UIView(frame: thumbnailFrame) thumbnail.isOpaque = false let label = UILabel(frame: thumbnailFrame) @@ -51,25 +54,22 @@ struct ThumbnailGenerator { logInfo("\(logPrefix) \(#function) something went wrong creating a thumbnail for local debug notification") return nil } - } private static var logPrefix: String { return "\(String(describing: SwiftWorkmanagerPlugin.self)) - \(ThumbnailGenerator.self)" } - } private extension UIView { - func renderAsImage() throws -> UIImage { - UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0) + UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0.0) defer { UIGraphicsEndImageContext() } guard let context = UIGraphicsGetCurrentContext() else { throw GraphicsError.noCurrentGraphicsContextFound } - self.layer.render(in: context) + layer.render(in: context) guard let image = UIGraphicsGetImageFromCurrentImageContext() else { throw GraphicsError.noCurrentGraphicsContextFound } @@ -83,12 +83,11 @@ private extension UIView { } private extension UIImage { - func persist(fileName: String, in directory: URL = URL(fileURLWithPath: NSTemporaryDirectory())) throws -> URL { let directoryURL = directory.appendingPathComponent(SwiftWorkmanagerPlugin.identifier, isDirectory: true) let fileURL = directoryURL.appendingPathComponent("\(fileName).png") try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - guard let imageData = self.pngData() else { + guard let imageData = pngData() else { throw ImageError.cannotRepresentAsPNG(self) } try imageData.write(to: fileURL) @@ -98,5 +97,4 @@ private extension UIImage { enum ImageError: Error { case cannotRepresentAsPNG(UIImage) } - } diff --git a/ios/Classes/WorkmanagerPlugin.h b/ios/Classes/WorkmanagerPlugin.h index 2644b493..21bf6032 100644 --- a/ios/Classes/WorkmanagerPlugin.h +++ b/ios/Classes/WorkmanagerPlugin.h @@ -11,11 +11,20 @@ + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier; /** - * Register a custom task identifier to be iOS Background Task /executed later on. + * Register a custom task identifier as iOS BGAppRefresh Task executed randomly in future. * @author Lars Huth * * @param taskIdentifier The identifier of the custom task. Must be set in info.plist */ + (void)registerPeriodicTaskWithIdentifier:(NSString *) taskIdentifier; +/** + * Register a custom task identifier as iOS BackgroundProcessingTask executed randomly in future. + * @author Lars Huth + * + * @param taskIdentifier The identifier of the custom task. Must be set in info.plist + */ ++ (void)registerBGProcessingTaskWithIdentifier:(NSString *) taskIdentifier; + + @end diff --git a/ios/Classes/WorkmanagerPlugin.m b/ios/Classes/WorkmanagerPlugin.m index 8fad301b..d96d3051 100644 --- a/ios/Classes/WorkmanagerPlugin.m +++ b/ios/Classes/WorkmanagerPlugin.m @@ -18,7 +18,7 @@ + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier { if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerBackgroundProcessingTaskWithTaskIdentifier:taskIdentifier]; + [SwiftWorkmanagerPlugin registerBGProcessingTaskWithIdentifier:taskIdentifier]; } } @@ -28,4 +28,10 @@ + (void)registerPeriodicTaskWithIdentifier:(NSString *)taskIdentifier{ } } ++ (void)registerBGProcessingTaskWithIdentifier:(NSString *) taskIdentifier{ + if (@available(iOS 13, *)) { + [SwiftWorkmanagerPlugin registerBGProcessingTaskWithIdentifier:taskIdentifier]; + } +} + @end diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index a170d7f1..b41b5000 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -81,7 +81,7 @@ class Workmanager { /// void callbackDispatcher() { /// Workmanager().executeTask((taskName, inputData) { /// switch (taskName) { - /// case Workmanager.iOSBackgroundTask: + /// case Workmanager.iOSBackgroundProcessingTask: /// stderr.writeln("The iOS background fetch was triggered"); /// break; /// case Workmanager.iOSBackgroundAppRefresh: @@ -93,29 +93,6 @@ class Workmanager { /// }); /// } /// ``` - static const String iOSBackgroundTask = "iOSPerformFetch"; - static const String iOSBackgroundAppRefresh = "iOSBackgroundAppRefresh"; - - /// Use this constant inside your callbackDispatcher to identify when an iOS Background Processing via BGTaskScheduler occurred. - /// - /// ``` - /// @pragma('vm:entry-point') - /// void callbackDispatcher() { - /// Workmanager().executeTask((taskName, inputData) { - /// switch (taskName) { - /// case Workmanager.iOSBackgroundProcessingTask: - /// stderr.writeln("A iOS BG processing task was initiated."); - /// break; - /// } - /// - /// return Future.value(true); - /// }); - /// } - /// ``` - @Deprecated( - 'Use custom iOS task names. Set keys in info.plist This property will be removed.') - static const String iOSBackgroundProcessingTask = - "workmanager.background.task"; static bool _isInDebugMode = false; @@ -124,6 +101,9 @@ class Workmanager { MethodChannel _foregroundChannel = const MethodChannel( "be.tramckrijte.workmanager/foreground_channel_work_manager"); + static const BACKGROUND_APPREFRESH_TASK_NAME = "iOSBackgroundAppRefresh"; + static const BACKGROUND_PROCESSING_TASK_NAME = "iOSBackgroundProcessingTask"; + /// A helper function so you only need to implement a [BackgroundTaskHandler] void executeTask(final BackgroundTaskHandler backgroundTask) { WidgetsFlutterBinding.ensureInitialized(); @@ -215,6 +195,12 @@ class Workmanager { /// decide to schedule the ask a lot later. final Duration initialDelay = Duration.zero, + /// set required [NetworkType] only iOS + final NetworkType? networkType = NetworkType.not_required, + + ///set if charging is needed + final bool? requiresCharging = false, + /// Fully supported on Android, but only partially supported on iOS. /// See [Constraints] for details. final Constraints? constraints, @@ -236,11 +222,48 @@ class Workmanager { backoffPolicy: backoffPolicy, backoffPolicyDelay: backoffPolicyDelay, outOfQuotaPolicy: outOfQuotaPolicy, + networkType: networkType, + requiresCharging: requiresCharging, inputData: inputData, ), ); - /// Schedules a periodic task that will run (if iOS random depending on iOS) provided [frequency]. + /// Schedule a BackgroundProcessingTask only for iOS + /// This can be a long running Task on iOS longer than 30seconds + /// See Apples documentation https://developer.apple.com/documentation/backgroundtasks + Future registeriOSBackgroundProcessingTask( + final String uniqueName, + final String taskName, { + + /// set required [NetworkType] only iOS + final NetworkType? networkType = NetworkType.not_required, + + ///set if charging is needed + final bool? requiresCharging = false, + + /// Only partially supported on iOS. + /// See [Constraints] for details. + final Constraints? constraints, + final BackoffPolicy? backoffPolicy, + final Duration backoffPolicyDelay = Duration.zero, + final OutOfQuotaPolicy? outOfQuotaPolicy, + final Map? inputData, + }) async => + await _foregroundChannel.invokeMethod( + "registeriOSBackgroundProcessingTask", + JsonMapperHelper.toRegisterMethodArgument( + isInDebugMode: _isInDebugMode, + uniqueName: uniqueName, + taskName: taskName, + constraints: constraints, + backoffPolicy: backoffPolicy, + backoffPolicyDelay: backoffPolicyDelay, + outOfQuotaPolicy: outOfQuotaPolicy, + networkType: networkType, + requiresCharging: requiresCharging), + ); + + /// Schedules a periodic task that will run (if iOS randomly started depending on iOS) 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] /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. @@ -257,6 +280,8 @@ class Workmanager { final BackoffPolicy? backoffPolicy, final Duration backoffPolicyDelay = Duration.zero, final OutOfQuotaPolicy? outOfQuotaPolicy, + final NetworkType? networkType = NetworkType.not_required, + final bool? requiresCharging = false, final Map? inputData, }) async => await _foregroundChannel.invokeMethod( @@ -273,6 +298,8 @@ class Workmanager { backoffPolicy: backoffPolicy, backoffPolicyDelay: backoffPolicyDelay, outOfQuotaPolicy: outOfQuotaPolicy, + networkType: networkType, + requiresCharging: requiresCharging, inputData: inputData, ), ); @@ -311,6 +338,8 @@ class JsonMapperHelper { final BackoffPolicy? backoffPolicy, final Duration? backoffPolicyDelay, final OutOfQuotaPolicy? outOfQuotaPolicy, + NetworkType? networkType, + bool? requiresCharging, final Map? inputData, }) { if (inputData != null) { diff --git a/test/workmanager_test.mocks.dart b/test/workmanager_test.mocks.dart deleted file mode 100644 index b9a9c021..00000000 --- a/test/workmanager_test.mocks.dart +++ /dev/null @@ -1,109 +0,0 @@ -// Mocks generated by Mockito 5.0.17 from annotations -// in workmanager/test/workmanager_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i3; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:workmanager/src/options.dart' as _i4; -import 'package:workmanager/src/workmanager.dart' as _i2; - -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -/// A class which mocks [Workmanager]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWorkmanager extends _i1.Mock implements _i2.Workmanager { - MockWorkmanager() { - _i1.throwOnMissingStub(this); - } - - @override - void executeTask(_i2.BackgroundTaskHandler? backgroundTask) => - super.noSuchMethod(Invocation.method(#executeTask, [backgroundTask]), - returnValueForMissingStub: null); - @override - _i3.Future initialize(Function? callbackDispatcher, - {bool? isInDebugMode = false}) => - (super.noSuchMethod( - Invocation.method(#initialize, [callbackDispatcher], - {#isInDebugMode: isInDebugMode}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future registerOneOffTask(String? uniqueName, String? taskName, - {String? tag, - _i4.ExistingWorkPolicy? existingWorkPolicy, - Duration? initialDelay = Duration.zero, - _i4.Constraints? constraints, - _i4.BackoffPolicy? backoffPolicy, - Duration? backoffPolicyDelay = Duration.zero, - _i4.OutOfQuotaPolicy? outOfQuotaPolicy, - Map? inputData}) => - (super.noSuchMethod( - Invocation.method(#registerOneOffTask, [ - uniqueName, - taskName - ], { - #tag: tag, - #existingWorkPolicy: existingWorkPolicy, - #initialDelay: initialDelay, - #constraints: constraints, - #backoffPolicy: backoffPolicy, - #backoffPolicyDelay: backoffPolicyDelay, - #outOfQuotaPolicy: outOfQuotaPolicy, - #inputData: inputData - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future registerPeriodicTask(String? uniqueName, String? taskName, - {Duration? frequency, - String? tag, - _i4.ExistingWorkPolicy? existingWorkPolicy, - Duration? initialDelay = Duration.zero, - _i4.Constraints? constraints, - _i4.BackoffPolicy? backoffPolicy, - Duration? backoffPolicyDelay = Duration.zero, - _i4.OutOfQuotaPolicy? outOfQuotaPolicy, - Map? inputData}) => - (super.noSuchMethod( - Invocation.method(#registerPeriodicTask, [ - uniqueName, - taskName - ], { - #frequency: frequency, - #tag: tag, - #existingWorkPolicy: existingWorkPolicy, - #initialDelay: initialDelay, - #constraints: constraints, - #backoffPolicy: backoffPolicy, - #backoffPolicyDelay: backoffPolicyDelay, - #outOfQuotaPolicy: outOfQuotaPolicy, - #inputData: inputData - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future cancelByUniqueName(String? uniqueName) => - (super.noSuchMethod(Invocation.method(#cancelByUniqueName, [uniqueName]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future cancelByTag(String? tag) => - (super.noSuchMethod(Invocation.method(#cancelByTag, [tag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future cancelAll() => - (super.noSuchMethod(Invocation.method(#cancelAll, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); -} From 5589538f4e9ef9a1972b01e6dfa704a22d70a80e Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Wed, 11 Jan 2023 21:49:14 +0100 Subject: [PATCH 07/26] fixed warning dead code and ! check --- example/lib/main.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 88185f76..77a4ec3c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,7 +49,7 @@ void callbackDispatcher() { sleep(Duration(seconds: 2)); return Future.value(true); } - final key = inputData!['key']!; + final key = inputData['key']!; prefs.setString("rescheduledTaskKey", DateTime.now().toString()); if (prefs.containsKey('unique-$key')) { print('has been running before, task is successful'); @@ -58,7 +58,6 @@ void callbackDispatcher() { prefs.setBool('unique-$key', true); //perhaps not working on iOS print('reschedule task'); return Future.value(true); - ; } case failedTaskKey: print('failed task'); From 6a42b10801f7563311c4930fb549304bec6ca15c Mon Sep 17 00:00:00 2001 From: Lars Huth Date: Thu, 12 Jan 2023 07:43:06 +0100 Subject: [PATCH 08/26] improved Task description (hints) --- example/lib/main.dart | 3 ++- lib/src/workmanager.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 77a4ec3c..2a02584c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -34,8 +34,9 @@ void callbackDispatcher() { print("callbackDispatcher for $task called"); await LogHelper.LogBGTask(data: "callbackDispatcher for $task called"); final prefs = await SharedPreferences - .getInstance(); //only working on Android ?! isolates on is has incorrect results. + .getInstance(); //only working on Android ?! isolates on iOS has incorrect results. switch (task) { + //simpleTaskKey:rescheduledTaskKey:failedTaskKey starts on iOS immediately with a timeout of 30 secs in background case simpleTaskKey: sleep(Duration(seconds: 12)); // sleep as sample print("$simpleTaskKey was executed. inputData = $inputData"); diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index b41b5000..e1a82099 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -173,6 +173,7 @@ class Workmanager { } /// Schedule a one off task + /// starts on iOS immediately with a timeout of 29 secs in background /// 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 @@ -263,7 +264,7 @@ class Workmanager { requiresCharging: requiresCharging), ); - /// Schedules a periodic task that will run (if iOS randomly started depending on iOS) provided [frequency]. + /// Schedules a repeated periodic task that (if iOS randomly started depending on iOS) /// A [uniqueName] is required so only one task can be registered. /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. From c9f193f247d3e522326cb3a551efca992993677e Mon Sep 17 00:00:00 2001 From: delfme <53510751+delfme@users.noreply.github.com> Date: Sat, 29 Jul 2023 14:49:24 +0200 Subject: [PATCH 09/26] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 8615b233..57913f3b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ # Flutter Workmanager +# Intro to our Fork +Our fork to original repo just integrates a PR which was not merged yet was good in solving issues on iOS. +After PR integration, here is how our fork works: + +registerOneOffTask +Now starts immediately on both android and iOS. On iOS it lasts only 29sec. + +registeriOSBackgroundProcessingTask +Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. + +registerPeriodicTask +This is for a scheduled task on both android and iOS. It's a 29sec task on iOS, but doesn't start immediately. + + [![pub package](https://img.shields.io/pub/v/workmanager.svg)](https://pub.dartlang.org/packages/workmanager) [![Build status](https://img.shields.io/cirrus/github/vrtdev/flutter_workmanager/master)](https://cirrus-ci.com/github/vrtdev/flutter_workmanager/) ======= From 808e1933b0a3464229fc68b6a6cf5e459163a2c0 Mon Sep 17 00:00:00 2001 From: delfme <53510751+delfme@users.noreply.github.com> Date: Sat, 29 Jul 2023 14:49:47 +0200 Subject: [PATCH 10/26] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 57913f3b..26df8f3a 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Our fork to original repo just integrates a PR which was not merged yet was good in solving issues on iOS. After PR integration, here is how our fork works: -registerOneOffTask +- registerOneOffTask Now starts immediately on both android and iOS. On iOS it lasts only 29sec. -registeriOSBackgroundProcessingTask +- registeriOSBackgroundProcessingTask Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. -registerPeriodicTask +- registerPeriodicTask This is for a scheduled task on both android and iOS. It's a 29sec task on iOS, but doesn't start immediately. From 0c393084bdb87702ac1adb2f4302814774536079 Mon Sep 17 00:00:00 2001 From: delfme <53510751+delfme@users.noreply.github.com> Date: Sat, 29 Jul 2023 14:50:15 +0200 Subject: [PATCH 11/26] Update README.md --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 26df8f3a..4d7b911b 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,11 @@ Our fork to original repo just integrates a PR which was not merged yet was good in solving issues on iOS. After PR integration, here is how our fork works: -- registerOneOffTask -Now starts immediately on both android and iOS. On iOS it lasts only 29sec. +- registerOneOffTask: Now starts immediately on both android and iOS. On iOS it lasts only 29sec. -- registeriOSBackgroundProcessingTask -Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. +- registeriOSBackgroundProcessingTask: Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. -- registerPeriodicTask -This is for a scheduled task on both android and iOS. It's a 29sec task on iOS, but doesn't start immediately. +- registerPeriodicTask: This is for a scheduled task on both android and iOS. It's a 29sec task on iOS, but doesn't start immediately. [![pub package](https://img.shields.io/pub/v/workmanager.svg)](https://pub.dartlang.org/packages/workmanager) From 8d184dabd684120fae8cbd448d36823716fe8b0c Mon Sep 17 00:00:00 2001 From: Absar Date: Thu, 14 Sep 2023 23:17:53 +0200 Subject: [PATCH 12/26] Format readme iOS examples --- README.md | 71 +++++++++++++++------------------------- lib/src/workmanager.dart | 2 +- 2 files changed, 27 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 4d7b911b..48ae724a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ void callbackDispatcher() { ``` Android tasks are identified using their `taskName`. -iOS tasks are identitied using their `taskIdentifier`. +iOS tasks are identified using their `taskIdentifier`. However, there is an exception for iOS background fetch: `Workmanager.iOSBackgroundTask`, a constant for iOS background fetch task. @@ -105,65 +105,46 @@ Refer to the example app for a successful, retrying and a failed task. # iOS specific setup and note -Initialize Workmanager only one once -You can use the background app refresh only on a real device +Initialize Workmanager only once. +Background app refresh can only be tested on a real device, it cannot be tested on a simulator. iOS supports **One off tasks** with a few basic constraints: ```dart -Workmanager -( -).registerOneOffTask -("task-identifier -" -, -simpleTaskKey, // Ignored on iOS -initialDelay: Duration -( -minutes: 30 -) -, -constraints: Constraints -( -// connected or metered mark the task as requiring internet -networkType: NetworkType.connected, -// require external power -requiresCharging: true -, -) -, -inputData: ... // fully supported +Workmanager().registerOneOffTask( + "task-identifier", + "simpleTaskKey", // Ignored on iOS + initialDelay: Duration(minutes: 30), + constraints: Constraints( + // Connected or metered mark the task as requiring internet + networkType: NetworkType.connected, + // Require a powered/charging device + requiresCharging: true, + ), + inputData: ..., // fully supported ); ``` -And alternative supports **PeriodicTask** with maximum 29sec execution time (see example). -Look also -at -Apple's documentation +iOS supports **Periodic tasks** with maximum 29sec execution time (see example). +For more information see the [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app). ```dart - -const iOSBackgroundAppRefresh = - "app.workmanagerExample.iOSBackgroundAppRefresh"; -Workmanager -( -).registerPeriodicTask -( -iOSBackgroundAppRefresh,iOSBackgroundAppRefresh,initialDelay: Duration -( -seconds: 10 -) -, //ignored +const iOSBackgroundAppRefresh = "app.workmanagerExample.iOSBackgroundAppRefresh"; +Workmanager().registerPeriodicTask( + iOSBackgroundAppRefresh, + iOSBackgroundAppRefresh, + initialDelay: Duration(seconds: 10), // Ignored on iOS ); ``` +For more information see the [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask). + Get permissions for iOS BackgroundRefresh - see example ```dart - if (Platform.isIOS) { -//here you can check whether background refresh is activated in iOS settings -var hasPermissions = await Workmanager() - .checkBackgroundRefreshPermission(); +if (Platform.isIOS) { +// Here you can check whether background refresh is activated in iOS settings +var hasPermissions = await Workmanager().checkBackgroundRefreshPermission(); if (hasPermissions != BackgroundAuthorisationState.available){...} } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index e1a82099..72f4bbd0 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -264,7 +264,7 @@ class Workmanager { requiresCharging: requiresCharging), ); - /// Schedules a repeated periodic task that (if iOS randomly started depending on iOS) + /// Schedules a periodic task that will run every provided [frequency], on iOS it is not guaranteed when or how often it will run. /// A [uniqueName] is required so only one task can be registered. /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. From b06db8775a54b5e8c46859a10ed6cc7e62604711 Mon Sep 17 00:00:00 2001 From: Absar Date: Mon, 18 Sep 2023 23:40:04 +0200 Subject: [PATCH 13/26] Improve code documentation --- README.md | 2 +- ios/Classes/SwiftWorkmanagerPlugin.swift | 12 +++++----- lib/src/workmanager.dart | 28 ++++++++++-------------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 48ae724a..3126939b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Our fork to original repo just integrates a PR which was not merged yet was good in solving issues on iOS. After PR integration, here is how our fork works: -- registerOneOffTask: Now starts immediately on both android and iOS. On iOS it lasts only 29sec. +- registerOneOffTask: Starts immediately on both android and iOS. On iOS it lasts only 29sec. - registeriOSBackgroundProcessingTask: Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 033ee997..17c3de1e 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -77,7 +77,6 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } - ///Handlers @available(iOS 13.0, *) private static func handleBGProcessingTask(_ task: BGProcessingTask) { NSLog("Workmanagerplugin handle handleBGProcessingTask") @@ -179,11 +178,10 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { // Create an operation that performs the main part of the background task. } - /// - @objc - /// First register names for BGAppRefresh - /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown + /// First register names for [BGAppRefresh]. + /// You must register task names before app finishes launching in AppDelegate. /// After that you can call [registerAppRefreshTaskScheduler] which schedules task in background + @objc public static func registerAppRefreshTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { print("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") @@ -234,8 +232,8 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { /// First register names for BGProcessingTask called by WorkmangerPlugin.m /// This happens on registering - /// you must register tasknames before app finishes launching in appdelegate --> else there is an error thrown - /// After that you can call [registerBackgroundProcessingTaskScheduler] which schedules task in background + /// you must register task names before app finishes launching in AppDelegate --> else there is an error thrown + /// After that you can call [registerProcessingTaskScheduler] which schedules task in background @objc public static func registerBGProcessingTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 72f4bbd0..6e7c22a5 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -53,11 +53,11 @@ typedef BackgroundTaskHandler = Future Function( /// ``` /// /// You can schedule a specific iOS task using: -/// - `Workmanager#registerOneOffTask()` +/// - `Workmanager.registerOneOffTask()` /// Please read the documentation on limitations for background processing on iOS. /// /// You can now schedule Android tasks using: -/// - `Workmanager#registerOneOffTask()` or `Workmanager#registerPeriodicTask` +/// - `Workmanager.registerOneOffTask()` or `Workmanager.registerPeriodicTask()` /// /// iOS periodic task is automatically scheduled if you setup the plugin properly. class Workmanager { @@ -143,18 +143,17 @@ class Workmanager { } } - ///iOS implementation - ///checks whether user or parental control avoids background refresh - /// + /// Checks whether user or parental control restricts background refresh. + /// Only available on iOS. Future checkBackgroundRefreshPermission() async { try { var result = await _foregroundChannel.invokeMethod( 'checkBackgroundRefreshPermission', JsonMapperHelper.toInitializeMethodArgument( - isInDebugMode: _isInDebugMode, - callbackHandle: - 0), //must provide an argument for switch statement on Swift + isInDebugMode: _isInDebugMode, + callbackHandle: 0, + ), ); switch (result.toString()) { case 'available': @@ -172,8 +171,8 @@ class Workmanager { return BackgroundAuthorisationState.unknown; } - /// Schedule a one off task - /// starts on iOS immediately with a timeout of 29 secs in background + /// Schedule a one off task. + /// On iOS immediately starts with a timeout of 29 secs in background. /// 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 @@ -183,7 +182,6 @@ class Workmanager { /// Only supported on Android. final String taskName, { - /// Only supported on Android. final String? tag, @@ -229,13 +227,11 @@ class Workmanager { ), ); - /// Schedule a BackgroundProcessingTask only for iOS - /// This can be a long running Task on iOS longer than 30seconds - /// See Apples documentation https://developer.apple.com/documentation/backgroundtasks - Future registeriOSBackgroundProcessingTask( + /// Schedule a background long running task, currently only available on iOS. + /// It can take longer than 30 seconds, to be used for tasks that might be time-consuming, such as downloading a large file or synchronizing data. + /// See Apple docs https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app final String uniqueName, final String taskName, { - /// set required [NetworkType] only iOS final NetworkType? networkType = NetworkType.not_required, From 7f807fe531b1b513d7bbed29a5c06170dc9c4a7b Mon Sep 17 00:00:00 2001 From: Absar Date: Tue, 19 Sep 2023 01:42:20 +0200 Subject: [PATCH 14/26] Cleanups in SwiftWorkmanagerPlugin.swift * Use logInfo instead of prints and NSLog * Log unnecessary logs only in debug mode * Remove unnecessary logs * Remove isInitalized flag in SwiftWorkmanagerPlugin which was not set to true anywhere --- ios/Classes/SwiftWorkmanagerPlugin.swift | 97 ++++++++++++------------ ios/Classes/WMPError.swift | 2 +- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 17c3de1e..db9ff83d 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -12,7 +12,6 @@ extension String { public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { static let identifier = "be.tramckrijte.workmanager" - private var _isInitalized = false private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback? private struct ForegroundMethodChannel { @@ -79,7 +78,10 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @available(iOS 13.0, *) private static func handleBGProcessingTask(_ task: BGProcessingTask) { - NSLog("Workmanagerplugin handle handleBGProcessingTask") + let isInDebug = UserDefaultsHelper.getIsDebug() + if isInDebug { + logInfo("WorkmanagerPlugin handle handleBGProcessingTask") + } let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task @@ -88,7 +90,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { inputData: "", //no data flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, backgroundMode: .backgroundProcessingTask, - isInDebug: UserDefaultsHelper.getIsDebug() + isInDebug: isInDebug ) // Provide an expiration handler for the background task @@ -110,7 +112,10 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc @available(iOS 13.0, *) public static func handleAppRefresh(task: BGAppRefreshTask) { - NSLog("Workmanagerplugin handle BGAppRefreshTask") + let isInDebug = UserDefaultsHelper.getIsDebug() + if isInDebug { + logInfo("WorkmanagerPlugin handle BGAppRefreshTask") + } guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), let _ = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { @@ -118,10 +123,9 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { return } - /// Could improved _ seconds are ignored on refresh //important to reschedule + // TODO Improve seconds are ignored on refresh, important to reschedule scheduleAppRefresh(taskIdentifier: task.identifier, earliestBeginInSeconds: 120) - NSLog("Workmanagerplugin handle BGAppRefreshTask") let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task let operation = BackgroundTaskOperation( @@ -129,7 +133,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { inputData: "", flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, backgroundMode: .backgroundAppRefresh, - isInDebug: UserDefaultsHelper.getIsDebug() + isInDebug: isInDebug ) // Provide an expiration handler for the background task @@ -141,7 +145,9 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { // Inform the system that the background task is complete // when the operation completes operation.completionBlock = { - NSLog("Workmanagerplugin handle BGAppRefreshTask completed") + if isInDebug { + logInfo("WorkmanagerPlugin handle BGAppRefreshTask completed") + } task.setTaskCompleted(success: !operation.isCancelled) } @@ -152,10 +158,13 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { /// Initialisation for all Tasks + /// Immediately start a background fetch with 29sec timeout - specification by iOS @available(iOS 13.0, *) - /// Immedately start a background fetch with 29sec timeout - specification by iOS public static func startOnOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData:String, delaySeconds: Int64) { - NSLog("Workmanagerplugin immedately startOnOffTask alias iOS backgroundFetch started - timeout after 30sec.") + let isInDebug = UserDefaultsHelper.getIsDebug() + if isInDebug { + logInfo("WorkmanagerPlugin immediately startOneOffTask") + } let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task @@ -164,12 +173,14 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { inputData: inputData, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, backgroundMode: .backgroundOnOffTask(identifier: identifier), - isInDebug: UserDefaultsHelper.getIsDebug() + isInDebug: isInDebug ) // Inform the system that the task is complete when the operation completes operation.completionBlock = { - NSLog("Background task ended \(identifier) : ID:\(taskIdentifier).") + if isInDebug { + logInfo("Background task ended \(identifier) : ID:\(taskIdentifier).") + } UIApplication.shared.endBackgroundTask(taskIdentifier) } @@ -184,7 +195,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc public static func registerAppRefreshTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { - print("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") + logInfo("Workmanager - registerAppRefreshTask withIdentifier \(identifier)") BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, @@ -203,13 +214,12 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { if #available(iOS 13.0, *) { - print("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") - // schedule on app did enter background + logInfo("Workmanager - registerAppRefreshTaskScheduler withIdentifier \(identifier)") + // Schedule on app did enter background NotificationCenter.default.addObserver( forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil ) { _ in - // schedule apprefresh scheduleAppRefresh(taskIdentifier: identifier, earliestBeginInSeconds: begin) } } @@ -223,21 +233,21 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { request.earliestBeginDate = Date(timeIntervalSinceNow: begin) do { try BGTaskScheduler.shared.submit(request) - print("scheduleAppRefresh workmanager (re)scheduled app refresh \(identifier)") + logInfo("scheduleAppRefresh workmanager (re)scheduled app refresh \(identifier)") } catch { - print("Couldn't schedule app refresh \(error.localizedDescription)") + logInfo("Couldn't schedule app refresh \(error.localizedDescription)") return } } - /// First register names for BGProcessingTask called by WorkmangerPlugin.m + /// First register names for BGProcessingTask called by WorkmanagerPlugin.m /// This happens on registering /// you must register task names before app finishes launching in AppDelegate --> else there is an error thrown /// After that you can call [registerProcessingTaskScheduler] which schedules task in background @objc public static func registerBGProcessingTask(withIdentifier identifier: String) { if #available(iOS 13.0, *) { - print("Workmanager - registerBackgroundProcessingTask withIdentifier \(identifier)") + logInfo("Workmanager - registerProcessingTask withIdentifier \(identifier)") BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, using: nil @@ -257,16 +267,14 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { requiresNetworkConnectivity: Bool, requiresExternalPower: Bool) { if #available(iOS 13.0, *) { - // avoid XCode line length issue in notificationcenterobserver maxx 200 chars line length let network = requiresNetworkConnectivity let extPower = requiresExternalPower - print("Workmanager - registerBackgroundProcessingTaskScheduler withIdentifier \(uniqueTaskIdentifier)") + logInfo("Workmanager - registerProcessingTaskScheduler withIdentifier \(uniqueTaskIdentifier)") NotificationCenter.default.addObserver( forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil ) { _ in - // schedule apprefresh scheduleBackgroundProcessingTask(withIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity: network, requiresExternalPower: extPower) } } @@ -289,11 +297,10 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { request.requiresExternalPower = requiresExternalPower do { try BGTaskScheduler.shared.submit(request) - print("Requested BackgroundProcessingTask \(uniqueTaskIdentifier)") + logInfo("Requested BackgroundProcessingTask \(uniqueTaskIdentifier)") } catch { - print("Couldn't schedule app BackgroundProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") - print("On BGTaskSchedulerErrorDomain error 1 - please run on real device") - print("On BGTaskSchedulerErrorDomain error 3 - check registered names") + logInfo("Couldn't schedule app BackgroundProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") + logInfo("Possible issues can be: running on a simulator instead of a real device, or the task name is not registered") } } @@ -328,16 +335,12 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { _ = checkBackgroundRefreshAuthorisation(result: result) return case (ForegroundMethodChannel.Methods.RegisterPeriodicTask.name, let .some(arguments)): - // register bgAppRefreshTask for less than 30 seconds backgroundtime registerPeriodicTask(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): - // register processingtask for less than 30 seconds backgroundtime - // Task starts immedatly registerOneOffTask(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.RegisteriOSBackgroundProcessingTask.name, let .some(arguments)): - // register long running iOs BGProcessingtask for more than 30 seconds backgroundtime registerBackgroundProcessingTask(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): @@ -353,16 +356,13 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } private func initialize(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - if _isInitalized { - result(WMPError.workmanagerIsAlreadyInitialized) - return - } #if targetEnvironment(simulator) - print("Workmanager Info: Please run on real device!" + - "No backgroundtask is automatic called in the simulator!!") + logInfo("Workmanager Info: Should be run on a real device," + + "background tasks might not run on simulators.") #endif let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result: result) if backgroundRefreshAvailable != BackgroundAuthorisationState.available { + // TODO what is the rationale of opening App settings without a message to user? instead it should be done by the calling App not this plugin UIApplication.shared.open(URL( string: UIApplication.openSettingsURLString)!, options: [:], @@ -380,7 +380,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } private func registerPeriodicTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - print("Registering periodic task in background (BGAppRefreshTask)") + logInfo("Registering periodic task in background (BGAppRefreshTask)") if !validateCallbackHandle(result: result) { return } @@ -394,11 +394,11 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } let initialDelaySeconds = arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 ?? 0 - // task will scheduled when app goes to background + // Task will be scheduled when app goes to background SwiftWorkmanagerPlugin.registerAppRefreshTaskScheduler( taskIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: Double(initialDelaySeconds)) - print("Registered PeriodicTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst) delaySeconds \(initialDelaySeconds)") + logInfo("Registered PeriodicTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst) delaySeconds \(initialDelaySeconds)") result(true) return } else { @@ -409,7 +409,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } private func registerOneOffTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - print("Registering OneOffTask") + logInfo("Registering OneOffTask") if !validateCallbackHandle(result: result) { return } @@ -436,7 +436,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { taskIdentifier = UIApplication.shared.beginBackgroundTask(withName: uniqueTaskIdentifier, expirationHandler: { - // Code to handle if takes way too long + // Code to handle if it takes way too long UIApplication.shared.endBackgroundTask(taskIdentifier) }) SwiftWorkmanagerPlugin.startOnOffTask(identifier: callBackIdentifier, @@ -444,11 +444,11 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { inputData: inputData ?? "", delaySeconds: delaySeconds) result(true) - print("Registered OnOffTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") + logInfo("Registered OneOffTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") return } else { result(FlutterError(code: "99", - message: "RegisterPeriodicTask is not registered", + message: "registerOneOffTask is not registered", details: "iOS Version lower than 13.0")) } } @@ -476,14 +476,14 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { requiresNetwork = true } - // task will scheduled by iOS when app goes to background SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler( + // Task will be scheduled by OS when app goes to background uniqueTaskIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: delaySeconds, requiresNetworkConnectivity: requiresCharging, requiresExternalPower: requiresNetwork) result(true) - print("Registered BackgroundProcessingTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") + logInfo("Registered BackgroundProcessingTask \(uniqueTaskIdentifier) , callbackId \(uniqueTaskIdentifier.lowercasingFirst)") return } else { @@ -512,9 +512,8 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { result(true) } - /// Checks wether getStoredCallbackHandle is set - /// Returns true wenn initilized - /// if false result contains errormessage + /// Checks whether getStoredCallbackHandle is set. + /// Returns true when initialized, if false result contains error message. private func validateCallbackHandle(result: @escaping FlutterResult) -> Bool { if UserDefaultsHelper.getStoredCallbackHandle() == nil { result( diff --git a/ios/Classes/WMPError.swift b/ios/Classes/WMPError.swift index d281aa72..6e11803e 100644 --- a/ios/Classes/WMPError.swift +++ b/ios/Classes/WMPError.swift @@ -31,7 +31,7 @@ enum WMPError: Error { case .unexpectedMethodArguments(let argumentsDescription): return "Unexpected call arguments \(argumentsDescription)" case .workmanagerIsAlreadyInitialized: - return "Workmanager was initialized once. It can not initilized a second time" + return "Workmanager already initialized, it should not be initialized again" case .bgTaskSchedulingFailed(let error): return """ Scheduling the task using BGTaskScheduler has failed. From 2fb38eb1943c57ad66715957a745b6a6d3a478ff Mon Sep 17 00:00:00 2001 From: Absar Date: Tue, 19 Sep 2023 01:52:33 +0200 Subject: [PATCH 15/26] * iOS, Rename registeriOSBackgroundProcessingTask to a generic name registerProcessingTask to be consistent with rest of the plugin and possible future Android implementation * iOS, Rename wrongly named startOnOffTask to startOneOffTask --- README.md | 2 +- example/lib/main.dart | 2 +- ios/Classes/BackgroundWorker.swift | 8 ++++---- ios/Classes/SwiftWorkmanagerPlugin.swift | 26 ++++++++++++------------ lib/src/workmanager.dart | 3 ++- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3126939b..5ef57d03 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ After PR integration, here is how our fork works: - registerOneOffTask: Starts immediately on both android and iOS. On iOS it lasts only 29sec. -- registeriOSBackgroundProcessingTask: Long running oneoff background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. +- registerProcessingTask: Long running one off background task to be used specifically on iOS. It last more than 29sec but doesnt start immediately. - registerPeriodicTask: This is for a scheduled task on both android and iOS. It's a 29sec task on iOS, but doesn't start immediately. diff --git a/example/lib/main.dart b/example/lib/main.dart index d96122f4..816ad631 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -354,7 +354,7 @@ class _MyAppState extends State with WidgetsBindingObserver { workmanagerInitialized = true; } await Workmanager() - .registeriOSBackgroundProcessingTask( + .registerProcessingTask( iOSBackgroundProcessingTask, iOSBackgroundProcessingTask); } diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index 1ea7a00e..82a27066 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -10,7 +10,7 @@ import Foundation enum BackgroundMode { case backgroundAppRefresh case backgroundProcessingTask - case backgroundOnOffTask(identifier: String) + case backgroundOneOffTask(identifier: String) var flutterThreadlabelPrefix: String { switch self { @@ -18,8 +18,8 @@ enum BackgroundMode { return "\(SwiftWorkmanagerPlugin.identifier).BackgroundAppRefreshTask" case .backgroundProcessingTask: return "\(SwiftWorkmanagerPlugin.identifier).BackgroundProcessingTask" - case .backgroundOnOffTask: - return "\(SwiftWorkmanagerPlugin.identifier).OnOffTask" + case .backgroundOneOffTask: + return "\(SwiftWorkmanagerPlugin.identifier).OneOffTask" } } @@ -29,7 +29,7 @@ enum BackgroundMode { return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundAppRefresh"] case .backgroundProcessingTask: return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundProcessingTask"] - case let .backgroundOnOffTask(identifier): + case let .backgroundOneOffTask(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] } } diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index db9ff83d..7bb1ad62 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -40,8 +40,8 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } - struct RegisteriOSBackgroundProcessingTask { - static let name = "\(RegisteriOSBackgroundProcessingTask.self)".lowercasingFirst + struct RegisterProcessingTask { + static let name = "\(RegisterProcessingTask.self)".lowercasingFirst enum Arguments: String { case taskName case uniqueName @@ -160,7 +160,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { /// Immediately start a background fetch with 29sec timeout - specification by iOS @available(iOS 13.0, *) - public static func startOnOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData:String, delaySeconds: Int64) { + public static func startOneOffTask(identifier: String, taskIdentifier: UIBackgroundTaskIdentifier, inputData:String, delaySeconds: Int64) { let isInDebug = UserDefaultsHelper.getIsDebug() if isInDebug { logInfo("WorkmanagerPlugin immediately startOneOffTask") @@ -172,7 +172,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { identifier, inputData: inputData, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundOnOffTask(identifier: identifier), + backgroundMode: .backgroundOneOffTask(identifier: identifier), isInDebug: isInDebug ) @@ -262,7 +262,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc /// Registers a long running BackgroundProcessingTask - randomly started by iOS when app in background /// Task will scheduled when app goes to background - public static func registerBackgroundProcessingTaskScheduler(uniqueTaskIdentifier: String, + public static func registerProcessingTaskScheduler(uniqueTaskIdentifier: String, earliestBeginInSeconds begin: Double, requiresNetworkConnectivity: Bool, requiresExternalPower: Bool) { @@ -282,7 +282,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc /// Schedules a long running BackgroundProcessingTask - randomly started by iOS when app in background - /// Called by UIApplication.didEnterBackgroundNotification in [registerBackgroundProcessingTaskScheduler] + /// Called by UIApplication.didEnterBackgroundNotification in [registerProcessingTaskScheduler] @available(iOS 13.0, *) private static func scheduleBackgroundProcessingTask( withIdentifier uniqueTaskIdentifier: String, @@ -340,8 +340,8 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)): registerOneOffTask(arguments: arguments, result: result) return - case (ForegroundMethodChannel.Methods.RegisteriOSBackgroundProcessingTask.name, let .some(arguments)): - registerBackgroundProcessingTask(arguments: arguments, result: result) + case (ForegroundMethodChannel.Methods.RegisterProcessingTask.name, let .some(arguments)): + registerProcessingTask(arguments: arguments, result: result) return case (ForegroundMethodChannel.Methods.CancelAllTasks.name, .none): cancelAllTasks(result: result) @@ -439,7 +439,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { // Code to handle if it takes way too long UIApplication.shared.endBackgroundTask(taskIdentifier) }) - SwiftWorkmanagerPlugin.startOnOffTask(identifier: callBackIdentifier, + SwiftWorkmanagerPlugin.startOneOffTask(identifier: callBackIdentifier, taskIdentifier: taskIdentifier, inputData: inputData ?? "", delaySeconds: delaySeconds) @@ -453,13 +453,13 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } } - private func registerBackgroundProcessingTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { - print("Registering backgroundProcessingTask") + private func registerProcessingTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { + logInfo("Registering backgroundProcessingTask") if !validateCallbackHandle(result: result) { return } if #available(iOS 13.0, *) { - let method = ForegroundMethodChannel.Methods.RegisteriOSBackgroundProcessingTask.self + let method = ForegroundMethodChannel.Methods.RegisterProcessingTask.self guard let uniqueTaskIdentifier = arguments[method.Arguments.uniqueName.rawValue] as? String else { result(WMPError.invalidParameters.asFlutterError) @@ -476,8 +476,8 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { requiresNetwork = true } - SwiftWorkmanagerPlugin.registerBackgroundProcessingTaskScheduler( // Task will be scheduled by OS when app goes to background + SwiftWorkmanagerPlugin.registerProcessingTaskScheduler( uniqueTaskIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: delaySeconds, requiresNetworkConnectivity: requiresCharging, diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 6e7c22a5..8fe75bb4 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -230,6 +230,7 @@ class Workmanager { /// Schedule a background long running task, currently only available on iOS. /// It can take longer than 30 seconds, to be used for tasks that might be time-consuming, such as downloading a large file or synchronizing data. /// See Apple docs https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app + Future registerProcessingTask( final String uniqueName, final String taskName, { /// set required [NetworkType] only iOS @@ -247,7 +248,7 @@ class Workmanager { final Map? inputData, }) async => await _foregroundChannel.invokeMethod( - "registeriOSBackgroundProcessingTask", + "registerProcessingTask", JsonMapperHelper.toRegisterMethodArgument( isInDebugMode: _isInDebugMode, uniqueName: uniqueName, From 2af1f55c2cb6f3fea371a811cbd5c96f918ee116 Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 20 Sep 2023 02:12:19 +0200 Subject: [PATCH 16/26] * Cleanup code to make it more close to original plugin so that change size is reduced and it will make it easy to review * Change new task identifier to be consistent with existing ones e.g. instead of app.workmanager... use be.tramckrijte... * Documentation update * Remove unnecessary logs, comments etc which were added in PRs which were not merged, and cleanup unnecessary code * Revert using a custom log helper OS file to use the plugins existing shared prefs * Bump example flutter sdk to < 4 instead of < 3 --- README.md | 17 +- example/ios/Runner/AppDelegate.swift | 4 +- example/ios/Runner/Info.plist | 4 +- example/lib/main.dart | 420 +++++++++++++-------------- example/pubspec.yaml | 2 +- lib/src/workmanager.dart | 10 +- pubspec.yaml | 2 +- 7 files changed, 235 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index 5ef57d03..2d8ee873 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,11 @@ Workmanager().registerOneOffTask( ); ``` -iOS supports **Periodic tasks** with maximum 29sec execution time (see example). +iOS supports **Periodic tasks** with maximum 29 seconds. For more information see the [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app). ```dart -const iOSBackgroundAppRefresh = "app.workmanagerExample.iOSBackgroundAppRefresh"; +const iOSBackgroundAppRefresh = "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh"; Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, @@ -139,6 +139,19 @@ Workmanager().registerPeriodicTask( For more information see the [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask). +iOS supports **Processing tasks** which can run for more than 30 seconds. +Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these. +Processing tasks run only when the device is idle. iOS terminates any background processing tasks running when the user starts using the device. However background refresh tasks aren’t affected. +For more information see the [Apple Docs](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask) + +```dart +const iOSBackgroundProcessingTask = "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask"; +Workmanager().registerProcessingTask( + iOSBackgroundProcessingTask, + iOSBackgroundProcessingTask, +); +``` + Get permissions for iOS BackgroundRefresh - see example ```dart diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 997d0656..c5f5bcdd 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -36,8 +36,8 @@ import workmanager WorkmanagerPlugin.registerTask(withIdentifier: "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask")*/ //important to register backgroundprocessingtask in Runner/AppDelegate and info.plist - WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "app.workmanagerExample.iOSBackgroundAppRefresh") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "app.workmanagerExample.iOSBackgroundProcessingTask") + WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh") + WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask") return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 2d0940f2..bb219cca 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -11,8 +11,8 @@ be.tramckrijte.workmanagerExample.simpleDelayedTask be.tramckrijte.workmanagerExample.simplePeriodicTask be.tramckrijte.workmanagerExample.simplePeriodic1HourTask - app.workmanagerExample.iOSBackgroundAppRefresh - app.workmanagerExample.iOSBackgroundProcessingTask + be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh + be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/example/lib/main.dart b/example/lib/main.dart index 816ad631..a1ac8a73 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,13 +3,11 @@ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; -import 'log_helper.dart'; - void main() { - //added MaterialApp for showdialog runApp(MaterialApp(home: MyApp())); } @@ -21,101 +19,79 @@ const simplePeriodicTask = "be.tramckrijte.workmanagerExample.simplePeriodicTask"; const simplePeriodic1HourTask = "be.tramckrijte.workmanagerExample.simplePeriodic1HourTask"; -//Don'T forget to register these two task in info.plist and AppDelegate.swift (iOS) const iOSBackgroundAppRefresh = - "app.workmanagerExample.iOSBackgroundAppRefresh"; + "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh"; const iOSBackgroundProcessingTask = - "app.workmanagerExample.iOSBackgroundProcessingTask"; + "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask"; + +final List allTasks = [ + simpleTaskKey, + rescheduledTaskKey, + failedTaskKey, + simpleDelayedTask, + simplePeriodicTask, + simplePeriodic1HourTask, + iOSBackgroundAppRefresh, + iOSBackgroundProcessingTask, +]; -@pragma( - 'vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ +// Pragma is mandatory if the App is obfuscated or using Flutter 3.1+ +@pragma('vm:entry-point') void callbackDispatcher() { Workmanager().executeTask((task, inputData) async { - print("callbackDispatcher for $task called"); - await LogHelper.LogBGTask(data: "callbackDispatcher for $task called"); - final prefs = await SharedPreferences - .getInstance(); //only working on Android ?! isolates on iOS has incorrect results. + final prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + print("$task started. inputData = $inputData"); + await prefs.setString(task, 'Last ran at: ${DateTime.now().toString()}'); + switch (task) { - //simpleTaskKey:rescheduledTaskKey:failedTaskKey starts on iOS immediately with a timeout of 30 secs in background case simpleTaskKey: - sleep(Duration(seconds: 12)); // sleep as sample - print("$simpleTaskKey was executed. inputData = $inputData"); - prefs.setString( - "simpleTaskKey", (DateTime.now().toString()) + ' data:$inputData'); - LogHelper.LogBGTask(data: 'simpleTaskKey --> data:$inputData'); + await prefs.setBool("test", true); + print("Bool from prefs: ${prefs.getBool("test")}"); break; case rescheduledTaskKey: - if (inputData == null) { - LogHelper.LogBGTask(data: "Rescheduled Task without inputData"); - sleep(Duration(seconds: 2)); - return Future.value(true); - } - final key = inputData['key']!; - prefs.setString("rescheduledTaskKey", DateTime.now().toString()); + final key = inputData!['key']!; if (prefs.containsKey('unique-$key')) { print('has been running before, task is successful'); return true; } else { - prefs.setBool('unique-$key', true); //perhaps not working on iOS + await prefs.setBool('unique-$key', true); print('reschedule task'); - return Future.value(true); + return false; } case failedTaskKey: print('failed task'); - prefs.setString("failedTask", DateTime.now().toString()); - LogHelper.LogBGTask(data: 'failedTaskKey --> data:$inputData'); return Future.error('failed'); case simpleDelayedTask: print("$simpleDelayedTask was executed"); - prefs.setString("simpleDelayedTask", DateTime.now().toString()); - LogHelper.LogBGTask(data: 'simpleDelayedTaskKey --> data:$inputData'); break; case simplePeriodicTask: print("$simplePeriodicTask was executed"); - prefs.setString("simplePeriodicTask", DateTime.now().toString()); - LogHelper.LogBGTask(data: 'simplePeriodicTaskKey --> data:$inputData'); break; case simplePeriodic1HourTask: print("$simplePeriodic1HourTask was executed"); - prefs.setString("simplePeriodic1HourTask", DateTime.now().toString()); - LogHelper.LogBGTask( - data: 'simplePeriodic1HourTask --> data:$inputData'); break; - case Workmanager - .BACKGROUND_APPREFRESH_TASK_NAME: //Fixed value can't change at the moment - see [BackgroundMode.onResultSendArguments] - //maximum duration 29seconds - App could perhaps killed by iOS when it takes a longer time than 30 seconds for BGAppRefresh included native work - print("The iOS-BackgroundAppRefresh was triggered"); - sleep(Duration(seconds: 14)); // sleep as sample - await LogHelper.LogBGTask(data: "iOSBackgroundAppRefresh"); - //iOS SharedPrefs does not work, because they will not updated in isolated - //**** - // prefs.setString(Workmanager.iOSBackgroundAppRefresh, DateTime.now().toString()); - //**** - // test on debugger - // push home-button an let app enter background - // pause debugger in xcode and enter in terminal ( Connected with real device ) - // pause app and enter in Terminal: - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundAppRefresh"] - // then resume app + case Workmanager.BACKGROUND_APPREFRESH_TASK_NAME: + // Currently fixed value, can't change at the moment - see [BackgroundMode.onResultSendArguments]. + // To test, follow the instructions on https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development + // and https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md + Directory? tempDir = await getTemporaryDirectory(); + String? tempPath = tempDir.path; + print( + "You can access other plugins in the background, for example Directory.getTemporaryDirectory(): $tempPath"); break; - case Workmanager - .BACKGROUND_PROCESSING_TASK_NAME: //Fixed value can't change at the moment - see [BackgroundMode.onResultSendArguments] - //here you can run a long running process longer than 30 seconds. It will randomly started by iOS-Operating-System - // test on debugger - pause debugger in xcode and enter in terminal ( Connected with real device ) - // push home-button an let app enter background - // pause app and enter in Terminal: - // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.workmanagerExample.iOSBackgroundProcessingTask"] - // then resume app - print("The iOS-BGProcessingTask was triggered"); - await LogHelper.LogBGTask(data: "iOSBackgroundProcessingTask started"); - sleep(Duration(seconds: 210)); // sleep as sample - await LogHelper.LogBGTask( - data: "iOSBackgroundProcessingTask finished (sleep 210 sec)"); + case Workmanager.BACKGROUND_PROCESSING_TASK_NAME: + // Currently fixed value, can't change at the moment - see [BackgroundMode.onResultSendArguments]. + // To test, follow the instructions on https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development + // and https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md + // Processing tasks are started by iOS only when phone is idle, hence + // you need to manually trigger by following the docs and putting the to + // background + await Future.delayed(Duration(seconds: 40)); + print("$task finished"); break; default: - print('callbackhandler: unknown task: $task data:$inputData'); - LogHelper.LogBGTask(data: 'unknown task: $task data:$inputData'); - sleep(Duration(seconds: 5)); return Future.value(false); } return Future.value(true); @@ -147,27 +123,13 @@ class _MyAppState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) async { super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.paused) { - //app switched to Background - } if (state == AppLifecycleState.resumed) { - //app came back to Foreground set infotext in example app - setState(() { - _lastResumed = DateTime.now().toString(); - }); - _updatePrefs(); + // App came back from background to foreground + setState(() => _lastResumed = DateTime.now().toString()); + _refreshStats(); } } - void _updatePrefs() async { - var prefsString = "BgDatalog" + "\n"; - var log = await LogHelper.ReadLogBGTask(); - prefsString += log; - setState(() { - _prefsString = prefsString; - }); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -189,7 +151,7 @@ class _MyAppState extends State with WidgetsBindingObserver { child: Text("Start the Flutter background service"), onPressed: () async { if (Platform.isIOS) { - //here you can check whether background refresh is activated in iOS settings + // Check whether background refresh is activated in iOS settings var hasPermissions = await Workmanager() .checkBackgroundRefreshPermission(); if (hasPermissions != @@ -219,152 +181,113 @@ class _MyAppState extends State with WidgetsBindingObserver { if (!workmanagerInitialized) { Workmanager().initialize( callbackDispatcher, - isInDebugMode: true, //Show notifications on iOS native + isInDebugMode: true, ); - setState(() { - workmanagerInitialized = true; - }); + setState(() => workmanagerInitialized = true); } }, ), - SizedBox(height: 5), - Text( - "Sample tasks to start", - style: Theme.of(context).textTheme.headline5, - ), + SizedBox(height: 16), + //This task runs once. //Most likely this will trigger immediately - ///Immedately start a background fetch with 29sec timeout - specification by iOS ElevatedButton( child: Text("Register OneOff Task"), - onPressed: workmanagerInitialized - ? () { - Workmanager().registerOneOffTask( - simpleTaskKey, - //unique Name - must same as in iOS registered Id in info.plist - simpleTaskKey, //ignored on iOS - inputData: { - 'int': 1, - 'bool': true, - 'double': 1.0, - 'string': 'string', - 'array': [1, 2, 3], - 'timeStamp': DateTime.now().toString() - }, - ); - } - : null, + onPressed: () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + Workmanager().registerOneOffTask( + simpleTaskKey, + simpleTaskKey, + inputData: { + 'int': 1, + 'bool': true, + 'double': 1.0, + 'string': 'string', + 'array': [1, 2, 3], + 'timeStamp': DateTime.now().toString() + }, + ); + }, ), ElevatedButton( - child: Text("Register rescheduled Task"), - onPressed: workmanagerInitialized - ? () { - Workmanager().registerOneOffTask( - rescheduledTaskKey, - rescheduledTaskKey, - requiresCharging: false, - networkType: NetworkType.not_required, - inputData: { - 'key': Random().nextInt(64000), - 'timeStamp': DateTime.now().toString() - }, - ); - } - : null), + child: Text("Register rescheduled Task"), + onPressed: () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + Workmanager().registerOneOffTask( + rescheduledTaskKey, + rescheduledTaskKey, + requiresCharging: false, + networkType: NetworkType.not_required, + inputData: { + 'key': Random().nextInt(64000), + 'timeStamp': DateTime.now().toString() + }, + ); + }, + ), ElevatedButton( child: Text("Register failed Task"), - onPressed: workmanagerInitialized - ? () { - Workmanager().registerOneOffTask( - failedTaskKey, - failedTaskKey, - ); - } - : null, + onPressed: () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + Workmanager().registerOneOffTask( + failedTaskKey, + failedTaskKey, + ); + }, ), //This task runs once - //This wait at least 120 seconds before running + //This wait at least 10 seconds before running ElevatedButton( - child: Text("Register Delayed OneOff Task"), - onPressed: workmanagerInitialized - ? () { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - Workmanager().registerOneOffTask( - simpleDelayedTask, - simpleDelayedTask, - initialDelay: Duration(seconds: 120), - ); - } - : null, - ), + child: Text("Register Delayed OneOff Task"), + onPressed: () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + Workmanager().registerOneOffTask( + simpleDelayedTask, + simpleDelayedTask, + initialDelay: Duration(seconds: 10), + ); + }), SizedBox(height: 8), //This task runs periodically - //It will wait at least 120 seconds before its first launch + //It will wait at least 10 seconds before its first launch //Since we have not provided a frequency it will be the default 15 minutes ElevatedButton( child: Text("Register Periodic Task (Android)"), - onPressed: Platform.isAndroid && workmanagerInitialized + onPressed: Platform.isAndroid ? () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } Workmanager().registerPeriodicTask( simplePeriodicTask, simplePeriodicTask, - initialDelay: Duration(seconds: 120), + initialDelay: Duration(seconds: 10), ); } : null), - //This task runs periodically dependening on iOS - there is no safe timing - see Apple doc - //Since we have not provided a frequency it will be the default 2 minutes - //register name in info.plist BGTaskSchedulerPermittedIdentifiers - //register name in iOS - Appdelegate - ElevatedButton( - child: - Text("Register Periodic Background App Refresh (iOS)"), - onPressed: Platform.isIOS && workmanagerInitialized - ? () async { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - await Workmanager().registerPeriodicTask( - iOSBackgroundAppRefresh, - iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 120), //ignored - inputData: {} //ignored on iOS - ); - } - : null), - ElevatedButton( - child: Text("Register BackgroundProcessingTask (iOS)"), - onPressed: Platform.isIOS && workmanagerInitialized - ? () async { - if (!workmanagerInitialized) { - Workmanager().initialize( - callbackDispatcher, - isInDebugMode: true, - ); - workmanagerInitialized = true; - } - await Workmanager() - .registerProcessingTask( - iOSBackgroundProcessingTask, - iOSBackgroundProcessingTask); - } - : null), //This task runs periodically //It will run about every hour ElevatedButton( child: Text("Register 1 hour Periodic Task (Android)"), - onPressed: Platform.isAndroid && workmanagerInitialized + onPressed: Platform.isAndroid ? () { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } Workmanager().registerPeriodicTask( simplePeriodic1HourTask, simplePeriodic1HourTask, @@ -372,6 +295,44 @@ class _MyAppState extends State with WidgetsBindingObserver { ); } : null), + // This task runs periodically depending on iOS - there is no safe timing - see Apple doc + // Since we have not provided a frequency it will be the default 2 minutes + ElevatedButton( + child: Text("Register Periodic Background App Refresh (iOS)"), + onPressed: Platform.isIOS + ? () async { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + await Workmanager().registerPeriodicTask( + iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, + initialDelay: Duration(seconds: 10), //ignored + inputData: {} //ignored on iOS + ); + } + : null, + ), + // This task runs only once, to perform a time consuming task at + // a later time decided by iOS. + // Processing tasks run only when the device is idle. iOS terminates + // any background processing tasks running when the user starts + // using the device. + ElevatedButton( + child: Text("Register BackgroundProcessingTask (iOS)"), + onPressed: Platform.isIOS + ? () async { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + await Workmanager().registerProcessingTask( + iOSBackgroundProcessingTask, + iOSBackgroundProcessingTask, + ); + } + : null, + ), SizedBox(height: 16), Text( "Task cancellation", @@ -379,25 +340,23 @@ class _MyAppState extends State with WidgetsBindingObserver { ), ElevatedButton( child: Text("Cancel All"), - onPressed: workmanagerInitialized - ? () async { - await Workmanager().cancelAll(); - print('Cancel all tasks completed'); - } - : null, + onPressed: () async { + if (!workmanagerInitialized) { + _showNotInitialized(); + return; + } + await Workmanager().cancelAll(); + print('Cancel all tasks completed'); + }, ), - //show entries in prefs on app resume + SizedBox(height: 10), GestureDetector( - onTap: () { - _updatePrefs(); - }, + onTap: _refreshStats, child: SingleChildScrollView( child: Text( - "Task Values(executed timestamps):\nTap here to update\n" + - _prefsString + - "\n" + - "Last-app resumed at: " + - _lastResumed)), + "Task run stats:\nTap here to update\n " + "$_prefsString\n Last App resumed at: $_lastResumed", + )), ), ], ), @@ -406,4 +365,35 @@ class _MyAppState extends State with WidgetsBindingObserver { ), ); } + + // Refresh/get saved prefs + void _refreshStats() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + _prefsString = ''; + for (final task in allTasks) { + _prefsString = '$_prefsString \n$task:\n${prefs.getString(task)}\n'; + } + + setState(() {}); + } + + void _showNotInitialized() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: new Text("Workmanager not initialized"), + content: new Text("Workmanager not initialized"), + actions: [ + new TextButton( + child: new Text("OK"), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a04ecbb1..76852aaa 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the workmanager plugin. publish_to: 'none' environment: - sdk: ">=2.12.0-0 <3.0.0" + sdk: ">=2.18.0 <4.0.0" dependencies: path_provider: ^2.0.11 diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 8fe75bb4..14e695c4 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -228,8 +228,16 @@ class Workmanager { ); /// Schedule a background long running task, currently only available on iOS. - /// It can take longer than 30 seconds, to be used for tasks that might be time-consuming, such as downloading a large file or synchronizing data. + /// + /// Processing tasks are for long processes like data processing and app maintenance. + /// Processing tasks can run for minutes, but the system can interrupt these. + /// Processing tasks run only when the device is idle. iOS terminates any + /// background processing tasks running when the user starts using the device. + /// However background refresh tasks aren’t affected. + /// + /// /// See Apple docs https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app + /// https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask Future registerProcessingTask( final String uniqueName, final String taskName, { diff --git a/pubspec.yaml b/pubspec.yaml index 12bd672f..97d73495 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/fluttercommunity/flutter_workmanager issue_tracker: https://github.com/fluttercommunity/flutter_workmanager/issues environment: - sdk: ">=2.18.0 <3.0.0" + sdk: ">=2.18.0 <4.0.0" flutter: ">=2.5.0" dependencies: From 35582585daaf7ad453aef702af04d69ac223b8fe Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 20 Sep 2023 19:05:14 +0200 Subject: [PATCH 17/26] Add task identifiers to iOS AppRefresh and ProcessingTask so that user can define task names instead of using hardcoded names --- README.md | 2 +- example/lib/log_helper.dart | 1 + example/lib/main.dart | 4 ++-- ios/Classes/BackgroundWorker.swift | 12 ++++++------ ios/Classes/SwiftWorkmanagerPlugin.swift | 15 +++++++-------- lib/src/workmanager.dart | 1 + 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 2d8ee873..8149319a 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ const iOSBackgroundAppRefresh = "be.tramckrijte.workmanagerExample.iOSBackground Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 10), // Ignored on iOS + initialDelay: Duration(seconds: 10), ); ``` diff --git a/example/lib/log_helper.dart b/example/lib/log_helper.dart index a51c1744..0e4a1044 100644 --- a/example/lib/log_helper.dart +++ b/example/lib/log_helper.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; +// TODO delete this file, it is not needed ///Helper to write events to a local file, because SharedPrefs doesn't sync datas between isolated class LogHelper { static const String _backgroundTaskLogFileName = "iOSBackgroundTask.log"; diff --git a/example/lib/main.dart b/example/lib/main.dart index a1ac8a73..8e43e201 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -72,7 +72,7 @@ void callbackDispatcher() { case simplePeriodic1HourTask: print("$simplePeriodic1HourTask was executed"); break; - case Workmanager.BACKGROUND_APPREFRESH_TASK_NAME: + case iOSBackgroundAppRefresh: // Currently fixed value, can't change at the moment - see [BackgroundMode.onResultSendArguments]. // To test, follow the instructions on https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development // and https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md @@ -81,7 +81,7 @@ void callbackDispatcher() { print( "You can access other plugins in the background, for example Directory.getTemporaryDirectory(): $tempPath"); break; - case Workmanager.BACKGROUND_PROCESSING_TASK_NAME: + case iOSBackgroundProcessingTask: // Currently fixed value, can't change at the moment - see [BackgroundMode.onResultSendArguments]. // To test, follow the instructions on https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development // and https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index 82a27066..0e65bb8b 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -8,8 +8,8 @@ import Foundation enum BackgroundMode { - case backgroundAppRefresh - case backgroundProcessingTask + case backgroundAppRefresh(identifier: String) + case backgroundProcessingTask(identifier: String) case backgroundOneOffTask(identifier: String) var flutterThreadlabelPrefix: String { @@ -25,10 +25,10 @@ enum BackgroundMode { var onResultSendArguments: [String: String] { switch self { - case .backgroundAppRefresh: - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundAppRefresh"] - case .backgroundProcessingTask: - return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSBackgroundProcessingTask"] + case .backgroundAppRefresh(identifier): + return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] + case .backgroundProcessingTask(identifier): + return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] case let .backgroundOneOffTask(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] } diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 7bb1ad62..ad93b5a5 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -77,7 +77,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } @available(iOS 13.0, *) - private static func handleBGProcessingTask(_ task: BGProcessingTask) { + private static func handleBGProcessingTask(identifier: String, task: BGProcessingTask) { let isInDebug = UserDefaultsHelper.getIsDebug() if isInDebug { logInfo("WorkmanagerPlugin handle handleBGProcessingTask") @@ -89,7 +89,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { task.identifier, inputData: "", //no data flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundProcessingTask, + backgroundMode: .backgroundProcessingTask(identifier: identifier), isInDebug: isInDebug ) @@ -111,7 +111,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc @available(iOS 13.0, *) - public static func handleAppRefresh(task: BGAppRefreshTask) { + public static func handleAppRefresh(identifier: String, task: BGAppRefreshTask) { let isInDebug = UserDefaultsHelper.getIsDebug() if isInDebug { logInfo("WorkmanagerPlugin handle BGAppRefreshTask") @@ -132,7 +132,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { task.identifier, inputData: "", flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundAppRefresh, + backgroundMode: .backgroundAppRefresh(identifier: identifier), isInDebug: isInDebug ) @@ -202,7 +202,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { using: nil ) { task in if let task = task as? BGAppRefreshTask { - handleAppRefresh(task: task) + handleAppRefresh(identifier: identifier, task: task) } } } @@ -228,8 +228,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc @available(iOS 13.0, *) private static func scheduleAppRefresh(taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { - let request = BGAppRefreshTaskRequest( - identifier: identifier) + let request = BGAppRefreshTaskRequest(identifier: identifier) request.earliestBeginDate = Date(timeIntervalSinceNow: begin) do { try BGTaskScheduler.shared.submit(request) @@ -253,7 +252,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { using: nil ) { task in if let task = task as? BGProcessingTask { - handleBGProcessingTask(task) + handleBGProcessingTask(identifier: identifier, task: task) } } } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 14e695c4..56c2a2ed 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -101,6 +101,7 @@ class Workmanager { MethodChannel _foregroundChannel = const MethodChannel( "be.tramckrijte.workmanager/foreground_channel_work_manager"); + // TODO remove these and convert to user named tasks, and change above example static const BACKGROUND_APPREFRESH_TASK_NAME = "iOSBackgroundAppRefresh"; static const BACKGROUND_PROCESSING_TASK_NAME = "iOSBackgroundProcessingTask"; From 43df0004b4c849087d0747cab6a501b445b148e4 Mon Sep 17 00:00:00 2001 From: Absar Date: Thu, 21 Sep 2023 01:28:54 +0200 Subject: [PATCH 18/26] * iOS AppRefresh task interval should be 15 minutes * Documentation update --- example/lib/main.dart | 5 +++-- ios/Classes/SwiftWorkmanagerPlugin.swift | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 8e43e201..aeec506a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -296,7 +296,8 @@ class _MyAppState extends State with WidgetsBindingObserver { } : null), // This task runs periodically depending on iOS - there is no safe timing - see Apple doc - // Since we have not provided a frequency it will be the default 2 minutes + // Currently we cannot provide frequency for iOS, hence it will be + // minimum 15 minutes after which iOS will reschedule ElevatedButton( child: Text("Register Periodic Background App Refresh (iOS)"), onPressed: Platform.isIOS @@ -307,7 +308,7 @@ class _MyAppState extends State with WidgetsBindingObserver { } await Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 10), //ignored + initialDelay: Duration(seconds: 10), inputData: {} //ignored on iOS ); } diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index ad93b5a5..9cdd6dac 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -123,8 +123,9 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { return } - // TODO Improve seconds are ignored on refresh, important to reschedule - scheduleAppRefresh(taskIdentifier: task.identifier, earliestBeginInSeconds: 120) + // Reschedule no earlier than 15 minutes from now. TODO interval should be configurable + // probably through AppDelegate.swift WorkmanagerPlugin.registerPeriodicTask + scheduleAppRefresh(taskIdentifier: task.identifier, earliestBeginInSeconds: 15 * 60) let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task @@ -136,14 +137,12 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { isInDebug: isInDebug ) - // Provide an expiration handler for the background task - // that cancels the operation + // Provide an expiration handler for the background task that cancels the operation task.expirationHandler = { operation.cancel() } - // Inform the system that the background task is complete - // when the operation completes + // Inform the system that the background task is complete when the operation completes operation.completionBlock = { if isInDebug { logInfo("WorkmanagerPlugin handle BGAppRefreshTask completed") @@ -261,10 +260,12 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { @objc /// Registers a long running BackgroundProcessingTask - randomly started by iOS when app in background /// Task will scheduled when app goes to background - public static func registerProcessingTaskScheduler(uniqueTaskIdentifier: String, - earliestBeginInSeconds begin: Double, - requiresNetworkConnectivity: Bool, - requiresExternalPower: Bool) { + public static func registerProcessingTaskScheduler( + uniqueTaskIdentifier: String, + earliestBeginInSeconds begin: Double, + requiresNetworkConnectivity: Bool, + requiresExternalPower: Bool + ) { if #available(iOS 13.0, *) { let network = requiresNetworkConnectivity let extPower = requiresExternalPower From 1bd6a6b88669d8a8e7198cca6259fbbebdec3800 Mon Sep 17 00:00:00 2001 From: Absar Date: Thu, 21 Sep 2023 20:34:45 +0200 Subject: [PATCH 19/26] Initialize should not auto open App settings if background refresh permission is not assigned. Initialize should return result --- ios/Classes/SwiftWorkmanagerPlugin.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 9cdd6dac..1256e213 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -360,15 +360,6 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { logInfo("Workmanager Info: Should be run on a real device," + "background tasks might not run on simulators.") #endif - let backgroundRefreshAvailable = checkBackgroundRefreshAuthorisation(result: result) - if backgroundRefreshAvailable != BackgroundAuthorisationState.available { - // TODO what is the rationale of opening App settings without a message to user? instead it should be done by the calling App not this plugin - UIApplication.shared.open(URL( - string: UIApplication.openSettingsURLString)!, - options: [:], - completionHandler: nil) - return - } let method = ForegroundMethodChannel.Methods.Initialize.self guard let isInDebug = arguments[method.Arguments.isInDebugMode.rawValue] as? Bool, let handle = arguments[method.Arguments.callbackHandle.rawValue] as? Int64 else { @@ -377,6 +368,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } UserDefaultsHelper.storeCallbackHandle(handle) UserDefaultsHelper.storeIsDebug(isInDebug) + result(true) } private func registerPeriodicTask(arguments: [AnyHashable: Any], result: @escaping FlutterResult) { From 222bdadebe9fb3abb78b7f63b35f8ca389ccc8fe Mon Sep 17 00:00:00 2001 From: Absar Date: Sat, 23 Sep 2023 01:21:10 +0200 Subject: [PATCH 20/26] Continue work on task identifiers for iOS AppRefresh and ProcessingTask. * Temporarily commented old iOS background fetch --- ios/Classes/BackgroundWorker.swift | 4 ++-- ios/Classes/SwiftWorkmanagerPlugin.swift | 28 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ios/Classes/BackgroundWorker.swift b/ios/Classes/BackgroundWorker.swift index 0e65bb8b..0dcbdd5f 100644 --- a/ios/Classes/BackgroundWorker.swift +++ b/ios/Classes/BackgroundWorker.swift @@ -25,9 +25,9 @@ enum BackgroundMode { var onResultSendArguments: [String: String] { switch self { - case .backgroundAppRefresh(identifier): + case let .backgroundAppRefresh(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] - case .backgroundProcessingTask(identifier): + case let .backgroundProcessingTask(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] case let .backgroundOneOffTask(identifier): return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier] diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 1256e213..915c7584 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -531,17 +531,17 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } // MARK: - AppDelegate conformance - -extension SwiftWorkmanagerPlugin { - override public func application( - _ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) -> Bool { - let worker = BackgroundWorker( - mode: .backgroundProcessingTask, - inputData: "", - flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback - ) - return worker.performBackgroundRequest(completionHandler) - } -} +// TODO this is temporarily commented, it might be needed for background fetch on iOS 12 and lower +// extension SwiftWorkmanagerPlugin { +// override public func application( +// _ application: UIApplication, +// performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void +// ) -> Bool { +// let worker = BackgroundWorker( +// mode: .backgroundProcessingTask, +// inputData: "", +// flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback +// ) +// return worker.performBackgroundRequest(completionHandler) +// } +// } From 7c256ae43642b2bfcf0bea1fc03c8f9fa90f3172 Mon Sep 17 00:00:00 2001 From: Absar Date: Mon, 25 Sep 2023 01:24:45 +0200 Subject: [PATCH 21/26] Fix extra commas on iOS --- ios/Classes/DebugNotificationHelper.swift | 6 ++---- ios/Classes/SwiftWorkmanagerPlugin.swift | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ios/Classes/DebugNotificationHelper.swift b/ios/Classes/DebugNotificationHelper.swift index 78a23d47..66ea0a9a 100644 --- a/ios/Classes/DebugNotificationHelper.swift +++ b/ios/Classes/DebugNotificationHelper.swift @@ -34,18 +34,16 @@ class DebugNotificationHelper { icon: .startWork) } - func showCompletedFetchNotification(identifier: String, - completedDate: Date, + func showCompletedFetchNotification(completedDate: Date, result: UIBackgroundFetchResult, elapsedTime: TimeInterval) { let message = """ Perform fetch completed: - • Identifier: '\(identifier)' • Elapsed time: \(elapsedTime.formatToSeconds()) • Result: UIBackgroundFetchResult.\(result) """ - DebugNotificationHelper.scheduleNotification(identifier: identifier, + DebugNotificationHelper.scheduleNotification(identifier: identifier.uuidString, title: completedDate.formatted(), body: message, icon: result == .newData ? .success : .failure) diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 575c7851..759c7d87 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -85,7 +85,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { task.identifier, inputData: "", flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundProcessingTask(identifier: identifier), + backgroundMode: .backgroundProcessingTask(identifier: identifier) ) // Provide an expiration handler for the background task @@ -124,7 +124,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { task.identifier, inputData: "", flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundPeriodicTask(identifier: identifier), + backgroundMode: .backgroundPeriodicTask(identifier: identifier) ) // Provide an expiration handler for the background task that cancels the operation @@ -150,7 +150,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { identifier, inputData: inputData, flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback, - backgroundMode: .backgroundOneOffTask(identifier: identifier), + backgroundMode: .backgroundOneOffTask(identifier: identifier) ) // Inform the system that the task is complete when the operation completes From 8a6f56012aac9078a0aad1ac82ca2725b3b76dde Mon Sep 17 00:00:00 2001 From: Absar Date: Tue, 26 Sep 2023 02:23:31 +0200 Subject: [PATCH 22/26] New iOS feature printScheduledTasks to print details of un-executed scheduled tasks. To be used during development/debugging. Format readme to improve readability --- README.md | 39 +++++++++++++++++------- ios/Classes/SwiftWorkmanagerPlugin.swift | 34 ++++++++++++++++++++- lib/src/workmanager.dart | 5 +++ 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f47f2cd4..8663ae00 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,9 @@ Refer to the example app for a successful, retrying and a failed task. Initialize Workmanager only once. Background app refresh can only be tested on a real device, it cannot be tested on a simulator. +### One off tasks iOS supports **One off tasks** only on iOS 13+ with a few basic constraints: -**registerOneOffTask** starts immediately. On iOS it might run for only 30 seconds due to iOS restrictions. +`registerOneOffTask` starts immediately. On iOS it might run for only 30 seconds due to iOS restrictions. ```dart Workmanager().registerOneOffTask( @@ -108,13 +109,14 @@ Workmanager().registerOneOffTask( ); ``` +### Periodic tasks iOS supports **Periodic tasks**. -On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md). -Note: On iOS 13+, adding a BGTaskSchedulerPermittedIdentifiers key to the Info.plist disables the performFetchWithCompletionHandler and setMinimumBackgroundFetchInterval -methods, which means you cannot use both old Background Fetch and new registerPeriodicTask at the same time, you have to choose one based on your minimum iOS target version. -For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app). +On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md) +Note: On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. +For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) -**registerPeriodicTask** only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. +`registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. ```dart const iOSBackgroundAppRefresh = "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh"; @@ -125,10 +127,11 @@ Workmanager().registerPeriodicTask( ); ``` -For more information see [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask). +For more information see [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask) +### Processing tasks iOS supports **Processing tasks** only on iOS 13+ which can run for more than 30 seconds. -**registerProcessingTask** is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging. +`registerProcessingTask` is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging. Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these. Processing tasks run only when the device is idle and there is also a possibility that it will only run if device is charging. iOS might terminate any running background processing tasks when the user starts using the device. However background app refresh tasks aren’t affected. For more information see [BGProcessingTask](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask) @@ -147,9 +150,9 @@ Workmanager().registerProcessingTask( ); ``` -**Background App Refresh permission** on iOS: -On iOS user can disable Background App Refresh permission anytime, hence background tasks can only run if user has granted the permission. -With Workmanager().checkBackgroundRefreshPermission() you can check whether background app refresh is enabled. If it is not enabled you might ask +### Background App Refresh permission +On iOS user can disable `Background App Refresh` permission anytime, hence background tasks can only run if user has granted the permission. +With `Workmanager().checkBackgroundRefreshPermission()` you can check whether background app refresh is enabled. If it is not enabled you might ask the user to enable it in app settings. ```dart @@ -163,6 +166,20 @@ if (Platform.isIOS) { For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks). +### Print scheduled tasks +On iOS you can print pending scheduled tasks using `Workmanager().printScheduledTasks()` +Which prints task details to console. To be used during development/debugging. +Currently only supported on iOS and only on iOS 13+. + +```dart +if (Platform.isIOS) { + Workmanager().printScheduledTasks(); + // Prints: [BGTaskScheduler] Task Identifier: iOSBackgroundAppRefresh earliestBeginDate: 2023.10.10 PM 11:10:12 + // Or: [BGTaskScheduler] There are no pending tasks +} +``` + + # Customisation (Android) Not every `Android WorkManager` feature is ported. diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 759c7d87..32f4c380 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -73,6 +73,13 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { case uniqueName } } + + struct PrintScheduledTasks { + static let name = "\(PrintScheduledTasks.self)".lowercasingFirst + enum Arguments: String { + case none + } + } } } @@ -201,7 +208,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { request.earliestBeginDate = Date(timeIntervalSinceNow: begin) do { try BGTaskScheduler.shared.submit(request) - logInfo("BGAppRefreshTask submitted \(identifier)") + logInfo("BGAppRefreshTask submitted \(identifier) earliestBeginInSeconds:\(begin)") } catch { logInfo("Could not schedule BGAppRefreshTask \(error.localizedDescription)") return @@ -315,6 +322,9 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { case (ForegroundMethodChannel.Methods.CancelTaskByUniqueName.name, let .some(arguments)): cancelTaskByUniqueName(arguments: arguments, result: result) return + case (ForegroundMethodChannel.Methods.PrintScheduledTasks.name, .none): + printScheduledTasks(result: result) + return default: result(WMPError.unhandledMethod(call.method).asFlutterError) return @@ -487,6 +497,28 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { } return true } + + /// Prints details of un-executed scheduled tasks. To be used during development/debugging + private func printScheduledTasks(result: @escaping FlutterResult) { + if #available(iOS 13.0, *) { + BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in + if taskRequests.isEmpty { + print("[BGTaskScheduler] There are no pending tasks") + result(true) + return + } + print("[BGTaskScheduler] Pending Tasks:") + for taskRequest in taskRequests { + print("[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")") + } + result(true) + } + } else { + result(FlutterError(code: "99", + message: "printScheduledTasks is only supported on iOS 13+", + details: "BGTaskScheduler.getPendingTaskRequests is only supported on iOS 13+")) + } + } } // MARK: - AppDelegate conformance diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 2489035f..99d34ff8 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -362,6 +362,11 @@ class Workmanager { /// Cancels all tasks Future cancelAll() async => await _foregroundChannel.invokeMethod("cancelAllTasks"); + + /// Prints details of un-executed scheduled tasks. To be used during development/debugging. + /// Currently only supported on iOS and only on iOS 13+. + Future printScheduledTasks() async => + await _foregroundChannel.invokeMethod("printScheduledTasks"); } /// A helper object to convert the selected options to JSON format. Mainly for testability. From 9625658f1f0fc57951256c9432775afd5f5141a4 Mon Sep 17 00:00:00 2001 From: Absar Date: Tue, 26 Sep 2023 17:58:04 +0200 Subject: [PATCH 23/26] iOS Periodic and processing tasks will be immediately scheduled, instead of waiting for App to go to background. Since doing on backgrounding will keep on changing earliest begin date. * Add printScheduledTasks to example app * Format example code --- README.md | 8 +-- example/ios/Runner/AppDelegate.swift | 8 +-- example/lib/main.dart | 47 ++++++---------- ios/Classes/SwiftWorkmanagerPlugin.swift | 70 ++++++------------------ lib/src/workmanager.dart | 10 +--- 5 files changed, 43 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 8663ae00..b81acf80 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ For more information see [BGAppRefreshTask](https://developer.apple.com/document iOS supports **Processing tasks** only on iOS 13+ which can run for more than 30 seconds. `registerProcessingTask` is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging. Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these. -Processing tasks run only when the device is idle and there is also a possibility that it will only run if device is charging. iOS might terminate any running background processing tasks when the user starts using the device. However background app refresh tasks aren’t affected. +iOS might terminate any running background processing tasks when the user starts using the device. For more information see [BGProcessingTask](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask) ```dart @@ -167,15 +167,15 @@ if (Platform.isIOS) { For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks). ### Print scheduled tasks -On iOS you can print pending scheduled tasks using `Workmanager().printScheduledTasks()` -Which prints task details to console. To be used during development/debugging. +On iOS you can print scheduled tasks using `Workmanager().printScheduledTasks()` +It prints task details to console. To be used during development/debugging. Currently only supported on iOS and only on iOS 13+. ```dart if (Platform.isIOS) { Workmanager().printScheduledTasks(); // Prints: [BGTaskScheduler] Task Identifier: iOSBackgroundAppRefresh earliestBeginDate: 2023.10.10 PM 11:10:12 - // Or: [BGTaskScheduler] There are no pending tasks + // Or: [BGTaskScheduler] There are no scheduled tasks } ``` diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 6b84befa..92233edd 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -22,11 +22,11 @@ import workmanager WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId") WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleTask") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.failedTask") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask") + WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask") + WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.failedTask") + WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask") WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask") - WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh") + WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh") return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/lib/main.dart b/example/lib/main.dart index 8dfd218e..54fafb0c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -99,32 +99,9 @@ class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); } -class _MyAppState extends State with WidgetsBindingObserver { +class _MyAppState extends State { bool workmanagerInitialized = false; String _prefsString = "empty"; - String _lastResumed = DateTime.now().toString(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) async { - super.didChangeAppLifecycleState(state); - if (state == AppLifecycleState.resumed) { - // App came back from background to foreground - setState(() => _lastResumed = DateTime.now().toString()); - _refreshStats(); - } - } @override Widget build(BuildContext context) { @@ -296,14 +273,18 @@ class _MyAppState extends State with WidgetsBindingObserver { print('Cancel all tasks completed'); }, ), + SizedBox(height: 15), + ElevatedButton( + child: Text('Refresh stats'), + onPressed: _refreshStats, + ), SizedBox(height: 10), - GestureDetector( - onTap: _refreshStats, - child: SingleChildScrollView( - child: Text( - 'Task run stats:\n$_prefsString\nTap here to refresh\n' - 'Last App resumed at: $_lastResumed', - )), + SingleChildScrollView( + child: Text( + 'Task run stats:\n' + '${workmanagerInitialized ? '' : 'Workmanager not initialized'}' + '\n$_prefsString', + ), ), ], ), @@ -323,6 +304,10 @@ class _MyAppState extends State with WidgetsBindingObserver { _prefsString = '$_prefsString \n$task:\n${prefs.getString(task)}\n'; } + if (Platform.isIOS) { + Workmanager().printScheduledTasks(); + } + setState(() {}); } diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index 32f4c380..f09f0423 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -185,33 +185,19 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } - /// Schedules an App Refresh task indirectly by adding a notification observer so that the task - /// will be scheduled when app enters background - @objc - public static func registerPeriodicTaskScheduler( - taskIdentifier identifier: String, - earliestBeginInSeconds begin: Double) { - if #available(iOS 13.0, *) { - NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, queue: nil - ) { _ in - schedulePeriodicTask(taskIdentifier: identifier, earliestBeginInSeconds: begin) - } - } - } - @objc @available(iOS 13.0, *) private static func schedulePeriodicTask(taskIdentifier identifier: String, earliestBeginInSeconds begin: Double) { - let request = BGAppRefreshTaskRequest(identifier: identifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: begin) - do { - try BGTaskScheduler.shared.submit(request) - logInfo("BGAppRefreshTask submitted \(identifier) earliestBeginInSeconds:\(begin)") - } catch { - logInfo("Could not schedule BGAppRefreshTask \(error.localizedDescription)") - return + if #available(iOS 13.0, *) { + let request = BGAppRefreshTaskRequest(identifier: identifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: begin) + do { + try BGTaskScheduler.shared.submit(request) + logInfo("BGAppRefreshTask submitted \(identifier) earliestBeginInSeconds:\(begin)") + } catch { + logInfo("Could not schedule BGAppRefreshTask \(error.localizedDescription)") + return + } } } @@ -231,28 +217,6 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { } } - /// Schedules a [BGProcessingTaskRequest] task indirectly by adding a notification observer so - /// that the task will be scheduled when app enters background - @objc - public static func registerProcessingTaskScheduler( - uniqueTaskIdentifier: String, - earliestBeginInSeconds begin: Double, - requiresNetworkConnectivity: Bool, - requiresExternalPower: Bool - ) { - if #available(iOS 13.0, *) { - let network = requiresNetworkConnectivity - let extPower = requiresExternalPower - - NotificationCenter.default.addObserver( - forName: UIApplication.didEnterBackgroundNotification, - object: nil, queue: nil - ) { _ in - scheduleBackgroundProcessingTask(withIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: begin, requiresNetworkConnectivity: network, requiresExternalPower: extPower) - } - } - } - /// Schedules a long running BackgroundProcessingTask @objc @available(iOS 13.0, *) @@ -268,7 +232,7 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { request.requiresExternalPower = requiresExternalPower do { try BGTaskScheduler.shared.submit(request) - logInfo("BGProcessingTask submitted \(uniqueTaskIdentifier)") + logInfo("BGProcessingTask submitted \(uniqueTaskIdentifier) earliestBeginInSeconds:\(begin)") } catch { logInfo("Could not schedule BGProcessingTask identifier:\(uniqueTaskIdentifier) error:\(error.localizedDescription)") logInfo("Possible issues can be: running on a simulator instead of a real device, or the task name is not registered") @@ -402,8 +366,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { let initialDelaySeconds = arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 ?? 0 - // Task will be scheduled by OS when app goes to background - SwiftWorkmanagerPlugin.registerPeriodicTaskScheduler( + SwiftWorkmanagerPlugin.schedulePeriodicTask( taskIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: Double(initialDelaySeconds)) result(true) @@ -438,9 +401,8 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { requiresNetwork = true } - // Task will be scheduled by OS when app goes to background - SwiftWorkmanagerPlugin.registerProcessingTaskScheduler( - uniqueTaskIdentifier: uniqueTaskIdentifier, + SwiftWorkmanagerPlugin.scheduleBackgroundProcessingTask( + withIdentifier: uniqueTaskIdentifier, earliestBeginInSeconds: delaySeconds, requiresNetworkConnectivity: requiresCharging, requiresExternalPower: requiresNetwork) @@ -503,11 +465,11 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { if #available(iOS 13.0, *) { BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in if taskRequests.isEmpty { - print("[BGTaskScheduler] There are no pending tasks") + print("[BGTaskScheduler] There are no scheduled tasks") result(true) return } - print("[BGTaskScheduler] Pending Tasks:") + print("[BGTaskScheduler] Scheduled Tasks:") for taskRequest in taskRequests { print("[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")") } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index 99d34ff8..b6184538 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -227,9 +227,6 @@ class Workmanager { /// it as per user's App usage pattern, iOS might terminate the task or throttle /// it's frequency if it takes more than 30 seconds. /// - /// On iOS after calling this, the task is only scheduled when app goes to the - /// background, hence make sure to background the App at least once. - /// /// A [uniqueName] is required so only one task can be registered. /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. @@ -273,9 +270,6 @@ class Workmanager { /// Schedule a background long running task, currently only available on iOS. /// - /// On iOS after calling this, the task is only scheduled when app goes to the - /// background, hence make sure to background the App at least once. - /// /// Processing tasks are for long processes like data processing and app maintenance. /// Processing tasks can run for minutes, but the system can interrupt these. /// Processing tasks run only when the device is idle. iOS might terminate any @@ -363,7 +357,9 @@ class Workmanager { Future cancelAll() async => await _foregroundChannel.invokeMethod("cancelAllTasks"); - /// Prints details of un-executed scheduled tasks. To be used during development/debugging. + /// Prints details of un-executed scheduled tasks to console. To be used during + /// development/debugging. + /// /// Currently only supported on iOS and only on iOS 13+. Future printScheduledTasks() async => await _foregroundChannel.invokeMethod("printScheduledTasks"); From 196bad20e16e3f0768fb7e418f85b22e4f02a2e7 Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 27 Sep 2023 02:16:24 +0200 Subject: [PATCH 24/26] Option to set frequency for iOS periodic tasks in AppDelegate.swift * Add initialDelay support for Workmanager.registerProcessingTask * Remove unnecessary WorkmanagerPlugin.registerBGProcessingTask calls from AppDelegate.swift * Cleanup unused params from Workmanager.registerProcessingTask * Update readme and iOS setup as per new iOS developments * Create migration steps for iOS Workmanager.registerOneOffTask to Workmanager.registerProcessingTask --- IOS_SETUP.md | 28 +++++++++++++--- README.md | 41 +++++++++++++++++++----- example/ios/Runner/AppDelegate.swift | 8 +++-- example/lib/main.dart | 10 +++--- ios/Classes/SwiftWorkmanagerPlugin.swift | 29 ++++++++--------- ios/Classes/WorkmanagerPlugin.h | 3 +- ios/Classes/WorkmanagerPlugin.m | 4 +-- lib/src/workmanager.dart | 24 +++++++------- 8 files changed, 96 insertions(+), 51 deletions(-) diff --git a/IOS_SETUP.md b/IOS_SETUP.md index b7f54fdd..3025f09c 100644 --- a/IOS_SETUP.md +++ b/IOS_SETUP.md @@ -10,6 +10,8 @@ This plugin is compatible with **Swift 4.2** and up. Make sure you are using **X > ⚠️ BGTaskScheduler is similar to Background Fetch described below and brings a similar set of constraints. Most notably, there are no guarantees when the background task will be run. Excerpt from the documentation: > > Schedule a processing task request to ask that the system launch your app when conditions are favorable for battery life to handle deferrable, longer-running processing, such as syncing, database maintenance, or similar tasks. The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week. +> +> Workmanager BGTaskScheduler methods `registerOneOffTask`, `registerPeriodicTask`, and `registerProcessingTask` are only available on iOS 13+ ![Screenshot of Background Fetch Capabilities tab in Xcode ](.art/ios_background_mode_background_processing.png) @@ -19,6 +21,9 @@ This will add the **UIBackgroundModes** key to your project's `Info.plist`: UIBackgroundModes processing + + + fetch ``` @@ -31,16 +36,25 @@ import workmanager ``` swift // In AppDelegate.application method -WorkmanagerPlugin.registerTask(withIdentifier: "task-identifier") +WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "task-identifier") + +// If you need period tasks in iOS 13+ +WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) ``` - Info.plist ``` xml BGTaskSchedulerPermittedIdentifiers - - task-identifier - + + task-identifier + + + be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh + ``` +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. +For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) And will set the correct *SystemCapabilities* for your target in the `project.pbxproj` file: @@ -64,7 +78,11 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith ## Enabling Background Fetch -> ⚠️ Background fetch is one supported way to do background work on iOS with work manager: **Periodic tasks** are available on Android only for now! (see #109) +> ⚠️ Background fetch is one supported way to do background work on iOS with work manager + +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. +For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) Background fetching is very different compared to Android's Background Jobs. In order for your app to support Background Fetch, you have to add the *Background Modes* capability in Xcode for your app's Target and check *Background fetch*: diff --git a/README.md b/README.md index b81acf80..ca5458cf 100644 --- a/README.md +++ b/README.md @@ -96,34 +96,56 @@ Refer to the example app for a successful, retrying and a failed task. Initialize Workmanager only once. Background app refresh can only be tested on a real device, it cannot be tested on a simulator. +### Migrate to 0.6.x +Version 0.6.x of this plugin has some breaking changes for iOS: +- Workmanager.registerOneOffTask was previously using iOS **BGProcessingTask**, now it will be an immediate run task which will continue in the background if user leaves the App. Since the previous solution meant the one off task will only run if the device is idle and as often experienced only when device is charging, in practice it means somewhere at night, or not at all during that day, because **BGProcessingTask** is meant for long running tasks. The new solution makes it more in line with Android except it does not support **initialDelay** +- If you need the old behavior you can use the new iOS only method `Workmanager.registerProcessingTask`: + 1. Replace `Workmanager().registerOneOffTask` with `Workmanager().registerProcessingTask` in your App + 1. Replace `WorkmanagerPlugin.registerTask` with `WorkmanagerPlugin.registerBGProcessingTask` in `AppDelegate.swift` +- Workmanager.registerOneOffTask does not support **initialDelay** +- Workmanager.registerOneOffTask now supports **inputData** which was always returning null in the previous solution +- Workmanager.registerOneOffTask now does NOT require `WorkmanagerPlugin.registerTask` call in `AppDelegate.swift` hence remove the call + ### One off tasks iOS supports **One off tasks** only on iOS 13+ with a few basic constraints: -`registerOneOffTask` starts immediately. On iOS it might run for only 30 seconds due to iOS restrictions. + +`registerOneOffTask` starts immediately. It might run for only 30 seconds due to iOS restrictions. ```dart Workmanager().registerOneOffTask( "task-identifier", simpleTaskKey, // Ignored on iOS - initialDelay: Duration(minutes: 30), + initialDelay: Duration(minutes: 30), // Ignored on iOS inputData: ... // fully supported ); ``` ### Periodic tasks -iOS supports **Periodic tasks**. -On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md) -Note: On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +iOS supports two types of **Periodic tasks**: +- On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md) + +- `registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. + +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) -`registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. +First register the task in `AppDelegate.swift` unlike Android for iOS you have to set the frequency in `AppDelegate.swift`. The frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern, iOS might take a few days to learn usage pattern. In reality frequency just means do not repeat the task before x seconds/minutes. If frequency is not provided it will default to 15 minutes. +```objc +// Register a periodic task with 20 minutes frequency. The frequency is in seconds. +WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) +``` + +Then schedule the task from your App ```dart const iOSBackgroundAppRefresh = "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh"; Workmanager().registerPeriodicTask( iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, initialDelay: Duration(seconds: 10), + frequency: Duration(hours: 1), // Ignored on iOS, rather set in AppDelegate.swift + inputData: ... // Not supported ); ``` @@ -131,6 +153,7 @@ For more information see [BGAppRefreshTask](https://developer.apple.com/document ### Processing tasks iOS supports **Processing tasks** only on iOS 13+ which can run for more than 30 seconds. + `registerProcessingTask` is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging. Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these. iOS might terminate any running background processing tasks when the user starts using the device. @@ -141,6 +164,7 @@ const iOSBackgroundProcessingTask = "be.tramckrijte.workmanagerExample.iOSBackgr Workmanager().registerProcessingTask( iOSBackgroundProcessingTask, iOSBackgroundProcessingTask, + initialDelay: Duration(minutes: 2), constraints: Constraints( // Connected or metered mark the task as requiring internet networkType: NetworkType.connected, @@ -152,7 +176,7 @@ Workmanager().registerProcessingTask( ### Background App Refresh permission On iOS user can disable `Background App Refresh` permission anytime, hence background tasks can only run if user has granted the permission. -With `Workmanager().checkBackgroundRefreshPermission()` you can check whether background app refresh is enabled. If it is not enabled you might ask +With `Workmanager.checkBackgroundRefreshPermission` you can check whether background app refresh is enabled. If it is not enabled you might ask the user to enable it in app settings. ```dart @@ -167,7 +191,8 @@ if (Platform.isIOS) { For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks). ### Print scheduled tasks -On iOS you can print scheduled tasks using `Workmanager().printScheduledTasks()` +On iOS you can print scheduled tasks using `Workmanager.printScheduledTasks` + It prints task details to console. To be used during development/debugging. Currently only supported on iOS and only on iOS 13+. diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 92233edd..08b909cb 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -21,12 +21,14 @@ import workmanager } WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.taskId") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleTask") WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.rescheduledTask") - WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.failedTask") WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.simpleDelayedTask") WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundProcessingTask") - WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh") + + // When this task is scheduled from dart it will run with minimum 20 minute frequency. The + // frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern. + // If frequency is not provided it will default to 15 minutes + WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) return super.application(application, didFinishLaunchingWithOptions: launchOptions) diff --git a/example/lib/main.dart b/example/lib/main.dart index 54fafb0c..c57f7d2b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -233,10 +233,11 @@ class _MyAppState extends State { return; } await Workmanager().registerPeriodicTask( - iOSBackgroundAppRefresh, iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 10), - inputData: {} //ignored on iOS - ); + iOSBackgroundAppRefresh, + iOSBackgroundAppRefresh, + initialDelay: Duration(seconds: 10), + inputData: {}, //ignored on iOS + ); } : null, ), @@ -257,6 +258,7 @@ class _MyAppState extends State { await Workmanager().registerProcessingTask( iOSBackgroundProcessingTask, iOSBackgroundProcessingTask, + initialDelay: Duration(seconds: 20), ); } : null, diff --git a/ios/Classes/SwiftWorkmanagerPlugin.swift b/ios/Classes/SwiftWorkmanagerPlugin.swift index f09f0423..03ed3e40 100644 --- a/ios/Classes/SwiftWorkmanagerPlugin.swift +++ b/ios/Classes/SwiftWorkmanagerPlugin.swift @@ -111,9 +111,8 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { operationQueue.addOperation(operation) } - @objc @available(iOS 13.0, *) - public static func handlePeriodicTask(identifier: String, task: BGAppRefreshTask) { + public static func handlePeriodicTask(identifier: String, task: BGAppRefreshTask, earliestBeginInSeconds: Double?) { guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(), let _ = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { @@ -121,9 +120,8 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { return } - // Reschedule no earlier than 15 minutes from now. TODO interval should be configurable - // probably through AppDelegate.swift WorkmanagerPlugin.registerPeriodicTask - schedulePeriodicTask(taskIdentifier: task.identifier, earliestBeginInSeconds: 15 * 60) + // If frequency is not provided it will default to 15 minutes + schedulePeriodicTask(taskIdentifier: task.identifier, earliestBeginInSeconds: earliestBeginInSeconds ?? (15 * 60)) let operationQueue = OperationQueue() // Create an operation that performs the main part of the background task @@ -172,14 +170,19 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate { /// Registers [BGAppRefresh] task name for the given identifier. /// You must register task names before app finishes launching in AppDelegate. @objc - public static func registerPeriodicTask(withIdentifier identifier: String) { + public static func registerPeriodicTask(withIdentifier identifier: String, frequency: NSNumber?) { if #available(iOS 13.0, *) { + var frequencyInSeconds: Double? + if let frequencyValue = frequency { + frequencyInSeconds = frequencyValue.doubleValue + } + BGTaskScheduler.shared.register( forTaskWithIdentifier: identifier, using: nil ) { task in if let task = task as? BGAppRefreshTask { - handlePeriodicTask(identifier: identifier, task: task) + handlePeriodicTask(identifier: identifier, task: task, earliestBeginInSeconds: frequencyInSeconds) } } } @@ -324,11 +327,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { result(WMPError.invalidParameters.asFlutterError) return } - guard let callBackIdentifier = - arguments[method.Arguments.taskName.rawValue] as? String else { - result(WMPError.invalidParameters.asFlutterError) - return - } + var taskIdentifier: UIBackgroundTaskIdentifier = .invalid let inputData = arguments[method.Arguments.inputData.rawValue] as? String @@ -338,7 +337,7 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { // Mark the task as ended if time is expired, otherwise iOS might terminate and will throttle future executions UIApplication.shared.endBackgroundTask(taskIdentifier) }) - SwiftWorkmanagerPlugin.startOneOffTask(identifier: callBackIdentifier, + SwiftWorkmanagerPlugin.startOneOffTask(identifier: uniqueTaskIdentifier, taskIdentifier: taskIdentifier, inputData: inputData ?? "", delaySeconds: delaySeconds) @@ -364,11 +363,11 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin { return } let initialDelaySeconds = - arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 ?? 0 + arguments[method.Arguments.initialDelaySeconds.rawValue] as? Double ?? 0.0 SwiftWorkmanagerPlugin.schedulePeriodicTask( taskIdentifier: uniqueTaskIdentifier, - earliestBeginInSeconds: Double(initialDelaySeconds)) + earliestBeginInSeconds: initialDelaySeconds) result(true) return } else { diff --git a/ios/Classes/WorkmanagerPlugin.h b/ios/Classes/WorkmanagerPlugin.h index 21bf6032..5c8fd0a3 100644 --- a/ios/Classes/WorkmanagerPlugin.h +++ b/ios/Classes/WorkmanagerPlugin.h @@ -15,8 +15,9 @@ * @author Lars Huth * * @param taskIdentifier The identifier of the custom task. Must be set in info.plist + * @param frequency The repeat frequency in seconds */ -+ (void)registerPeriodicTaskWithIdentifier:(NSString *) taskIdentifier; ++ (void)registerPeriodicTaskWithIdentifier:(NSString *) taskIdentifier frequency:(NSNumber *) frequency; /** * Register a custom task identifier as iOS BackgroundProcessingTask executed randomly in future. diff --git a/ios/Classes/WorkmanagerPlugin.m b/ios/Classes/WorkmanagerPlugin.m index d51f82d3..62a6fd1d 100644 --- a/ios/Classes/WorkmanagerPlugin.m +++ b/ios/Classes/WorkmanagerPlugin.m @@ -22,9 +22,9 @@ + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier { } } -+ (void)registerPeriodicTaskWithIdentifier:(NSString *)taskIdentifier{ ++ (void)registerPeriodicTaskWithIdentifier:(NSString *)taskIdentifier frequency:(NSNumber *) frequency { if (@available(iOS 13, *)) { - [SwiftWorkmanagerPlugin registerPeriodicTaskWithIdentifier:taskIdentifier]; + [SwiftWorkmanagerPlugin registerPeriodicTaskWithIdentifier:taskIdentifier frequency:frequency]; } } diff --git a/lib/src/workmanager.dart b/lib/src/workmanager.dart index b6184538..78243f2f 100644 --- a/lib/src/workmanager.dart +++ b/lib/src/workmanager.dart @@ -228,10 +228,11 @@ class Workmanager { /// it's frequency if it takes more than 30 seconds. /// /// A [uniqueName] is required so only one task can be registered. - /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler] + /// The [taskName] is the value that will be returned in the [BackgroundTaskHandler], ignored on iOS where you should use [uniqueName]. /// a [frequency] is not required and will be defaulted to 15 minutes if not provided. /// a [frequency] has a minimum of 15 min. Android will automatically change your frequency to 15 min if you have configured a lower frequency. - /// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list + /// Unlike Android, you cannot set [frequency] for iOS here rather you have to set in `AppDelegate.swift` while registering the task. + /// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list. It is not supported on iOS. /// /// For iOS see Apple docs: /// [iOS 13+ Using background tasks to update your app](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app/) @@ -283,24 +284,21 @@ class Workmanager { Future registerProcessingTask( final String uniqueName, final String taskName, { + final Duration initialDelay = Duration.zero, + /// Only partially supported on iOS. /// See [Constraints] for details. final Constraints? constraints, - final BackoffPolicy? backoffPolicy, - final Duration backoffPolicyDelay = Duration.zero, - final OutOfQuotaPolicy? outOfQuotaPolicy, - final Map? inputData, }) async => await _foregroundChannel.invokeMethod( "registerProcessingTask", JsonMapperHelper.toRegisterMethodArgument( - isInDebugMode: _isInDebugMode, - uniqueName: uniqueName, - taskName: taskName, - constraints: constraints, - backoffPolicy: backoffPolicy, - backoffPolicyDelay: backoffPolicyDelay, - outOfQuotaPolicy: outOfQuotaPolicy), + isInDebugMode: _isInDebugMode, + uniqueName: uniqueName, + taskName: taskName, + initialDelay: initialDelay, + constraints: constraints, + ), ); /// Check whether background app refresh is enabled. If it is not enabled you From 5a8cbf2b1057de912201c569b3a06d043ece48d6 Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 27 Sep 2023 15:46:00 +0200 Subject: [PATCH 25/26] Update iOS docs --- IOS_SETUP.md | 12 ++++++------ README.md | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/IOS_SETUP.md b/IOS_SETUP.md index 3025f09c..a7525bb6 100644 --- a/IOS_SETUP.md +++ b/IOS_SETUP.md @@ -22,7 +22,7 @@ This will add the **UIBackgroundModes** key to your project's `Info.plist`: processing - + fetch ``` @@ -38,7 +38,7 @@ import workmanager // In AppDelegate.application method WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "task-identifier") -// If you need period tasks in iOS 13+ +// Register a periodic task in iOS 13+ WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) ``` @@ -48,11 +48,11 @@ WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "be.tramckrijte.workmanag task-identifier - + be.tramckrijte.workmanagerExample.iOSBackgroundAppRefresh ``` -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) @@ -78,9 +78,9 @@ e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWith ## Enabling Background Fetch -> ⚠️ Background fetch is one supported way to do background work on iOS with work manager +> ⚠️ Background fetch is one supported way to do background work on iOS with work manager. Note that this API is deprecated starting iOS 13, however it still works on iOS 13+ as of writing this article -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) diff --git a/README.md b/README.md index ca5458cf..28d0872a 100644 --- a/README.md +++ b/README.md @@ -122,15 +122,16 @@ Workmanager().registerOneOffTask( ### Periodic tasks iOS supports two types of **Periodic tasks**: -- On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md) +- On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](./IOS_SETUP.md), even though the API is +deprecated by iOS it still works on iOS 13+ as of writing this article - `registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` +> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) -First register the task in `AppDelegate.swift` unlike Android for iOS you have to set the frequency in `AppDelegate.swift`. The frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern, iOS might take a few days to learn usage pattern. In reality frequency just means do not repeat the task before x seconds/minutes. If frequency is not provided it will default to 15 minutes. +To use `registerPeriodicTask` first register the task in `Info.plist` and `AppDelegate.swift` [iOS Setup](./IOS_SETUP.md). Unlike Android, for iOS you have to set the frequency in `AppDelegate.swift`. The frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern, iOS might take a few days to learn usage pattern. In reality frequency just means do not repeat the task before x seconds/minutes. If frequency is not provided it will default to 15 minutes. ```objc // Register a periodic task with 20 minutes frequency. The frequency is in seconds. @@ -181,8 +182,8 @@ the user to enable it in app settings. ```dart if (Platform.isIOS) { - var hasPermissions = await Workmanager().checkBackgroundRefreshPermission(); - if (hasPermissions != BackgroundRefreshPermissionState.available){ + final hasPermission = await Workmanager().checkBackgroundRefreshPermission(); + if (hasPermission != BackgroundRefreshPermissionState.available){ // Inform the user that background app refresh is disabled } } From c59058d4885bf15278b3a339dd29f5f507d83045 Mon Sep 17 00:00:00 2001 From: Absar Date: Wed, 27 Sep 2023 17:38:04 +0200 Subject: [PATCH 26/26] TODO for cleanups later --- ios/Classes/WorkmanagerPlugin.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Classes/WorkmanagerPlugin.m b/ios/Classes/WorkmanagerPlugin.m index 62a6fd1d..8d388193 100644 --- a/ios/Classes/WorkmanagerPlugin.m +++ b/ios/Classes/WorkmanagerPlugin.m @@ -16,6 +16,7 @@ + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { [SwiftWorkmanagerPlugin setPluginRegistrantCallback:callback]; } +// TODO this might not be needed anymore + (void)registerTaskWithIdentifier:(NSString *) taskIdentifier { if (@available(iOS 13, *)) { [SwiftWorkmanagerPlugin registerBGProcessingTaskWithIdentifier:taskIdentifier];