@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.
- package/.eslintrc.js +2 -0
- package/CHANGELOG.md +185 -0
- package/README.md +115 -0
- package/android/build.gradle +21 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskConsumer.kt +60 -0
- package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskExceptions.kt +13 -0
- package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskModule.kt +56 -0
- package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskScheduler.kt +294 -0
- package/android/src/main/java/expo/modules/backgroundtask/BackgroundTaskWork.kt +38 -0
- package/app.plugin.js +1 -0
- package/build/BackgroundTask.d.ts +63 -0
- package/build/BackgroundTask.d.ts.map +1 -0
- package/build/BackgroundTask.js +141 -0
- package/build/BackgroundTask.js.map +1 -0
- package/build/BackgroundTask.types.d.ts +71 -0
- package/build/BackgroundTask.types.d.ts.map +1 -0
- package/build/BackgroundTask.types.js +31 -0
- package/build/BackgroundTask.types.js.map +1 -0
- package/build/ExpoBackgroundTaskModule.d.ts +14 -0
- package/build/ExpoBackgroundTaskModule.d.ts.map +1 -0
- package/build/ExpoBackgroundTaskModule.js +3 -0
- package/build/ExpoBackgroundTaskModule.js.map +1 -0
- package/build/ExpoBackgroundTaskModule.web.d.ts +6 -0
- package/build/ExpoBackgroundTaskModule.web.d.ts.map +1 -0
- package/build/ExpoBackgroundTaskModule.web.js +7 -0
- package/build/ExpoBackgroundTaskModule.web.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/BackgorundTaskExceptions.swift +48 -0
- package/ios/BackgroundTaskAppDelegate.swift +74 -0
- package/ios/BackgroundTaskConstants.swift +13 -0
- package/ios/BackgroundTaskConsumer.swift +62 -0
- package/ios/BackgroundTaskDebugHelper.swift +21 -0
- package/ios/BackgroundTaskModule.swift +80 -0
- package/ios/BackgroundTaskRecords.swift +12 -0
- package/ios/BackgroundTaskScheduler.swift +206 -0
- package/ios/ExpoBackgroundTask.podspec +33 -0
- package/package.json +42 -0
- package/plugin/build/withBackgroundTask.d.ts +3 -0
- package/plugin/build/withBackgroundTask.js +32 -0
- package/plugin/src/withBackgroundTask.ts +39 -0
- package/plugin/tsconfig.json +9 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/src/BackgroundTask.ts +162 -0
- package/src/BackgroundTask.types.ts +75 -0
- package/src/ExpoBackgroundTaskModule.ts +16 -0
- package/src/ExpoBackgroundTaskModule.web.ts +7 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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,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
|
+
}
|