Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ void main() {
callbackDispatcher, // The top level function, aka callbackDispatcher
isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
);
Workmanager().registerOneOffTask("task-identifier", "simpleTask");
Workmanager().registerOneOffTask("task-identifier", "simpleTask"); // Android and 'iOS Processing task' (see options below)
if (Platform.isIOS) {
Workmanager().registerAppRefreshTask(initialDelay: const Duration(minutes: 15), frequency: const Duration(minutes: 60)); // iOS Only
}
runApp(MyApp());
}
```
Expand Down Expand Up @@ -90,11 +93,12 @@ Refer to the example app for a successful, retrying and a failed task.

# iOS specific setup and note

iOS supports **One off tasks** with a few basic constraints:
iOS supports **One off tasks** with a few basic constraints
(Note on iOS this is for long-running Processing tasks run every 1-2 days):

```dart
Workmanager().registerOneOffTask(
"task-identifier",
"task-identifier", // Ignored on iOS
simpleTaskKey, // Ignored on iOS
initialDelay: Duration(minutes: 30),
constraints: Constraints(
Expand All @@ -103,7 +107,16 @@ Workmanager().registerOneOffTask(
// require external power
requiresCharging: true,
),
inputData: ... // fully supported
inputData: ... // Android Only ?
);
```

Tasks registered this way will appear in the callback dispatcher using as `Workmanager.iOSBackgroundProcessingTask`.

```dart
Workmanager().registerAppRefreshTask( // iOS only
initialDelay: const Duration(minutes: 15), // 'Suggested' initial delay, won't start sooner than this. Default: Duration.zero
frequency: const Duration(minutes: 60) // 'Suggested' frequency to run after initial execution. If Duration.zero, will not repeat execution. Defalut: Duration.zero
);
```

Expand Down
147 changes: 128 additions & 19 deletions ios/Classes/SwiftWorkmanagerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ extension String {

public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
static let identifier = "be.tramckrijte.workmanager"

// note: this must also be in info.plist - see key: BGTaskSchedulerPermittedIdentifiers
static let defaultBGAppRefreshTaskIdentifier = "workmanager.background.refresh.task"

// gets set when task is scheduled. If value is 0.0 it doesn't Repeat
static var bgRefreshTaskFrequency = 0.0

private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?

Expand All @@ -34,6 +40,15 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
case requiresCharging
}
}

struct RegisterAppRefreshTask {
static let name = "\(RegisterAppRefreshTask.self)".lowercasingFirst
enum Arguments: String {
case initialDelaySeconds
case refreshFrequencySeconds
}
}

struct CancelAllTasks {
static let name = "\(CancelAllTasks.self)".lowercasingFirst
enum Arguments: String {
Expand Down Expand Up @@ -75,6 +90,51 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
operationQueue.addOperation(operation)
}

@available(iOS 13.0, *)
private static func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier)

request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshTaskFrequency * 60)

do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}

@available(iOS 13.0, *)
private static func handleAppRefresh(task: BGAppRefreshTask) {
print("handleAppRefresh()", task.identifier)

if (bgRefreshTaskFrequency > 0) {
// Schedule a new refresh task.
scheduleAppRefresh()
}

let operationQueue = OperationQueue()

// Create an operation that performs the main part of the background task.
let operation = BackgroundTaskOperation(
task.identifier,
flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback
)

// Provide the background task with an expiration handler that cancels the operation.
task.expirationHandler = {
operation.cancel()
}

// Inform the system that the background task is complete
// when the operation completes.
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}

// Start the operation.
operationQueue.addOperation(operation)
}

@objc
public static func registerTask(withIdentifier identifier: String) {
if #available(iOS 13.0, *) {
Expand All @@ -86,6 +146,11 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
handleBGProcessingTask(task)
}
}

// just using the static identifier for now.
BGTaskScheduler.shared.register(forTaskWithIdentifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier, using: nil) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
}
}
}
Expand Down Expand Up @@ -124,24 +189,49 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
UserDefaultsHelper.storeIsDebug(isInDebug)
result(true)

case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)):
if !validateCallbackHandle() {
result(
FlutterError(
code: "1",
message: "You have not properly initialized the Flutter WorkManager Package. " +
"You should ensure you have called the 'initialize' function first! " +
"Example: \n" +
"\n" +
"`Workmanager().initialize(\n" +
" callbackDispatcher,\n" +
" )`" +
"\n" +
"\n" +
"The `callbackDispatcher` is a top level function. See example in repository.",
details: nil
)
case (ForegroundMethodChannel.Methods.RegisterAppRefreshTask.name, let .some(arguments)):
print("ForegroundMethodChannel.Methods.RegisterAppRefreshTask")
if !SwiftWorkmanagerPlugin.validateCallbackHandle(result: result) {
return
}

if #available(iOS 13.0, *) {
let method = ForegroundMethodChannel.Methods.RegisterAppRefreshTask.self
guard let initialDelaySeconds =
arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else {
result(WMPError.invalidParameters.asFlutterError)
return
}

guard let refreshFrequencySeconds =
arguments[method.Arguments.refreshFrequencySeconds.rawValue] as? Int64 else {
result(WMPError.invalidParameters.asFlutterError)
return
}

// save this, can't store in task
SwiftWorkmanagerPlugin.bgRefreshTaskFrequency = Double(refreshFrequencySeconds)

let request = BGAppRefreshTaskRequest(
identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier
)

request.earliestBeginDate = Date(timeIntervalSinceNow: Double(initialDelaySeconds))

do {
try BGTaskScheduler.shared.submit(request)
result(true)
} catch {
result(WMPError.bgTaskSchedulingFailed(error).asFlutterError)
}

return
} else {
result(WMPError.unhandledMethod(call.method).asFlutterError)
}

