@capacitor/background-runner 1.0.0-rc.1

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 (61) hide show
  1. package/CapacitorBackgroundRunner.podspec +17 -0
  2. package/README.md +310 -0
  3. package/android/build.gradle +67 -0
  4. package/android/src/main/AndroidManifest.xml +12 -0
  5. package/android/src/main/java/io/ionic/backgroundrunner/plugin/BackgroundRunner.kt +166 -0
  6. package/android/src/main/java/io/ionic/backgroundrunner/plugin/BackgroundRunnerPlugin.kt +102 -0
  7. package/android/src/main/java/io/ionic/backgroundrunner/plugin/CoroutineHelper.kt +21 -0
  8. package/android/src/main/java/io/ionic/backgroundrunner/plugin/RunnerConfig.kt +33 -0
  9. package/android/src/main/java/io/ionic/backgroundrunner/plugin/RunnerWorker.kt +46 -0
  10. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Device.kt +99 -0
  11. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Geolocation.kt +55 -0
  12. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/KV.kt +29 -0
  13. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/NetworkStatus.kt +10 -0
  14. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Notification.kt +59 -0
  15. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Notifications.kt +158 -0
  16. package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/TimedNotificationPublisher.kt +40 -0
  17. package/android/src/main/libs/android-js-engine-release.aar +0 -0
  18. package/android/src/main/res/.gitkeep +0 -0
  19. package/dist/docs.json +208 -0
  20. package/dist/esm/apis.d.ts +52 -0
  21. package/dist/esm/apis.js +2 -0
  22. package/dist/esm/apis.js.map +1 -0
  23. package/dist/esm/definitions.d.ts +82 -0
  24. package/dist/esm/definitions.js +3 -0
  25. package/dist/esm/definitions.js.map +1 -0
  26. package/dist/esm/index.d.ts +5 -0
  27. package/dist/esm/index.js +8 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/web.d.ts +8 -0
  30. package/dist/esm/web.js +16 -0
  31. package/dist/esm/web.js.map +1 -0
  32. package/dist/plugin.cjs.js +32 -0
  33. package/dist/plugin.cjs.js.map +1 -0
  34. package/dist/plugin.js +35 -0
  35. package/dist/plugin.js.map +1 -0
  36. package/ios/Plugin/BackgroundRunner.swift +223 -0
  37. package/ios/Plugin/BackgroundRunnerErrors.swift +8 -0
  38. package/ios/Plugin/BackgroundRunnerPlugin.h +10 -0
  39. package/ios/Plugin/BackgroundRunnerPlugin.m +11 -0
  40. package/ios/Plugin/BackgroundRunnerPlugin.swift +161 -0
  41. package/ios/Plugin/CapacitorAPI/Device.swift +116 -0
  42. package/ios/Plugin/CapacitorAPI/Geolocation.swift +190 -0
  43. package/ios/Plugin/CapacitorAPI/KV.swift +22 -0
  44. package/ios/Plugin/CapacitorAPI/Notifications.swift +165 -0
  45. package/ios/Plugin/CapacitorAPI/Reachability.swift +395 -0
  46. package/ios/Plugin/CapacitorAPI/Wearable.swift +48 -0
  47. package/ios/Plugin/Context+CapacitorAPI.swift +11 -0
  48. package/ios/Plugin/Info.plist +24 -0
  49. package/ios/Plugin/RunnerConfig.swift +36 -0
  50. package/ios/Plugin/RunnerEngine/Context.swift +151 -0
  51. package/ios/Plugin/RunnerEngine/Errors.swift +8 -0
  52. package/ios/Plugin/RunnerEngine/JSConsole.swift +38 -0
  53. package/ios/Plugin/RunnerEngine/JSCrypto.swift +38 -0
  54. package/ios/Plugin/RunnerEngine/JSError.swift +33 -0
  55. package/ios/Plugin/RunnerEngine/JSFetch.swift +50 -0
  56. package/ios/Plugin/RunnerEngine/JSResponse.swift +69 -0
  57. package/ios/Plugin/RunnerEngine/JSTextDecoder.swift +60 -0
  58. package/ios/Plugin/RunnerEngine/JSTextEncoder.swift +12 -0
  59. package/ios/Plugin/RunnerEngine/Runner.swift +23 -0
  60. package/ios/Plugin/WatchConnectivtySessionDelegate.swift +34 -0
  61. package/package.json +81 -0
