@tsachit/react-native-geo-service 1.0.3 → 1.0.5
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 +41 -10
- package/lib/GeoDebugPanel.js +91 -14
- package/lib/GeoSessionStore.d.ts +65 -0
- package/lib/GeoSessionStore.js +158 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -2
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -83,13 +83,17 @@ In your app's **`index.js`** (top level, outside any component):
|
|
|
83
83
|
|
|
84
84
|
```js
|
|
85
85
|
import { AppRegistry } from 'react-native';
|
|
86
|
+
import RNGSAppRegistry from '@tsachit/react-native-geo-service';
|
|
86
87
|
import App from './App';
|
|
87
88
|
|
|
88
89
|
AppRegistry.registerComponent('YourApp', () => App);
|
|
89
90
|
|
|
90
91
|
// Handles location events when the React context is not active.
|
|
91
92
|
// Runs even when the app is killed (foreground service must be running).
|
|
92
|
-
|
|
93
|
+
// Using RNGSAppRegistry.registerHeadlessTask() is preferred over
|
|
94
|
+
// AppRegistry.registerHeadlessTask() directly — it automatically keeps
|
|
95
|
+
// the debug panel's session store in sync while the app is killed.
|
|
96
|
+
RNGSAppRegistry.registerHeadlessTask(async (location) => {
|
|
93
97
|
console.log('[Background] Location:', location);
|
|
94
98
|
// Send to your server using a pre-stored auth token (e.g. SecureStore/Keychain).
|
|
95
99
|
// Do not rely on in-memory state — this JS context is isolated.
|
|
@@ -188,10 +192,12 @@ const tracking = await RNGeoService.isTracking();
|
|
|
188
192
|
|
|
189
193
|
### Register headless task via the module
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
Use `registerHeadlessTask()` from the package instead of `AppRegistry.registerHeadlessTask()` directly — it wraps your handler to automatically keep the debug panel's session metrics in sync while the app is killed:
|
|
192
196
|
|
|
193
197
|
```ts
|
|
194
|
-
|
|
198
|
+
import RNGSAppRegistry from '@tsachit/react-native-geo-service';
|
|
199
|
+
|
|
200
|
+
RNGSAppRegistry.registerHeadlessTask(async (location) => {
|
|
195
201
|
await sendToServer(location);
|
|
196
202
|
});
|
|
197
203
|
```
|
|
@@ -246,7 +252,7 @@ Subscribe to location updates. Call `.remove()` on the returned subscription to
|
|
|
246
252
|
Subscribe to location errors (e.g. permission revoked mid-session).
|
|
247
253
|
|
|
248
254
|
### `registerHeadlessTask(handler)` *(Android only)*
|
|
249
|
-
Register a function to handle location events when the app is not in the foreground.
|
|
255
|
+
Register a function to handle location events when the app is not in the foreground. Preferred over `AppRegistry.registerHeadlessTask()` directly — automatically keeps `GeoSessionStore` in sync so the debug panel shows accurate Geopoints counts while the app is killed.
|
|
250
256
|
|
|
251
257
|
### `getBatteryInfo(): Promise<BatteryInfo>`
|
|
252
258
|
Returns battery and session tracking metrics. See [Debug mode](#debug-mode) below.
|
|
@@ -343,7 +349,8 @@ await RNGeoService.stop(); // panel hides automatically
|
|
|
343
349
|
|
|
344
350
|
| Minimized | Opened |
|
|
345
351
|
|--------|-------------|
|
|
346
|
-
| <img width="349" height="261" alt="image" src="https://github.com/user-attachments/assets/a6b43b93-7a93-485d-a68d-f4e4fe658011" /> | <img width="
|
|
352
|
+
| <img width="349" height="261" alt="image" src="https://github.com/user-attachments/assets/a6b43b93-7a93-485d-a68d-f4e4fe658011" /> | <img width="321" height="325" alt="image" src="https://github.com/user-attachments/assets/8715d657-0984-46c3-bf38-d782968ddc99" /> |
|
|
353
|
+
|
|
347
354
|
|
|
348
355
|
### Debug panel behaviour
|
|
349
356
|
|
|
@@ -352,17 +359,20 @@ The panel is a **draggable, minimizable floating overlay** that starts minimized
|
|
|
352
359
|
- **Tap the 📍 circle** to expand
|
|
353
360
|
- **Drag** by holding the striped header bar
|
|
354
361
|
- **Minimize** with the ⊖ button — collapses back to the 📍 circle
|
|
362
|
+
- **Geopoints updates in real time** on every location event — no need to wait for the poll interval
|
|
363
|
+
- **"↺ Reset stats"** at the bottom right clears all accumulated data; Geopoints, elapsed time, battery drain, and the start timestamp all reset to zero
|
|
355
364
|
|
|
356
|
-
**Metrics shown
|
|
365
|
+
**Metrics shown** (all values are cumulative across app restarts — see [GeoSessionStore](#geosessionstore)):
|
|
357
366
|
|
|
358
367
|
| Metric | Description |
|
|
359
368
|
|--------|-------------|
|
|
360
|
-
|
|
|
361
|
-
|
|
|
369
|
+
| Started | Local date/time the very first tracking session began |
|
|
370
|
+
| Tracking for | Cumulative duration across all sessions |
|
|
371
|
+
| Geopoints | Total locations received across all sessions |
|
|
362
372
|
| Updates/min | Average frequency of location updates |
|
|
363
|
-
| GPS active | % of
|
|
373
|
+
| GPS active | % of total time the GPS chip was on vs idle |
|
|
364
374
|
| Battery now | Current device battery level |
|
|
365
|
-
| Drained | Total device battery % dropped since `start()` |
|
|
375
|
+
| Drained | Total device battery % dropped since first `start()` |
|
|
366
376
|
| Drain rate | Battery consumed per hour (total device, not just location) |
|
|
367
377
|
|
|
368
378
|
**Smart suggestions** are shown automatically:
|
|
@@ -385,6 +395,27 @@ import { GeoDebugPanel } from '@tsachit/react-native-geo-service';
|
|
|
385
395
|
<GeoDebugPanel pollInterval={15000} />
|
|
386
396
|
```
|
|
387
397
|
|
|
398
|
+
### GeoSessionStore
|
|
399
|
+
|
|
400
|
+
All debug panel metrics are stored in-memory on the native side and would normally reset every time tracking restarts (app killed, OS killed the service, device rebooted). `GeoSessionStore` persists snapshots to `AsyncStorage` so the panel shows **cumulative totals** across sessions.
|
|
401
|
+
|
|
402
|
+
Requires [`@react-native-async-storage/async-storage`](https://github.com/react-native-async-storage/async-storage) to be installed in your app (optional peer dependency — the panel silently skips persistence if it is not present).
|
|
403
|
+
|
|
404
|
+
**Session boundaries** are detected automatically: when `batteryLevelAtStart` changes between snapshots, the previous session is archived before the new one begins. This prevents double-counting when the Android foreground service keeps running after the app is reopened.
|
|
405
|
+
|
|
406
|
+
The **"↺ Reset stats"** button inside the panel clears all accumulated data and the recorded start time so you can re-measure from scratch.
|
|
407
|
+
|
|
408
|
+
If you use `RNGSAppRegistry.registerHeadlessTask()`, `GeoSessionStore` is updated automatically on each headless location event — no extra code required. If you register via `AppRegistry.registerHeadlessTask()` directly, you can increment the counter manually:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import { GeoSessionStore } from '@tsachit/react-native-geo-service';
|
|
412
|
+
|
|
413
|
+
AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
|
|
414
|
+
await sendToServer(location);
|
|
415
|
+
await GeoSessionStore.onHeadlessLocation();
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
388
419
|
---
|
|
389
420
|
|
|
390
421
|
## Headless mode explained
|
package/lib/GeoDebugPanel.js
CHANGED
|
@@ -49,9 +49,14 @@ exports.GeoDebugPanel = void 0;
|
|
|
49
49
|
const react_1 = __importStar(require("react"));
|
|
50
50
|
const react_native_1 = require("react-native");
|
|
51
51
|
const index_1 = __importDefault(require("./index"));
|
|
52
|
+
const GeoSessionStore_1 = require("./GeoSessionStore");
|
|
52
53
|
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
|
|
53
54
|
const PANEL_WIDTH = Math.round(SCREEN_WIDTH * 0.95);
|
|
54
55
|
const PILL_SIZE = 50;
|
|
56
|
+
// Conservative estimate used when the panel hasn't rendered yet (onLayout not fired)
|
|
57
|
+
const PANEL_ESTIMATED_HEIGHT = 440;
|
|
58
|
+
const PANEL_BOTTOM_MARGIN = 20;
|
|
59
|
+
const PILL_INITIAL_BOTTOM_MARGIN = 120;
|
|
55
60
|
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
|
56
61
|
function formatBattery(level) {
|
|
57
62
|
return level < 0 ? 'N/A' : `${level.toFixed(1)}%`;
|
|
@@ -152,17 +157,37 @@ function makePanResponder(pan, lastPos, maxWidth, maxHeight, onTap) {
|
|
|
152
157
|
});
|
|
153
158
|
}
|
|
154
159
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
160
|
+
function formatStartedAt(ts) {
|
|
161
|
+
if (!ts)
|
|
162
|
+
return '—';
|
|
163
|
+
const d = new Date(ts);
|
|
164
|
+
return d.toLocaleString(undefined, {
|
|
165
|
+
month: 'short', day: 'numeric',
|
|
166
|
+
hour: '2-digit', minute: '2-digit',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
155
169
|
const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
156
|
-
var _a, _b, _c, _d, _e
|
|
170
|
+
var _a, _b, _c, _d, _e;
|
|
157
171
|
const [info, setInfo] = (0, react_1.useState)(null);
|
|
158
172
|
const [minimized, setMinimized] = (0, react_1.useState)(true);
|
|
159
|
-
const
|
|
160
|
-
|
|
173
|
+
const [storeData, setStoreData] = (0, react_1.useState)({
|
|
174
|
+
accumulated: { updateCount: 0, elapsedSeconds: 0, gpsActiveSeconds: 0, drain: 0 },
|
|
175
|
+
lastSnapshot: null,
|
|
176
|
+
trackingStartedAt: null,
|
|
177
|
+
});
|
|
178
|
+
// Live count updated on every location event so Geopoints doesn't wait for the poll
|
|
179
|
+
const [realtimeCount, setRealtimeCount] = (0, react_1.useState)(0);
|
|
180
|
+
// Native updateCount value at the last reset — subtracted so the display starts from 0
|
|
181
|
+
const countBaseline = (0, react_1.useRef)(0);
|
|
182
|
+
const initialY = SCREEN_HEIGHT - PANEL_ESTIMATED_HEIGHT - PILL_INITIAL_BOTTOM_MARGIN;
|
|
183
|
+
const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 8, y: initialY })).current;
|
|
184
|
+
const lastPos = (0, react_1.useRef)({ x: 8, y: initialY });
|
|
161
185
|
const panelHeight = (0, react_1.useRef)(0);
|
|
162
186
|
const pillPanResponder = (0, react_1.useRef)(makePanResponder(pan, lastPos, PILL_SIZE, PILL_SIZE, () => {
|
|
187
|
+
const expandedHeight = panelHeight.current > 0 ? panelHeight.current : PANEL_ESTIMATED_HEIGHT;
|
|
163
188
|
const clamped = {
|
|
164
189
|
x: Math.min(lastPos.current.x, SCREEN_WIDTH - PANEL_WIDTH),
|
|
165
|
-
y: Math.min(lastPos.current.y, SCREEN_HEIGHT -
|
|
190
|
+
y: Math.min(lastPos.current.y, SCREEN_HEIGHT - expandedHeight - PANEL_BOTTOM_MARGIN),
|
|
166
191
|
};
|
|
167
192
|
lastPos.current = clamped;
|
|
168
193
|
pan.setValue(clamped);
|
|
@@ -188,13 +213,43 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
|
188
213
|
try {
|
|
189
214
|
const data = yield index_1.default.getBatteryInfo();
|
|
190
215
|
setInfo(data);
|
|
216
|
+
// Persist snapshot and reload accumulated so totals stay up to date
|
|
217
|
+
yield GeoSessionStore_1.GeoSessionStore.saveSnapshot(data);
|
|
218
|
+
const store = yield GeoSessionStore_1.GeoSessionStore.load();
|
|
219
|
+
setStoreData(store);
|
|
191
220
|
}
|
|
192
221
|
catch (_) { }
|
|
193
222
|
}), []);
|
|
223
|
+
const handleReset = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
var _a;
|
|
225
|
+
yield GeoSessionStore_1.GeoSessionStore.clear();
|
|
226
|
+
setStoreData({ accumulated: { updateCount: 0, elapsedSeconds: 0, gpsActiveSeconds: 0, drain: 0 }, lastSnapshot: null, trackingStartedAt: null });
|
|
227
|
+
// Capture current native count as baseline so post-reset display starts from 0
|
|
228
|
+
countBaseline.current = (_a = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _a !== void 0 ? _a : 0;
|
|
229
|
+
setRealtimeCount(0);
|
|
230
|
+
}), [info]);
|
|
194
231
|
(0, react_1.useEffect)(() => {
|
|
232
|
+
// Load accumulated history on mount
|
|
233
|
+
GeoSessionStore_1.GeoSessionStore.load().then(setStoreData).catch(() => { });
|
|
195
234
|
refresh();
|
|
196
235
|
const id = setInterval(refresh, pollInterval);
|
|
197
|
-
|
|
236
|
+
// Real-time Geopoints counter — increments on every location event without waiting for the poll
|
|
237
|
+
const locationSub = index_1.default.onLocation(() => {
|
|
238
|
+
setRealtimeCount(c => c + 1);
|
|
239
|
+
});
|
|
240
|
+
// Save snapshot when app is sent to background so stats survive unexpected kills
|
|
241
|
+
const appStateSub = react_native_1.AppState.addEventListener('change', status => {
|
|
242
|
+
if (status === 'background' || status === 'inactive') {
|
|
243
|
+
index_1.default.getBatteryInfo()
|
|
244
|
+
.then(GeoSessionStore_1.GeoSessionStore.saveSnapshot)
|
|
245
|
+
.catch(() => { });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return () => {
|
|
249
|
+
clearInterval(id);
|
|
250
|
+
locationSub.remove();
|
|
251
|
+
appStateSub.remove();
|
|
252
|
+
};
|
|
198
253
|
}, [refresh, pollInterval]);
|
|
199
254
|
// ── Minimized pill ──
|
|
200
255
|
if (minimized) {
|
|
@@ -203,16 +258,27 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
|
203
258
|
</react_native_1.Animated.View>);
|
|
204
259
|
}
|
|
205
260
|
// Safely normalise — native side returns undefined for new fields until rebuilt
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
261
|
+
const sessionElapsed = (_a = info === null || info === void 0 ? void 0 : info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
|
|
262
|
+
const sessionGpsActive = (_b = info === null || info === void 0 ? void 0 : info.gpsActiveSeconds) !== null && _b !== void 0 ? _b : 0;
|
|
263
|
+
const sessionDrain = (_c = info === null || info === void 0 ? void 0 : info.drainSinceStart) !== null && _c !== void 0 ? _c : 0;
|
|
264
|
+
const level = (_d = info === null || info === void 0 ? void 0 : info.level) !== null && _d !== void 0 ? _d : -1;
|
|
265
|
+
// Subtract baseline so the count restarts from 0 after a reset.
|
|
266
|
+
// Use the higher of the adjusted native count vs the live subscription count
|
|
267
|
+
// so we never show a number lower than what the native side knows about.
|
|
268
|
+
const nativeCountSinceReset = Math.max(0, ((_e = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _e !== void 0 ? _e : 0) - countBaseline.current);
|
|
269
|
+
const sessionUpdates = Math.max(nativeCountSinceReset, realtimeCount);
|
|
270
|
+
// Combine current session with accumulated history from previous sessions
|
|
271
|
+
const acc = storeData.accumulated;
|
|
272
|
+
const elapsed = acc.elapsedSeconds + sessionElapsed;
|
|
273
|
+
const updates = acc.updateCount + sessionUpdates;
|
|
274
|
+
const gpsActive = acc.gpsActiveSeconds + sessionGpsActive;
|
|
275
|
+
const drain = acc.drain + sessionDrain;
|
|
276
|
+
const upm = elapsed > 0 ? updates / (elapsed / 60) : 0;
|
|
277
|
+
const drainRate = elapsed > 0 ? drain / (elapsed / 3600) : 0;
|
|
213
278
|
const safeInfo = info
|
|
214
279
|
? Object.assign(Object.assign({}, info), { trackingElapsedSeconds: elapsed, updateCount: updates, updatesPerMinute: upm, gpsActiveSeconds: gpsActive, drainRatePerHour: drainRate }) : null;
|
|
215
280
|
const suggestions = safeInfo ? getSuggestions(safeInfo) : [];
|
|
281
|
+
const startedAt = formatStartedAt(storeData.trackingStartedAt);
|
|
216
282
|
// ── Expanded panel ──
|
|
217
283
|
return (<react_native_1.Animated.View style={[styles.container, { transform: pan.getTranslateTransform() }]} onLayout={e => { panelHeight.current = e.nativeEvent.layout.height; }}>
|
|
218
284
|
{/* Drag header */}
|
|
@@ -231,7 +297,8 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
|
231
297
|
{info ? (<>
|
|
232
298
|
{/* ── Metrics grid ── */}
|
|
233
299
|
<react_native_1.View style={styles.grid}>
|
|
234
|
-
<MetricBox label="
|
|
300
|
+
<MetricBox label="Started" value={startedAt} desc="first session began"/>
|
|
301
|
+
<MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="cumulative duration"/>
|
|
235
302
|
<MetricBox label="Geopoints" value={`${updates}`} desc="total locations received"/>
|
|
236
303
|
<MetricBox label="Updates/min" value={upm > 0 ? upm.toFixed(1) : '—'} desc="higher = more battery"/>
|
|
237
304
|
<MetricBox label="GPS active" value={elapsed > 0 ? formatGpsPercent(gpsActive, elapsed) : '—'} desc="lower = adaptive saving"/>
|
|
@@ -248,6 +315,13 @@ const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
|
248
315
|
<react_native_1.Text style={[styles.suggestionText, { color: s.color }]}>{s.text}</react_native_1.Text>
|
|
249
316
|
</react_native_1.View>))}
|
|
250
317
|
</>) : (<react_native_1.Text style={styles.label}>Waiting for data…</react_native_1.Text>)}
|
|
318
|
+
|
|
319
|
+
{/* ── Reset button ── */}
|
|
320
|
+
<react_native_1.View style={styles.resetRow}>
|
|
321
|
+
<react_native_1.TouchableOpacity onPress={handleReset} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
322
|
+
<react_native_1.Text style={styles.resetBtn}>↺ Reset stats</react_native_1.Text>
|
|
323
|
+
</react_native_1.TouchableOpacity>
|
|
324
|
+
</react_native_1.View>
|
|
251
325
|
</react_native_1.View>
|
|
252
326
|
</react_native_1.Animated.View>);
|
|
253
327
|
};
|
|
@@ -298,7 +372,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
298
372
|
minWidth: '30%', flex: 1,
|
|
299
373
|
alignItems: 'center',
|
|
300
374
|
},
|
|
301
|
-
metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
|
|
375
|
+
metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15, textAlign: 'center' },
|
|
302
376
|
metricLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, marginTop: 2, textAlign: 'center' },
|
|
303
377
|
metricSub: { color: 'rgba(255,255,255,0.3)', fontSize: 8, textAlign: 'center' },
|
|
304
378
|
// Suggestions
|
|
@@ -307,6 +381,9 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
307
381
|
suggestion: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 5, gap: 6 },
|
|
308
382
|
suggestionEmoji: { fontSize: 12, lineHeight: 16 },
|
|
309
383
|
suggestionText: { fontSize: 11, lineHeight: 16, flex: 1 },
|
|
384
|
+
// Reset
|
|
385
|
+
resetRow: { alignItems: 'flex-end', marginTop: 10 },
|
|
386
|
+
resetBtn: { color: 'rgba(255,255,255,0.45)', fontSize: 13, paddingVertical: 6, paddingHorizontal: 10 },
|
|
310
387
|
// Minimized pill
|
|
311
388
|
pill: {
|
|
312
389
|
position: 'absolute',
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeoSessionStore — persists debug panel metrics across app sessions.
|
|
3
|
+
*
|
|
4
|
+
* Metrics live in-memory on the native side and reset whenever tracking
|
|
5
|
+
* restarts (app killed, OS killed the service, device rebooted). This store
|
|
6
|
+
* snapshots values to AsyncStorage so the debug panel can show cumulative
|
|
7
|
+
* totals spanning multiple sessions.
|
|
8
|
+
*
|
|
9
|
+
* Storage key: @rn_geo_service/debug_session
|
|
10
|
+
*
|
|
11
|
+
* Schema:
|
|
12
|
+
* accumulated — sum of all fully-closed previous sessions
|
|
13
|
+
* lastSnapshot — most recent known state; used to detect session boundaries
|
|
14
|
+
* trackingStartedAt — Unix ms timestamp of the very first start() ever recorded
|
|
15
|
+
* (not overwritten until clear() is called)
|
|
16
|
+
*/
|
|
17
|
+
import type { BatteryInfo } from './types';
|
|
18
|
+
export interface AccumulatedStats {
|
|
19
|
+
updateCount: number;
|
|
20
|
+
elapsedSeconds: number;
|
|
21
|
+
gpsActiveSeconds: number;
|
|
22
|
+
drain: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SnapshotStats extends AccumulatedStats {
|
|
25
|
+
batteryLevelAtStart: number;
|
|
26
|
+
}
|
|
27
|
+
export interface StoreData {
|
|
28
|
+
accumulated: AccumulatedStats;
|
|
29
|
+
lastSnapshot: SnapshotStats | null;
|
|
30
|
+
/** Unix ms — when the very first session began. Null until first saveSnapshot(). */
|
|
31
|
+
trackingStartedAt: number | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load accumulated stats + trackingStartedAt from storage.
|
|
35
|
+
* Returns zero defaults if nothing has been stored yet.
|
|
36
|
+
*/
|
|
37
|
+
declare function load(): Promise<StoreData>;
|
|
38
|
+
/**
|
|
39
|
+
* Snapshot the current session's BatteryInfo into storage.
|
|
40
|
+
*
|
|
41
|
+
* - Detects session boundaries via batteryLevelAtStart:
|
|
42
|
+
* if it differs from the stored lastSnapshot, the previous session is
|
|
43
|
+
* archived into accumulated before storing the new snapshot.
|
|
44
|
+
* - Records trackingStartedAt on the very first call (never overwritten).
|
|
45
|
+
*/
|
|
46
|
+
declare function saveSnapshot(info: BatteryInfo): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Called from the Android headless task when the app is killed but the
|
|
49
|
+
* foreground service is still delivering location updates.
|
|
50
|
+
* Increments the updateCount in lastSnapshot by 1.
|
|
51
|
+
* Safe to call from a HeadlessJS context (no native bridge required).
|
|
52
|
+
*/
|
|
53
|
+
declare function onHeadlessLocation(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Clear all accumulated data, lastSnapshot, and trackingStartedAt.
|
|
56
|
+
* The panel reverts to current-session-only view after calling this.
|
|
57
|
+
*/
|
|
58
|
+
declare function clear(): Promise<void>;
|
|
59
|
+
export declare const GeoSessionStore: {
|
|
60
|
+
load: typeof load;
|
|
61
|
+
saveSnapshot: typeof saveSnapshot;
|
|
62
|
+
onHeadlessLocation: typeof onHeadlessLocation;
|
|
63
|
+
clear: typeof clear;
|
|
64
|
+
};
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GeoSessionStore — persists debug panel metrics across app sessions.
|
|
4
|
+
*
|
|
5
|
+
* Metrics live in-memory on the native side and reset whenever tracking
|
|
6
|
+
* restarts (app killed, OS killed the service, device rebooted). This store
|
|
7
|
+
* snapshots values to AsyncStorage so the debug panel can show cumulative
|
|
8
|
+
* totals spanning multiple sessions.
|
|
9
|
+
*
|
|
10
|
+
* Storage key: @rn_geo_service/debug_session
|
|
11
|
+
*
|
|
12
|
+
* Schema:
|
|
13
|
+
* accumulated — sum of all fully-closed previous sessions
|
|
14
|
+
* lastSnapshot — most recent known state; used to detect session boundaries
|
|
15
|
+
* trackingStartedAt — Unix ms timestamp of the very first start() ever recorded
|
|
16
|
+
* (not overwritten until clear() is called)
|
|
17
|
+
*/
|
|
18
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
19
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
20
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
21
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
22
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
23
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
24
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.GeoSessionStore = void 0;
|
|
29
|
+
const STORAGE_KEY = '@rn_geo_service/debug_session';
|
|
30
|
+
// Lazily required so the package doesn't hard-crash if AsyncStorage is not
|
|
31
|
+
// installed in the host app (it's a peerDependency).
|
|
32
|
+
function getStorage() {
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
35
|
+
return require('@react-native-async-storage/async-storage').default;
|
|
36
|
+
}
|
|
37
|
+
catch (_) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const ZERO_ACCUMULATED = {
|
|
42
|
+
updateCount: 0,
|
|
43
|
+
elapsedSeconds: 0,
|
|
44
|
+
gpsActiveSeconds: 0,
|
|
45
|
+
drain: 0,
|
|
46
|
+
};
|
|
47
|
+
function addStats(a, b) {
|
|
48
|
+
return {
|
|
49
|
+
updateCount: a.updateCount + b.updateCount,
|
|
50
|
+
elapsedSeconds: a.elapsedSeconds + b.elapsedSeconds,
|
|
51
|
+
gpsActiveSeconds: a.gpsActiveSeconds + b.gpsActiveSeconds,
|
|
52
|
+
drain: a.drain + b.drain,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function readRaw() {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
const storage = getStorage();
|
|
58
|
+
if (!storage)
|
|
59
|
+
return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
|
|
60
|
+
try {
|
|
61
|
+
const raw = yield storage.getItem(STORAGE_KEY);
|
|
62
|
+
if (!raw)
|
|
63
|
+
return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch (_) {
|
|
67
|
+
return { accumulated: Object.assign({}, ZERO_ACCUMULATED), lastSnapshot: null, trackingStartedAt: null };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function writeRaw(data) {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
const storage = getStorage();
|
|
74
|
+
if (!storage)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
yield storage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
78
|
+
}
|
|
79
|
+
catch (_) { }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load accumulated stats + trackingStartedAt from storage.
|
|
84
|
+
* Returns zero defaults if nothing has been stored yet.
|
|
85
|
+
*/
|
|
86
|
+
function load() {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
return readRaw();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Snapshot the current session's BatteryInfo into storage.
|
|
93
|
+
*
|
|
94
|
+
* - Detects session boundaries via batteryLevelAtStart:
|
|
95
|
+
* if it differs from the stored lastSnapshot, the previous session is
|
|
96
|
+
* archived into accumulated before storing the new snapshot.
|
|
97
|
+
* - Records trackingStartedAt on the very first call (never overwritten).
|
|
98
|
+
*/
|
|
99
|
+
function saveSnapshot(info) {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
var _a, _b, _c, _d, _e;
|
|
102
|
+
const data = yield readRaw();
|
|
103
|
+
const newSnapshot = {
|
|
104
|
+
updateCount: (_a = info.updateCount) !== null && _a !== void 0 ? _a : 0,
|
|
105
|
+
elapsedSeconds: (_b = info.trackingElapsedSeconds) !== null && _b !== void 0 ? _b : 0,
|
|
106
|
+
gpsActiveSeconds: (_c = info.gpsActiveSeconds) !== null && _c !== void 0 ? _c : 0,
|
|
107
|
+
drain: (_d = info.drainSinceStart) !== null && _d !== void 0 ? _d : 0,
|
|
108
|
+
batteryLevelAtStart: (_e = info.levelAtStart) !== null && _e !== void 0 ? _e : -1,
|
|
109
|
+
};
|
|
110
|
+
let { accumulated, trackingStartedAt } = data;
|
|
111
|
+
// Detect session boundary — a different batteryLevelAtStart means start() was called again
|
|
112
|
+
if (data.lastSnapshot !== null &&
|
|
113
|
+
data.lastSnapshot.batteryLevelAtStart !== newSnapshot.batteryLevelAtStart) {
|
|
114
|
+
accumulated = addStats(accumulated, data.lastSnapshot);
|
|
115
|
+
}
|
|
116
|
+
// Record start time once — on the very first snapshot ever, or after a clear()
|
|
117
|
+
if (trackingStartedAt === null) {
|
|
118
|
+
trackingStartedAt = Date.now();
|
|
119
|
+
}
|
|
120
|
+
yield writeRaw({ accumulated, lastSnapshot: newSnapshot, trackingStartedAt });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Called from the Android headless task when the app is killed but the
|
|
125
|
+
* foreground service is still delivering location updates.
|
|
126
|
+
* Increments the updateCount in lastSnapshot by 1.
|
|
127
|
+
* Safe to call from a HeadlessJS context (no native bridge required).
|
|
128
|
+
*/
|
|
129
|
+
function onHeadlessLocation() {
|
|
130
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
+
var _a;
|
|
132
|
+
const data = yield readRaw();
|
|
133
|
+
const snap = (_a = data.lastSnapshot) !== null && _a !== void 0 ? _a : {
|
|
134
|
+
updateCount: 0,
|
|
135
|
+
elapsedSeconds: 0,
|
|
136
|
+
gpsActiveSeconds: 0,
|
|
137
|
+
drain: 0,
|
|
138
|
+
batteryLevelAtStart: -1,
|
|
139
|
+
};
|
|
140
|
+
yield writeRaw(Object.assign(Object.assign({}, data), { lastSnapshot: Object.assign(Object.assign({}, snap), { updateCount: snap.updateCount + 1 }) }));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clear all accumulated data, lastSnapshot, and trackingStartedAt.
|
|
145
|
+
* The panel reverts to current-session-only view after calling this.
|
|
146
|
+
*/
|
|
147
|
+
function clear() {
|
|
148
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
149
|
+
const storage = getStorage();
|
|
150
|
+
if (!storage)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
yield storage.removeItem(STORAGE_KEY);
|
|
154
|
+
}
|
|
155
|
+
catch (_) { }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
exports.GeoSessionStore = { load, saveSnapshot, onHeadlessLocation, clear };
|
package/lib/index.d.ts
CHANGED
|
@@ -86,3 +86,5 @@ declare const RNGeoService: {
|
|
|
86
86
|
export default RNGeoService;
|
|
87
87
|
export { GeoDebugPanel } from './GeoDebugPanel';
|
|
88
88
|
export { GeoDebugOverlay } from './GeoDebugOverlay';
|
|
89
|
+
export { GeoSessionStore } from './GeoSessionStore';
|
|
90
|
+
export type { AccumulatedStats, StoreData } from './GeoSessionStore';
|
package/lib/index.js
CHANGED
|
@@ -23,7 +23,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.GeoDebugOverlay = exports.GeoDebugPanel = void 0;
|
|
26
|
+
exports.GeoSessionStore = exports.GeoDebugOverlay = exports.GeoDebugPanel = void 0;
|
|
27
27
|
exports._isDebugMode = _isDebugMode;
|
|
28
28
|
const react_native_1 = require("react-native");
|
|
29
29
|
__exportStar(require("./types"), exports);
|
|
@@ -148,7 +148,12 @@ function registerHeadlessTask(handler) {
|
|
|
148
148
|
if (react_native_1.Platform.OS !== 'android')
|
|
149
149
|
return;
|
|
150
150
|
const taskName = DEFAULT_CONFIG.backgroundTaskName;
|
|
151
|
-
react_native_1.AppRegistry.registerHeadlessTask(taskName, () =>
|
|
151
|
+
react_native_1.AppRegistry.registerHeadlessTask(taskName, () => (location) => __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
// Keep GeoSessionStore's updateCount in sync while the app is killed
|
|
153
|
+
const { GeoSessionStore } = require('./GeoSessionStore');
|
|
154
|
+
yield GeoSessionStore.onHeadlessLocation();
|
|
155
|
+
yield handler(location);
|
|
156
|
+
}));
|
|
152
157
|
}
|
|
153
158
|
/**
|
|
154
159
|
* Returns battery information including current level and drain since tracking started.
|
|
@@ -185,3 +190,5 @@ var GeoDebugPanel_1 = require("./GeoDebugPanel");
|
|
|
185
190
|
Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
|
|
186
191
|
var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
|
|
187
192
|
Object.defineProperty(exports, "GeoDebugOverlay", { enumerable: true, get: function () { return GeoDebugOverlay_1.GeoDebugOverlay; } });
|
|
193
|
+
var GeoSessionStore_1 = require("./GeoSessionStore");
|
|
194
|
+
Object.defineProperty(exports, "GeoSessionStore", { enumerable: true, get: function () { return GeoSessionStore_1.GeoSessionStore; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsachit/react-native-geo-service",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Battery-efficient background geolocation for React Native with headless support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -25,7 +25,13 @@
|
|
|
25
25
|
"author": "Sachit Karki",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"react-native": ">=0.60.0"
|
|
28
|
+
"react-native": ">=0.60.0",
|
|
29
|
+
"@react-native-async-storage/async-storage": ">=1.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@react-native-async-storage/async-storage": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@types/react": "^18.3.28",
|