Skip to content

Commit 6121548

Browse files
committed
Add BGAppRefreshTask
1 parent bb6af7d commit 6121548

3 files changed

Lines changed: 173 additions & 24 deletions

File tree

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ void main() {
3535
callbackDispatcher, // The top level function, aka callbackDispatcher
3636
isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
3737
);
38-
Workmanager().registerOneOffTask("1", "simpleTask"); //Android only (see below)
38+
Workmanager().registerOneOffTask("1", "simpleTask"); // Android and 'iOS Processing task' (see options below)
39+
if (Platform.isIOS) {
40+
Workmanager().registerAppRefreshTask(initialDelay: const Duration(minutes: 15), frequency: const Duration(minutes: 60)); // iOS Only
41+
}
3942
runApp(MyApp());
4043
}
4144
```
@@ -86,7 +89,8 @@ Refer to the example app for a successful, retrying and a failed task.
8689

8790
# Customisation (iOS - BGTaskScheduler only)
8891

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

9195
```dart
9296
Workmanager().registerOneOffTask(
@@ -99,12 +103,19 @@ Workmanager().registerOneOffTask(
99103
// require external power
100104
requiresCharging: true,
101105
),
102-
inputData: ... // fully supported
106+
inputData: ... // Android Only
103107
);
104108
```
105109

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

112+
```dart
113+
Workmanager().registerAppRefreshTask( // iOS only
114+
initialDelay: const Duration(minutes: 15), // 'Suggested' initial delay, won't start sooner than this. Default: Duration.zero
115+
frequency: const Duration(minutes: 60) // 'Suggested' frequency to run after initial execution. If Duration.zero, will not repeat execution. Defalut: Duration.zero
116+
);
117+
```
118+
108119
For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks).
109120

110121
# Customisation (Android)

ios/Classes/SwiftWorkmanagerPlugin.swift

Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
1414
static let identifier = "be.tramckrijte.workmanager"
1515

1616
static let defaultBGProcessingTaskIdentifier = "workmanager.background.task"
17-
17+
static let defaultBGAppRefreshTaskIdentifier = "workmanager.background.refresh.task"
18+
var bgRefreshTaskFrequency = 0.0
19+
1820
private static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
1921

2022
private struct ForegroundMethodChannel {
@@ -36,6 +38,15 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
3638
case requiresCharging
3739
}
3840
}
41+
42+
struct RegisterAppRefreshTask {
43+
static let name = "\(RegisterAppRefreshTask.self)".lowercasingFirst
44+
enum Arguments: String {
45+
case initialDelaySeconds
46+
case refreshFrequencySeconds
47+
}
48+
}
49+
3950
struct CancelAllTasks {
4051
static let name = "\(CancelAllTasks.self)".lowercasingFirst
4152
enum Arguments: String {
@@ -77,6 +88,51 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
7788
operationQueue.addOperation(operation)
7889
}
7990

91+
@available(iOS 13.0, *)
92+
private func scheduleAppRefresh() {
93+
let request = BGAppRefreshTaskRequest(identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier)
94+
95+
request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshTaskFrequency * 60)
96+
97+
do {
98+
try BGTaskScheduler.shared.submit(request)
99+
} catch {
100+
print("Could not schedule app refresh: \(error)")
101+
}
102+
}
103+
104+
@available(iOS 13.0, *)
105+
private func handleAppRefresh(task: BGAppRefreshTask) {
106+
print("handleAppRefresh()", task.identifier)
107+
108+
if (bgRefreshTaskFrequency > 0) {
109+
// Schedule a new refresh task.
110+
scheduleAppRefresh()
111+
}
112+
113+
let operationQueue = OperationQueue()
114+
115+
// Create an operation that performs the main part of the background task.
116+
let operation = BackgroundTaskOperation(
117+
task.identifier,
118+
flutterPluginRegistrantCallback: SwiftWorkmanagerPlugin.flutterPluginRegistrantCallback
119+
)
120+
121+
// Provide the background task with an expiration handler that cancels the operation.
122+
task.expirationHandler = {
123+
operation.cancel()
124+
}
125+
126+
// Inform the system that the background task is complete
127+
// when the operation completes.
128+
operation.completionBlock = {
129+
task.setTaskCompleted(success: !operation.isCancelled)
130+
}
131+
132+
// Start the operation.
133+
operationQueue.addOperation(operation)
134+
}
135+
80136
public override func application(_ application: UIApplication,
81137
didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any] = [:]) -> Bool {
82138
if #available(iOS 13.0, *) {
@@ -88,6 +144,10 @@ public class SwiftWorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate {
88144
self.handleBGProcessingTask(task)
89145
}
90146
}
147+
148+
BGTaskScheduler.shared.register(forTaskWithIdentifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier, using: nil) { task in
149+
self.handleAppRefresh(task: task as! BGAppRefreshTask)
150+
}
91151
}
92152

93153
return true
@@ -128,24 +188,49 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
128188
UserDefaultsHelper.storeIsDebug(isInDebug)
129189
result(true)
130190

131-
case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)):
132-
if !validateCallbackHandle() {
133-
result(
134-
FlutterError(
135-
code: "1",
136-
message: "You have not properly initialized the Flutter WorkManager Package. " +
137-
"You should ensure you have called the 'initialize' function first! " +
138-
"Example: \n" +
139-
"\n" +
140-
"`Workmanager().initialize(\n" +
141-
" callbackDispatcher,\n" +
142-
" )`" +
143-
"\n" +
144-
"\n" +
145-
"The `callbackDispatcher` is a top level function. See example in repository.",
146-
details: nil
147-
)
191+
case (ForegroundMethodChannel.Methods.RegisterAppRefreshTask.name, let .some(arguments)):
192+
print("ForegroundMethodChannel.Methods.RegisterAppRefreshTask")
193+
if !validateCallbackHandle(result: result) {
194+
return
195+
}
196+
197+
if #available(iOS 13.0, *) {
198+
let method = ForegroundMethodChannel.Methods.RegisterAppRefreshTask.self
199+
guard let initialDelaySeconds =
200+
arguments[method.Arguments.initialDelaySeconds.rawValue] as? Int64 else {
201+
result(WMPError.invalidParameters.asFlutterError)
202+
return
203+
}
204+
205+
guard let regreshFrequencySeconds =
206+
arguments[method.Arguments.refreshFrequencySeconds.rawValue] as? Int64 else {
207+
result(WMPError.invalidParameters.asFlutterError)
208+
return
209+
}
210+
211+
// save this, can't store in task
212+
bgRefreshTaskFrequency = Double(regreshFrequencySeconds)
213+
214+
let request = BGAppRefreshTaskRequest(
215+
identifier: SwiftWorkmanagerPlugin.defaultBGAppRefreshTaskIdentifier
148216
)
217+
218+
request.earliestBeginDate = Date(timeIntervalSinceNow: Double(initialDelaySeconds))
219+
220+
do {
221+
try BGTaskScheduler.shared.submit(request)
222+
result(true)
223+
} catch {
224+
result(WMPError.bgTaskSchedulingFailed(error).asFlutterError)
225+
}
226+
227+
return
228+
} else {
229+
result(WMPError.unhandledMethod(call.method).asFlutterError)
230+
}
231+
232+
case (ForegroundMethodChannel.Methods.RegisterOneOffTask.name, let .some(arguments)):
233+
if !validateCallbackHandle(result: result) {
149234
return
150235
}
151236

