@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,17 @@
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 = 'CapacitorBackgroundRunner'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '13.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/README.md ADDED
@@ -0,0 +1,310 @@
1
+ # @capacitor/background-runner
2
+
3
+ Background Runner provides an event-based standalone JavaScript environment for executing your Javascript code outside of the web view.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @capacitor/background-runner
9
+ npx cap sync
10
+ ```
11
+
12
+ Background Runner has support for various device APIs that require permission from the user prior to use.
13
+ ## iOS
14
+
15
+ On iOS you must enable the Background Modes capability.
16
+
17
+ ![Enable Background Mode Capability in Xcode](docs/enable_background_mode_capability.png)
18
+
19
+ Once added, you must enable the `Background fetch` and `Background processing` modes at a minimum to enable the ability to register and schedule your background tasks.
20
+
21
+ If you will be making use of Geolocation or Push Notifications, enable `Location updates` or `Remote notifications` respectively.
22
+
23
+ ![Configure Background Modes in Xcode](docs/configure_background_modes.png)
24
+
25
+ After enabling the Background Modes capability, add the following to your app's `AppDelegate.swift`:
26
+
27
+ ```swift
28
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
29
+
30
+ // ....
31
+ BackgroundRunnerPlugin.registerBackgroundTask()
32
+ BackgroundRunnerPlugin.handleApplicationDidFinishLaunching(launchOptions: launchOptions)
33
+ // ....
34
+
35
+ return true
36
+ }
37
+ ```
38
+
39
+ To allow the Background Runner to handle remote notifications, add the following:
40
+ ```swift
41
+ func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
42
+ // ....
43
+ BackgroundRunnerPlugin.dispatchEvent(event: "remoteNotification", eventArgs: userInfo) { result in
44
+ switch result {
45
+ case .success:
46
+ completionHandler(.newData)
47
+ case .failure:
48
+ completionHandler(.failed)
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Geolocation
55
+ Apple requires privacy descriptions to be specified in `Info.plist` for location information:
56
+
57
+ - `NSLocationAlwaysUsageDescription` (`Privacy - Location Always Usage Description`)
58
+ - `NSLocationWhenInUseUsageDescription` (`Privacy - Location When In Use Usage Description`)
59
+
60
+ Read about [Configuring `Info.plist`](https://capacitorjs.com/docs/ios/configuration#configuring-infoplist) in the [iOS Guide](https://capacitorjs.com/docs/ios) for more information on setting iOS permissions in Xcode
61
+
62
+ ## Android
63
+
64
+ ### Geolocation
65
+ This API requires the following permissions be added to your `AndroidManifest.xml`:
66
+
67
+ ```xml
68
+ <!-- Geolocation API -->
69
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
70
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
71
+ <uses-feature android:name="android.hardware.location.gps" />
72
+ ```
73
+
74
+ The first two permissions ask for location data, both fine and coarse, and the last line is optional but necessary if your app _requires_ GPS to function. You may leave it out, though keep in mind that this may mean your app is installed on devices lacking GPS hardware.
75
+
76
+ ### Local Notifications
77
+ Android 13 requires a permission check in order to send notifications. You are required to call `checkPermissions()` and `requestPermissions()` accordingly.
78
+
79
+ On Android 12 and older it won't show a prompt and will just return as granted.
80
+
81
+ Starting on Android 12, scheduled notifications won't be exact unless this permission is added to your `AndroidManifest.xml`:
82
+
83
+ ```xml
84
+ <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
85
+ ```
86
+
87
+ Note that even if the permission is present, users can still disable exact notifications from the app settings.
88
+
89
+ Read about [Setting Permissions](https://capacitorjs.com/docs/android/configuration#setting-permissions) in the [Android Guide](https://capacitorjs.com/docs/android) for more information on setting Android permissions.
90
+
91
+ ## Using Background Runner
92
+ Background Runner is an event based JavaScript environment that emits events to a javascript runner file that you designate in your `capacitor.config.ts` file. If the runner finds a event handler corresponding to incoming event in your runner file, it will execute the event handler, then shutdown once `details.completed()` is called (or if the OS force kills your process).
93
+
94
+ #### Example Runner JS File
95
+
96
+ ```js
97
+ addEventListener("myCustomEvent", (details) => {
98
+ console.log("do something to update the system here");
99
+ details.completed();
100
+ });
101
+
102
+ addEventListener("myCustomEventWithReturnData", (details) => {
103
+ console.log("accepted this data: " + JSON.stringify(details.user));
104
+
105
+ const updatedUser = details.user;
106
+ updatedUser.firstName = updatedUser.firstName + " HELLO";
107
+ updatedUser.lastName = updatedUser.lastName + " WORLD";
108
+
109
+ details.completed(updatedUser);
110
+ });
111
+
112
+ addEventListener("remoteNotification", (details) => {
113
+ console.log("received silent push notification");
114
+
115
+ CapacitorNotifications.schedule([
116
+ {
117
+ id: 100,
118
+ title: "Enterprise Background Runner",
119
+ body: "Received silent push notification",
120
+ },
121
+ ]);
122
+
123
+ details.completed();
124
+ });
125
+
126
+ ```
127
+ Calling `details.completed()` is **required** within every event handler called by the runner. Failure to do this could result in your runner being killed by the OS if your event is called while the app is in the background. If the app is in the foreground, async calls to `dispatchEvent` may not resolve.
128
+
129
+ ## Configuring Background Runner
130
+ On load, Background Runner will automatically register a background task that will be scheduled and ran once your app is backgrounded. The settings for this behavior is defined in your `capacitor.config.ts` file:
131
+
132
+ ```ts
133
+ const config: CapacitorConfig = {
134
+ plugins: {
135
+ BackgroundRunner: {
136
+ label: "com.example.background.task",
137
+ src: "background.js",
138
+ event: "myCustomEvent",
139
+ repeat: true,
140
+ interval: 2,
141
+ autoStart: false,
142
+ },
143
+ },
144
+ }
145
+ ```
146
+
147
+
148
+ ## JavaScript API
149
+
150
+ Background Runner does not execute your Javascript code in a browser or web view, therefore the typical Web APIs you may be used to may not be available. This includes DOM APIs nor ability to interact with your application's DOM.
151
+
152
+ Below is a list of the available Web APIs provided in Background Runner:
153
+
154
+ - [console](https://developer.mozilla.org/en-US/docs/Web/API/console)
155
+ - only `info`, `log`, `warn`, `error` , and `debug` are available
156
+ - [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder)
157
+ - only `decode` is available
158
+ - [TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder)
159
+ - only `encode` is available
160
+ - [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
161
+ - Event Listener options and `useCapture` not supported
162
+ - [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)
163
+ - [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
164
+ - [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
165
+ - [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
166
+ - [crypto](https://developer.mozilla.org/en-US/docs/Web/API/Crypto)
167
+ - [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
168
+ - Request object not yet supported
169
+ - Only `method`, `headers` and `body` supported in options object
170
+
171
+ In addition to the standard Web APIs, Background Runner also supports a number of custom APIs that expose relevant mobile device functionality:
172
+
173
+ - CapacitorKV - a simple string key / value store backed by UserDefaults on iOS and Shared Preferences on Android.
174
+ - `get(key: string): string`
175
+ - `set(key: string, value: string)`
176
+ - `remove(key: string)`
177
+ - CapacitorNotifications - basic support for sending local notifications.
178
+ - `schedule()`
179
+ - CapacitorDevice - provides information on the device, such as network connectivity and battery status.
180
+ - `getBatteryStatus()`
181
+ - `getNetworkStatus()`
182
+ - CapacitorGeolocation - provides access to the device location information
183
+ - `getCurrentPosition()`
184
+
185
+ ## Runner Lifetimes
186
+
187
+ Currently, the runners are designed for performing periodic bursts of work while your app is in the background, or for executing asynchronous work in a separate thread from your UI while your app is in the foreground. As a result, runners are not long lived. State is not maintained between calls to events in the runner. Each call to `dispatchEvent()` creates a new context in with your runner code is loaded and executed, and once `completed()` is called, the context is destroyed.
188
+
189
+ ## Limitations of Background Tasks
190
+
191
+ It’s not possible to run persistent, always running background services on mobile operating systems. Due to the limitations imposed by iOS and Android designed to reduce battery and data consumption, background tasks are constrained with various limitations that you must keep in mind while designing and implementing your background task.
192
+
193
+ ### iOS
194
+
195
+ - Each invocation of your task has approximately up to 30 seconds of runtime before you must call `completed()` or your task is killed.
196
+ - While you can set an interval to define when your task runs after the app is backgrounded, or how often it should run, this is not guaranteed. iOS will determine when and how often you task will ultimately run, determined in part by how often you app is used.
197
+
198
+ ### Android
199
+
200
+ - Your task has a maximum of 10 minutes to perform work, but to keep your task cross platform compatible, you should limit your work to 30 seconds at most.
201
+ - Repeating background tasks have a minimal interval of at least 15 minutes. Similar to iOS, any interval you request may not be hit exactly - actual execution time is subject to OS battery optimizations and other heuristics.
202
+
203
+ ## API
204
+
205
+ <docgen-index>
206
+
207
+ * [`checkPermissions()`](#checkpermissions)
208
+ * [`requestPermissions(...)`](#requestpermissions)
209
+ * [`dispatchEvent(...)`](#dispatchevent)
210
+ * [Interfaces](#interfaces)
211
+ * [Type Aliases](#type-aliases)
212
+
213
+ </docgen-index>
214
+
215
+ <docgen-api>
216
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
217
+
218
+ ### checkPermissions()
219
+
220
+ ```typescript
221
+ checkPermissions() => any
222
+ ```
223
+
224
+ Check permissions for the various Capacitor device APIs.
225
+
226
+ **Returns:** <code>any</code>
227
+
228
+ **Since:** 1.0.0
229
+
230
+ --------------------
231
+
232
+
233
+ ### requestPermissions(...)
234
+
235
+ ```typescript
236
+ requestPermissions(options: RequestPermissionOptions) => any
237
+ ```
238
+
239
+ Request permission to display local notifications.
240
+
241
+ | Param | Type |
242
+ | ------------- | ----------------------------------------------------------------------------- |
243
+ | **`options`** | <code><a href="#requestpermissionoptions">RequestPermissionOptions</a></code> |
244
+
245
+ **Returns:** <code>any</code>
246
+
247
+ **Since:** 1.0.0
248
+
249
+ --------------------
250
+
251
+
252
+ ### dispatchEvent(...)
253
+
254
+ ```typescript
255
+ dispatchEvent(options: DispatchEventOptions) => any
256
+ ```
257
+
258
+ Dispatches an event to the configured runner.
259
+
260
+ | Param | Type |
261
+ | ------------- | --------------------------------------------------------------------- |
262
+ | **`options`** | <code><a href="#dispatcheventoptions">DispatchEventOptions</a></code> |
263
+
264
+ **Returns:** <code>any</code>
265
+
266
+ **Since:** 1.0.0
267
+
268
+ --------------------
269
+
270
+
271
+ ### Interfaces
272
+
273
+
274
+ #### PermissionStatus
275
+
276
+ | Prop | Type |
277
+ | ------------------- | ----------------------------------------------------------- |
278
+ | **`geolocation`** | <code><a href="#permissionstate">PermissionState</a></code> |
279
+ | **`notifications`** | <code><a href="#permissionstate">PermissionState</a></code> |
280
+
281
+
282
+ #### RequestPermissionOptions
283
+
284
+ | Prop | Type |
285
+ | ---------- | --------------- |
286
+ | **`apis`** | <code>{}</code> |
287
+
288
+
289
+ #### DispatchEventOptions
290
+
291
+ | Prop | Type | Description | Since |
292
+ | ------------- | ------------------------------------ | ------------------------------------------ | ----- |
293
+ | **`label`** | <code>string</code> | The runner label to dispatch the event to | 1.0.0 |
294
+ | **`event`** | <code>string</code> | The name of the registered event listener. | 1.0.0 |
295
+ | **`details`** | <code>{ [key: string]: any; }</code> | | |
296
+
297
+
298
+ ### Type Aliases
299
+
300
+
301
+ #### PermissionState
302
+
303
+ <code>'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'</code>
304
+
305
+
306
+ #### API
307
+
308
+ <code>'geolocation' | 'notifications'</code>
309
+
310
+ </docgen-api>
@@ -0,0 +1,67 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
6
+ workManagerVersion = project.hasProperty('workManagerVersion') ? rootProject.ext.workManagerVersion : '2.8.1'
7
+
8
+ }
9
+
10
+ buildscript {
11
+ ext {
12
+ kotlin_version = '1.8.20'
13
+ }
14
+ repositories {
15
+ google()
16
+ mavenCentral()
17
+ }
18
+ dependencies {
19
+ classpath 'com.android.tools.build:gradle:7.2.2'
20
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
21
+ }
22
+ }
23
+
24
+ apply plugin: 'com.android.library'
25
+ apply plugin: 'org.jetbrains.kotlin.android'
26
+
27
+ android {
28
+ compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 33
29
+ defaultConfig {
30
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
31
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 33
32
+ versionCode 1
33
+ versionName "1.0"
34
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
35
+ }
36
+ buildTypes {
37
+ release {
38
+ minifyEnabled false
39
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
40
+ }
41
+ }
42
+ lintOptions {
43
+ abortOnError false
44
+ }
45
+ compileOptions {
46
+ sourceCompatibility JavaVersion.VERSION_11
47
+ targetCompatibility JavaVersion.VERSION_11
48
+ }
49
+ }
50
+
51
+ repositories {
52
+ google()
53
+ mavenCentral()
54
+ flatDir { dirs "./src/main/libs" }
55
+ }
56
+
57
+ dependencies {
58
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
59
+ implementation project(':capacitor-android')
60
+ implementation (name: "android-js-engine-release", ext: "aar")
61
+ implementation "androidx.work:work-runtime-ktx:$workManagerVersion"
62
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
63
+ implementation 'androidx.core:core-ktx:1.10.1'
64
+ testImplementation "junit:junit:$junitVersion"
65
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
66
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
67
+ }
@@ -0,0 +1,12 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="io.ionic.backgroundrunner.plugin">
3
+
4
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
5
+ <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
6
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
7
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8
+
9
+ <application>
10
+ <receiver android:name=".api.TimedNotificationPublisher" />
11
+ </application>
12
+ </manifest>
@@ -0,0 +1,166 @@
1
+ package io.ionic.backgroundrunner.plugin
2
+
3
+ import android.content.res.AssetManager
4
+ import androidx.work.Data
5
+ import androidx.work.ExistingPeriodicWorkPolicy
6
+ import androidx.work.ExistingWorkPolicy
7
+ import androidx.work.OneTimeWorkRequest
8
+ import androidx.work.PeriodicWorkRequest
9
+ import androidx.work.WorkManager
10
+ import io.ionic.android_js_engine.Context
11
+ import io.ionic.android_js_engine.JSFunction
12
+ import io.ionic.android_js_engine.Runner
13
+ import io.ionic.android_js_engine.api.CapacitorAPI
14
+ import io.ionic.backgroundrunner.plugin.api.Device
15
+ import io.ionic.backgroundrunner.plugin.api.Geolocation
16
+ import io.ionic.backgroundrunner.plugin.api.KV
17
+ import io.ionic.backgroundrunner.plugin.api.Notifications
18
+ import kotlinx.coroutines.flow.MutableStateFlow
19
+ import org.json.JSONObject
20
+ import java.io.BufferedReader
21
+ import java.io.InputStreamReader
22
+ import java.lang.Exception
23
+ import java.util.concurrent.TimeUnit
24
+
25
+
26
+ class BackgroundRunner(context: android.content.Context) {
27
+ public val config: RunnerConfig?
28
+ private var runner: Runner? = null
29
+ private var context: Context? = null
30
+
31
+ companion object {
32
+ @Volatile private var instance: BackgroundRunner? = null
33
+
34
+ fun getInstance(context: android.content.Context): BackgroundRunner = instance ?: synchronized(this) {
35
+ instance ?: BackgroundRunner(context).also { instance = it }
36
+ }
37
+ }
38
+
39
+ private val started: Boolean
40
+ get() {
41
+ return runner != null && context != null
42
+ }
43
+
44
+ init {
45
+ config = loadRunnerConfig(context.assets)
46
+ }
47
+
48
+ fun start(androidContext: android.content.Context) {
49
+ config ?: throw Exception("...no runner config to start")
50
+
51
+ runner = Runner()
52
+ context = initContext(config, androidContext)
53
+ }
54
+
55
+ fun scheduleBackgroundTask(androidContext: android.content.Context) {
56
+ config ?: throw Exception("...no runner config to schedule")
57
+
58
+ val interval = config.interval ?: throw Exception("cannot register background task without a configured interval")
59
+
60
+ if (!config.autoSchedule) {
61
+ return
62
+ }
63
+
64
+ val data = Data.Builder()
65
+ .putString("label", config.label)
66
+ .putString("src", config.src)
67
+ .putString("event", config.event)
68
+ .build()
69
+
70
+ if (!config.repeats) {
71
+ val work = OneTimeWorkRequest.Builder(RunnerWorker::class.java)
72
+ .setInitialDelay(interval.toLong(), TimeUnit.MINUTES)
73
+ .setInputData(data)
74
+ .addTag(config.label)
75
+ .build()
76
+ WorkManager.getInstance(androidContext).enqueueUniqueWork(config.label, ExistingWorkPolicy.REPLACE, work)
77
+ } else {
78
+ val work = PeriodicWorkRequest.Builder(RunnerWorker::class.java, interval.toLong(), TimeUnit.MINUTES)
79
+ .setInitialDelay(interval.toLong(), TimeUnit.MINUTES)
80
+ .setInputData(data)
81
+ .addTag(config.label)
82
+ .build()
83
+ WorkManager.getInstance(androidContext).enqueueUniquePeriodicWork(config.label, ExistingPeriodicWorkPolicy.UPDATE, work)
84
+ }
85
+ }
86
+
87
+ suspend fun execute(androidContext: android.content.Context, config: RunnerConfig, args: JSONObject = JSONObject()): JSONObject? {
88
+ if (!started) {
89
+ start(androidContext)
90
+ }
91
+
92
+ context ?: throw Exception("no loaded context for config")
93
+
94
+ val future = MutableStateFlow<Result<JSONObject?>?>(null)
95
+
96
+ class CompletionCallback : JSFunction(jsName = "completed") {
97
+ override fun run() {
98
+ super.run()
99
+ future.value = Result.success(this.args)
100
+ }
101
+ }
102
+
103
+ val callback = CompletionCallback()
104
+ context!!.registerFunction("_backgroundCallback", callback)
105
+
106
+ args.put("completed", "__cbr::_backgroundCallback")
107
+
108
+ context!!.dispatchEvent(config.event, args)
109
+
110
+ val finished = future.conditionalAwait {
111
+ it != null
112
+ }
113
+
114
+ runner?.destroy()
115
+
116
+ runner = null
117
+
118
+ if (finished!!.isSuccess) {
119
+ return finished.getOrNull()
120
+ } else {
121
+ val ex = finished.exceptionOrNull()
122
+ ex ?: throw Exception("unknown exception")
123
+ throw ex
124
+ }
125
+ }
126
+
127
+ private fun loadRunnerConfig(assetManager: AssetManager): RunnerConfig {
128
+ BufferedReader(InputStreamReader(assetManager.open("capacitor.config.json"))).use { reader ->
129
+ val buffer = StringBuilder()
130
+ var line: String?
131
+ while (reader.readLine().also { line = it } != null) {
132
+ buffer.append(line).append("\n")
133
+ }
134
+ val fileContents = buffer.toString();
135
+
136
+ val configObject = JSONObject(fileContents)
137
+ val plugins = configObject.getJSONObject("plugins") ?: throw Exception("could not read config file")
138
+ val runnerConfigObj = plugins.getJSONObject("BackgroundRunner") ?: throw Exception("could not read config file")
139
+
140
+ return RunnerConfig(runnerConfigObj)
141
+ }
142
+ }
143
+
144
+ private fun initContext(config: RunnerConfig, context: android.content.Context): Context {
145
+ runner ?: throw Exception("runner is not initialized")
146
+
147
+ val srcFile = context.assets.open("public/${config.src}").bufferedReader().use {
148
+ it.readText()
149
+ }
150
+
151
+ val newContext = runner!!.createContext(config.label)
152
+
153
+ val api = CapacitorAPI(config.label)
154
+ api.initNotificationsAPI(Notifications(context))
155
+ api.initDeviceAPI(Device(context))
156
+ api.initGeolocationAPI(Geolocation(context))
157
+ api.initKVAPI(KV(context, config.label))
158
+
159
+ newContext.setCapacitorAPI(api)
160
+
161
+
162
+ newContext.execute(srcFile, false)
163
+
164
+ return newContext
165
+ }
166
+ }
@@ -0,0 +1,102 @@
1
+ package io.ionic.backgroundrunner.plugin;
2
+
3
+ import android.Manifest
4
+ import android.util.Log
5
+ import com.getcapacitor.JSObject
6
+ import com.getcapacitor.Plugin
7
+ import com.getcapacitor.PluginCall
8
+ import com.getcapacitor.PluginMethod
9
+ import com.getcapacitor.annotation.CapacitorPlugin
10
+ import com.getcapacitor.annotation.Permission
11
+ import com.getcapacitor.annotation.PermissionCallback
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.GlobalScope
14
+ import kotlinx.coroutines.launch
15
+
16
+
17
+ @CapacitorPlugin(
18
+ name = "BackgroundRunner",
19
+ permissions = [
20
+ Permission(
21
+ strings = [Manifest.permission.ACCESS_COARSE_LOCATION],
22
+ alias = BackgroundRunnerPlugin.GEOLOCATION
23
+ ),
24
+ Permission(
25
+ strings = [Manifest.permission.POST_NOTIFICATIONS],
26
+ alias = BackgroundRunnerPlugin.NOTIFICATIONS
27
+ )
28
+ ]
29
+ )
30
+ class BackgroundRunnerPlugin: Plugin() {
31
+ private var impl: BackgroundRunner? = null
32
+
33
+ companion object {
34
+ const val GEOLOCATION = "geolocation"
35
+ const val NOTIFICATIONS = "notifications"
36
+ }
37
+
38
+ override fun load() {
39
+ super.load()
40
+ impl = BackgroundRunner.getInstance(this.context)
41
+
42
+ bridge.app.setStatusChangeListener {
43
+ if (!it) {
44
+ Log.d("Background Runner", "registering runner workers")
45
+ impl?.scheduleBackgroundTask(this.context)
46
+ }
47
+ }
48
+ }
49
+
50
+ @PluginMethod
51
+ override fun checkPermissions(call: PluginCall) {
52
+ super.checkPermissions(call)
53
+ }
54
+
55
+ @PluginMethod
56
+ override fun requestPermissions(call: PluginCall) {
57
+ val apiToRequest = call.getArray("apis").toList<String>()
58
+ super.requestPermissionForAliases(apiToRequest.toTypedArray(), call, "completePermissionsCallback")
59
+ }
60
+
61
+ @PermissionCallback
62
+ fun completePermissionsCallback(call: PluginCall) {
63
+ super.checkPermissions(call)
64
+ }
65
+
66
+ @PluginMethod
67
+ fun dispatchEvent(call: PluginCall) {
68
+ try {
69
+ val impl = BackgroundRunner.getInstance(this.context)
70
+
71
+ val runnerEvent = call.getString("event") ?: throw Exception("event is missing or invalid")
72
+ val details = call.getObject("details")
73
+ val config = impl.config ?: throw Exception("no runner config loaded")
74
+ config.event = runnerEvent
75
+
76
+ GlobalScope.launch(Dispatchers.Default) {
77
+ try {
78
+ val returnData = impl.execute(
79
+ this@BackgroundRunnerPlugin.context,
80
+ config,
81
+ details
82
+ )
83
+
84
+ if (returnData != null) {
85
+ call.resolve(JSObject.fromJSONObject(returnData))
86
+ } else {
87
+ call.resolve()
88
+ }
89
+ } catch (ex: Exception) {
90
+ call.reject(ex.message)
91
+ }
92
+ }
93
+ } catch(ex: Exception) {
94
+ call.reject(ex.message)
95
+ }
96
+ }
97
+
98
+ @PluginMethod
99
+ fun registerBackgroundTask(call: PluginCall) {
100
+ call.resolve()
101
+ }
102
+ }