diff --git a/docs/debugging.mdx b/docs/debugging.mdx
index 3626d292..23fd7ecb 100644
--- a/docs/debugging.mdx
+++ b/docs/debugging.mdx
@@ -69,7 +69,10 @@ class AppDelegate: FlutterAppDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -77,12 +80,50 @@ class AppDelegate: FlutterAppDelegate {
### Notification Debug Handler
-Shows debug information as notifications:
+Shows debug information as notifications (helpful for seeing background task execution):
```swift
-WorkmanagerDebug.setCurrent(NotificationDebugHandler())
+// In your AppDelegate.swift
+import workmanager_apple
+
+@main
+class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+
+ // Set notification delegate first
+ UNUserNotificationCenter.current().delegate = self
+
+ // Request notification permission
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
+ if granted {
+ print("Notification permission granted")
+ }
+ }
+
+ // Enable notification debug handler
+ WorkmanagerDebug.setCurrent(NotificationDebugHandler())
+
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ // REQUIRED: Override to show notifications when app is in foreground
+ override func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ completionHandler(.alert) // Show notification banner even if app is in foreground
+ }
+}
```
+
+**Important:** The `userNotificationCenter` override is required to display debug notifications when your app is in the foreground. Without it, you'll only see notifications when the app is in the background.
+
+
## Custom Debug Handlers
Create your own debug handler for custom logging needs:
@@ -361,4 +402,4 @@ Future isTaskHealthy(String taskName, Duration maxAge) async {
- [ ] Error handling with try-catch blocks
- [ ] iOS 30-second execution limit respected
-Remember: Background task execution is controlled by the operating system and is never guaranteed. Always design your app to work gracefully when background tasks don't run as expected.
\ No newline at end of file
+Remember: Background task execution is controlled by the operating system and is never guaranteed. Always design your app to work gracefully when background tasks don't run as expected.
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index a54d114f..7c66fca3 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -77,6 +77,10 @@ WorkmanagerPlugin.registerBGProcessingTask(
)
```
+
+**iOS Task Identifier Matching:** The task name in your Dart code must **exactly match** the identifier in Info.plist and AppDelegate. Using short names like `"data_sync"` in Dart while having `com.yourapp.processing_task` in native code will cause `BGTaskSchedulerErrorDomain Code 3` errors.
+
+
**Why BGTaskScheduler registration is needed:** iOS requires explicit registration of background task identifiers for security and system resource management. The task identifier in Info.plist tells iOS which background tasks your app can schedule, while the AppDelegate registration connects the identifier to the actual task handler. Background Fetch (Option A) doesn't require this since it uses the simpler, system-managed approach.
@@ -108,6 +112,10 @@ WorkmanagerPlugin.registerPeriodicTask(
)
```
+
+**iOS Task Identifier Matching:** The task name in your Dart code must **exactly match** the identifier in Info.plist and AppDelegate. Using short names like `"cleanup"` in Dart while having `com.yourapp.periodic_task` in native code will cause `BGTaskSchedulerErrorDomain Code 3` errors.
+
+
**Which option to choose?**
- **Option A (Background Fetch)** for non-critical updates that can happen once daily (data sync, content refresh)
@@ -124,10 +132,10 @@ WorkmanagerPlugin.registerPeriodicTask(
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
switch (task) {
- case "data_sync":
+ case "com.yourapp.processing_task": // Must match Info.plist and AppDelegate
await syncDataWithServer();
break;
- case "cleanup":
+ case "com.yourapp.periodic_task": // Must match Info.plist and AppDelegate
await cleanupOldFiles();
break;
case Workmanager.iOSBackgroundTask:
@@ -166,14 +174,14 @@ void main() {
// Schedule a one-time task
Workmanager().registerOneOffTask(
"sync-task",
- "data_sync",
+ "com.yourapp.processing_task", // Must match Info.plist and AppDelegate
initialDelay: Duration(seconds: 10),
);
// Schedule a periodic task
Workmanager().registerPeriodicTask(
"cleanup-task",
- "cleanup",
+ "com.yourapp.periodic_task", // Must match Info.plist and AppDelegate
frequency: Duration(hours: 24),
);
```
@@ -191,6 +199,7 @@ Your background tasks can return:
- **Callback Dispatcher**: Must be a top-level function (not inside a class)
- **Separate Isolate**: Background tasks run in isolation - initialize dependencies inside the task
+- **iOS Task Identifiers**: When using BGTaskScheduler (Options B & C), task names in Dart must exactly match identifiers in Info.plist and AppDelegate
- **Platform Differences**:
- Android: Reliable background execution, 15-minute minimum frequency
- iOS: 30-second limit, execution depends on user patterns and device state
@@ -200,4 +209,4 @@ Your background tasks can return:
- **[Task Customization](customization)** - Advanced configuration with constraints, input data, and management
- **[Debugging Guide](debugging)** - Learn how to debug and troubleshoot background tasks
-- **[Example App](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** - Complete working demo
\ No newline at end of file
+- **[Example App](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** - Complete working demo
diff --git a/workmanager/lib/src/workmanager_impl.dart b/workmanager/lib/src/workmanager_impl.dart
index c045c946..b6ef07c6 100644
--- a/workmanager/lib/src/workmanager_impl.dart
+++ b/workmanager/lib/src/workmanager_impl.dart
@@ -281,6 +281,30 @@ class Workmanager {
);
}
+ /// Schedule a health research background task, currently only available on iOS 15+.
+ ///
+ /// Health research tasks use [BGHealthResearchTask] and are designed for
+ /// apps that need to process HealthKit data in the background. The system
+ /// grants these tasks more runtime than standard background tasks.
+ ///
+ /// Requires iOS 15.0 or later. Will throw [UnsupportedError] on Android.
+ ///
+ /// For iOS see Apple docs:
+ /// [iOS 15+ BGHealthResearchTask](https://developer.apple.com/documentation/backgroundtasks/bghealthresearchtask/)
+ Future registerHealthResearchTask(
+ String uniqueName,
+ String taskName, {
+ Duration? initialDelay,
+ Map? inputData,
+ }) async {
+ return _platform.registerHealthResearchTask(
+ uniqueName,
+ taskName,
+ initialDelay: initialDelay,
+ inputData: inputData,
+ );
+ }
+
/// Cancels task by [uniqueName]
Future cancelByUniqueName(String uniqueName) async =>
_platform.cancelByUniqueName(uniqueName);
diff --git a/workmanager/test/workmanager_test.dart b/workmanager/test/workmanager_test.dart
index 4033e2fe..fe00e19a 100644
--- a/workmanager/test/workmanager_test.dart
+++ b/workmanager/test/workmanager_test.dart
@@ -47,5 +47,48 @@ void main() {
verify(GetIt.I().initialize(testCallBackDispatcher));
verify(GetIt.I().cancelByUniqueName(testTaskName));
});
+
+ test("registerHealthResearchTask - It calls methods on the mocked class",
+ () {
+ when(GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ )).thenAnswer((_) => Future.value());
+
+ GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ );
+
+ verify(GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ ));
+ });
+
+ test(
+ "registerHealthResearchTask with optional params - It calls methods on the mocked class",
+ () {
+ when(GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ initialDelay: const Duration(minutes: 10),
+ inputData: {'dataType': 'heartRate'},
+ )).thenAnswer((_) => Future.value());
+
+ GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ initialDelay: const Duration(minutes: 10),
+ inputData: {'dataType': 'heartRate'},
+ );
+
+ verify(GetIt.I().registerHealthResearchTask(
+ 'com.example.health-task',
+ 'healthTask',
+ initialDelay: const Duration(minutes: 10),
+ inputData: {'dataType': 'heartRate'},
+ ));
+ });
});
}
diff --git a/workmanager/test/workmanager_test.mocks.dart b/workmanager/test/workmanager_test.mocks.dart
index c7926dd4..51c6309d 100644
--- a/workmanager/test/workmanager_test.mocks.dart
+++ b/workmanager/test/workmanager_test.mocks.dart
@@ -24,6 +24,7 @@ import 'package:workmanager_platform_interface/workmanager_platform_interface.da
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
+// ignore_for_file: invalid_use_of_internal_member
/// A class which mocks [Workmanager].
///
@@ -165,6 +166,29 @@ class MockWorkmanager extends _i1.Mock implements _i2.Workmanager {
returnValueForMissingStub: _i3.Future.value(),
) as _i3.Future);
+ @override
+ _i3.Future registerHealthResearchTask(
+ String? uniqueName,
+ String? taskName, {
+ Duration? initialDelay,
+ Map? inputData,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(
+ #registerHealthResearchTask,
+ [
+ uniqueName,
+ taskName,
+ ],
+ {
+ #initialDelay: initialDelay,
+ #inputData: inputData,
+ },
+ ),
+ returnValue: _i3.Future.value(),
+ returnValueForMissingStub: _i3.Future.value(),
+ ) as _i3.Future);
+
@override
_i3.Future cancelByUniqueName(String? uniqueName) =>
(super.noSuchMethod(
diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt
index d7774bcc..3dd063b3 100644
--- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt
+++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt
@@ -1,5 +1,6 @@
package dev.fluttercommunity.workmanager
+import dev.fluttercommunity.workmanager.pigeon.HealthResearchTaskRequest
import dev.fluttercommunity.workmanager.pigeon.InitializeRequest
import dev.fluttercommunity.workmanager.pigeon.OneOffTaskRequest
import dev.fluttercommunity.workmanager.pigeon.PeriodicTaskRequest
@@ -103,6 +104,14 @@ class WorkmanagerPlugin :
callback(Result.failure(UnsupportedOperationException("Processing tasks are not supported on Android")))
}
+ override fun registerHealthResearchTask(
+ request: HealthResearchTaskRequest,
+ callback: (Result) -> Unit,
+ ) {
+ // Health research tasks are iOS 15+ specific
+ callback(Result.failure(UnsupportedOperationException("Health research tasks are not supported on Android")))
+ }
+
override fun cancelByUniqueName(
uniqueName: String,
callback: (Result) -> Unit,
diff --git a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt
index f274eeac..d70810b8 100644
--- a/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt
+++ b/workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt
@@ -1,7 +1,7 @@
// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved.
// // Use of this source code is governed by a MIT-style license that can be
// // found in the LICENSE file.
-// Autogenerated from Pigeon (v26.0.1), do not edit directly.
+// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -510,6 +510,43 @@ data class ProcessingTaskRequest (
override fun hashCode(): Int = toList().hashCode()
}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class HealthResearchTaskRequest (
+ val uniqueName: String,
+ val taskName: String,
+ val inputData: Map? = null,
+ val initialDelaySeconds: Long? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): HealthResearchTaskRequest {
+ val uniqueName = pigeonVar_list[0] as String
+ val taskName = pigeonVar_list[1] as String
+ val inputData = pigeonVar_list[2] as Map?
+ val initialDelaySeconds = pigeonVar_list[3] as Long?
+ return HealthResearchTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ uniqueName,
+ taskName,
+ inputData,
+ initialDelaySeconds,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other !is HealthResearchTaskRequest) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return WorkmanagerApiPigeonUtils.deepEquals(toList(), other.toList()) }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -573,6 +610,11 @@ private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
ProcessingTaskRequest.fromList(it)
}
}
+ 141.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ HealthResearchTaskRequest.fromList(it)
+ }
+ }
else -> super.readValueOfType(type, buffer)
}
}
@@ -626,6 +668,10 @@ private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
stream.write(140)
writeValue(stream, value.toList())
}
+ is HealthResearchTaskRequest -> {
+ stream.write(141)
+ writeValue(stream, value.toList())
+ }
else -> super.writeValue(stream, value)
}
}
@@ -638,6 +684,7 @@ interface WorkmanagerHostApi {
fun registerOneOffTask(request: OneOffTaskRequest, callback: (Result) -> Unit)
fun registerPeriodicTask(request: PeriodicTaskRequest, callback: (Result) -> Unit)
fun registerProcessingTask(request: ProcessingTaskRequest, callback: (Result) -> Unit)
+ fun registerHealthResearchTask(request: HealthResearchTaskRequest, callback: (Result) -> Unit)
fun cancelByUniqueName(uniqueName: String, callback: (Result) -> Unit)
fun cancelByTag(tag: String, callback: (Result) -> Unit)
fun cancelAll(callback: (Result) -> Unit)
@@ -729,6 +776,25 @@ interface WorkmanagerHostApi {
channel.setMessageHandler(null)
}
}
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerHealthResearchTask$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val requestArg = args[0] as HealthResearchTaskRequest
+ api.registerHealthResearchTask(requestArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(WorkmanagerApiPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(WorkmanagerApiPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
run {
val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName$separatedMessageChannelSuffix", codec)
if (api != null) {
diff --git a/workmanager_android/lib/workmanager_android.dart b/workmanager_android/lib/workmanager_android.dart
index d6917fb5..37e30bcd 100644
--- a/workmanager_android/lib/workmanager_android.dart
+++ b/workmanager_android/lib/workmanager_android.dart
@@ -103,6 +103,17 @@ class WorkmanagerAndroid extends WorkmanagerPlatform {
throw UnsupportedError('Processing tasks are not supported on Android');
}
+ @override
+ Future registerHealthResearchTask(
+ String uniqueName,
+ String taskName, {
+ Duration? initialDelay,
+ Map? inputData,
+ }) async {
+ throw UnsupportedError(
+ 'Health research tasks are not supported on Android');
+ }
+
@override
Future cancelByUniqueName(String uniqueName) async {
await _api.cancelByUniqueName(uniqueName);
diff --git a/workmanager_apple/ios/Assets/.gitkeep b/workmanager_apple/ios/workmanager_apple/Assets/.gitkeep
similarity index 100%
rename from workmanager_apple/ios/Assets/.gitkeep
rename to workmanager_apple/ios/workmanager_apple/Assets/.gitkeep
diff --git a/workmanager_apple/ios/Package.swift b/workmanager_apple/ios/workmanager_apple/Package.swift
similarity index 86%
rename from workmanager_apple/ios/Package.swift
rename to workmanager_apple/ios/workmanager_apple/Package.swift
index 66351ca9..64695ee5 100644
--- a/workmanager_apple/ios/Package.swift
+++ b/workmanager_apple/ios/workmanager_apple/Package.swift
@@ -10,7 +10,7 @@ let package = Package(
],
products: [
.library(
- name: "workmanager_apple",
+ name: "workmanager-apple",
targets: ["workmanager_apple"]
)
],
@@ -19,7 +19,7 @@ let package = Package(
name: "workmanager_apple",
path: "Sources/workmanager_apple",
resources: [
- .process("../Resources")
+ .process("../../Resources")
]
)
]
diff --git a/workmanager_apple/ios/Resources/PrivacyInfo.xcprivacy b/workmanager_apple/ios/workmanager_apple/Resources/PrivacyInfo.xcprivacy
similarity index 100%
rename from workmanager_apple/ios/Resources/PrivacyInfo.xcprivacy
rename to workmanager_apple/ios/workmanager_apple/Resources/PrivacyInfo.xcprivacy
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/BackgroundTaskOperation.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/BackgroundTaskOperation.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/BackgroundTaskOperation.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/BackgroundTaskOperation.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/BackgroundWorker.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/BackgroundWorker.swift
similarity index 95%
rename from workmanager_apple/ios/Sources/workmanager_apple/BackgroundWorker.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/BackgroundWorker.swift
index 9f85ff2e..7a6bab85 100644
--- a/workmanager_apple/ios/Sources/workmanager_apple/BackgroundWorker.swift
+++ b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/BackgroundWorker.swift
@@ -20,6 +20,7 @@ enum BackgroundMode {
case backgroundProcessingTask(identifier: String)
case backgroundPeriodicTask(identifier: String)
case backgroundOneOffTask(identifier: String)
+ case backgroundHealthResearchTask(identifier: String)
var flutterThreadlabelPrefix: String {
switch self {
@@ -31,6 +32,8 @@ enum BackgroundMode {
return "\(WorkmanagerPlugin.identifier).BackgroundPeriodicTask"
case .backgroundOneOffTask:
return "\(WorkmanagerPlugin.identifier).OneOffTask"
+ case .backgroundHealthResearchTask:
+ return "\(WorkmanagerPlugin.identifier).BackgroundHealthResearchTask"
}
}
@@ -44,6 +47,8 @@ enum BackgroundMode {
return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier]
case let .backgroundOneOffTask(identifier):
return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier]
+ case let .backgroundHealthResearchTask(identifier):
+ return ["\(WorkmanagerPlugin.identifier).DART_TASK": identifier]
}
}
}
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/Extensions.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/Extensions.swift
similarity index 98%
rename from workmanager_apple/ios/Sources/workmanager_apple/Extensions.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/Extensions.swift
index ba53f9ef..ca3f47ed 100644
--- a/workmanager_apple/ios/Sources/workmanager_apple/Extensions.swift
+++ b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/Extensions.swift
@@ -6,6 +6,7 @@
//
import Foundation
+import UIKit
extension UIBackgroundFetchResult: CustomDebugStringConvertible {
public var debugDescription: String {
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/LoggingDebugHandler.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/LoggingDebugHandler.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/LoggingDebugHandler.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/LoggingDebugHandler.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/NotificationDebugHandler.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/NotificationDebugHandler.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/NotificationDebugHandler.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/NotificationDebugHandler.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/SimpleLogger.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/SimpleLogger.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/SimpleLogger.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/SimpleLogger.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/ThumbnailGenerator.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/ThumbnailGenerator.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/ThumbnailGenerator.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/ThumbnailGenerator.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/UserDefaultsHelper.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/UserDefaultsHelper.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/UserDefaultsHelper.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/UserDefaultsHelper.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/WMPError.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WMPError.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/WMPError.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WMPError.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/WorkmanagerDebugHandler.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WorkmanagerDebugHandler.swift
similarity index 100%
rename from workmanager_apple/ios/Sources/workmanager_apple/WorkmanagerDebugHandler.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WorkmanagerDebugHandler.swift
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/WorkmanagerPlugin.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WorkmanagerPlugin.swift
similarity index 83%
rename from workmanager_apple/ios/Sources/workmanager_apple/WorkmanagerPlugin.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WorkmanagerPlugin.swift
index 16b14c70..e86d73a9 100644
--- a/workmanager_apple/ios/Sources/workmanager_apple/WorkmanagerPlugin.swift
+++ b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/WorkmanagerPlugin.swift
@@ -32,6 +32,21 @@ public class WorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate, FlutterPlugin
operationQueue.addOperation(operation)
}
+ @available(iOS 15.0, *)
+ private static func handleHealthResearchTask(identifier: String, task: BGHealthResearchTask) {
+ let operationQueue = OperationQueue()
+ let operation = createBackgroundOperation(
+ identifier: task.identifier,
+ inputData: nil,
+ backgroundMode: .backgroundHealthResearchTask(identifier: identifier)
+ )
+
+ task.expirationHandler = { operation.cancel() }
+ operation.completionBlock = { task.setTaskCompleted(success: !operation.isCancelled) }
+
+ operationQueue.addOperation(operation)
+ }
+
@available(iOS 13.0, *)
public static func handlePeriodicTask(identifier: String, task: BGAppRefreshTask, earliestBeginInSeconds: Double?) {
guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(),
@@ -116,6 +131,20 @@ public class WorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate, FlutterPlugin
}
}
+ @objc
+ public static func registerHealthResearchTask(withIdentifier identifier: String) {
+ if #available(iOS 15.0, *) {
+ BGTaskScheduler.shared.register(
+ forTaskWithIdentifier: identifier,
+ using: nil
+ ) { task in
+ if let task = task as? BGHealthResearchTask {
+ handleHealthResearchTask(identifier: identifier, task: task)
+ }
+ }
+ }
+ }
+
@objc
@available(iOS 13.0, *)
private static func scheduleBackgroundProcessingTask(
@@ -138,6 +167,23 @@ public class WorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate, FlutterPlugin
}
}
+ @objc
+ @available(iOS 15.0, *)
+ private static func scheduleHealthResearchTask(
+ taskIdentifier identifier: String,
+ earliestBeginInSeconds begin: Double
+ ) {
+ let request = BGHealthResearchTaskRequest(identifier: identifier)
+ request.earliestBeginDate = Date(timeIntervalSinceNow: begin)
+
+ do {
+ try BGTaskScheduler.shared.submit(request)
+ logInfo("BGHealthResearchTask submitted \(identifier) earliestBeginInSeconds:\(begin)")
+ } catch {
+ logInfo("Could not schedule BGHealthResearchTask \(error.localizedDescription)")
+ }
+ }
+
// MARK: - FlutterPlugin conformance
@objc
@@ -226,6 +272,36 @@ public class WorkmanagerPlugin: FlutterPluginAppLifeCycleDelegate, FlutterPlugin
}
}
+ func registerHealthResearchTask(request: HealthResearchTaskRequest, completion: @escaping (Result) -> Void) {
+ guard validateCallbackHandle() else {
+ completion(.failure(createInitializationError()))
+ return
+ }
+
+ if #available(iOS 15.0, *) {
+ let delaySeconds = Double(request.initialDelaySeconds ?? 0)
+ WorkmanagerPlugin.scheduleHealthResearchTask(
+ taskIdentifier: request.uniqueName,
+ earliestBeginInSeconds: delaySeconds
+ )
+
+ let taskInfo = TaskDebugInfo(
+ taskName: request.taskName,
+ uniqueName: request.uniqueName,
+ inputData: request.inputData as? [String: Any],
+ startTime: Date().timeIntervalSince1970
+ )
+ WorkmanagerDebug.getCurrent().onTaskStatusUpdate(taskInfo: taskInfo, status: .scheduled, result: nil)
+ completion(.success(()))
+ } else {
+ completion(.failure(PigeonError(
+ code: "99",
+ message: "HealthResearchTask could not be registered",
+ details: "BGHealthResearchTask is only supported on iOS 15+"
+ )))
+ }
+ }
+
func cancelByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void) {
executeIfSupportedVoid(completion: completion, feature: "cancelByUniqueName") {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: uniqueName)
diff --git a/workmanager_apple/ios/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift
similarity index 93%
rename from workmanager_apple/ios/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift
rename to workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift
index b5b845ad..a1a5a6d9 100644
--- a/workmanager_apple/ios/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift
+++ b/workmanager_apple/ios/workmanager_apple/Sources/workmanager_apple/pigeon/WorkmanagerApi.g.swift
@@ -1,7 +1,7 @@
// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved.
// // Use of this source code is governed by a MIT-style license that can be
// // found in the LICENSE file.
-// Autogenerated from Pigeon (v26.0.1), do not edit directly.
+// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -507,6 +507,43 @@ struct ProcessingTaskRequest: Hashable {
}
}
+/// Generated class from Pigeon that represents data sent in messages.
+struct HealthResearchTaskRequest: Hashable {
+ var uniqueName: String
+ var taskName: String
+ var inputData: [String?: Any?]? = nil
+ var initialDelaySeconds: Int64? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> HealthResearchTaskRequest? {
+ let uniqueName = pigeonVar_list[0] as! String
+ let taskName = pigeonVar_list[1] as! String
+ let inputData: [String?: Any?]? = nilOrValue(pigeonVar_list[2])
+ let initialDelaySeconds: Int64? = nilOrValue(pigeonVar_list[3])
+
+ return HealthResearchTaskRequest(
+ uniqueName: uniqueName,
+ taskName: taskName,
+ inputData: inputData,
+ initialDelaySeconds: initialDelaySeconds
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ uniqueName,
+ taskName,
+ inputData,
+ initialDelaySeconds,
+ ]
+ }
+ static func == (lhs: HealthResearchTaskRequest, rhs: HealthResearchTaskRequest) -> Bool {
+ return deepEqualsWorkmanagerApi(lhs.toList(), rhs.toList()) }
+ func hash(into hasher: inout Hasher) {
+ deepHashWorkmanagerApi(value: toList(), hasher: &hasher)
+ }
+}
+
private class WorkmanagerApiPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
@@ -558,6 +595,8 @@ private class WorkmanagerApiPigeonCodecReader: FlutterStandardReader {
return PeriodicTaskRequest.fromList(self.readValue() as! [Any?])
case 140:
return ProcessingTaskRequest.fromList(self.readValue() as! [Any?])
+ case 141:
+ return HealthResearchTaskRequest.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
@@ -602,6 +641,9 @@ private class WorkmanagerApiPigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? ProcessingTaskRequest {
super.writeByte(140)
super.writeValue(value.toList())
+ } else if let value = value as? HealthResearchTaskRequest {
+ super.writeByte(141)
+ super.writeValue(value.toList())
} else {
super.writeValue(value)
}
@@ -629,6 +671,7 @@ protocol WorkmanagerHostApi {
func registerOneOffTask(request: OneOffTaskRequest, completion: @escaping (Result) -> Void)
func registerPeriodicTask(request: PeriodicTaskRequest, completion: @escaping (Result) -> Void)
func registerProcessingTask(request: ProcessingTaskRequest, completion: @escaping (Result) -> Void)
+ func registerHealthResearchTask(request: HealthResearchTaskRequest, completion: @escaping (Result) -> Void)
func cancelByUniqueName(uniqueName: String, completion: @escaping (Result) -> Void)
func cancelByTag(tag: String, completion: @escaping (Result) -> Void)
func cancelAll(completion: @escaping (Result) -> Void)
@@ -710,6 +753,23 @@ class WorkmanagerHostApiSetup {
} else {
registerProcessingTaskChannel.setMessageHandler(nil)
}
+ let registerHealthResearchTaskChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerHealthResearchTask\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ registerHealthResearchTaskChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let requestArg = args[0] as! HealthResearchTaskRequest
+ api.registerHealthResearchTask(request: requestArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ registerHealthResearchTaskChannel.setMessageHandler(nil)
+ }
let cancelByUniqueNameChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
cancelByUniqueNameChannel.setMessageHandler { message, reply in
diff --git a/workmanager_apple/lib/workmanager_apple.dart b/workmanager_apple/lib/workmanager_apple.dart
index 2092a31b..ade9c96f 100644
--- a/workmanager_apple/lib/workmanager_apple.dart
+++ b/workmanager_apple/lib/workmanager_apple.dart
@@ -109,6 +109,21 @@ class WorkmanagerApple extends WorkmanagerPlatform {
));
}
+ @override
+ Future registerHealthResearchTask(
+ String uniqueName,
+ String taskName, {
+ Duration? initialDelay,
+ Map? inputData,
+ }) async {
+ await _api.registerHealthResearchTask(HealthResearchTaskRequest(
+ uniqueName: uniqueName,
+ taskName: taskName,
+ inputData: inputData?.cast(),
+ initialDelaySeconds: initialDelay?.inSeconds,
+ ));
+ }
+
@override
Future cancelByUniqueName(String uniqueName) async {
await _api.cancelByUniqueName(uniqueName);
diff --git a/workmanager_apple/test/workmanager_apple_test.dart b/workmanager_apple/test/workmanager_apple_test.dart
index e05e4331..9947ce6b 100644
--- a/workmanager_apple/test/workmanager_apple_test.dart
+++ b/workmanager_apple/test/workmanager_apple_test.dart
@@ -131,6 +131,54 @@ void main() {
});
});
+ group('iOS-specific health research task request validation', () {
+ test('should handle HealthResearchTaskRequest creation with all fields',
+ () {
+ final request = HealthResearchTaskRequest(
+ uniqueName: 'com.example.health-research',
+ taskName: 'Health Research Task',
+ inputData: {'dataType': 'heartRate', 'batchSize': 100},
+ initialDelaySeconds: 600, // 10 minutes
+ );
+
+ expect(request.uniqueName, 'com.example.health-research');
+ expect(request.taskName, 'Health Research Task');
+ expect(request.inputData?['dataType'], 'heartRate');
+ expect(request.inputData?['batchSize'], 100);
+ expect(request.initialDelaySeconds, 600);
+ });
+
+ test('should handle minimal HealthResearchTaskRequest', () {
+ final request = HealthResearchTaskRequest(
+ uniqueName: 'minimal-health-task',
+ taskName: 'Minimal Health Task',
+ );
+
+ expect(request.uniqueName, 'minimal-health-task');
+ expect(request.taskName, 'Minimal Health Task');
+ expect(request.inputData, null);
+ expect(request.initialDelaySeconds, null);
+ });
+
+ test('should not have constraint fields unlike ProcessingTaskRequest',
+ () {
+ // BGHealthResearchTask does not support network/charging constraints
+ // Verify HealthResearchTaskRequest only has the expected fields
+ final request = HealthResearchTaskRequest(
+ uniqueName: 'com.example.health-task',
+ taskName: 'Health Task',
+ inputData: {'key': 'value'},
+ initialDelaySeconds: 0,
+ );
+
+ expect(request.uniqueName, isNotNull);
+ expect(request.taskName, isNotNull);
+ expect(request.inputData, isNotNull);
+ expect(request.initialDelaySeconds, isNotNull);
+ // No networkType or requiresCharging fields exist on this type
+ });
+ });
+
group('iOS constraint handling differences', () {
test('should handle battery constraints appropriately for iOS', () {
// iOS handles battery constraints differently than Android
diff --git a/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart b/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart
index 75474851..f89737fa 100644
--- a/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart
+++ b/workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart
@@ -1,7 +1,7 @@
// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved.
// // Use of this source code is governed by a MIT-style license that can be
// // found in the LICENSE file.
-// Autogenerated from Pigeon (v26.0.1), do not edit directly.
+// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
@@ -537,6 +537,62 @@ class ProcessingTaskRequest {
;
}
+class HealthResearchTaskRequest {
+ HealthResearchTaskRequest({
+ required this.uniqueName,
+ required this.taskName,
+ this.inputData,
+ this.initialDelaySeconds,
+ });
+
+ String uniqueName;
+
+ String taskName;
+
+ Map? inputData;
+
+ int? initialDelaySeconds;
+
+ List