case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)):
if !SwiftWorkmanagerPlugin.validateCallbackHandle(result: result) {
return
}

Expand Down Expand Up @@ -208,8 +298,27 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
}
}

private func validateCallbackHandle() -> Bool {
return UserDefaultsHelper.getStoredCallbackHandle() != nil
private static func validateCallbackHandle(result: @escaping FlutterResult) -> Bool {
let valid = UserDefaultsHelper.getStoredCallbackHandle() != nil
if (!valid) {
result(
FlutterError(
code: "1",
message: "You have not properly initialized the Flutter WorkManager Package. " +
"You should ensure you have called the 'initialize' function first! " +
"Example: \n" +
"\n" +
"`Workmanager().initialize(\n" +
" callbackDispatcher,\n" +
" )`" +
"\n" +
"\n" +
"The `callbackDispatcher` is a top level function. See example in repository.",
details: nil
)
)
}
return valid
}
}

Expand Down
36 changes: 35 additions & 1 deletion lib/src/workmanager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class Workmanager {
/// Schedule a one off task
/// A [uniqueName] is required so only one task can be registered.
/// The [taskName] is the value that will be returned in the [BackgroundTaskHandler]
/// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list
/// The [inputData] is the input data for task. Android Only. Valid value types are: int, bool, double, String and their list
Future<void> registerOneOffTask(
/// Only supported on Android.
final String uniqueName,
Expand Down Expand Up @@ -201,6 +201,27 @@ class Workmanager {
),
);

/// Schedule an iOS BGAppRefreshTask
Future<void> registerAppRefreshTask({

/// Configures an initial delay.
///
/// The delay configured here is not guaranteed. The underlying system may
/// decide to schedule the task a lot later.
final Duration initialDelay = Duration.zero,

/// Frequency to repeat task after initial execution. Duration.zero does not repeat.
final Duration frequency = Duration.zero,
}) async =>
await _foregroundChannel.invokeMethod(
"registerAppRefreshTask",
JsonMapperHelper.toRegisterAppRefreshMethodArgument(
isInDebugMode: _isInDebugMode,
initialDelay: initialDelay,
frequency: frequency
),
);

/// Schedules a periodic task that will run every provided [frequency].
/// A [uniqueName] is required so only one task can be registered.
/// The [taskName] is the value that will be returned in the [BackgroundTaskHandler]
Expand Down Expand Up @@ -314,6 +335,19 @@ class JsonMapperHelper {
};
}

@visibleForTesting
static Map<String, Object?> toRegisterAppRefreshMethodArgument({
final bool isInDebugMode = false,
final Duration? initialDelay,
final Duration? frequency
}) {
return {
"isInDebugMode": isInDebugMode,
"initialDelaySeconds": initialDelay?.inSeconds,
"refreshFrequencySeconds": frequency?.inSeconds
};
}

@visibleForTesting
static Map<String, Object?> toInitializeMethodArgument({
required final bool isInDebugMode,
Expand Down
Loading