@zykeco/expo-background-task 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +185 -0
  3. package/README.md +115 -0
  4. package/android/build.gradle +21 -0
  5. package/android/proguard-rules.pro +1 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskConsumer.kt +60 -0
  8. package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskExceptions.kt +13 -0
  9. package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskModule.kt +56 -0
  10. package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskScheduler.kt +294 -0
  11. package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskWork.kt +38 -0
  12. package/app.plugin.js +1 -0
  13. package/build/BackgroundTask.d.ts +63 -0
  14. package/build/BackgroundTask.d.ts.map +1 -0
  15. package/build/BackgroundTask.js +141 -0
  16. package/build/BackgroundTask.js.map +1 -0
  17. package/build/BackgroundTask.types.d.ts +71 -0
  18. package/build/BackgroundTask.types.d.ts.map +1 -0
  19. package/build/BackgroundTask.types.js +31 -0
  20. package/build/BackgroundTask.types.js.map +1 -0
  21. package/build/ExpoBackgroundTaskModule.d.ts +14 -0
  22. package/build/ExpoBackgroundTaskModule.d.ts.map +1 -0
  23. package/build/ExpoBackgroundTaskModule.js +3 -0
  24. package/build/ExpoBackgroundTaskModule.js.map +1 -0
  25. package/build/ExpoBackgroundTaskModule.web.d.ts +6 -0
  26. package/build/ExpoBackgroundTaskModule.web.d.ts.map +1 -0
  27. package/build/ExpoBackgroundTaskModule.web.js +7 -0
  28. package/build/ExpoBackgroundTaskModule.web.js.map +1 -0
  29. package/expo-module.config.json +10 -0
  30. package/ios/BackgorundTaskExceptions.swift +48 -0
  31. package/ios/BackgroundTaskAppDelegate.swift +74 -0
  32. package/ios/BackgroundTaskConstants.swift +13 -0
  33. package/ios/BackgroundTaskConsumer.swift +62 -0
  34. package/ios/BackgroundTaskDebugHelper.swift +21 -0
  35. package/ios/BackgroundTaskModule.swift +80 -0
  36. package/ios/BackgroundTaskRecords.swift +12 -0
  37. package/ios/BackgroundTaskScheduler.swift +206 -0
  38. package/ios/ExpoBackgroundTask.podspec +33 -0
  39. package/package.json +42 -0
  40. package/plugin/build/withBackgroundTask.d.ts +3 -0
  41. package/plugin/build/withBackgroundTask.js +32 -0
  42. package/plugin/src/withBackgroundTask.ts +39 -0
  43. package/plugin/tsconfig.json +9 -0
  44. package/plugin/tsconfig.tsbuildinfo +1 -0
  45. package/src/BackgroundTask.ts +162 -0
  46. package/src/BackgroundTask.types.ts +75 -0
  47. package/src/ExpoBackgroundTaskModule.ts +16 -0
  48. package/src/ExpoBackgroundTaskModule.web.ts +7 -0
  49. package/tsconfig.json +9 -0
