Skip to content

Commit e8f9d39

Browse files
enedclaude
andcommitted
feat: Bugfix release 0.7.1 with multiple contributor fixes
- Android: Fix v2 embedding import in BackgroundWorker by @jogapps - Android: Fix documentation formatting and typo by @jogapps - iOS: Fix swapped constraints bug by @thegriffen - iOS: Add Privacy Manifest for App Store compliance by @navaronbracke - iOS: Replace print statements with proper os_log - iOS: printScheduledTasks now returns String by @yarith28 - iOS: Add Swift Package Manager support - Fix broken documentation links 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0705ce2 commit e8f9d39

23 files changed

+1334
-15
lines changed

workmanager/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# 0.7.1
2+
3+
* Android: Fix v2 embedding import in BackgroundWorker by @jogapps (from PR #595)
4+
* Android: Fix documentation formatting and typo in BackgroundWorker by @jogapps (from PR #595)
5+
* iOS: Fix swapped constraints bug for requiresNetworkConnectivity and requiresExternalPower by @thegriffen (from PR #562)
6+
* iOS: Add Privacy Manifest for App Store compliance by @navaronbracke (from PR #555)
7+
* iOS: Replace print statements with proper os_log for better logging
8+
* iOS: printScheduledTasks now returns String instead of void by @yarith28 (from PR #585)
9+
* iOS: Add Swift Package Manager support for future Flutter compatibility
10+
111
# 0.7.0
212

313
* **BREAKING**: Minimum Dart SDK bumped to 3.2.0

workmanager/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Flutter WorkManager is a wrapper around [Android's WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager), [iOS' performFetchWithCompletionHandler](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) and [iOS BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask), effectively enabling headless execution of Dart code in the background.
88

9-
For iOS users, please watch this video on a general introduction to background processing: https://developer.apple.com/videos/play/wwdc2019/707. All of the constraints discussed in the video also apply to this plugin.
9+
For iOS users, please watch this video on a general introduction to background processing: https://developer.apple.com/videos/play/wwdc2019/707. All of the constraints discussed in the video also apply to this plugin.
1010

1111
This is especially useful to run periodic tasks, such as fetching remote data on a regular basis.
1212

workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/BackgroundWorker.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import com.google.common.util.concurrent.ListenableFuture
1111
import io.flutter.embedding.engine.FlutterEngine
1212
import io.flutter.embedding.engine.dart.DartExecutor
1313
import io.flutter.embedding.engine.loader.FlutterLoader
14+
import io.flutter.embedding.engine.loader.FlutterCallbackInformation
1415
import io.flutter.plugin.common.MethodCall
1516
import io.flutter.plugin.common.MethodChannel
16-
import io.flutter.view.FlutterCallbackInformation
1717
import java.util.Random
1818

19-
/***
20-
* A simple worker that will post your input back to your Flutter application.
19+
/**
20+
* A simple worker that posts your input back to your Flutter application.
2121
*
2222
* It will block the background thread until a value of either true or false is received back from Flutter code.
2323
*/
@@ -165,8 +165,8 @@ class BackgroundWorker(
165165
}
166166

167167
override fun success(receivedResult: Any?) {
168-
val wasSuccessFul = receivedResult?.let { it as Boolean? } == true
169-
stopEngine(if (wasSuccessFul) Result.success() else Result.retry())
168+
val wasSuccessful = receivedResult as? Boolean == true
169+
stopEngine(if (wasSuccessful) Result.success() else Result.retry())
170170
}
171171
},
172172
)

workmanager/android/src/main/kotlin/dev/fluttercommunity/workmanager/WorkmanagerPlugin.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
55
import io.flutter.plugin.common.BinaryMessenger
66
import io.flutter.plugin.common.MethodChannel
77

8+
/**
9+
* A Flutter plugin that provides a foreground channel for workmanager operations.
10+
*
11+
* This implementation uses Flutter's v2 embedding API.
12+
*/
813
class WorkmanagerPlugin : FlutterPlugin {
914
private var methodChannel: MethodChannel? = null
1015
private var workmanagerCallHandler: WorkmanagerCallHandler? = null

workmanager/ios/Classes/SwiftWorkmanagerPlugin.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,8 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
395395
SwiftWorkmanagerPlugin.scheduleBackgroundProcessingTask(
396396
withIdentifier: uniqueTaskIdentifier,
397397
earliestBeginInSeconds: delaySeconds,
398-
requiresNetworkConnectivity: requiresCharging,
399-
requiresExternalPower: requiresNetwork)
398+
requiresNetworkConnectivity: requiresNetwork,
399+
requiresExternalPower: requiresCharging)
400400

401401
result(true)
402402
return
@@ -456,15 +456,17 @@ extension SwiftWorkmanagerPlugin: FlutterPlugin {
456456
if #available(iOS 13.0, *) {
457457
BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in
458458
if taskRequests.isEmpty {
459-
print("[BGTaskScheduler] There are no scheduled tasks")
460-
result(true)
459+
let message = "[BGTaskScheduler] There are no scheduled tasks"
460+
os_log("%{public}@", log: OSLog.default, type: .debug, message)
461+
result(message)
461462
return
462463
}
463-
print("[BGTaskScheduler] Scheduled Tasks:")
464+
var message = "[BGTaskScheduler] Scheduled Tasks:"
464465
for taskRequest in taskRequests {
465-
print("[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")")
466+
message += "\n[BGTaskScheduler] Task Identifier: \(taskRequest.identifier) earliestBeginDate: \(taskRequest.earliestBeginDate?.formatted() ?? "")"
466467
}
467-
result(true)
468+
os_log("%{public}@", log: OSLog.default, type: .debug, message)
469+
result(message)
468470
}
469471
} else {
470472
result(FlutterError(code: "99",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyAccessedAPITypes</key>
6+
<array>
7+
<dict>
8+
<key>NSPrivacyAccessedAPITypeReasons</key>
9+
<array>
10+
<string>CA92.1</string>
11+
</array>
12+
<key>NSPrivacyAccessedAPIType</key>
13+
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
14+
</dict>
15+
</array>
16+
<key>NSPrivacyCollectedDataTypes</key>
17+
<array/>
18+
<key>NSPrivacyTrackingDomains</key>
19+
<array/>
20+
<key>NSPrivacyTracking</key>
21+
<false/>
22+
</dict>
23+
</plist>

workmanager/ios/workmanager.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ Flutter Android Workmanager
1919
s.ios.deployment_target = '13.0'
2020
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
2121
s.swift_version = '5.0'
22+
s.resource_bundles = { 'flutter_workmanager_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
2223
end
2324

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "workmanager",
8+
platforms: [
9+
.iOS("13.0")
10+
],
11+
products: [
12+
.library(name: "workmanager", targets: ["workmanager"])
13+
],
14+
dependencies: [
15+
.package(url: "https://github.com/flutter/engine", from: "0.0.0")
16+
],
17+
targets: [
18+
.target(
19+
name: "workmanager",
20+
dependencies: [
21+
.product(name: "Flutter", package: "engine")
22+
],
23+
resources: [
24+
.process("Resources")
25+
]
26+
)
27+
]
28+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// BackgroundTaskOperation.swift
3+
// workmanager
4+
//
5+
// Created by Sebastian Roth on 10/06/2021.
6+
//
7+
8+
import Foundation
9+
10+
class BackgroundTaskOperation: Operation {
11+
12+
private let identifier: String
13+
private let flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
14+
private let inputData: String
15+
private let backgroundMode: BackgroundMode
16+
17+
init(_ identifier: String,
18+
inputData: String,
19+
flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?,
20+
backgroundMode: BackgroundMode) {
21+
self.identifier = identifier
22+
self.inputData = inputData
23+
self.flutterPluginRegistrantCallback = flutterPluginRegistrantCallback
24+
self.backgroundMode = backgroundMode
25+
}
26+
27+
override func main() {
28+
let semaphore = DispatchSemaphore(value: 0)
29+
let worker = BackgroundWorker(mode: self.backgroundMode,
30+
inputData: self.inputData,
31+
flutterPluginRegistrantCallback: self.flutterPluginRegistrantCallback)
32+
DispatchQueue.main.async {
33+
worker.performBackgroundRequest { _ in
34+
semaphore.signal()
35+
}
36+
}
37+
38+
semaphore.wait()
39+
}
40+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// BackgroundWorker.swift
3+
// workmanager
4+
//
5+
// Created by Sebastian Roth on 10/06/2021.
6+
//
7+
8+
import Foundation
9+
10+
enum BackgroundMode {
11+
case backgroundFetch
12+
case backgroundProcessingTask(identifier: String)
13+
case backgroundPeriodicTask(identifier: String)
14+
case backgroundOneOffTask(identifier: String)
15+
16+
var flutterThreadlabelPrefix: String {
17+
switch self {
18+
case .backgroundFetch:
19+
return "\(SwiftWorkmanagerPlugin.identifier).BackgroundFetch"
20+
case .backgroundProcessingTask:
21+
return "\(SwiftWorkmanagerPlugin.identifier).BackgroundProcessingTask"
22+
case .backgroundPeriodicTask:
23+
return "\(SwiftWorkmanagerPlugin.identifier).BackgroundPeriodicTask"
24+
case .backgroundOneOffTask:
25+
return "\(SwiftWorkmanagerPlugin.identifier).OneOffTask"
26+
}
27+
}
28+
29+
var onResultSendArguments: [String: String] {
30+
switch self {
31+
case .backgroundFetch:
32+
return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": "iOSPerformFetch"]
33+
case let .backgroundProcessingTask(identifier):
34+
return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier]
35+
case let .backgroundPeriodicTask(identifier):
36+
return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier]
37+
case let .backgroundOneOffTask(identifier):
38+
return ["\(SwiftWorkmanagerPlugin.identifier).DART_TASK": identifier]
39+
}
40+
}
41+
}
42+
43+
class BackgroundWorker {
44+
45+
let backgroundMode: BackgroundMode
46+
let flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
47+
let inputData: String
48+
49+
init(mode: BackgroundMode, inputData: String, flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?) {
50+
backgroundMode = mode
51+
self.inputData = inputData
52+
self.flutterPluginRegistrantCallback = flutterPluginRegistrantCallback
53+
}
54+
55+
private struct BackgroundChannel {
56+
static let name = "\(SwiftWorkmanagerPlugin.identifier)/background_channel_work_manager"
57+
static let initialized = "backgroundChannelInitialized"
58+
static let onResultSendCommand = "onResultSend"
59+
}
60+
61+
/// The result is discardable due to how [BackgroundTaskOperation] works.
62+
@discardableResult
63+
func performBackgroundRequest(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
64+
guard let callbackHandle = UserDefaultsHelper.getStoredCallbackHandle(),
65+
let flutterCallbackInformation = FlutterCallbackCache.lookupCallbackInformation(callbackHandle)
66+
else {
67+
logError("[\(String(describing: self))] \(WMPError.workmanagerNotInitialized.message)")
68+
completionHandler(.failed)
69+
return false
70+
}
71+
72+
let taskSessionStart = Date()
73+
let taskSessionIdentifier = UUID()
74+
75+
let debugHelper = DebugNotificationHelper(taskSessionIdentifier)
76+
debugHelper.showStartFetchNotification(
77+
startDate: taskSessionStart,
78+
callBackHandle: callbackHandle,
79+
callbackInfo: flutterCallbackInformation
80+
)
81+
82+
var flutterEngine: FlutterEngine? = FlutterEngine(
83+
name: backgroundMode.flutterThreadlabelPrefix,
84+
project: nil,
85+
allowHeadlessExecution: true
86+
)
87+
88+
flutterEngine!.run(
89+
withEntrypoint: flutterCallbackInformation.callbackName,
90+
libraryURI: flutterCallbackInformation.callbackLibraryPath
91+
)
92+
flutterPluginRegistrantCallback?(flutterEngine!)
93+
94+
var backgroundMethodChannel: FlutterMethodChannel? = FlutterMethodChannel(
95+
name: BackgroundChannel.name,
96+
binaryMessenger: flutterEngine!.binaryMessenger
97+
)
98+
99+
func cleanupFlutterResources() {
100+
flutterEngine?.destroyContext()
101+
backgroundMethodChannel = nil
102+
flutterEngine = nil
103+
}
104+
105+
backgroundMethodChannel?.setMethodCallHandler { call, result in
106+
switch call.method {
107+
case BackgroundChannel.initialized:
108+
result(true) // Agree to Flutter's method invocation
109+
var arguments = self.backgroundMode.onResultSendArguments
110+
if self.inputData != "" {
111+
arguments = arguments.merging(["be.tramckrijte.workmanager.INPUT_DATA": self.inputData]) { current, _ in current }
112+
}
113+
114+
backgroundMethodChannel?.invokeMethod(
115+
BackgroundChannel.onResultSendCommand,
116+
arguments: arguments,
117+
result: { flutterResult in
118+
cleanupFlutterResources()
119+
let taskSessionCompleter = Date()
120+
let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
121+
let taskDuration = taskSessionCompleter.timeIntervalSince(taskSessionStart)
122+
logInfo("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration.formatToSeconds()))")
123+
124+
debugHelper.showCompletedFetchNotification(
125+
completedDate: taskSessionCompleter,
126+
result: result,
127+
elapsedTime: taskDuration
128+
)
129+
completionHandler(result)
130+
})
131+
default:
132+
result(WMPError.unhandledMethod(call.method).asFlutterError)
133+
cleanupFlutterResources()
134+
completionHandler(UIBackgroundFetchResult.failed)
135+
}
136+
}
137+
138+
return true
139+
}
140+
}

0 commit comments

Comments
 (0)