@@ -0,0 +1,223 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import BackgroundTasks
4
+ import JavaScriptCore
5
+
6
+ public class BackgroundRunner {
7
+ public static let shared = BackgroundRunner()
8
+
9
+ private var runner: Runner?
10
+ private var config: RunnerConfig?
11
+
12
+ private var context: Context?
13
+
14
+ private var started: Bool {
15
+ return runner != nil && context != nil
16
+ }
17
+
18
+ public init() {
19
+ do {
20
+ config = try self.loadRunnerConfig()
21
+ } catch {
22
+ print("could not initialize BackgroundRunner: \(error)")
23
+ }
24
+ }
25
+
26
+ public func registerBackgroundTask() {
27
+ guard let config = config else {
28
+ print("no runner to register")
29
+ return
30
+ }
31
+
32
+ if config.event == nil {
33
+ print("cannot register background task without a event to call")
34
+ return
35
+ }
36
+
37
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: config.label, using: nil) { task in
38
+ do {
39
+ task.expirationHandler = {
40
+ print("task timed out")
41
+ }
42
+
43
+ guard let event = config.event else {
44
+ throw BackgroundRunnerPluginError.invalidRunnerConfig(reason: "runner event is missing or invalid")
45
+ }
46
+
47
+ _ = try BackgroundRunner.shared.execute(config: config, event: event)
48
+
49
+ task.setTaskCompleted(success: true)
50
+ } catch {
51
+ print("background task error: \(error)")
52
+ task.setTaskCompleted(success: false)
53
+ }
54
+ }
55
+ }
56
+
57
+ public func scheduleBackgroundTasks() {
58
+ guard let config = config else {
59
+ return
60
+ }
61
+
62
+ guard let interval = config.interval else {
63
+ print("cannot register background task without a configured interval")
64
+ return
65
+ }
66
+
67
+ let request = BGAppRefreshTaskRequest(identifier: config.label)
68
+ request.earliestBeginDate = Date(timeIntervalSinceNow: Double(interval) * 60)
69
+
70
+ do {
71
+ print("Scheduling \(config.label)")
72
+
73
+ try BGTaskScheduler.shared.submit(request)
74
+ } catch {
75
+ print("Could not schedule app refresh: \(error)")
76
+ }
77
+ }
78
+
79
+ public func start() throws {
80
+ print("starting runner and loading contexts...")
81
+
82
+ guard let config = config else {
83
+ print("...no runner config to start")
84
+ return
85
+ }
86
+
87
+ self.runner = Runner()
88
+ context = try initContext(config: config)
89
+
90
+ }
91
+
92
+ public func stop() {
93
+ print("...stopping runner and removing all contexts")
94
+ guard let runner = runner, let config = config else {
95
+ return
96
+ }
97
+
98
+ runner.destroyContext(name: config.label)
99
+
100
+ context = nil
101
+ self.runner = nil
102
+ }
103
+
104
+ public func getConfig() -> RunnerConfig? {
105
+ return config
106
+ }
107
+
108
+ public func dispatchEvent(event: String, inputArgs: [String: Any]?) throws {
109
+ if !started {
110
+ try start()
111
+ }
112
+
113
+ if let config = config {
114
+ let waitGroup = DispatchGroup()
115
+ waitGroup.enter()
116
+
117
+ var err: Error?
118
+
119
+ DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
120
+ do {
121
+ _ = try self.execute(config: config, event: event, inputArgs: inputArgs)
122
+ } catch {
123
+ err = error
124
+ print("[\(config.label)]: \(error)")
125
+ }
126
+
127
+ waitGroup.leave()
128
+ }
129
+
130
+ waitGroup.wait()
131
+
132
+ if let err = err {
133
+ throw err
134
+ }
135
+ }
136
+ }
137
+
138
+ public func execute(config: RunnerConfig, event: String, inputArgs: [String: Any]? = nil) throws -> [String: Any]? {
139
+ do {
140
+ if !started {
141
+ try self.start()
142
+ }
143
+
144
+ guard let context = context else {
145
+ throw BackgroundRunnerPluginError.runnerError(reason: "no loaded context for config")
146
+ }
147
+
148
+ var inputArgsWithCallback = inputArgs
149
+
150
+ if inputArgsWithCallback == nil {
151
+ inputArgsWithCallback = [:]
152
+ }
153
+
154
+ let waitGroup = DispatchGroup()
155
+
156
+ var result: [String: Any]?
157
+
158
+ let completedFunc: @convention(block)(JavaScriptCore.JSValue) -> Void = { returnObj in
159
+ if !returnObj.isUndefined && !returnObj.isNull {
160
+ var dict: [String: Any] = [:]
161
+
162
+ returnObj.toDictionary().forEach { key, value in
163
+ dict[String(describing: key)] = value
164
+ }
165
+
166
+ result = dict
167
+ }
168
+
169
+ waitGroup.leave()
170
+ return
171
+ }
172
+
173
+ inputArgsWithCallback?["__ebr::completed"] = completedFunc
174
+
175
+ waitGroup.enter()
176
+
177
+ try context.dispatchEvent(event: event, details: inputArgsWithCallback)
178
+
179
+ waitGroup.wait()
180
+
181
+ return result
182
+ } catch {
183
+ print("error executing task: \(error)")
184
+ throw error
185
+ }
186
+ }
187
+
188
+ private func loadRunnerConfig() throws -> RunnerConfig? {
189
+ guard let capacitorConfigFileURL = Bundle.main.url(forResource: "capacitor.config.json", withExtension: nil) else {
190
+ throw BackgroundRunnerPluginError.runnerError(reason: "capacitor.config.json file not found")
191
+ }
192
+
193
+ let capacitorConfigFileData = try Data(contentsOf: capacitorConfigFileURL)
194
+
195
+ if let capConfig = JSTypes.coerceDictionaryToJSObject(try JSONSerialization.jsonObject(with: capacitorConfigFileData) as? [String: Any]) {
196
+ if let jsonConfig = capConfig[keyPath: "plugins.BackgroundRunner"] as? JSObject {
197
+ return try RunnerConfig(from: jsonConfig)
198
+ }
199
+ }
200
+
201
+ return nil
202
+ }
203
+
204
+ private func initContext(config: RunnerConfig) throws -> Context {
205
+ guard let runner = runner else {
206
+ throw BackgroundRunnerPluginError.runnerError(reason: "runner is not initialized")
207
+ }
208
+
209
+ guard let srcFileURL = Bundle.main.url(forResource: config.src, withExtension: nil, subdirectory: "public") else {
210
+ throw BackgroundRunnerPluginError.runnerError(reason: "source file not found")
211
+ }
212
+
213
+ let srcFile = try String(contentsOf: srcFileURL)
214
+
215
+ let context = try runner.createContext(name: config.label)
216
+
217
+ context.setupCapacitorAPI()
218
+
219
+ _ = try context.execute(code: srcFile)
220
+
221
+ return context
222
+ }
223
+ }
@@ -0,0 +1,8 @@
1
+ import Foundation
2
+
3
+ public enum BackgroundRunnerPluginError: Error, Equatable {
4
+ case invalidRunnerConfig(reason: String)
5
+ case invalidArgument(reason: String)
6
+ case runnerError(reason: String)
7
+ case genericError(reason: String)
8
+ }
@@ -0,0 +1,10 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ //! Project version number for Plugin.
4
+ FOUNDATION_EXPORT double PluginVersionNumber;
5
+
6
+ //! Project version string for Plugin.
7
+ FOUNDATION_EXPORT const unsigned char PluginVersionString[];
8
+
9
+ // In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
10
+
@@ -0,0 +1,11 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <Capacitor/Capacitor.h>
3
+
4
+ // Define the plugin using the CAP_PLUGIN Macro, and
5
+ // each method the plugin supports using the CAP_PLUGIN_METHOD macro.
6
+ CAP_PLUGIN(BackgroundRunnerPlugin, "BackgroundRunner",
7
+ CAP_PLUGIN_METHOD(dispatchEvent, CAPPluginReturnPromise);
8
+ CAP_PLUGIN_METHOD(registerBackgroundTask, CAPPluginReturnPromise);
9
+ CAP_PLUGIN_METHOD(checkPermissions, CAPPluginReturnPromise);
10
+ CAP_PLUGIN_METHOD(requestPermissions, CAPPluginReturnPromise);
11
+ )
@@ -0,0 +1,161 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import WatchConnectivity
4
+
5
+ @objc(BackgroundRunnerPlugin)
6
+ public class BackgroundRunnerPlugin: CAPPlugin {
7
+ private let impl = BackgroundRunner()
8
+
9
+ override public func load() {
10
+ NotificationCenter.default.addObserver(
11
+ self,
12
+ selector: #selector(didEnterBackground),
13
+ name: UIApplication.didEnterBackgroundNotification,
14
+ object: nil
15
+ )
16
+ initWatchConnectivity()
17
+ }
18
+
19
+ @objc override public func checkPermissions(_ call: CAPPluginCall) {
20
+ // check geolocation permissions
21
+ let geolocationState = CapacitorGeolocation.checkPermission()
22
+
23
+ // check notification permissions
24
+ let notificationState = CapacitorNotifications.checkPermission()
25
+
26
+ call.resolve([
27
+ "geolocation": geolocationState,
28
+ "notifications": notificationState
29
+ ])
30
+ }
31
+
32
+ @objc override public func requestPermissions(_ call: CAPPluginCall) {
33
+ Task {
34
+ let permissions = call.getArray("apis") as? [String] ?? []
35
+
36
+ do {
37
+ if permissions.contains("notifications") {
38
+ try CapacitorNotifications.requestPermission()
39
+ }
40
+
41
+ if permissions.contains("geolocation") {
42
+ let geolocation = CapacitorGeolocation()
43
+ print("geolocation requested...")
44
+ try await geolocation.requestPermission()
45
+ print("geolocation requested...done")
46
+ }
47
+
48
+ self.checkPermissions(call)
49
+ } catch {
50
+ call.reject("\(error)")
51
+ }
52
+ }
53
+ }
54
+
55
+ @objc func dispatchEvent(_ call: CAPPluginCall) {
56
+ do {
57
+ guard let runnerEvent = call.getString("event") else {
58
+ throw BackgroundRunnerPluginError.invalidArgument(reason: "event is missing or invalid")
59
+ }
60
+
61
+ let details = call.getObject("details", JSObject())
62
+
63
+ guard let config = impl.getConfig() else {
64
+ throw BackgroundRunnerPluginError.runnerError(reason: "no runner config loaded")
65
+ }
66
+
67
+ DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
68
+ do {
69
+ let result = try self.impl.execute(
70
+ config: config,
71
+ event: runnerEvent,
72
+ inputArgs: details as [String: Any]
73
+ )
74
+
75
+ if let result = result {
76
+ call.resolve(result)
77
+ return
78
+ }
79
+
80
+ call.resolve()
81
+ } catch {
82
+ call.reject("\(error)")
83
+ }
84
+ }
85
+ } catch {
86
+ call.reject("\(error)")
87
+ }
88
+ }
89
+
90
+ @objc func registerBackgroundTask(_ call: CAPPluginCall) {
91
+ call.resolve()
92
+ }
93
+
94
+ @objc private func didEnterBackground() {
95
+ impl.scheduleBackgroundTasks()
96
+ }
97
+
98
+ public static func registerBackgroundTask() {
99
+ BackgroundRunner.shared.registerBackgroundTask()
100
+ }
101
+
102
+ public static func dispatchEvent(event: String, eventArgs: [AnyHashable: Any], completionHandler: ((Result<Bool, Error>) -> Void)) {
103
+ var args: [String: Any] = [:]
104
+
105
+ eventArgs.forEach { (key: AnyHashable, value: Any) in
106
+ if let strKey = key as? String {
107
+ args[strKey] = value
108
+ }
109
+ }
110
+
111
+ do {
112
+ try BackgroundRunner.shared.dispatchEvent(event: event, inputArgs: args)
113
+ completionHandler(.success(true))
114
+ } catch {
115
+ completionHandler(.failure(error))
116
+ }
117
+ }
118
+
119
+ public static func handleApplicationDidFinishLaunching(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
120
+ guard let launchOptions = launchOptions else {
121
+ return
122
+ }
123
+
124
+ var events: [String: [String: Any]?] = [:]
125
+
126
+ if launchOptions[.location] != nil {
127
+ // application was launched for a location event
128
+ var details: [String: Any] = [:]
129
+ details["locations"] = nil
130
+
131
+ if let location = CapacitorGeolocation.getCurrentKnownLocation() {
132
+ var details: [String: Any] = [:]
133
+ details["locations"] = [location]
134
+ }
135
+
136
+ events["currentLocation"] = details
137
+ }
138
+
139
+ print("emit events: \(events)")
140
+
141
+ }
142
+
143
+ private func initWatchConnectivity() {
144
+ if !WCSession.isSupported() {
145
+ return
146
+ }
147
+
148
+ guard let config = impl.getConfig() else {
149
+ return
150
+ }
151
+
152
+ if !config.enableWatchConnectivity {
153
+ return
154
+ }
155
+
156
+ WCSession.default.delegate = self
157
+ WCSession.default.activate()
158
+
159
+ print("Watch Connectivity Enabled")
160
+ }
161
+ }
@@ -0,0 +1,116 @@
1
+ import Foundation
2
+ import UIKit
3
+ import JavaScriptCore
4
+
5
+ @objc protocol CapacitorDeviceExports: JSExport {
6
+ func getNetworkStatus() -> [String: Any]?
7
+ func getBatteryStatus() -> [String: Any]
8
+ }
9
+
10
+ enum CapacitorDeviceErrors: Error, Equatable {
11
+ case unknownError(reason: String)
12
+ }
13
+
14
+ class CapacitorDevice: NSObject, CapacitorDeviceExports {
15
+ var reachability: Reachability?
16
+ var initError: Error?
17
+
18
+ enum Connection {
19
+ case unavailable, wifi, cellular
20
+ }
21
+
22
+ override init() {
23
+ super.init()
24
+
25
+ do {
26
+ reachability = try Reachability()
27
+ } catch {
28
+ initError = error
29
+ }
30
+ }
31
+
32
+ private func checkError() throws {
33
+ if let err = initError {
34
+ throw CapacitorDeviceErrors.unknownError(reason: "\(err)")
35
+ }
36
+
37
+ if reachability == nil {
38
+ throw CapacitorDeviceErrors.unknownError(reason: "reachability failed to initialize")
39
+ }
40
+ }
41
+
42
+ func getNetworkStatus() -> [String: Any]? {
43
+ do {
44
+ try checkError()
45
+
46
+ let status = reachability?.connection.equivalentEnum ?? Connection.unavailable
47
+
48
+ var networkStatusDict: [String: Any] = [:]
49
+ networkStatusDict["connected"] = status.isConnected
50
+ networkStatusDict["connectionType"] = status.jsStringValue
51
+
52
+ return networkStatusDict
53
+ } catch {
54
+ JSContext.current().exception = JSValue(newErrorFromMessage: "\(error)", in: JSContext.current())
55
+ return nil
56
+ }
57
+ }
58
+
59
+ func getBatteryStatus() -> [String: Any] {
60
+ UIDevice.current.isBatteryMonitoringEnabled = true
61
+
62
+ var batteryInfoDict: [String: Any] = [:]
63
+ batteryInfoDict["batteryLevel"] = UIDevice.current.batteryLevel
64
+ batteryInfoDict["isCharging"] = UIDevice.current.batteryState == .charging || UIDevice.current.batteryState == .full
65
+
66
+ UIDevice.current.isBatteryMonitoringEnabled = false
67
+
68
+ return batteryInfoDict
69
+ }
70
+ }
71
+
72
+ fileprivate extension Reachability.Connection {
73
+ var equivalentEnum: CapacitorDevice.Connection {
74
+ switch self {
75
+ case .unavailable:
76
+ return .unavailable
77
+ case .wifi:
78
+ return .wifi
79
+ case .cellular:
80
+ return .cellular
81
+ }
82
+ }
83
+ }
84
+
85
+ extension CapacitorDevice.Connection {
86
+ internal var jsStringValue: String {
87
+ switch self {
88
+ case .cellular:
89
+ return "cellular"
90
+ case .wifi:
91
+ return "wifi"
92
+ case .unavailable:
93
+ return "none"
94
+ }
95
+ }
96
+
97
+ internal var isConnected: Bool {
98
+ switch self {
99
+ case .cellular, .wifi:
100
+ return true
101
+ case .unavailable:
102
+ return false
103
+ }
104
+ }
105
+
106
+ internal var logMessage: String {
107
+ switch self {
108
+ case .cellular:
109
+ return "Reachable via Cellular"
110
+ case .wifi:
111
+ return "Reachable via WiFi"
112
+ case .unavailable:
113
+ return "Not reachable"
114
+ }
115
+ }
116
+ }