@@ -207,8 +292,27 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
207292
}
208293
}
209294

210-
private func validateCallbackHandle() -> Bool {
211-
return UserDefaultsHelper.getStoredCallbackHandle() != nil
295+
private func validateCallbackHandle(result: @escaping FlutterResult) -> Bool {
296+
let valid = UserDefaultsHelper.getStoredCallbackHandle() != nil
297+
if (!valid) {
298+
result(
299+
FlutterError(
300+
code: "1",
301+
message: "You have not properly initialized the Flutter WorkManager Package. " +
302+
"You should ensure you have called the 'initialize' function first! " +
303+
"Example: \n" +
304+
"\n" +
305+
"`Workmanager().initialize(\n" +
306+
" callbackDispatcher,\n" +
307+
" )`" +
308+
"\n" +
309+
"\n" +
310+
"The `callbackDispatcher` is a top level function. See example in repository.",
311+
details: nil
312+
)
313+
)
314+
}
315+
return valid
212316
}
213317
}
214318

lib/src/workmanager.dart

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class Workmanager {
155155
/// Schedule a one off task
156156
/// A [uniqueName] is required so only one task can be registered.
157157
/// The [taskName] is the value that will be returned in the [BackgroundTaskHandler]
158-
/// The [inputData] is the input data for task. Valid value types are: int, bool, double, String and their list
158+
/// The [inputData] is the input data for task. Android Only. Valid value types are: int, bool, double, String and their list
159159
Future<void> registerOneOffTask(
160160
/// Only supported on Android.
161161
final String uniqueName,
@@ -200,6 +200,27 @@ class Workmanager {
200200
),
201201
);
202202

203+
/// Schedule an iOS BGAppRefreshTask
204+
Future<void> registerAppRefreshTask({
205+
206+
/// Configures an initial delay.
207+
///
208+
/// The delay configured here is not guaranteed. The underlying system may
209+
/// decide to schedule the task a lot later.
210+
final Duration initialDelay = Duration.zero,
211+
212+
/// Frequency to repeat task after initial execution. Duration.zero does not repeat.
213+
final Duration frequency = Duration.zero,
214+
}) async =>
215+
await _foregroundChannel.invokeMethod(
216+
"registerAppRefreshTask",
217+
JsonMapperHelper.toRegisterAppRefreshMethodArgument(
218+
isInDebugMode: _isInDebugMode,
219+
initialDelay: initialDelay,
220+
frequency: frequency
221+
),
222+
);
223+
203224
/// Schedules a periodic task that will run every provided [frequency].
204225
/// A [uniqueName] is required so only one task can be registered.
205226
/// The [taskName] is the value that will be returned in the [BackgroundTaskHandler]
@@ -313,6 +334,19 @@ class JsonMapperHelper {
313334
};
314335
}
315336

337+
@visibleForTesting
338+
static Map<String, Object?> toRegisterAppRefreshMethodArgument({
339+
final bool isInDebugMode = false,
340+
final Duration? initialDelay,
341+
final Duration? frequency
342+
}) {
343+
return {
344+
"isInDebugMode": isInDebugMode,
345+
"initialDelaySeconds": initialDelay?.inSeconds,
346+
"refreshFrequencySeconds": frequency?.inSeconds
347+
};
348+
}
349+
316350
@visibleForTesting
317351
static Map<String, Object?> toInitializeMethodArgument({
318352
required final bool isInDebugMode,

0 commit comments

Comments
 (0)