capacitor-mobilecron 0.1.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/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # capacitor-mobilecron
2
+
3
+ > Lightweight Capacitor scheduling primitive — register jobs, get `jobDue` events when they fire, across web, Android, and iOS.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/capacitor-mobilecron)](https://www.npmjs.com/package/capacitor-mobilecron)
6
+ [![CI](https://github.com/rogelioRuiz/capacitor-mobilecron/actions/workflows/ci.yml/badge.svg)](https://github.com/rogelioRuiz/capacitor-mobilecron/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ ## Overview
10
+
11
+ `capacitor-mobilecron` manages a set of named, persistent cron-like jobs and emits events when they are due. It handles:
12
+
13
+ - **Repeating intervals** (`every: N ms`) with optional anchor alignment
14
+ - **One-shot schedules** (`at: epoch ms`) that auto-disable after firing
15
+ - **Active-hour windows** — restrict jobs to HH:MM–HH:MM ranges with timezone support
16
+ - **Network / charging constraints** — skip a job when connectivity or power is absent
17
+ - **Scheduling modes** — `eco` (60s watchdog), `balanced` (30s), `aggressive` (15s)
18
+ - **App foreground wakeup** — immediately checks overdue jobs when the app comes to the foreground
19
+ - **Persistent state** — job registry survives app restarts via `@capacitor/preferences` (falls back to `localStorage` on web)
20
+
21
+ The web implementation is fully functional and self-contained. Android and iOS stubs satisfy the Capacitor plugin contract and can be extended with native WorkManager / BGTaskScheduler wakeups.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install capacitor-mobilecron
27
+ npx cap sync
28
+ ```
29
+
30
+ **Peer dependencies** you need in your Capacitor app:
31
+
32
+ ```bash
33
+ npm install @capacitor/core @capacitor/preferences
34
+ # @capacitor/app is optional — enables foreground-wakeup check
35
+ npm install @capacitor/app
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```typescript
41
+ import { MobileCron } from 'capacitor-mobilecron'
42
+
43
+ // Listen for due events
44
+ await MobileCron.addListener('jobDue', ({ id, name, firedAt, source, data }) => {
45
+ console.log(`Job "${name}" fired at ${new Date(firedAt).toISOString()} via ${source}`)
46
+ })
47
+
48
+ // Register a repeating job — every 5 minutes
49
+ const { id } = await MobileCron.register({
50
+ name: 'sync-data',
51
+ schedule: { kind: 'every', everyMs: 5 * 60 * 1000 },
52
+ })
53
+
54
+ // Register a one-shot job — fires at a specific time
55
+ await MobileCron.register({
56
+ name: 'daily-reminder',
57
+ schedule: { kind: 'at', atMs: Date.now() + 24 * 60 * 60 * 1000 },
58
+ })
59
+ ```
60
+
61
+ ## API
62
+
63
+ ### `register(options)`
64
+
65
+ Register a new job. Returns the job `id`.
66
+
67
+ ```typescript
68
+ const { id } = await MobileCron.register({
69
+ name: 'my-job',
70
+ schedule: { kind: 'every', everyMs: 60_000 }, // every 60 s
71
+ activeHours: { start: '08:00', end: '22:00', tz: 'America/Chicago' },
72
+ requiresNetwork: true,
73
+ requiresCharging: false,
74
+ priority: 'normal',
75
+ data: { userId: '42' }, // passed back in jobDue event
76
+ })
77
+ ```
78
+
79
+ | Option | Type | Description |
80
+ |--------|------|-------------|
81
+ | `name` | `string` | Human-readable label |
82
+ | `schedule` | `CronSchedule` | When to fire (see below) |
83
+ | `activeHours` | `ActiveHours?` | Restrict firing to a time window |
84
+ | `requiresNetwork` | `boolean?` | Skip when offline |
85
+ | `requiresCharging` | `boolean?` | Skip when not charging |
86
+ | `priority` | `'low' \| 'normal' \| 'high'?` | Scheduling hint |
87
+ | `data` | `Record<string, unknown>?` | Arbitrary payload returned in events |
88
+
89
+ ### Schedules
90
+
91
+ ```typescript
92
+ // Repeat every N ms (minimum 60 000 ms on native)
93
+ { kind: 'every', everyMs: 300_000 }
94
+
95
+ // Repeat every N ms, aligned to an anchor timestamp
96
+ { kind: 'every', everyMs: 3600_000, anchorMs: Date.now() }
97
+
98
+ // Fire once at an absolute epoch timestamp
99
+ { kind: 'at', atMs: Date.parse('2025-01-01T09:00:00Z') }
100
+ ```
101
+
102
+ ### `unregister({ id })`
103
+
104
+ Remove a job and stop it from firing.
105
+
106
+ ```typescript
107
+ await MobileCron.unregister({ id })
108
+ ```
109
+
110
+ ### `update({ id, ...partial })`
111
+
112
+ Patch an existing job without losing its state.
113
+
114
+ ```typescript
115
+ await MobileCron.update({
116
+ id,
117
+ schedule: { kind: 'every', everyMs: 10 * 60 * 1000 },
118
+ activeHours: { start: '09:00', end: '17:00' },
119
+ })
120
+ ```
121
+
122
+ ### `list()`
123
+
124
+ Returns all registered jobs sorted by next due time.
125
+
126
+ ```typescript
127
+ const { jobs } = await MobileCron.list()
128
+ for (const job of jobs) {
129
+ console.log(job.name, 'next due at', new Date(job.nextDueAt ?? 0).toISOString())
130
+ }
131
+ ```
132
+
133
+ ### `triggerNow({ id })`
134
+
135
+ Force a job to fire immediately (source = `'manual'`).
136
+
137
+ ```typescript
138
+ await MobileCron.triggerNow({ id })
139
+ ```
140
+
141
+ ### `pauseAll()` / `resumeAll()`
142
+
143
+ Suspend / resume all job checks globally.
144
+
145
+ ```typescript
146
+ await MobileCron.pauseAll()
147
+ await MobileCron.resumeAll()
148
+ ```
149
+
150
+ ### `setMode({ mode })`
151
+
152
+ Control how frequently the watchdog timer checks for due jobs.
153
+
154
+ | Mode | Interval | Use case |
155
+ |------|----------|---------|
156
+ | `'eco'` | 60 s | Battery-sensitive background |
157
+ | `'balanced'` | 30 s | Default |
158
+ | `'aggressive'` | 15 s | Real-time UX needs |
159
+
160
+ ```typescript
161
+ await MobileCron.setMode({ mode: 'aggressive' })
162
+ ```
163
+
164
+ ### `getStatus()`
165
+
166
+ Returns scheduler diagnostics.
167
+
168
+ ```typescript
169
+ const status = await MobileCron.getStatus()
170
+ // {
171
+ // paused: false,
172
+ // mode: 'balanced',
173
+ // platform: 'android',
174
+ // activeJobCount: 3,
175
+ // nextDueAt: 1719000000000,
176
+ // android: { workManagerActive: true, chargingReceiverActive: false }
177
+ // }
178
+ ```
179
+
180
+ ### Events
181
+
182
+ | Event | Payload | Description |
183
+ |-------|---------|-------------|
184
+ | `jobDue` | `JobDueEvent` | A job fired |
185
+ | `jobSkipped` | `JobSkippedEvent` | A due job was skipped (constraint not met) |
186
+ | `overdueJobs` | `OverdueEvent` | Emitted on foreground resume if jobs are overdue |
187
+ | `statusChanged` | `CronStatus` | Scheduler state changed |
188
+
189
+ ```typescript
190
+ MobileCron.addListener('jobSkipped', ({ id, name, reason }) => {
191
+ // reason: 'outside_active_hours' | 'paused' | 'requires_network' | 'requires_charging'
192
+ console.warn(`Job ${name} skipped: ${reason}`)
193
+ })
194
+
195
+ MobileCron.addListener('overdueJobs', ({ count, jobs }) => {
196
+ console.warn(`${count} jobs were overdue on resume`)
197
+ })
198
+ ```
199
+
200
+ ## Advanced: `MobileCronScheduler`
201
+
202
+ The package also exports the plain TypeScript scheduler class that powers the web plugin. Use it directly in Node.js, React Native, or any non-Capacitor environment:
203
+
204
+ ```typescript
205
+ import { MobileCronScheduler } from 'capacitor-mobilecron'
206
+
207
+ const scheduler = new MobileCronScheduler({
208
+ platform: 'web',
209
+ onJobDue: (event) => handleJobDue(event),
210
+ onJobSkipped: (event) => console.warn('skipped', event),
211
+ })
212
+
213
+ await scheduler.init()
214
+
215
+ const { id } = await scheduler.register({
216
+ name: 'heartbeat',
217
+ schedule: { kind: 'every', everyMs: 30_000 },
218
+ })
219
+
220
+ // Later — check due jobs from a native wakeup callback
221
+ scheduler.checkDueJobs('workmanager')
222
+
223
+ // Teardown
224
+ await scheduler.destroy()
225
+ ```
226
+
227
+ ## iOS / Android native wakeups
228
+
229
+ The web/JS watchdog is the primary scheduling mechanism. For true background execution extend the native stubs:
230
+
231
+ ### Android
232
+
233
+ Wire `CronWorker` into `WorkManager` periodic tasks and call `bridge.checkDueJobs("workmanager")`. Register `ChargingReceiver` in the manifest for charging wakeups.
234
+
235
+ ### iOS
236
+
237
+ Register a `BGAppRefreshTask` / `BGProcessingTask` in your `AppDelegate` and call the plugin method to check due jobs from there.
238
+
239
+ ## Testing
240
+
241
+ ### Unit tests — no device needed
242
+
243
+ The scheduler core is pure TypeScript and fully testable without a phone:
244
+
245
+ ```bash
246
+ npm test # run 52 unit tests (instant)
247
+ npm run test:watch # TDD watch mode
248
+ npm run test:coverage # coverage report
249
+ ```
250
+
251
+ Tests cover schedule computation, active-hour windows, persistence, pause/resume, skip logic, and more.
252
+
253
+ ### Device E2E tests (Android, via CDP)
254
+
255
+ Full integration suite against a running Android app — 7 sections, 40+ tests:
256
+
257
+ ```bash
258
+ # 1. Forward CDP port from device
259
+ adb forward tcp:9222 localabstract:webview_devtools_remote_$(adb shell pidof io.mobileclaw.reference)
260
+
261
+ # 2. Run
262
+ npm run test:e2e
263
+ ```
264
+
265
+ See [`tests/e2e/test-e2e.mjs`](tests/e2e/test-e2e.mjs) for coverage details.
266
+
267
+ ## Contributing
268
+
269
+ ```bash
270
+ git clone https://github.com/rogelioRuiz/capacitor-mobilecron.git
271
+ cd capacitor-mobilecron
272
+ npm install
273
+ npm test # unit tests — runs in seconds, no device
274
+ npm run build # compile TypeScript
275
+ npm run lint # Biome linter
276
+ npm run typecheck # TypeScript strict mode
277
+ ```
278
+
279
+ ## License
280
+
281
+ MIT © Rogelio Ruiz
@@ -0,0 +1,45 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
4
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
5
+ }
6
+
7
+ apply plugin: 'com.android.library'
8
+ apply plugin: 'kotlin-android'
9
+
10
+ android {
11
+ namespace "io.mobilecron"
12
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
13
+
14
+ defaultConfig {
15
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
16
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
17
+ versionCode 1
18
+ versionName "0.1.0"
19
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20
+ consumerProguardFiles 'proguard-rules.pro'
21
+ }
22
+
23
+ compileOptions {
24
+ sourceCompatibility JavaVersion.VERSION_17
25
+ targetCompatibility JavaVersion.VERSION_17
26
+ }
27
+
28
+ kotlinOptions {
29
+ jvmTarget = '17'
30
+ }
31
+ }
32
+
33
+ repositories {
34
+ google()
35
+ mavenCentral()
36
+ }
37
+
38
+ dependencies {
39
+ implementation project(':capacitor-android')
40
+ implementation "org.jetbrains.kotlin:kotlin-stdlib"
41
+ implementation "androidx.work:work-runtime-ktx:2.9.0"
42
+ testImplementation "junit:junit:$junitVersion"
43
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
44
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
45
+ }
@@ -0,0 +1,2 @@
1
+ # capacitor-mobilecron ProGuard rules
2
+ -keep class io.mobilecron.** { *; }
@@ -0,0 +1,5 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.mobilecron">
2
+ <application>
3
+ <!-- Receiver is dynamically registered in phase 1; manifest entry not required. -->
4
+ </application>
5
+ </manifest>
@@ -0,0 +1,13 @@
1
+ package io.mobilecron
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+
7
+ class ChargingReceiver : BroadcastReceiver() {
8
+ override fun onReceive(context: Context?, intent: Intent?) {
9
+ if (intent?.action == Intent.ACTION_POWER_CONNECTED) {
10
+ CronBridge.wake("charging")
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,20 @@
1
+ package io.mobilecron
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+
6
+ object CronBridge {
7
+ @Volatile
8
+ var plugin: MobileCronPlugin? = null
9
+
10
+ fun wake(source: String) {
11
+ val current = plugin ?: return
12
+ if (Looper.myLooper() == Looper.getMainLooper()) {
13
+ current.notifyFromBackground(source)
14
+ } else {
15
+ Handler(Looper.getMainLooper()).post {
16
+ plugin?.notifyFromBackground(source)
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ package io.mobilecron
2
+
3
+ import android.content.Context
4
+ import androidx.work.CoroutineWorker
5
+ import androidx.work.ExistingWorkPolicy
6
+ import androidx.work.OneTimeWorkRequestBuilder
7
+ import androidx.work.WorkManager
8
+ import androidx.work.WorkerParameters
9
+ import java.util.concurrent.TimeUnit
10
+
11
+ class CronChainWorker(
12
+ appContext: Context,
13
+ params: WorkerParameters
14
+ ) : CoroutineWorker(appContext, params) {
15
+ override suspend fun doWork(): Result {
16
+ CronBridge.wake("workmanager_chain")
17
+ enqueueNext(applicationContext, 5)
18
+ return Result.success()
19
+ }
20
+
21
+ companion object {
22
+ private const val UNIQUE_NAME = "mobilecron_chain"
23
+
24
+ fun enqueueNext(context: Context, delayMinutes: Long) {
25
+ val request = OneTimeWorkRequestBuilder<CronChainWorker>()
26
+ .setInitialDelay(delayMinutes, TimeUnit.MINUTES)
27
+ .build()
28
+
29
+ WorkManager.getInstance(context).enqueueUniqueWork(
30
+ UNIQUE_NAME,
31
+ ExistingWorkPolicy.REPLACE,
32
+ request
33
+ )
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,15 @@
1
+ package io.mobilecron
2
+
3
+ import android.content.Context
4
+ import androidx.work.CoroutineWorker
5
+ import androidx.work.WorkerParameters
6
+
7
+ class CronWorker(
8
+ appContext: Context,
9
+ params: WorkerParameters
10
+ ) : CoroutineWorker(appContext, params) {
11
+ override suspend fun doWork(): Result {
12
+ CronBridge.wake("workmanager")
13
+ return Result.success()
14
+ }
15
+ }
@@ -0,0 +1,234 @@
1
+ package io.mobilecron
2
+
3
+ import android.content.Intent
4
+ import android.content.IntentFilter
5
+ import androidx.work.Constraints
6
+ import androidx.work.ExistingPeriodicWorkPolicy
7
+ import androidx.work.ExistingWorkPolicy
8
+ import androidx.work.NetworkType
9
+ import androidx.work.OneTimeWorkRequestBuilder
10
+ import androidx.work.PeriodicWorkRequestBuilder
11
+ import androidx.work.WorkManager
12
+ import com.getcapacitor.JSArray
13
+ import com.getcapacitor.JSObject
14
+ import com.getcapacitor.Plugin
15
+ import com.getcapacitor.PluginCall
16
+ import com.getcapacitor.PluginMethod
17
+ import com.getcapacitor.annotation.CapacitorPlugin
18
+ import java.util.UUID
19
+ import java.util.concurrent.ConcurrentHashMap
20
+ import java.util.concurrent.TimeUnit
21
+
22
+ @CapacitorPlugin(name = "MobileCron")
23
+ class MobileCronPlugin : Plugin() {
24
+ private val jobs = ConcurrentHashMap<String, JSObject>()
25
+ private var paused = false
26
+ private var mode = "balanced"
27
+ private var chargingReceiver: ChargingReceiver? = null
28
+
29
+ override fun load() {
30
+ super.load()
31
+ CronBridge.plugin = this
32
+ registerChargingReceiver()
33
+ scheduleWorkManager()
34
+ }
35
+
36
+ override fun handleOnDestroy() {
37
+ super.handleOnDestroy()
38
+ if (CronBridge.plugin === this) {
39
+ CronBridge.plugin = null
40
+ }
41
+ chargingReceiver?.let {
42
+ runCatching { context.unregisterReceiver(it) }
43
+ }
44
+ chargingReceiver = null
45
+ }
46
+
47
+ internal fun notifyFromBackground(source: String) {
48
+ // Native phase-1 skeleton: background wakes are surfaced, but due-job evaluation is implemented in TS web layer.
49
+ val payload = JSObject()
50
+ payload.put("source", source)
51
+ payload.put("paused", paused)
52
+ notifyListeners("statusChanged", buildStatus())
53
+ notifyListeners("nativeWake", payload)
54
+ }
55
+
56
+ private fun scheduleWorkManager() {
57
+ val wm = WorkManager.getInstance(context)
58
+ if (mode == "aggressive") {
59
+ val request = OneTimeWorkRequestBuilder<CronChainWorker>()
60
+ .setInitialDelay(5, TimeUnit.MINUTES)
61
+ .build()
62
+ wm.enqueueUniqueWork("mobilecron_chain", ExistingWorkPolicy.REPLACE, request)
63
+ return
64
+ }
65
+
66
+ val constraintsBuilder = Constraints.Builder()
67
+ if (mode == "eco") {
68
+ constraintsBuilder.setRequiredNetworkType(NetworkType.UNMETERED)
69
+ constraintsBuilder.setRequiresBatteryNotLow(true)
70
+ } else {
71
+ constraintsBuilder.setRequiredNetworkType(NetworkType.CONNECTED)
72
+ }
73
+
74
+ val request = PeriodicWorkRequestBuilder<CronWorker>(15, TimeUnit.MINUTES)
75
+ .setConstraints(constraintsBuilder.build())
76
+ .build()
77
+
78
+ wm.enqueueUniquePeriodicWork(
79
+ "mobilecron_periodic",
80
+ ExistingPeriodicWorkPolicy.UPDATE,
81
+ request
82
+ )
83
+ }
84
+
85
+ private fun registerChargingReceiver() {
86
+ if (chargingReceiver != null) return
87
+ val receiver = ChargingReceiver()
88
+ context.registerReceiver(receiver, IntentFilter(Intent.ACTION_POWER_CONNECTED))
89
+ chargingReceiver = receiver
90
+ }
91
+
92
+ @PluginMethod
93
+ fun register(call: PluginCall) {
94
+ val name = call.getString("name")?.trim()
95
+ if (name.isNullOrEmpty()) {
96
+ call.reject("Job name is required")
97
+ return
98
+ }
99
+
100
+ val id = UUID.randomUUID().toString()
101
+ val record = JSObject()
102
+ record.put("id", id)
103
+ record.put("name", name)
104
+ record.put("enabled", true)
105
+ record.put("schedule", call.getObject("schedule") ?: JSObject())
106
+ record.put("activeHours", call.getObject("activeHours"))
107
+ record.put("requiresNetwork", call.getBoolean("requiresNetwork", false))
108
+ record.put("requiresCharging", call.getBoolean("requiresCharging", false))
109
+ record.put("priority", call.getString("priority", "normal"))
110
+ call.getObject("data")?.let { record.put("data", it) }
111
+ record.put("consecutiveSkips", 0)
112
+ jobs[id] = record
113
+
114
+ val result = JSObject()
115
+ result.put("id", id)
116
+ call.resolve(result)
117
+ notifyListeners("statusChanged", buildStatus())
118
+ }
119
+
120
+ @PluginMethod
121
+ fun unregister(call: PluginCall) {
122
+ val id = call.getString("id")
123
+ if (id == null) {
124
+ call.reject("id is required")
125
+ return
126
+ }
127
+ jobs.remove(id)
128
+ call.resolve()
129
+ notifyListeners("statusChanged", buildStatus())
130
+ }
131
+
132
+ @PluginMethod
133
+ fun update(call: PluginCall) {
134
+ val id = call.getString("id")
135
+ if (id == null) {
136
+ call.reject("id is required")
137
+ return
138
+ }
139
+ val existing = jobs[id]
140
+ if (existing == null) {
141
+ call.reject("Job not found")
142
+ return
143
+ }
144
+
145
+ call.getString("name")?.let { existing.put("name", it) }
146
+ call.getObject("schedule")?.let { existing.put("schedule", it) }
147
+ if (call.data.has("activeHours")) existing.put("activeHours", call.getObject("activeHours"))
148
+ if (call.data.has("requiresNetwork")) existing.put("requiresNetwork", call.getBoolean("requiresNetwork", false))
149
+ if (call.data.has("requiresCharging")) existing.put("requiresCharging", call.getBoolean("requiresCharging", false))
150
+ call.getString("priority")?.let { existing.put("priority", it) }
151
+ if (call.data.has("data")) existing.put("data", call.getObject("data"))
152
+
153
+ jobs[id] = existing
154
+ call.resolve()
155
+ notifyListeners("statusChanged", buildStatus())
156
+ }
157
+
158
+ @PluginMethod
159
+ fun list(call: PluginCall) {
160
+ val arr = JSArray()
161
+ jobs.values.forEach { arr.put(it) }
162
+ val result = JSObject()
163
+ result.put("jobs", arr)
164
+ call.resolve(result)
165
+ }
166
+
167
+ @PluginMethod
168
+ fun triggerNow(call: PluginCall) {
169
+ val id = call.getString("id")
170
+ if (id == null) {
171
+ call.reject("id is required")
172
+ return
173
+ }
174
+ val job = jobs[id]
175
+ if (job == null) {
176
+ call.reject("Job not found")
177
+ return
178
+ }
179
+
180
+ val payload = JSObject()
181
+ payload.put("id", id)
182
+ payload.put("name", job.getString("name"))
183
+ payload.put("firedAt", System.currentTimeMillis())
184
+ payload.put("source", "manual")
185
+ if (job.has("data")) payload.put("data", job.getJSONObject("data"))
186
+ notifyListeners("jobDue", payload)
187
+ call.resolve()
188
+ }
189
+
190
+ @PluginMethod
191
+ fun pauseAll(call: PluginCall) {
192
+ paused = true
193
+ call.resolve()
194
+ notifyListeners("statusChanged", buildStatus())
195
+ }
196
+
197
+ @PluginMethod
198
+ fun resumeAll(call: PluginCall) {
199
+ paused = false
200
+ call.resolve()
201
+ notifyListeners("statusChanged", buildStatus())
202
+ }
203
+
204
+ @PluginMethod
205
+ fun setMode(call: PluginCall) {
206
+ val next = call.getString("mode")
207
+ if (next !in listOf("eco", "balanced", "aggressive")) {
208
+ call.reject("mode must be eco|balanced|aggressive")
209
+ return
210
+ }
211
+ mode = next!!
212
+ scheduleWorkManager()
213
+ call.resolve()
214
+ notifyListeners("statusChanged", buildStatus())
215
+ }
216
+
217
+ @PluginMethod
218
+ fun getStatus(call: PluginCall) {
219
+ call.resolve(buildStatus())
220
+ }
221
+
222
+ private fun buildStatus(): JSObject {
223
+ val status = JSObject()
224
+ status.put("paused", paused)
225
+ status.put("mode", mode)
226
+ status.put("platform", "android")
227
+ status.put("activeJobCount", jobs.size)
228
+ status.put("android", JSObject().apply {
229
+ put("workManagerActive", true)
230
+ put("chargingReceiverActive", chargingReceiver != null)
231
+ })
232
+ return status
233
+ }
234
+ }
package/dist/.gitkeep ADDED
File without changes