@@ -0,0 +1,14 @@
1
+ import { type NativeModule } from 'expo';
2
+ import { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';
3
+ type ExpoBackgroundTaskEvents = {
4
+ onTasksExpired(): void;
5
+ };
6
+ declare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEvents> {
7
+ getStatusAsync(): Promise<BackgroundTaskStatus>;
8
+ registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;
9
+ unregisterTaskAsync(name: string): Promise<void>;
10
+ triggerTaskWorkerForTestingAsync(): Promise<boolean>;
11
+ }
12
+ declare const _default: ExpoBackgroundTaskModule;
13
+ export default _default;
14
+ //# sourceMappingURL=ExpoBackgroundTaskModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,MAAM,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAErF,KAAK,wBAAwB,GAAG;IAC9B,cAAc,IAAI,IAAI,CAAC;CACxB,CAAC;AAEF,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,wBAAwB,CAAC;IACnF,cAAc,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAC/C,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9E,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAChD,gCAAgC,IAAI,OAAO,CAAC,OAAO,CAAC;CACrD;;AAED,wBAAmF"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo';
2
+ export default requireNativeModule('ExpoBackgroundTask');
3
+ //# sourceMappingURL=ExpoBackgroundTaskModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.js","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAqB,MAAM,MAAM,CAAC;AAe9D,eAAe,mBAAmB,CAA2B,oBAAoB,CAAC,CAAC","sourcesContent":["import { requireNativeModule, type NativeModule } from 'expo';\n\nimport { BackgroundTaskOptions, BackgroundTaskStatus } from './BackgroundTask.types';\n\ntype ExpoBackgroundTaskEvents = {\n onTasksExpired(): void;\n};\n\ndeclare class ExpoBackgroundTaskModule extends NativeModule<ExpoBackgroundTaskEvents> {\n getStatusAsync(): Promise<BackgroundTaskStatus>;\n registerTaskAsync(name: string, options: BackgroundTaskOptions): Promise<void>;\n unregisterTaskAsync(name: string): Promise<void>;\n triggerTaskWorkerForTestingAsync(): Promise<boolean>;\n}\n\nexport default requireNativeModule<ExpoBackgroundTaskModule>('ExpoBackgroundTask');\n"]}
@@ -0,0 +1,6 @@
1
+ import { BackgroundTaskStatus } from './BackgroundTask.types';
2
+ declare const _default: {
3
+ getStatusAsync(): Promise<BackgroundTaskStatus>;
4
+ };
5
+ export default _default;
6
+ //# sourceMappingURL=ExpoBackgroundTaskModule.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;;sBAGpC,OAAO,CAAC,oBAAoB,CAAC;;AADvD,wBAIE"}
@@ -0,0 +1,7 @@
1
+ import { BackgroundTaskStatus } from './BackgroundTask.types';
2
+ export default {
3
+ async getStatusAsync() {
4
+ return BackgroundTaskStatus.Restricted;
5
+ },
6
+ };
7
+ //# sourceMappingURL=ExpoBackgroundTaskModule.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoBackgroundTaskModule.web.js","sourceRoot":"","sources":["../src/ExpoBackgroundTaskModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,eAAe;IACb,KAAK,CAAC,cAAc;QAClB,OAAO,oBAAoB,CAAC,UAAU,CAAC;IACzC,CAAC;CACF,CAAC","sourcesContent":["import { BackgroundTaskStatus } from './BackgroundTask.types';\n\nexport default {\n async getStatusAsync(): Promise<BackgroundTaskStatus> {\n return BackgroundTaskStatus.Restricted;\n },\n};\n"]}
@@ -0,0 +1,10 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "appDelegateSubscribers": ["BackgroundTaskAppDelegateSubscriber"],
5
+ "modules": ["BackgroundTaskModule"]
6
+ },
7
+ "android": {
8
+ "modules": ["expo.modules.backgroundtask.BackgroundTaskModule"]
9
+ }
10
+ }
@@ -0,0 +1,48 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import ExpoModulesCore
3
+
4
+ internal final class BackgroundTasksNotConfigured: Exception {
5
+ override var reason: String {
6
+ "Background Task has not been configured. To enable it, add `process` to `UIBackgroundModes` in the application's Info.plist file"
7
+ }
8
+ }
9
+
10
+ internal final class BackgroundTasksRestricted: Exception {
11
+ override var reason: String {
12
+ #if targetEnvironment(simulator)
13
+ "Background Task is not available on simulators. Use a device to test it."
14
+ #else
15
+ "Background Task is not available in the current context."
16
+ #endif
17
+ }
18
+ }
19
+
20
+ internal final class TaskManagerNotFound: Exception {
21
+ override var reason: String {
22
+ "TaskManager not found. Are you sure that Expo modules are properly linked?"
23
+ }
24
+ }
25
+
26
+ internal final class CouldNotRegisterWorkerTask: GenericException<String> {
27
+ override var reason: String {
28
+ "Expo BackgroundTasks: The task could not be registered: \(param)"
29
+ }
30
+ }
31
+
32
+ internal final class CouldNotRegisterWorker: Exception {
33
+ override var reason: String {
34
+ "Expo BackgroundTasks: Could not register native worker task"
35
+ }
36
+ }
37
+
38
+ internal final class ErrorInvokingTaskHandler: Exception {
39
+ override var reason: String {
40
+ "Expo BackgroundTasks: An error occured when running the task handler"
41
+ }
42
+ }
43
+
44
+ internal final class InvalidFinishTaskRun: Exception {
45
+ override var reason: String {
46
+ "Expo BackgroundTasks: Tried to mark task run as finished when there are no task runs active"
47
+ }
48
+ }
@@ -0,0 +1,74 @@
1
+ // Copyright 2018-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+ import BackgroundTasks
5
+ import ExpoModulesCore
6
+
7
+ public class BackgroundTaskAppDelegateSubscriber: ExpoAppDelegateSubscriber {
8
+ public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
9
+ if BackgroundTaskScheduler.supportsBackgroundTasks() {
10
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: BackgroundTaskConstants.BackgroundWorkerIdentifier, using: nil) { task in
11
+ log.debug("Expo Background Tasks - starting background work")
12
+
13
+ // Set up expiration handler
14
+ task.expirationHandler = { ()
15
+ log.warn("Expo Background Tasks - task expired")
16
+ // Send message to Expo module
17
+ NotificationCenter.default.post(name: onTasksExpiredNotification, object: self, userInfo: [:])
18
+ task.setTaskCompleted(success: false)
19
+ self.reschedule()
20
+ }
21
+
22
+ // FIX: EXTaskService singleton lookup
23
+ //
24
+ // The original upstream code used:
25
+ // ModuleRegistryProvider.singletonModules().first(where: { $0 is EXTaskServiceInterface })
26
+ //
27
+ // This never works because EXTaskService registers itself via the +shared class
28
+ // method (a standard ObjC singleton pattern), NOT via EX_REGISTER_SINGLETON_MODULE.
29
+ // As a result, singletonModules() never contains it, the handler falls through to
30
+ // the else branch, task.setTaskCompleted(success: false) is called, and the JS
31
+ // callback never executes — even though iOS successfully launched the background task.
32
+ //
33
+ // We resolve this by looking up EXTaskService via the ObjC runtime:
34
+ // 1. NSClassFromString("EXTaskService") — finds the class by name
35
+ // 2. responds(to: "shared") — safely checks the +shared selector exists
36
+ // 3. perform("shared").takeUnretainedValue() — calls +shared to get the singleton
37
+ //
38
+ // We intentionally avoid KVC (value(forKey:)) here because a missing key would
39
+ // throw an NSUnknownKeyException — an ObjC exception that Swift cannot catch,
40
+ // causing an unrecoverable crash.
41
+ let sharedSelector = NSSelectorFromString("shared")
42
+ if let taskServiceClass = NSClassFromString("EXTaskService") as? NSObject.Type,
43
+ taskServiceClass.responds(to: sharedSelector),
44
+ let taskService = taskServiceClass.perform(sharedSelector)?.takeUnretainedValue() as? EXTaskServiceInterface {
45
+ taskService.runTasks(with: EXTaskLaunchReasonBackgroundTask, userInfo: nil, completionHandler: { _ in
46
+ // Mark iOS task as finished - this is important so that we can continue calling it
47
+ task.setTaskCompleted(success: true)
48
+ self.reschedule()
49
+ })
50
+ } else {
51
+ task.setTaskCompleted(success: false)
52
+ log.error("Expo Background Tasks: Could not find TaskService module")
53
+ }
54
+ }
55
+
56
+ // Signal to the scheduler that we're done.
57
+ BackgroundTaskScheduler.bgTaskSchedulerDidFinishRegister()
58
+ }
59
+
60
+ return true
61
+ }
62
+
63
+ private func reschedule() {
64
+ // Reschedule
65
+ Task {
66
+ do {
67
+ log.debug("Background task successfully finished. Rescheduling")
68
+ try await BackgroundTaskScheduler.tryScheduleWorker()
69
+ } catch {
70
+ log.error("Could not reschedule the worker after task finished: \(error.localizedDescription)")
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,13 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import Foundation
3
+
4
+ public struct BackgroundTaskConstants {
5
+ public static let BackgroundWorkerIdentifier = "com.expo.modules.backgroundtask.processing"
6
+ public static let EVENT_PERFORM_WORK = "onPerformWork"
7
+ public static let EVENT_WORK_DONE = "onWorkDone"
8
+
9
+ /**
10
+ Startup argument that will cause us to simulate starting from the background
11
+ */
12
+ public static let EXPO_RUN_BACKGROUND_TASK = "EXPO_RUN_BACKGROUND_TASK"
13
+ }
@@ -0,0 +1,62 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import ExpoModulesCore
3
+
4
+ class BackgroundTaskConsumer: NSObject, EXTaskConsumerInterface {
5
+ var task: EXTaskInterface?
6
+
7
+ static func supportsLaunchReason(_ launchReason: EXTaskLaunchReason) -> Bool {
8
+ return launchReason == EXTaskLaunchReasonBackgroundTask
9
+ }
10
+
11
+ func taskType() -> String {
12
+ return "backgroundTask"
13
+ }
14
+
15
+ func normalizeTaskResult(_ result: Any?) -> UInt {
16
+ guard let result = result as? Int else {
17
+ return UIBackgroundFetchResult.noData.rawValue
18
+ }
19
+
20
+ switch result {
21
+ case BackgroundTaskResult.success.rawValue:
22
+ return UIBackgroundFetchResult.newData.rawValue
23
+ case BackgroundTaskResult.failed.rawValue:
24
+ return UIBackgroundFetchResult.failed.rawValue
25
+ default:
26
+ return UIBackgroundFetchResult.newData.rawValue
27
+ }
28
+ }
29
+
30
+ func didBecomeReadyToExecute(withData data: [AnyHashable: Any]?) {
31
+ // Run on main thread. The task execution needs to be called on the main thread
32
+ // since it accesses the UIApplication's state
33
+ EXUtilities.performSynchronously {
34
+ self.task?.execute(withData: data, withError: nil)
35
+ }
36
+ }
37
+
38
+ func didRegisterTask(_ task: EXTaskInterface) {
39
+ self.task = task
40
+
41
+ if !BackgroundTaskScheduler.supportsBackgroundTasks() {
42
+ return
43
+ }
44
+
45
+ BackgroundTaskScheduler.didRegisterTask(
46
+ minutes: self.task?.options?["minimumInterval"] as? Int,
47
+ requiresNetwork: self.task?.options?["requiresNetworkConnectivity"] as? Bool,
48
+ taskType: self.task?.options?["iosTaskType"] as? String,
49
+ requiresExternalPower: self.task?.options?["requiresExternalPower"] as? Bool
50
+ )
51
+ }
52
+
53
+ func didUnregister() {
54
+ self.task = nil
55
+
56
+ if !BackgroundTaskScheduler.supportsBackgroundTasks() {
57
+ return
58
+ }
59
+
60
+ BackgroundTaskScheduler.didUnregisterTask()
61
+ }
62
+ }
@@ -0,0 +1,21 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import BackgroundTasks
3
+
4
+ final class BackgroundTaskDebugHelper {
5
+ static func triggerBackgroundTaskTest() {
6
+ #if DEBUG
7
+ let selector = NSSelectorFromString("_simulate".appending("LaunchForTaskWithIdentifier:"))
8
+
9
+ if let method = class_getInstanceMethod(BGTaskScheduler.self, selector) {
10
+ typealias MethodImplementation = @convention(c) (AnyObject, Selector, String) -> Void
11
+ let implementation = unsafeBitCast(method_getImplementation(method), to: MethodImplementation.self)
12
+
13
+ implementation(BGTaskScheduler.shared, selector, BackgroundTaskConstants.BackgroundWorkerIdentifier)
14
+ } else {
15
+ print("BackgroundTaskScheduler: _simulateLaunchForTaskWithIdentifier method not found on BGTaskScheduler.")
16
+ }
17
+ #else
18
+ fatalError("Triggering background tasks are not allowed in release builds.")
19
+ #endif
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import ExpoModulesCore
3
+
4
+ private let onTasksExpired = "onTasksExpired"
5
+ public let onTasksExpiredNotification = Notification.Name(onTasksExpired)
6
+
7
+ public class BackgroundTaskModule: Module {
8
+ private lazy var taskManager: EXTaskManagerInterface? = appContext?.legacyModule(implementing: EXTaskManagerInterface.self)
9
+
10
+ public func definition() -> ModuleDefinition {
11
+ Name("ExpoBackgroundTask")
12
+
13
+ Events(onTasksExpired)
14
+
15
+ OnStartObserving(onTasksExpired) {
16
+ NotificationCenter.default.addObserver(
17
+ self,
18
+ selector: #selector(handleTasksExpiredNotification),
19
+ name: onTasksExpiredNotification,
20
+ object: nil)
21
+ }
22
+
23
+ OnStopObserving(onTasksExpired) {
24
+ // swiftlint:disable:next notification_center_detachment
25
+ NotificationCenter.default.removeObserver(self)
26
+ }
27
+
28
+ AsyncFunction("triggerTaskWorkerForTestingAsync") {
29
+ BackgroundTaskDebugHelper.triggerBackgroundTaskTest()
30
+ }
31
+
32
+ AsyncFunction("registerTaskAsync") { (name: String, options: [String: Any]) in
33
+ guard let taskManager else {
34
+ throw TaskManagerNotFound()
35
+ }
36
+
37
+ if !BackgroundTaskScheduler.supportsBackgroundTasks() {
38
+ throw BackgroundTasksRestricted()
39
+ }
40
+
41
+ if !taskManager.hasBackgroundModeEnabled("processing") {
42
+ throw BackgroundTasksNotConfigured()
43
+ }
44
+
45
+ // Register task
46
+ taskManager.registerTask(
47
+ withName: name, consumer: BackgroundTaskConsumer.self, options: options)
48
+ }
49
+
50
+ AsyncFunction("unregisterTaskAsync") { (name: String) in
51
+ guard let taskManager else {
52
+ throw TaskManagerNotFound()
53
+ }
54
+
55
+ if !BackgroundTaskScheduler.supportsBackgroundTasks() {
56
+ throw BackgroundTasksRestricted()
57
+ }
58
+
59
+ if !taskManager.hasBackgroundModeEnabled("processing") {
60
+ throw BackgroundTasksNotConfigured()
61
+ }
62
+
63
+ try EXUtilities.catchException {
64
+ taskManager.unregisterTask(withName: name, consumerClass: BackgroundTaskConsumer.self)
65
+ }
66
+ }
67
+
68
+ AsyncFunction("getStatusAsync") {
69
+ return BackgroundTaskScheduler.supportsBackgroundTasks()
70
+ ? BackgroundTaskStatus.available : .restricted
71
+ }
72
+ }
73
+
74
+ @objc func handleTasksExpiredNotification(_ notification: Notification) {
75
+ guard let url = notification.userInfo?["url"] as? URL else {
76
+ return
77
+ }
78
+ self.sendEvent(onTasksExpired, [:])
79
+ }
80
+ }
@@ -0,0 +1,12 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import ExpoModulesCore
3
+
4
+ enum BackgroundTaskStatus: Int, Enumerable {
5
+ case restricted = 1
6
+ case available = 2
7
+ }
8
+
9
+ enum BackgroundTaskResult: Int, Enumerable {
10
+ case success = 1
11
+ case failed = 2
12
+ }
@@ -0,0 +1,206 @@
1
+ // Copyright 2024-present 650 Industries. All rights reserved.
2
+ import BackgroundTasks
3
+
4
+ public class BackgroundTaskScheduler {
5
+ /**
6
+ * Keep track of number of registered task consumers
7
+ */
8
+ static var numberOfRegisteredTasksOfThisType: Int = 0
9
+
10
+ /**
11
+ * Interval for task scheduler. The iOS BGTaskScheduler does not guarantee that the number of minutes will be
12
+ * exact, but it indicates when we'd like the task to start. This will be set to at least 12 hours
13
+ */
14
+ private static var intervalSeconds: TimeInterval = 12 * 60 * 60
15
+
16
+ /**
17
+ * Task type: "refresh" (default) for BGAppRefreshTaskRequest, "processing" for BGProcessingTaskRequest
18
+ */
19
+ private static var taskType: String = "refresh"
20
+
21
+ /**
22
+ * Whether the task requires network connectivity
23
+ */
24
+ private static var requiresNetwork: Bool = true
25
+
26
+ /**
27
+ * Whether the task requires external power (only relevant for processing tasks)
28
+ */
29
+ private static var requiresExternalPower: Bool = false
30
+
31
+ /**
32
+ A one-time async gate that becomes ready after BGTaskScheduler registration finishes.
33
+ Multiple awaiters will be resumed when ready; subsequent awaiters return immediately.
34
+ */
35
+ private actor RegistrationGate {
36
+ private var ready: Bool = false
37
+ private var waiters: [CheckedContinuation<Void, Never>] = []
38
+
39
+ func awaitReady() async {
40
+ if ready { return }
41
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
42
+ if ready {
43
+ continuation.resume()
44
+ } else {
45
+ waiters.append(continuation)
46
+ }
47
+ }
48
+ }
49
+
50
+ func signalReady() {
51
+ guard !ready else { return }
52
+ ready = true
53
+ let continuations = waiters
54
+ waiters.removeAll()
55
+ for c in continuations {
56
+ c.resume()
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ Registration gate that will only be signaled when the bgTaskSchedulerDidFinishRegister is called.
63
+ */
64
+ private static let registrationGate = RegistrationGate()
65
+
66
+ /**
67
+ Call from the BackgroundTaskAppDelegate after the call to BGTaskScheduler.shared.register has finished
68
+ so that we can hold back any tryScheduleWorker calls, especially the call to BGTaskScheduler.shared.submit
69
+ that will fail if called before the BGTaskScheduler has successfully registered its handler.
70
+ */
71
+ public static func bgTaskSchedulerDidFinishRegister() {
72
+ Task {
73
+ await registrationGate.signalReady()
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Call when a task is registered to keep track of how many background task consumers we have
79
+ */
80
+ public static func didRegisterTask(
81
+ minutes: Int?,
82
+ requiresNetwork network: Bool? = nil,
83
+ taskType type: String? = nil,
84
+ requiresExternalPower power: Bool? = nil
85
+ ) {
86
+ if let minutes = minutes {
87
+ intervalSeconds = Double(minutes) * 60
88
+ }
89
+ if let network = network {
90
+ requiresNetwork = network
91
+ }
92
+ if let type = type {
93
+ taskType = type
94
+ }
95
+ if let power = power {
96
+ requiresExternalPower = power
97
+ }
98
+ numberOfRegisteredTasksOfThisType += 1
99
+
100
+ if numberOfRegisteredTasksOfThisType == 1 {
101
+ Task {
102
+ // Only schedule if no pending request exists.
103
+ // Task restoration on cold start must not cancel-and-resubmit,
104
+ // otherwise earliestBeginDate gets reset on every app start
105
+ // and the task never fires.
106
+ if await !isWorkerRunning() {
107
+ try await tryScheduleWorker()
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Call when a task is unregistered to keep track of how many background task consumers we have
115
+ */
116
+ public static func didUnregisterTask() {
117
+ numberOfRegisteredTasksOfThisType -= 1
118
+ if numberOfRegisteredTasksOfThisType == 0 {
119
+ Task {
120
+ await stopWorker()
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Tries to schedule the worker task to run
127
+ */
128
+ public static func tryScheduleWorker() async throws {
129
+ if numberOfRegisteredTasksOfThisType == 0 {
130
+ print("Background Task: skipping scheduling. No registered tasks")
131
+ return
132
+ }
133
+
134
+ // Wait until BGTaskScheduler registration has completed.
135
+ await registrationGate.awaitReady()
136
+
137
+ // Stop existing tasks
138
+ await stopWorker()
139
+
140
+ // Create request based on task type
141
+ let request: BGTaskRequest
142
+
143
+ if taskType == "processing" {
144
+ let processingRequest = BGProcessingTaskRequest(
145
+ identifier: BackgroundTaskConstants.BackgroundWorkerIdentifier
146
+ )
147
+ processingRequest.requiresNetworkConnectivity = requiresNetwork
148
+ processingRequest.requiresExternalPower = requiresExternalPower
149
+ request = processingRequest
150
+ } else {
151
+ // Default: refresh
152
+ request = BGAppRefreshTaskRequest(
153
+ identifier: BackgroundTaskConstants.BackgroundWorkerIdentifier
154
+ )
155
+ }
156
+
157
+ // Set up minimum start date
158
+ request.earliestBeginDate = Date().addingTimeInterval(intervalSeconds)
159
+
160
+ do {
161
+ try BGTaskScheduler.shared.submit(request)
162
+ } catch let error as BGTaskScheduler.Error {
163
+ switch error.code {
164
+ case .unavailable:
165
+ throw CouldNotRegisterWorkerTask("Background task scheduling is unavailable.")
166
+ case .tooManyPendingTaskRequests:
167
+ throw CouldNotRegisterWorkerTask("Too many pending task requests.")
168
+ case .notPermitted:
169
+ throw CouldNotRegisterWorkerTask("Task request not permitted.")
170
+ @unknown default:
171
+ print("An unknown BGTaskScheduler error occurred.")
172
+ // Handle any future cases added by Apple
173
+ }
174
+ } catch {
175
+ // All other errors
176
+ throw CouldNotRegisterWorkerTask("Unknown error occurred.")
177
+ }
178
+ }
179
+
180
+ /**
181
+ Cancels the worker task
182
+ */
183
+ public static func stopWorker() async {
184
+ BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundTaskConstants.BackgroundWorkerIdentifier)
185
+ }
186
+
187
+ /**
188
+ Returns true if the worker task is pending
189
+ */
190
+ public static func isWorkerRunning() async -> Bool {
191
+ let requests = await BGTaskScheduler.shared.pendingTaskRequests()
192
+ return requests.contains(where: { $0.identifier == BackgroundTaskConstants.BackgroundWorkerIdentifier })
193
+ }
194
+
195
+ /**
196
+ Returns true if we're on a device that supports background tasks
197
+ */
198
+ public static func supportsBackgroundTasks() -> Bool {
199
+ #if targetEnvironment(simulator)
200
+ // If we're on emulator we should definetly return restricted
201
+ return false
202
+ #else
203
+ return true
204
+ #endif
205
+ }
206
+ }
@@ -0,0 +1,33 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoBackgroundTask'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.source = { git: 'https://github.com/expo/expo.git' }
18
+ s.static_framework = true
19
+
20
+ s.dependency 'ExpoModulesCore'
21
+ # Swift/Objective-C compatibility
22
+ s.pod_target_xcconfig = {
23
+ 'DEFINES_MODULE' => 'YES',
24
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
25
+ }
26
+
27
+ if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
28
+ s.source_files = "**/*.h"
29
+ s.vendored_frameworks = "#{s.name}.xcframework"
30
+ else
31
+ s.source_files = "**/*.{h,m,swift}"
32
+ end
33
+ end
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@zykeco/expo-background-task",
3
+ "version": "1.0.0",
4
+ "description": "Expo Android and iOS module for Background Task APIs",
5
+ "main": "build/BackgroundTask.js",
6
+ "types": "build/BackgroundTask.d.ts",
7
+ "sideEffects": false,
8
+ "scripts": {
9
+ "build": "expo-module build",
10
+ "clean": "expo-module clean",
11
+ "lint": "expo-module lint",
12
+ "test": "expo-module test",
13
+ "prepare": "expo-module prepare",
14
+ "prepublishOnly": "expo-module prepublishOnly",
15
+ "expo-module": "expo-module"
16
+ },
17
+ "keywords": [
18
+ "expo",
19
+ "react-native",
20
+ "background",
21
+ "background-task"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/expo/expo.git",
26
+ "directory": "packages/expo-background-task"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/expo/expo/issues"
30
+ },
31
+ "license": "MIT",
32
+ "homepage": "https://docs.expo.dev/versions/latest/sdk/background-task/",
33
+ "dependencies": {
34
+ "expo-task-manager": "~55.0.9"
35
+ },
36
+ "devDependencies": {
37
+ "expo-module-scripts": "^55.0.2"
38
+ },
39
+ "peerDependencies": {
40
+ "expo": ">=55.0.0"
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ import { ConfigPlugin } from 'expo/config-plugins';
2
+ declare const _default: ConfigPlugin<void>;
3
+ export default _default;