react-native-step-tracker-pro 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/LICENSE +21 -0
- package/README.md +148 -0
- package/android/build.gradle +119 -0
- package/android/consumer-rules.pro +13 -0
- package/android/gradle.properties +3 -0
- package/android/src/main/AndroidManifest.xml +78 -0
- package/android/src/main/java/com/steptrackerpro/StepTrackerProModule.kt +598 -0
- package/android/src/main/java/com/steptrackerpro/StepTrackerProPackage.kt +26 -0
- package/android/src/main/java/com/steptrackerpro/core/DateKeys.kt +90 -0
- package/android/src/main/java/com/steptrackerpro/core/GoalTracker.kt +66 -0
- package/android/src/main/java/com/steptrackerpro/core/MetricsCalculator.kt +48 -0
- package/android/src/main/java/com/steptrackerpro/core/Models.kt +73 -0
- package/android/src/main/java/com/steptrackerpro/core/StepCounterEngine.kt +338 -0
- package/android/src/main/java/com/steptrackerpro/core/StepStateStore.kt +137 -0
- package/android/src/main/java/com/steptrackerpro/core/StepTrackerConfig.kt +184 -0
- package/android/src/main/java/com/steptrackerpro/core/StepTrackerCore.kt +280 -0
- package/android/src/main/java/com/steptrackerpro/db/Entities.kt +67 -0
- package/android/src/main/java/com/steptrackerpro/db/StepDao.kt +118 -0
- package/android/src/main/java/com/steptrackerpro/db/StepDatabase.kt +60 -0
- package/android/src/main/java/com/steptrackerpro/db/StepRepository.kt +146 -0
- package/android/src/main/java/com/steptrackerpro/health/HealthConnectManager.kt +198 -0
- package/android/src/main/java/com/steptrackerpro/health/HealthPermissionActivity.kt +99 -0
- package/android/src/main/java/com/steptrackerpro/service/BootReceiver.kt +54 -0
- package/android/src/main/java/com/steptrackerpro/service/NotificationActionReceiver.kt +17 -0
- package/android/src/main/java/com/steptrackerpro/service/NotificationFactory.kt +170 -0
- package/android/src/main/java/com/steptrackerpro/service/ServiceCommands.kt +42 -0
- package/android/src/main/java/com/steptrackerpro/service/StepTrackerService.kt +409 -0
- package/android/src/main/java/com/steptrackerpro/sync/SyncScheduler.kt +102 -0
- package/android/src/main/java/com/steptrackerpro/sync/SyncWorkers.kt +175 -0
- package/android/src/main/java/com/steptrackerpro/util/BatteryOptimizationHelper.kt +94 -0
- package/android/src/main/java/com/steptrackerpro/util/Bridge.kt +149 -0
- package/android/src/main/java/com/steptrackerpro/util/PermissionHelper.kt +92 -0
- package/android/src/main/java/com/steptrackerpro/util/StepEventBus.kt +53 -0
- package/android/src/main/res/drawable/stp_ic_steps.xml +9 -0
- package/android/src/main/res/values/strings.xml +9 -0
- package/android/src/main/res/values/styles.xml +8 -0
- package/android/src/newarch/java/com/steptrackerpro/StepTrackerProSpec.kt +10 -0
- package/android/src/oldarch/java/com/steptrackerpro/StepTrackerProSpec.kt +60 -0
- package/docs/API.md +329 -0
- package/docs/ARCHITECTURE.md +203 -0
- package/docs/INSTALLATION.md +175 -0
- package/docs/PLAY_STORE_COMPLIANCE.md +122 -0
- package/docs/TESTING.md +312 -0
- package/docs/TROUBLESHOOTING.md +96 -0
- package/lib/commonjs/NativeStepTrackerPro.js +14 -0
- package/lib/commonjs/NativeStepTrackerPro.js.map +1 -0
- package/lib/commonjs/StepTracker.js +290 -0
- package/lib/commonjs/StepTracker.js.map +1 -0
- package/lib/commonjs/constants.js +30 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/errors.js +24 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/hooks/index.js +20 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/useStepStats.js +51 -0
- package/lib/commonjs/hooks/useStepStats.js.map +1 -0
- package/lib/commonjs/hooks/useStepTracker.js +134 -0
- package/lib/commonjs/hooks/useStepTracker.js.map +1 -0
- package/lib/commonjs/index.js +64 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeStepTrackerPro.js +12 -0
- package/lib/module/NativeStepTrackerPro.js.map +1 -0
- package/lib/module/StepTracker.js +284 -0
- package/lib/module/StepTracker.js.map +1 -0
- package/lib/module/constants.js +26 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +18 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/hooks/index.js +5 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useStepStats.js +46 -0
- package/lib/module/hooks/useStepStats.js.map +1 -0
- package/lib/module/hooks/useStepTracker.js +129 -0
- package/lib/module/hooks/useStepTracker.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/NativeStepTrackerPro.d.ts +50 -0
- package/lib/typescript/NativeStepTrackerPro.d.ts.map +1 -0
- package/lib/typescript/StepTracker.d.ts +76 -0
- package/lib/typescript/StepTracker.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +10 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/errors.d.ts +8 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/hooks/index.d.ts +5 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -0
- package/lib/typescript/hooks/useStepStats.d.ts +11 -0
- package/lib/typescript/hooks/useStepStats.d.ts.map +1 -0
- package/lib/typescript/hooks/useStepTracker.d.ts +26 -0
- package/lib/typescript/hooks/useStepTracker.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +183 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/NativeStepTrackerPro.ts +66 -0
- package/src/StepTracker.ts +410 -0
- package/src/constants.ts +43 -0
- package/src/errors.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useStepStats.ts +55 -0
- package/src/hooks/useStepTracker.ts +167 -0
- package/src/index.ts +27 -0
- package/src/types.ts +203 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
package com.steptrackerpro
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactMethod
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap
|
|
10
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
12
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
13
|
+
import com.steptrackerpro.core.DateKeys
|
|
14
|
+
import com.steptrackerpro.core.StepTrackerConfig
|
|
15
|
+
import com.steptrackerpro.core.StepTrackerCore
|
|
16
|
+
import com.steptrackerpro.core.TrackingState
|
|
17
|
+
import com.steptrackerpro.health.HealthConnectManager
|
|
18
|
+
import com.steptrackerpro.health.HealthPermissionActivity
|
|
19
|
+
import com.steptrackerpro.service.ServiceCommands
|
|
20
|
+
import com.steptrackerpro.sync.SyncScheduler
|
|
21
|
+
import com.steptrackerpro.util.Bridge
|
|
22
|
+
import com.steptrackerpro.util.optBoolean
|
|
23
|
+
import com.steptrackerpro.util.optDouble
|
|
24
|
+
import com.steptrackerpro.util.optInt
|
|
25
|
+
import com.steptrackerpro.util.optLong
|
|
26
|
+
import com.steptrackerpro.util.optString
|
|
27
|
+
import com.steptrackerpro.util.BatteryOptimizationHelper
|
|
28
|
+
import com.steptrackerpro.util.PermissionHelper
|
|
29
|
+
import com.steptrackerpro.util.StepEventBus
|
|
30
|
+
import kotlinx.coroutines.CoroutineScope
|
|
31
|
+
import kotlinx.coroutines.Dispatchers
|
|
32
|
+
import kotlinx.coroutines.SupervisorJob
|
|
33
|
+
import kotlinx.coroutines.cancel
|
|
34
|
+
import kotlinx.coroutines.launch
|
|
35
|
+
import java.time.Instant
|
|
36
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The JS-facing surface. Deliberately thin: it validates arguments, delegates
|
|
40
|
+
* to [StepTrackerCore] or the service, and converts results for the bridge.
|
|
41
|
+
* Nothing here is required for counting to continue.
|
|
42
|
+
*/
|
|
43
|
+
class StepTrackerProModule(private val reactContext: ReactApplicationContext) :
|
|
44
|
+
StepTrackerProSpec(reactContext), LifecycleEventListener {
|
|
45
|
+
|
|
46
|
+
private val core = StepTrackerCore.get(reactContext)
|
|
47
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
48
|
+
private var listenerCount = 0
|
|
49
|
+
|
|
50
|
+
private val subscriber = StepEventBus.Subscriber { name, payload -> emit(name, payload) }
|
|
51
|
+
|
|
52
|
+
init {
|
|
53
|
+
StepEventBus.subscribe(subscriber)
|
|
54
|
+
reactContext.addLifecycleEventListener(this)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun getName(): String = NAME
|
|
58
|
+
|
|
59
|
+
override fun invalidate() {
|
|
60
|
+
StepEventBus.unsubscribe(subscriber)
|
|
61
|
+
reactContext.removeLifecycleEventListener(this)
|
|
62
|
+
scope.cancel()
|
|
63
|
+
super.invalidate()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onHostResume() = Unit
|
|
67
|
+
override fun onHostPause() = Unit
|
|
68
|
+
override fun onHostDestroy() = Unit
|
|
69
|
+
|
|
70
|
+
// ---- events ----------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
@ReactMethod
|
|
73
|
+
override fun addListener(eventName: String) {
|
|
74
|
+
listenerCount++
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@ReactMethod
|
|
78
|
+
override fun removeListeners(count: Double) {
|
|
79
|
+
listenerCount = (listenerCount - count.toInt()).coerceAtLeast(0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun emit(name: String, payload: Map<String, Any?>) {
|
|
83
|
+
if (!reactContext.hasActiveReactInstance()) return
|
|
84
|
+
runCatching {
|
|
85
|
+
reactContext
|
|
86
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
87
|
+
.emit("$EVENT_PREFIX$name", Bridge.map(payload))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- lifecycle -------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
@ReactMethod
|
|
94
|
+
override fun initialize(config: ReadableMap, promise: Promise) {
|
|
95
|
+
runSafely(promise) {
|
|
96
|
+
val merged = mergeConfig(core.config(), config)
|
|
97
|
+
core.updateConfig(merged)
|
|
98
|
+
core.engine.reconcile()
|
|
99
|
+
if (core.state.trackingState == TrackingState.RUNNING) {
|
|
100
|
+
// The service may have been killed while JS was gone. This has
|
|
101
|
+
// to be START: every other command starts the service without
|
|
102
|
+
// registering a sensor listener, which would leave the tracker
|
|
103
|
+
// reporting itself as running while counting nothing.
|
|
104
|
+
ServiceCommands.start(reactContext)
|
|
105
|
+
}
|
|
106
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@ReactMethod
|
|
111
|
+
override fun updateConfig(config: ReadableMap, promise: Promise) {
|
|
112
|
+
runSafely(promise) {
|
|
113
|
+
val merged = mergeConfig(core.config(), config)
|
|
114
|
+
core.updateConfig(merged)
|
|
115
|
+
sendIfTracking(ServiceCommands.ACTION_CONFIG_CHANGED)
|
|
116
|
+
promise.resolve(Bridge.map(configToMap(merged)))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@ReactMethod
|
|
121
|
+
override fun getConfig(promise: Promise) {
|
|
122
|
+
runSafely(promise) { promise.resolve(Bridge.map(configToMap(core.config()))) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@ReactMethod
|
|
126
|
+
override fun startTracking(promise: Promise) {
|
|
127
|
+
runSafely(promise) {
|
|
128
|
+
if (!PermissionHelper.canStartTracking(reactContext)) {
|
|
129
|
+
promise.reject(
|
|
130
|
+
"E_PERMISSION_DENIED",
|
|
131
|
+
"ACTIVITY_RECOGNITION must be granted before tracking can start"
|
|
132
|
+
)
|
|
133
|
+
return@runSafely
|
|
134
|
+
}
|
|
135
|
+
val capabilities = PermissionHelper.capabilities(reactContext)
|
|
136
|
+
if (capabilities["supported"] != true) {
|
|
137
|
+
promise.reject("E_NO_SENSOR", "No step sensor on this device")
|
|
138
|
+
return@runSafely
|
|
139
|
+
}
|
|
140
|
+
ServiceCommands.start(reactContext)
|
|
141
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@ReactMethod
|
|
146
|
+
override fun pauseTracking(promise: Promise) {
|
|
147
|
+
runSafely(promise) {
|
|
148
|
+
if (!core.state.shouldAutoStart) {
|
|
149
|
+
promise.reject("E_NOT_TRACKING", "Tracking is not running")
|
|
150
|
+
return@runSafely
|
|
151
|
+
}
|
|
152
|
+
ServiceCommands.send(reactContext, ServiceCommands.ACTION_PAUSE)
|
|
153
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ReactMethod
|
|
158
|
+
override fun resumeTracking(promise: Promise) {
|
|
159
|
+
runSafely(promise) {
|
|
160
|
+
if (!core.state.shouldAutoStart) {
|
|
161
|
+
promise.reject("E_NOT_TRACKING", "Tracking is not running")
|
|
162
|
+
return@runSafely
|
|
163
|
+
}
|
|
164
|
+
ServiceCommands.send(reactContext, ServiceCommands.ACTION_RESUME)
|
|
165
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@ReactMethod
|
|
170
|
+
override fun stopTracking(promise: Promise) {
|
|
171
|
+
runSafely(promise) {
|
|
172
|
+
ServiceCommands.send(reactContext, ServiceCommands.ACTION_STOP)
|
|
173
|
+
SyncScheduler.cancelAll(reactContext)
|
|
174
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@ReactMethod
|
|
179
|
+
override fun getTrackingState(promise: Promise) {
|
|
180
|
+
runSafely(promise) {
|
|
181
|
+
val snapshot = core.engine.snapshot()
|
|
182
|
+
promise.resolve(
|
|
183
|
+
Bridge.map(
|
|
184
|
+
mapOf(
|
|
185
|
+
"state" to snapshot.state.jsValue,
|
|
186
|
+
"source" to snapshot.source.jsValue
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@ReactMethod
|
|
194
|
+
override fun isTracking(promise: Promise) {
|
|
195
|
+
runSafely(promise) {
|
|
196
|
+
promise.resolve(core.state.trackingState == TrackingState.RUNNING)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- reads -----------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
@ReactMethod
|
|
203
|
+
override fun getTodaySteps(promise: Promise) {
|
|
204
|
+
runSafely(promise) {
|
|
205
|
+
core.engine.reconcile()
|
|
206
|
+
promise.resolve(Bridge.snapshot(core.engine.snapshot()))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@ReactMethod
|
|
211
|
+
override fun getStepsForDate(date: String, promise: Promise) {
|
|
212
|
+
launchSafely(promise) { promise.resolve(Bridge.day(core.dayTotals(date))) }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@ReactMethod
|
|
216
|
+
override fun getYesterdaySteps(promise: Promise) {
|
|
217
|
+
launchSafely(promise) {
|
|
218
|
+
promise.resolve(Bridge.day(core.repository.getDay(DateKeys.yesterday())))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@ReactMethod
|
|
223
|
+
override fun getStatsForRange(startDate: String, endDate: String, promise: Promise) {
|
|
224
|
+
launchSafely(promise) {
|
|
225
|
+
promise.resolve(Bridge.stats(core.stats(startDate, endDate, null)))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@ReactMethod
|
|
230
|
+
override fun getWeeklyStats(options: ReadableMap, promise: Promise) {
|
|
231
|
+
launchSafely(promise) {
|
|
232
|
+
val (start, end) = window(options, 7) { offset -> DateKeys.calendarWeek(offset) }
|
|
233
|
+
promise.resolve(Bridge.stats(core.stats(start, end, core.config().weeklyGoal)))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@ReactMethod
|
|
238
|
+
override fun getMonthlyStats(options: ReadableMap, promise: Promise) {
|
|
239
|
+
launchSafely(promise) {
|
|
240
|
+
val (start, end) = window(options, 30) { offset -> DateKeys.calendarMonth(offset) }
|
|
241
|
+
promise.resolve(Bridge.stats(core.stats(start, end, core.config().monthlyGoal)))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@ReactMethod
|
|
246
|
+
override fun getYearlyStats(options: ReadableMap, promise: Promise) {
|
|
247
|
+
launchSafely(promise) {
|
|
248
|
+
val (start, end) = window(options, 365) { offset -> DateKeys.calendarYear(offset) }
|
|
249
|
+
promise.resolve(Bridge.stats(core.stats(start, end, null)))
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@ReactMethod
|
|
254
|
+
override fun getHistory(startDate: String, endDate: String, promise: Promise) {
|
|
255
|
+
launchSafely(promise) {
|
|
256
|
+
val days = core.repository.getRange(startDate, endDate)
|
|
257
|
+
promise.resolve(Bridge.map(mapOf("records" to days.map { day ->
|
|
258
|
+
mapOf(
|
|
259
|
+
"date" to day.date,
|
|
260
|
+
"steps" to day.steps,
|
|
261
|
+
"distance" to day.distance,
|
|
262
|
+
"calories" to day.calories,
|
|
263
|
+
"synced" to day.synced,
|
|
264
|
+
"syncedRemote" to day.syncedRemote
|
|
265
|
+
)
|
|
266
|
+
})))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---- writes ----------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
@ReactMethod
|
|
273
|
+
override fun resetToday(promise: Promise) {
|
|
274
|
+
launchSafely(promise) {
|
|
275
|
+
core.engine.resetToday()
|
|
276
|
+
core.goals.reset()
|
|
277
|
+
// Deliberate write: saveDay() refuses to lower a day, which would
|
|
278
|
+
// leave history and the live counter permanently disagreeing.
|
|
279
|
+
core.repository.overwriteDay(core.liveToday())
|
|
280
|
+
sendIfTracking(ServiceCommands.ACTION_REFRESH)
|
|
281
|
+
promise.resolve(true)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@ReactMethod
|
|
286
|
+
override fun clearHistory(promise: Promise) {
|
|
287
|
+
launchSafely(promise) {
|
|
288
|
+
core.repository.clear()
|
|
289
|
+
promise.resolve(true)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@ReactMethod
|
|
294
|
+
override fun pruneHistory(retentionDays: Double, promise: Promise) {
|
|
295
|
+
launchSafely(promise) {
|
|
296
|
+
promise.resolve(core.repository.prune(retentionDays.toInt()))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- permissions -----------------------------------------------------
|
|
301
|
+
|
|
302
|
+
@ReactMethod
|
|
303
|
+
override fun checkPermissions(promise: Promise) {
|
|
304
|
+
runSafely(promise) { promise.resolve(Bridge.map(PermissionHelper.status(reactContext))) }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@ReactMethod
|
|
308
|
+
override fun requestPermissions(promise: Promise) {
|
|
309
|
+
val activity = currentActivity
|
|
310
|
+
if (activity !is PermissionAwareActivity) {
|
|
311
|
+
promise.reject("E_NO_ACTIVITY", "No foreground activity to attach the dialog to")
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
val required = PermissionHelper.required()
|
|
315
|
+
.filter { !PermissionHelper.isGranted(reactContext, it) }
|
|
316
|
+
if (required.isEmpty()) {
|
|
317
|
+
promise.resolve(Bridge.map(PermissionHelper.status(reactContext)))
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
val listener = PermissionListener { _, _, _ ->
|
|
321
|
+
promise.resolve(Bridge.map(PermissionHelper.status(reactContext)))
|
|
322
|
+
true
|
|
323
|
+
}
|
|
324
|
+
runCatching {
|
|
325
|
+
activity.requestPermissions(required.toTypedArray(), PERMISSION_REQUEST_CODE, listener)
|
|
326
|
+
}.onFailure { promise.reject("E_UNKNOWN", it.message, it) }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@ReactMethod
|
|
330
|
+
override fun getDeviceCapabilities(promise: Promise) {
|
|
331
|
+
runSafely(promise) {
|
|
332
|
+
promise.resolve(Bridge.map(PermissionHelper.capabilities(reactContext)))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@ReactMethod
|
|
337
|
+
override fun openAppSettings(promise: Promise) {
|
|
338
|
+
runSafely(promise) { promise.resolve(PermissionHelper.openAppSettings(reactContext)) }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---- battery ---------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
@ReactMethod
|
|
344
|
+
override fun isBatteryOptimizationEnabled(promise: Promise) {
|
|
345
|
+
runSafely(promise) {
|
|
346
|
+
promise.resolve(BatteryOptimizationHelper.isOptimizationEnabled(reactContext))
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@ReactMethod
|
|
351
|
+
override fun requestDisableBatteryOptimization(promise: Promise) {
|
|
352
|
+
runSafely(promise) {
|
|
353
|
+
val host = currentActivity ?: reactContext
|
|
354
|
+
promise.resolve(BatteryOptimizationHelper.requestExemption(host))
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@ReactMethod
|
|
359
|
+
override fun openBatteryOptimizationSettings(promise: Promise) {
|
|
360
|
+
runSafely(promise) {
|
|
361
|
+
promise.resolve(BatteryOptimizationHelper.openSettings(currentActivity ?: reactContext))
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@ReactMethod
|
|
366
|
+
override fun openManufacturerAutoStartSettings(promise: Promise) {
|
|
367
|
+
runSafely(promise) {
|
|
368
|
+
promise.resolve(
|
|
369
|
+
BatteryOptimizationHelper.openAutoStartSettings(currentActivity ?: reactContext)
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---- health connect --------------------------------------------------
|
|
375
|
+
|
|
376
|
+
@ReactMethod
|
|
377
|
+
override fun getHealthConnectStatus(promise: Promise) {
|
|
378
|
+
launchSafely(promise) { promise.resolve(Bridge.map(core.healthConnect.status())) }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@ReactMethod
|
|
382
|
+
override fun requestHealthConnectPermissions(promise: Promise) {
|
|
383
|
+
val availability = core.healthConnect.availability()
|
|
384
|
+
if (availability != HealthConnectManager.Availability.AVAILABLE) {
|
|
385
|
+
promise.reject(
|
|
386
|
+
"E_HEALTH_CONNECT_UNAVAILABLE",
|
|
387
|
+
"Health Connect is not available on this device"
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
val activity: Activity? = currentActivity
|
|
392
|
+
if (activity == null) {
|
|
393
|
+
promise.reject("E_NO_ACTIVITY", "No foreground activity to launch the request from")
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
// The callback is registered only after the launch succeeds. Registering
|
|
397
|
+
// first left it armed when startActivity threw: the promise was rejected
|
|
398
|
+
// here and then resolved again by the next request's result, which
|
|
399
|
+
// React Native rejects as a second invocation of a one-shot callback.
|
|
400
|
+
val settled = AtomicBoolean(false)
|
|
401
|
+
HealthPermissionActivity.Callbacks.expect()
|
|
402
|
+
runCatching {
|
|
403
|
+
activity.startActivity(
|
|
404
|
+
Intent(activity, HealthPermissionActivity::class.java)
|
|
405
|
+
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
406
|
+
)
|
|
407
|
+
}.onSuccess {
|
|
408
|
+
HealthPermissionActivity.Callbacks.await { _ ->
|
|
409
|
+
if (settled.compareAndSet(false, true)) {
|
|
410
|
+
scope.launch {
|
|
411
|
+
runCatching { promise.resolve(Bridge.map(core.healthConnect.status())) }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}.onFailure { error ->
|
|
416
|
+
if (settled.compareAndSet(false, true)) {
|
|
417
|
+
promise.reject("E_UNKNOWN", error.message, error)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@ReactMethod
|
|
423
|
+
override fun openHealthConnectSettings(promise: Promise) {
|
|
424
|
+
runSafely(promise) {
|
|
425
|
+
runCatching {
|
|
426
|
+
reactContext.startActivity(core.healthConnect.settingsIntent())
|
|
427
|
+
promise.resolve(true)
|
|
428
|
+
}.onFailure { promise.resolve(false) }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@ReactMethod
|
|
433
|
+
override fun readHealthConnectSteps(startIso: String, endIso: String, promise: Promise) {
|
|
434
|
+
launchSafely(promise) {
|
|
435
|
+
val days = core.healthConnect.readDailySteps(
|
|
436
|
+
Instant.parse(startIso),
|
|
437
|
+
Instant.parse(endIso)
|
|
438
|
+
)
|
|
439
|
+
promise.resolve(
|
|
440
|
+
Bridge.map(
|
|
441
|
+
mapOf(
|
|
442
|
+
"totalSteps" to days.sumOf { it.steps },
|
|
443
|
+
"records" to days.map { day ->
|
|
444
|
+
mapOf(
|
|
445
|
+
"date" to day.date,
|
|
446
|
+
"steps" to day.steps,
|
|
447
|
+
"distance" to day.distance,
|
|
448
|
+
"calories" to day.calories,
|
|
449
|
+
"synced" to true,
|
|
450
|
+
"syncedRemote" to false
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@ReactMethod
|
|
460
|
+
override fun writeHealthConnectSteps(date: String, promise: Promise) {
|
|
461
|
+
launchSafely(promise) {
|
|
462
|
+
promise.resolve(core.healthConnect.writeDay(core.dayTotals(date)))
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@ReactMethod
|
|
467
|
+
override fun syncWithHealthConnect(promise: Promise) {
|
|
468
|
+
launchSafely(promise) { promise.resolve(Bridge.map(core.syncHealthConnect())) }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- sync ------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
@ReactMethod
|
|
474
|
+
override fun getPendingSyncCount(promise: Promise) {
|
|
475
|
+
launchSafely(promise) { promise.resolve(core.repository.countUnsynced()) }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
@ReactMethod
|
|
479
|
+
override fun syncNow(promise: Promise) {
|
|
480
|
+
launchSafely(promise) {
|
|
481
|
+
val results = ArrayList<Map<String, Any?>>()
|
|
482
|
+
results.add(core.syncHealthConnect())
|
|
483
|
+
val config = core.config()
|
|
484
|
+
if (!config.remoteSyncUrl.isNullOrEmpty()) {
|
|
485
|
+
SyncScheduler.runNow(reactContext, config)
|
|
486
|
+
results.add(
|
|
487
|
+
mapOf(
|
|
488
|
+
"target" to "remote",
|
|
489
|
+
"syncedRecords" to 0,
|
|
490
|
+
"failedRecords" to 0,
|
|
491
|
+
"success" to true,
|
|
492
|
+
"error" to "Queued: listen for syncCompleted"
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
promise.resolve(Bridge.map(mapOf("results" to results)))
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---- helpers ---------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Sends a service command only when tracking is actually on. Commands go out
|
|
504
|
+
* through startForegroundService(), which starts the service to deliver
|
|
505
|
+
* them, so sending one to a stopped tracker would leave a foreground service
|
|
506
|
+
* and its notification running for something the user turned off.
|
|
507
|
+
*/
|
|
508
|
+
private fun sendIfTracking(action: String) {
|
|
509
|
+
if (core.state.shouldAutoStart) ServiceCommands.send(reactContext, action)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private inline fun runSafely(promise: Promise, block: () -> Unit) {
|
|
513
|
+
runCatching { block() }.onFailure { promise.reject("E_UNKNOWN", it.message, it) }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private fun launchSafely(promise: Promise, block: suspend () -> Unit) {
|
|
517
|
+
scope.launch {
|
|
518
|
+
runCatching { block() }.onFailure { promise.reject("E_UNKNOWN", it.message, it) }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private fun window(
|
|
523
|
+
options: ReadableMap?,
|
|
524
|
+
rollingDays: Int,
|
|
525
|
+
calendar: (Int) -> Pair<String, String>
|
|
526
|
+
): Pair<String, String> {
|
|
527
|
+
val mode = options?.optString("mode", "calendar") ?: "calendar"
|
|
528
|
+
val offset = options?.optInt("offset", 0) ?: 0
|
|
529
|
+
return if (mode == "rolling") DateKeys.rolling(rollingDays) else calendar(offset)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private fun mergeConfig(current: StepTrackerConfig, patch: ReadableMap): StepTrackerConfig =
|
|
533
|
+
current.copy(
|
|
534
|
+
heightCm = patch.optDouble("height", current.heightCm),
|
|
535
|
+
weightKg = patch.optDouble("weight", current.weightKg),
|
|
536
|
+
strideLengthM = patch.optDouble("strideLength", current.strideLengthM),
|
|
537
|
+
sex = patch.optString("sex", current.sex) ?: current.sex,
|
|
538
|
+
dailyGoal = patch.optInt("dailyGoal", current.dailyGoal),
|
|
539
|
+
weeklyGoal = patch.optInt("weeklyGoal", current.weeklyGoal),
|
|
540
|
+
monthlyGoal = patch.optInt("monthlyGoal", current.monthlyGoal),
|
|
541
|
+
calorieCoefficient = patch.optDouble(
|
|
542
|
+
"calorieCoefficient", current.calorieCoefficient
|
|
543
|
+
),
|
|
544
|
+
historyRetentionDays = patch.optInt(
|
|
545
|
+
"historyRetentionDays", current.historyRetentionDays
|
|
546
|
+
),
|
|
547
|
+
notificationTitle = patch.optString("notificationTitle", current.notificationTitle),
|
|
548
|
+
notificationText = patch.optString("notificationText", current.notificationText),
|
|
549
|
+
notificationIcon = patch.optString("notificationIcon", current.notificationIcon),
|
|
550
|
+
notificationChannelName = patch.optString(
|
|
551
|
+
"notificationChannelName", current.notificationChannelName
|
|
552
|
+
),
|
|
553
|
+
notificationActions = patch.optBoolean(
|
|
554
|
+
"notificationActions", current.notificationActions
|
|
555
|
+
),
|
|
556
|
+
notificationThrottleMs = patch.optLong(
|
|
557
|
+
"notificationThrottleMs", current.notificationThrottleMs
|
|
558
|
+
),
|
|
559
|
+
eventThrottleMs = patch.optLong("eventThrottleMs", current.eventThrottleMs),
|
|
560
|
+
persistEveryNSteps = patch.optInt("persistEveryNSteps", current.persistEveryNSteps),
|
|
561
|
+
healthConnectEnabled = patch.optBoolean(
|
|
562
|
+
"healthConnectEnabled", current.healthConnectEnabled
|
|
563
|
+
),
|
|
564
|
+
healthConnectSyncIntervalMinutes = patch.optInt(
|
|
565
|
+
"healthConnectSyncIntervalMinutes", current.healthConnectSyncIntervalMinutes
|
|
566
|
+
),
|
|
567
|
+
remoteSyncUrl = patch.optString("remoteSyncUrl", current.remoteSyncUrl),
|
|
568
|
+
remoteSyncHeaders = if (patch.hasKey("remoteSyncHeaders")) {
|
|
569
|
+
Bridge.stringMap(patch.getMap("remoteSyncHeaders"))
|
|
570
|
+
} else {
|
|
571
|
+
current.remoteSyncHeaders
|
|
572
|
+
},
|
|
573
|
+
autoStartOnBoot = patch.optBoolean("autoStartOnBoot", current.autoStartOnBoot)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
private fun configToMap(config: StepTrackerConfig): Map<String, Any?> = mapOf(
|
|
577
|
+
"height" to config.heightCm,
|
|
578
|
+
"weight" to config.weightKg,
|
|
579
|
+
"strideLength" to config.strideLengthM,
|
|
580
|
+
"sex" to config.sex,
|
|
581
|
+
"dailyGoal" to config.dailyGoal,
|
|
582
|
+
"weeklyGoal" to config.weeklyGoal,
|
|
583
|
+
"monthlyGoal" to config.monthlyGoal,
|
|
584
|
+
"calorieCoefficient" to config.calorieCoefficient,
|
|
585
|
+
"historyRetentionDays" to config.historyRetentionDays,
|
|
586
|
+
"notificationActions" to config.notificationActions,
|
|
587
|
+
"healthConnectEnabled" to config.healthConnectEnabled,
|
|
588
|
+
"healthConnectSyncIntervalMinutes" to config.healthConnectSyncIntervalMinutes,
|
|
589
|
+
"remoteSyncUrl" to config.remoteSyncUrl,
|
|
590
|
+
"autoStartOnBoot" to config.autoStartOnBoot
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
companion object {
|
|
594
|
+
const val NAME = "StepTrackerPro"
|
|
595
|
+
const val EVENT_PREFIX = "StepTrackerPro:"
|
|
596
|
+
private const val PERMISSION_REQUEST_CODE = 7731
|
|
597
|
+
}
|
|
598
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.steptrackerpro
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class StepTrackerProPackage : BaseReactPackage() {
|
|
10
|
+
|
|
11
|
+
override fun getModule(name: String, context: ReactApplicationContext): NativeModule? =
|
|
12
|
+
if (name == StepTrackerProModule.NAME) StepTrackerProModule(context) else null
|
|
13
|
+
|
|
14
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
15
|
+
mapOf(
|
|
16
|
+
StepTrackerProModule.NAME to ReactModuleInfo(
|
|
17
|
+
StepTrackerProModule.NAME,
|
|
18
|
+
StepTrackerProModule.NAME,
|
|
19
|
+
false, // canOverrideExistingModule
|
|
20
|
+
false, // needsEagerInit
|
|
21
|
+
false, // isCxxModule
|
|
22
|
+
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|