@tsachit/react-native-geo-service 1.0.1 → 1.0.2
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 +270 -92
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/geoservice/GeoServiceModule.kt +64 -0
- package/android/src/main/java/com/geoservice/LocationService.kt +111 -9
- package/android/src/main/java/com/geoservice/WatchdogWorker.kt +87 -0
- package/debug-panel.d.ts +1 -0
- package/debug-panel.js +1 -0
- package/ios/RNGeoService.m +223 -30
- package/lib/GeoDebugOverlay.d.ts +9 -0
- package/lib/GeoDebugOverlay.js +86 -0
- package/lib/GeoDebugPanel.d.ts +7 -0
- package/lib/GeoDebugPanel.js +319 -0
- package/lib/autoDebug.d.ts +2 -0
- package/lib/autoDebug.js +22 -0
- package/lib/index.d.ts +19 -1
- package/lib/index.js +60 -3
- package/lib/setup.d.ts +1 -0
- package/lib/setup.js +17 -0
- package/lib/types.d.ts +20 -0
- package/package.json +9 -3
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.GeoDebugPanel = void 0;
|
|
49
|
+
const react_1 = __importStar(require("react"));
|
|
50
|
+
const react_native_1 = require("react-native");
|
|
51
|
+
const index_1 = __importDefault(require("./index"));
|
|
52
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = react_native_1.Dimensions.get('window');
|
|
53
|
+
const PANEL_WIDTH = Math.round(SCREEN_WIDTH * 0.95);
|
|
54
|
+
const PILL_SIZE = 50;
|
|
55
|
+
// ─── Formatting helpers ──────────────────────────────────────────────────────
|
|
56
|
+
function formatBattery(level) {
|
|
57
|
+
return level < 0 ? 'N/A' : `${level.toFixed(1)}%`;
|
|
58
|
+
}
|
|
59
|
+
function formatElapsed(seconds) {
|
|
60
|
+
if (seconds < 60)
|
|
61
|
+
return `${Math.round(seconds)}s`;
|
|
62
|
+
const m = Math.floor(seconds / 60);
|
|
63
|
+
const s = Math.round(seconds % 60);
|
|
64
|
+
if (m < 60)
|
|
65
|
+
return `${m}m ${s}s`;
|
|
66
|
+
const h = Math.floor(m / 60);
|
|
67
|
+
return `${h}h ${m % 60}m`;
|
|
68
|
+
}
|
|
69
|
+
function formatGpsPercent(gpsActiveSeconds, elapsedSeconds) {
|
|
70
|
+
if (elapsedSeconds <= 0)
|
|
71
|
+
return 'N/A';
|
|
72
|
+
return `${Math.round((gpsActiveSeconds / elapsedSeconds) * 100)}%`;
|
|
73
|
+
}
|
|
74
|
+
function getSuggestions(info) {
|
|
75
|
+
var _a, _b, _c, _d, _e;
|
|
76
|
+
const suggestions = [];
|
|
77
|
+
const elapsed = (_a = info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
|
|
78
|
+
const updates = (_b = info.updateCount) !== null && _b !== void 0 ? _b : 0;
|
|
79
|
+
const upm = (_c = info.updatesPerMinute) !== null && _c !== void 0 ? _c : 0;
|
|
80
|
+
const gpsActive = (_d = info.gpsActiveSeconds) !== null && _d !== void 0 ? _d : 0;
|
|
81
|
+
const drainRate = (_e = info.drainRatePerHour) !== null && _e !== void 0 ? _e : 0;
|
|
82
|
+
const gpsPercent = elapsed > 0 ? (gpsActive / elapsed) * 100 : 0;
|
|
83
|
+
// Not enough data yet
|
|
84
|
+
if (updates === 0) {
|
|
85
|
+
suggestions.push({ emoji: '⏳', color: '#aaa', text: 'No location updates received yet — make sure tracking has started and permission is granted' });
|
|
86
|
+
return suggestions;
|
|
87
|
+
}
|
|
88
|
+
if (elapsed < 60) {
|
|
89
|
+
suggestions.push({ emoji: '⏳', color: '#aaa', text: `${Math.round(elapsed)}s into session — rate metrics need ~1 min to stabilise` });
|
|
90
|
+
}
|
|
91
|
+
// ── Frequency ──
|
|
92
|
+
if (upm > 20) {
|
|
93
|
+
suggestions.push({ emoji: '🔴', color: '#f55', text: `Very frequent updates (${upm.toFixed(1)}/min) — increase minDistanceMeters or updateIntervalMs to reduce battery drain` });
|
|
94
|
+
}
|
|
95
|
+
else if (upm > 8) {
|
|
96
|
+
suggestions.push({ emoji: '⚠️', color: '#fa0', text: `Frequent updates (${upm.toFixed(1)}/min) — consider increasing minDistanceMeters` });
|
|
97
|
+
}
|
|
98
|
+
else if (upm > 0) {
|
|
99
|
+
suggestions.push({ emoji: '✅', color: '#4c4', text: `Update frequency is good (${upm.toFixed(1)}/min)` });
|
|
100
|
+
}
|
|
101
|
+
// ── GPS active time (only meaningful after 60s) ──
|
|
102
|
+
if (elapsed >= 60) {
|
|
103
|
+
if (gpsPercent > 80) {
|
|
104
|
+
suggestions.push({ emoji: '🔴', color: '#f55', text: `GPS on ${Math.round(gpsPercent)}% of the time — enable adaptiveAccuracy or switch to coarseTracking to save battery` });
|
|
105
|
+
}
|
|
106
|
+
else if (gpsPercent > 50) {
|
|
107
|
+
suggestions.push({ emoji: '⚠️', color: '#fa0', text: `GPS on ${Math.round(gpsPercent)}% of time — device is mostly moving, adaptive accuracy is active` });
|
|
108
|
+
}
|
|
109
|
+
else if (gpsPercent > 0) {
|
|
110
|
+
suggestions.push({ emoji: '✅', color: '#4c4', text: `GPS on ${Math.round(gpsPercent)}% of time — adaptive accuracy is saving battery` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── Drain rate ──
|
|
114
|
+
if (drainRate > 8) {
|
|
115
|
+
suggestions.push({ emoji: '🔴', color: '#f55', text: `High drain (${drainRate.toFixed(1)}%/hr) — try accuracy: 'balanced' or increase update intervals` });
|
|
116
|
+
}
|
|
117
|
+
else if (drainRate > 4) {
|
|
118
|
+
suggestions.push({ emoji: '⚠️', color: '#fa0', text: `Moderate drain (${drainRate.toFixed(1)}%/hr) — normal for active GPS, consider adaptiveAccuracy` });
|
|
119
|
+
}
|
|
120
|
+
else if (drainRate > 0) {
|
|
121
|
+
suggestions.push({ emoji: '✅', color: '#4c4', text: `Low drain (${drainRate.toFixed(1)}%/hr) — battery usage is efficient` });
|
|
122
|
+
}
|
|
123
|
+
return suggestions;
|
|
124
|
+
}
|
|
125
|
+
// ─── Pan responder factory ───────────────────────────────────────────────────
|
|
126
|
+
function makePanResponder(pan, lastPos, maxWidth, maxHeight, onTap) {
|
|
127
|
+
const didDrag = { current: false };
|
|
128
|
+
return react_native_1.PanResponder.create({
|
|
129
|
+
onStartShouldSetPanResponder: () => true,
|
|
130
|
+
onMoveShouldSetPanResponder: () => true,
|
|
131
|
+
onPanResponderGrant: () => {
|
|
132
|
+
didDrag.current = false;
|
|
133
|
+
pan.setOffset({ x: lastPos.current.x, y: lastPos.current.y });
|
|
134
|
+
pan.setValue({ x: 0, y: 0 });
|
|
135
|
+
},
|
|
136
|
+
onPanResponderMove: (_, g) => {
|
|
137
|
+
if (Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4)
|
|
138
|
+
didDrag.current = true;
|
|
139
|
+
react_native_1.Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false })(_, g);
|
|
140
|
+
},
|
|
141
|
+
onPanResponderRelease: (_, g) => {
|
|
142
|
+
pan.flattenOffset();
|
|
143
|
+
if (!didDrag.current && onTap) {
|
|
144
|
+
onTap();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const newX = Math.max(0, Math.min(lastPos.current.x + g.dx, SCREEN_WIDTH - maxWidth));
|
|
148
|
+
const newY = Math.max(0, Math.min(lastPos.current.y + g.dy, SCREEN_HEIGHT - maxHeight));
|
|
149
|
+
lastPos.current = { x: newX, y: newY };
|
|
150
|
+
pan.setValue({ x: newX, y: newY });
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
155
|
+
const GeoDebugPanel = ({ pollInterval = 30000 }) => {
|
|
156
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
157
|
+
const [info, setInfo] = (0, react_1.useState)(null);
|
|
158
|
+
const [minimized, setMinimized] = (0, react_1.useState)(true);
|
|
159
|
+
const pan = (0, react_1.useRef)(new react_native_1.Animated.ValueXY({ x: 8, y: SCREEN_HEIGHT - 320 })).current;
|
|
160
|
+
const lastPos = (0, react_1.useRef)({ x: 8, y: SCREEN_HEIGHT - 320 });
|
|
161
|
+
const panelHeight = (0, react_1.useRef)(0);
|
|
162
|
+
const pillPanResponder = (0, react_1.useRef)(makePanResponder(pan, lastPos, PILL_SIZE, PILL_SIZE, () => {
|
|
163
|
+
const clamped = {
|
|
164
|
+
x: Math.min(lastPos.current.x, SCREEN_WIDTH - PANEL_WIDTH),
|
|
165
|
+
y: Math.min(lastPos.current.y, SCREEN_HEIGHT - panelHeight.current),
|
|
166
|
+
};
|
|
167
|
+
lastPos.current = clamped;
|
|
168
|
+
pan.setValue(clamped);
|
|
169
|
+
setMinimized(false);
|
|
170
|
+
})).current;
|
|
171
|
+
const headerPanResponder = (0, react_1.useRef)(react_native_1.PanResponder.create({
|
|
172
|
+
onStartShouldSetPanResponder: () => false,
|
|
173
|
+
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4,
|
|
174
|
+
onPanResponderGrant: () => {
|
|
175
|
+
pan.setOffset({ x: lastPos.current.x, y: lastPos.current.y });
|
|
176
|
+
pan.setValue({ x: 0, y: 0 });
|
|
177
|
+
},
|
|
178
|
+
onPanResponderMove: react_native_1.Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false }),
|
|
179
|
+
onPanResponderRelease: (_, g) => {
|
|
180
|
+
pan.flattenOffset();
|
|
181
|
+
const newX = Math.max(0, Math.min(lastPos.current.x + g.dx, SCREEN_WIDTH - PANEL_WIDTH));
|
|
182
|
+
const newY = Math.max(0, Math.min(lastPos.current.y + g.dy, SCREEN_HEIGHT - panelHeight.current));
|
|
183
|
+
lastPos.current = { x: newX, y: newY };
|
|
184
|
+
pan.setValue({ x: newX, y: newY });
|
|
185
|
+
},
|
|
186
|
+
})).current;
|
|
187
|
+
const refresh = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
try {
|
|
189
|
+
const data = yield index_1.default.getBatteryInfo();
|
|
190
|
+
setInfo(data);
|
|
191
|
+
}
|
|
192
|
+
catch (_) { }
|
|
193
|
+
}), []);
|
|
194
|
+
(0, react_1.useEffect)(() => {
|
|
195
|
+
refresh();
|
|
196
|
+
const id = setInterval(refresh, pollInterval);
|
|
197
|
+
return () => clearInterval(id);
|
|
198
|
+
}, [refresh, pollInterval]);
|
|
199
|
+
// ── Minimized pill ──
|
|
200
|
+
if (minimized) {
|
|
201
|
+
return (<react_native_1.Animated.View style={[styles.pill, { transform: pan.getTranslateTransform() }]} {...pillPanResponder.panHandlers}>
|
|
202
|
+
<react_native_1.Text style={styles.pillText}>📍</react_native_1.Text>
|
|
203
|
+
</react_native_1.Animated.View>);
|
|
204
|
+
}
|
|
205
|
+
// Safely normalise — native side returns undefined for new fields until rebuilt
|
|
206
|
+
const elapsed = (_a = info === null || info === void 0 ? void 0 : info.trackingElapsedSeconds) !== null && _a !== void 0 ? _a : 0;
|
|
207
|
+
const updates = (_b = info === null || info === void 0 ? void 0 : info.updateCount) !== null && _b !== void 0 ? _b : 0;
|
|
208
|
+
const upm = (_c = info === null || info === void 0 ? void 0 : info.updatesPerMinute) !== null && _c !== void 0 ? _c : 0;
|
|
209
|
+
const gpsActive = (_d = info === null || info === void 0 ? void 0 : info.gpsActiveSeconds) !== null && _d !== void 0 ? _d : 0;
|
|
210
|
+
const level = (_e = info === null || info === void 0 ? void 0 : info.level) !== null && _e !== void 0 ? _e : -1;
|
|
211
|
+
const drain = (_f = info === null || info === void 0 ? void 0 : info.drainSinceStart) !== null && _f !== void 0 ? _f : 0;
|
|
212
|
+
const drainRate = (_g = info === null || info === void 0 ? void 0 : info.drainRatePerHour) !== null && _g !== void 0 ? _g : 0;
|
|
213
|
+
const safeInfo = info
|
|
214
|
+
? Object.assign(Object.assign({}, info), { trackingElapsedSeconds: elapsed, updateCount: updates, updatesPerMinute: upm, gpsActiveSeconds: gpsActive, drainRatePerHour: drainRate }) : null;
|
|
215
|
+
const suggestions = safeInfo ? getSuggestions(safeInfo) : [];
|
|
216
|
+
// ── Expanded panel ──
|
|
217
|
+
return (<react_native_1.Animated.View style={[styles.container, { transform: pan.getTranslateTransform() }]} onLayout={e => { panelHeight.current = e.nativeEvent.layout.height; }}>
|
|
218
|
+
{/* Drag header */}
|
|
219
|
+
<react_native_1.View style={styles.header} {...headerPanResponder.panHandlers}>
|
|
220
|
+
<react_native_1.View style={styles.dragPill}/>
|
|
221
|
+
<react_native_1.View style={styles.titleRow}>
|
|
222
|
+
<react_native_1.Text style={styles.title}>📍 GeoService Debug</react_native_1.Text>
|
|
223
|
+
<react_native_1.TouchableOpacity onPress={() => setMinimized(true)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
|
224
|
+
<react_native_1.Text style={styles.minimizeBtn}>⊖</react_native_1.Text>
|
|
225
|
+
</react_native_1.TouchableOpacity>
|
|
226
|
+
</react_native_1.View>
|
|
227
|
+
<react_native_1.Text style={styles.dragHint}>⠿ hold & drag to move</react_native_1.Text>
|
|
228
|
+
</react_native_1.View>
|
|
229
|
+
|
|
230
|
+
<react_native_1.View style={styles.body}>
|
|
231
|
+
{info ? (<>
|
|
232
|
+
{/* ── Metrics grid ── */}
|
|
233
|
+
<react_native_1.View style={styles.grid}>
|
|
234
|
+
<MetricBox label="Tracking for" value={elapsed > 0 ? formatElapsed(elapsed) : '—'} desc="session duration"/>
|
|
235
|
+
<MetricBox label="Updates" value={`${updates}`} desc="location fixes received"/>
|
|
236
|
+
<MetricBox label="Updates/min" value={upm > 0 ? upm.toFixed(1) : '—'} desc="higher = more battery"/>
|
|
237
|
+
<MetricBox label="GPS active" value={elapsed > 0 ? formatGpsPercent(gpsActive, elapsed) : '—'} desc="lower = adaptive saving"/>
|
|
238
|
+
<MetricBox label="Battery now" value={formatBattery(level)} desc={info.isCharging ? '⚡ charging' : 'not charging'}/>
|
|
239
|
+
<MetricBox label="Drained" value={level < 0 ? 'N/A' : `${drain.toFixed(2)}%`} desc="since tracking started"/>
|
|
240
|
+
<MetricBox label="Drain rate" value={drainRate > 0 ? `${drainRate.toFixed(1)}%/hr` : '—'} desc="whole device, not just GPS"/>
|
|
241
|
+
</react_native_1.View>
|
|
242
|
+
|
|
243
|
+
{/* ── Suggestions ── */}
|
|
244
|
+
<react_native_1.View style={styles.divider}/>
|
|
245
|
+
<react_native_1.Text style={styles.sectionLabel}>SUGGESTIONS</react_native_1.Text>
|
|
246
|
+
{suggestions.map((s, i) => (<react_native_1.View key={i} style={styles.suggestion}>
|
|
247
|
+
<react_native_1.Text style={styles.suggestionEmoji}>{s.emoji}</react_native_1.Text>
|
|
248
|
+
<react_native_1.Text style={[styles.suggestionText, { color: s.color }]}>{s.text}</react_native_1.Text>
|
|
249
|
+
</react_native_1.View>))}
|
|
250
|
+
</>) : (<react_native_1.Text style={styles.label}>Waiting for data…</react_native_1.Text>)}
|
|
251
|
+
</react_native_1.View>
|
|
252
|
+
</react_native_1.Animated.View>);
|
|
253
|
+
};
|
|
254
|
+
exports.GeoDebugPanel = GeoDebugPanel;
|
|
255
|
+
// ─── MetricBox sub-component ─────────────────────────────────────────────────
|
|
256
|
+
const MetricBox = ({ label, value, desc }) => (<react_native_1.View style={styles.metricBox}>
|
|
257
|
+
<react_native_1.Text style={styles.metricValue}>{value}</react_native_1.Text>
|
|
258
|
+
<react_native_1.Text style={styles.metricLabel}>{label}</react_native_1.Text>
|
|
259
|
+
{desc && <react_native_1.Text style={styles.metricSub}>{desc}</react_native_1.Text>}
|
|
260
|
+
</react_native_1.View>);
|
|
261
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
262
|
+
const styles = react_native_1.StyleSheet.create({
|
|
263
|
+
container: {
|
|
264
|
+
position: 'absolute',
|
|
265
|
+
width: PANEL_WIDTH,
|
|
266
|
+
backgroundColor: 'rgba(20,20,20,0.92)',
|
|
267
|
+
borderRadius: 12,
|
|
268
|
+
overflow: 'hidden',
|
|
269
|
+
borderWidth: 1,
|
|
270
|
+
borderColor: 'rgba(255,255,255,0.15)',
|
|
271
|
+
},
|
|
272
|
+
header: {
|
|
273
|
+
paddingTop: 6,
|
|
274
|
+
paddingBottom: 8,
|
|
275
|
+
paddingHorizontal: 12,
|
|
276
|
+
backgroundColor: 'rgba(255,255,255,0.07)',
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
},
|
|
279
|
+
dragPill: {
|
|
280
|
+
width: 40, height: 4, borderRadius: 2,
|
|
281
|
+
backgroundColor: 'rgba(255,255,255,0.5)',
|
|
282
|
+
marginBottom: 8,
|
|
283
|
+
},
|
|
284
|
+
titleRow: {
|
|
285
|
+
flexDirection: 'row', justifyContent: 'space-between',
|
|
286
|
+
alignItems: 'center', width: '100%',
|
|
287
|
+
},
|
|
288
|
+
title: { color: '#fff', fontWeight: 'bold', fontSize: 12 },
|
|
289
|
+
minimizeBtn: { color: '#aaa', fontSize: 18 },
|
|
290
|
+
dragHint: { color: 'rgba(255,255,255,0.35)', fontSize: 9, marginTop: 4 },
|
|
291
|
+
body: { padding: 12 },
|
|
292
|
+
label: { color: '#ddd', fontSize: 12 },
|
|
293
|
+
// Metrics grid
|
|
294
|
+
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 10 },
|
|
295
|
+
metricBox: {
|
|
296
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
297
|
+
borderRadius: 8, padding: 8,
|
|
298
|
+
minWidth: '30%', flex: 1,
|
|
299
|
+
alignItems: 'center',
|
|
300
|
+
},
|
|
301
|
+
metricValue: { color: '#fff', fontWeight: 'bold', fontSize: 15 },
|
|
302
|
+
metricLabel: { color: 'rgba(255,255,255,0.5)', fontSize: 9, marginTop: 2, textAlign: 'center' },
|
|
303
|
+
metricSub: { color: 'rgba(255,255,255,0.3)', fontSize: 8, textAlign: 'center' },
|
|
304
|
+
// Suggestions
|
|
305
|
+
divider: { height: 1, backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 8 },
|
|
306
|
+
sectionLabel: { color: 'rgba(255,255,255,0.35)', fontSize: 9, letterSpacing: 1, marginBottom: 6 },
|
|
307
|
+
suggestion: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 5, gap: 6 },
|
|
308
|
+
suggestionEmoji: { fontSize: 12, lineHeight: 16 },
|
|
309
|
+
suggestionText: { fontSize: 11, lineHeight: 16, flex: 1 },
|
|
310
|
+
// Minimized pill
|
|
311
|
+
pill: {
|
|
312
|
+
position: 'absolute',
|
|
313
|
+
width: PILL_SIZE, height: PILL_SIZE, borderRadius: PILL_SIZE / 2,
|
|
314
|
+
backgroundColor: 'rgba(20,20,20,0.92)',
|
|
315
|
+
borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)',
|
|
316
|
+
alignItems: 'center', justifyContent: 'center',
|
|
317
|
+
},
|
|
318
|
+
pillText: { fontSize: 24, textAlign: 'center' },
|
|
319
|
+
});
|
package/lib/autoDebug.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mountDebugOverlay = mountDebugOverlay;
|
|
7
|
+
exports.unmountDebugOverlay = unmountDebugOverlay;
|
|
8
|
+
const react_1 = __importDefault(require("react"));
|
|
9
|
+
const react_native_root_siblings_1 = __importDefault(require("react-native-root-siblings"));
|
|
10
|
+
const GeoDebugOverlay_1 = require("./GeoDebugOverlay");
|
|
11
|
+
let sibling = null;
|
|
12
|
+
function mountDebugOverlay() {
|
|
13
|
+
if (!sibling) {
|
|
14
|
+
sibling = new react_native_root_siblings_1.default(react_1.default.createElement(GeoDebugOverlay_1.GeoDebugOverlay));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function unmountDebugOverlay() {
|
|
18
|
+
if (sibling) {
|
|
19
|
+
sibling.destroy();
|
|
20
|
+
sibling = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { GeoServiceConfig, GeoSubscription, Location, LocationCallback, ErrorCallback } from './types';
|
|
1
|
+
import { GeoServiceConfig, GeoSubscription, Location, LocationCallback, ErrorCallback, BatteryInfo } from './types';
|
|
2
2
|
export * from './types';
|
|
3
|
+
export declare function _isDebugMode(): boolean;
|
|
3
4
|
/**
|
|
4
5
|
* Configure the geo service. Call this before start().
|
|
5
6
|
* Safe to call multiple times; subsequent calls update the config.
|
|
7
|
+
*
|
|
8
|
+
* When debug: true, a draggable debug overlay is mounted automatically —
|
|
9
|
+
* no component needs to be added to the app.
|
|
6
10
|
*/
|
|
7
11
|
declare function configure(config: GeoServiceConfig): Promise<void>;
|
|
8
12
|
/**
|
|
@@ -60,6 +64,16 @@ declare function onError(callback: ErrorCallback): GeoSubscription;
|
|
|
60
64
|
* @platform android
|
|
61
65
|
*/
|
|
62
66
|
declare function registerHeadlessTask(handler: (location: Location) => Promise<void>): void;
|
|
67
|
+
/**
|
|
68
|
+
* Returns battery information including current level and drain since tracking started.
|
|
69
|
+
* Only meaningful after start() has been called.
|
|
70
|
+
*/
|
|
71
|
+
declare function getBatteryInfo(): Promise<BatteryInfo>;
|
|
72
|
+
/**
|
|
73
|
+
* Show or hide the status bar location indicator at runtime (iOS only).
|
|
74
|
+
* On Android this is a no-op — the foreground notification handles visibility.
|
|
75
|
+
*/
|
|
76
|
+
declare function setLocationIndicator(show: boolean): Promise<void>;
|
|
63
77
|
declare const RNGeoService: {
|
|
64
78
|
configure: typeof configure;
|
|
65
79
|
start: typeof start;
|
|
@@ -69,5 +83,9 @@ declare const RNGeoService: {
|
|
|
69
83
|
onLocation: typeof onLocation;
|
|
70
84
|
onError: typeof onError;
|
|
71
85
|
registerHeadlessTask: typeof registerHeadlessTask;
|
|
86
|
+
getBatteryInfo: typeof getBatteryInfo;
|
|
87
|
+
setLocationIndicator: typeof setLocationIndicator;
|
|
72
88
|
};
|
|
73
89
|
export default RNGeoService;
|
|
90
|
+
export { GeoDebugPanel } from './GeoDebugPanel';
|
|
91
|
+
export { GeoDebugOverlay } from './GeoDebugOverlay';
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,8 @@ 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;
|
|
27
|
+
exports._isDebugMode = _isDebugMode;
|
|
26
28
|
const react_native_1 = require("react-native");
|
|
27
29
|
__exportStar(require("./types"), exports);
|
|
28
30
|
// The native bridge module registered as "RNGeoService" on both platforms
|
|
@@ -52,14 +54,35 @@ const DEFAULT_CONFIG = {
|
|
|
52
54
|
idleSampleCount: 3,
|
|
53
55
|
debug: false,
|
|
54
56
|
};
|
|
57
|
+
// Tracks the debug flag set via configure() so GeoDebugOverlay can
|
|
58
|
+
// read it without the consuming app having to pass it as a prop.
|
|
59
|
+
let _debugMode = false;
|
|
60
|
+
function _isDebugMode() { return _debugMode; }
|
|
55
61
|
/**
|
|
56
62
|
* Configure the geo service. Call this before start().
|
|
57
63
|
* Safe to call multiple times; subsequent calls update the config.
|
|
64
|
+
*
|
|
65
|
+
* When debug: true, a draggable debug overlay is mounted automatically —
|
|
66
|
+
* no component needs to be added to the app.
|
|
58
67
|
*/
|
|
59
68
|
function configure(config) {
|
|
60
69
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
var _a;
|
|
71
|
+
const wasDebug = _debugMode;
|
|
72
|
+
_debugMode = (_a = config.debug) !== null && _a !== void 0 ? _a : false;
|
|
61
73
|
const merged = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
|
|
62
|
-
|
|
74
|
+
const result = nativeModule.configure(merged);
|
|
75
|
+
// Lazy require to avoid circular dependency at module init time.
|
|
76
|
+
// autoDebug → GeoDebugOverlay → GeoDebugPanel → index (all lazy references).
|
|
77
|
+
if (_debugMode && !wasDebug) {
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
79
|
+
require('./autoDebug').mountDebugOverlay();
|
|
80
|
+
}
|
|
81
|
+
else if (!_debugMode && wasDebug) {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
83
|
+
require('./autoDebug').unmountDebugOverlay();
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
63
86
|
});
|
|
64
87
|
}
|
|
65
88
|
/**
|
|
@@ -69,7 +92,12 @@ function configure(config) {
|
|
|
69
92
|
*/
|
|
70
93
|
function start() {
|
|
71
94
|
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
-
|
|
95
|
+
const result = nativeModule.start();
|
|
96
|
+
if (_debugMode) {
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
98
|
+
require('./autoDebug').mountDebugOverlay();
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
73
101
|
});
|
|
74
102
|
}
|
|
75
103
|
/**
|
|
@@ -77,7 +105,12 @@ function start() {
|
|
|
77
105
|
*/
|
|
78
106
|
function stop() {
|
|
79
107
|
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
-
|
|
108
|
+
const result = nativeModule.stop();
|
|
109
|
+
if (_debugMode) {
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
111
|
+
require('./autoDebug').unmountDebugOverlay();
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
81
114
|
});
|
|
82
115
|
}
|
|
83
116
|
/**
|
|
@@ -142,6 +175,24 @@ function registerHeadlessTask(handler) {
|
|
|
142
175
|
const taskName = DEFAULT_CONFIG.backgroundTaskName;
|
|
143
176
|
react_native_1.AppRegistry.registerHeadlessTask(taskName, () => handler);
|
|
144
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Returns battery information including current level and drain since tracking started.
|
|
180
|
+
* Only meaningful after start() has been called.
|
|
181
|
+
*/
|
|
182
|
+
function getBatteryInfo() {
|
|
183
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
184
|
+
return nativeModule.getBatteryInfo();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Show or hide the status bar location indicator at runtime (iOS only).
|
|
189
|
+
* On Android this is a no-op — the foreground notification handles visibility.
|
|
190
|
+
*/
|
|
191
|
+
function setLocationIndicator(show) {
|
|
192
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
return nativeModule.setLocationIndicator(show);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
145
196
|
const RNGeoService = {
|
|
146
197
|
configure,
|
|
147
198
|
start,
|
|
@@ -151,5 +202,11 @@ const RNGeoService = {
|
|
|
151
202
|
onLocation,
|
|
152
203
|
onError,
|
|
153
204
|
registerHeadlessTask,
|
|
205
|
+
getBatteryInfo,
|
|
206
|
+
setLocationIndicator,
|
|
154
207
|
};
|
|
155
208
|
exports.default = RNGeoService;
|
|
209
|
+
var GeoDebugPanel_1 = require("./GeoDebugPanel");
|
|
210
|
+
Object.defineProperty(exports, "GeoDebugPanel", { enumerable: true, get: function () { return GeoDebugPanel_1.GeoDebugPanel; } });
|
|
211
|
+
var GeoDebugOverlay_1 = require("./GeoDebugOverlay");
|
|
212
|
+
Object.defineProperty(exports, "GeoDebugOverlay", { enumerable: true, get: function () { return GeoDebugOverlay_1.GeoDebugOverlay; } });
|
package/lib/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/setup.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Import this file ONCE at the top of your app's index.js, before
|
|
5
|
+
* AppRegistry.registerComponent. This registers RootSiblingParent as
|
|
6
|
+
* the app wrapper so GeoService can render the debug overlay automatically
|
|
7
|
+
* when configure({ debug: true }) is called — no component needed in the tree.
|
|
8
|
+
*
|
|
9
|
+
* index.js:
|
|
10
|
+
* import '@tsachit/react-native-geo-service/setup'; // ← add this line
|
|
11
|
+
* import { AppRegistry } from 'react-native';
|
|
12
|
+
* import App from './App';
|
|
13
|
+
* AppRegistry.registerComponent('MyApp', () => App);
|
|
14
|
+
*/
|
|
15
|
+
const react_native_1 = require("react-native");
|
|
16
|
+
const react_native_root_siblings_1 = require("react-native-root-siblings");
|
|
17
|
+
react_native_1.AppRegistry.setWrapperComponentProvider(() => react_native_root_siblings_1.RootSiblingParent);
|
package/lib/types.d.ts
CHANGED
|
@@ -137,3 +137,23 @@ export type ErrorCallback = (error: LocationError) => void;
|
|
|
137
137
|
export interface GeoSubscription {
|
|
138
138
|
remove(): void;
|
|
139
139
|
}
|
|
140
|
+
export interface BatteryInfo {
|
|
141
|
+
/** Current battery level 0–100 */
|
|
142
|
+
level: number;
|
|
143
|
+
/** Whether the device is currently charging */
|
|
144
|
+
isCharging: boolean;
|
|
145
|
+
/** Battery level when tracking was started (0–100) */
|
|
146
|
+
levelAtStart: number;
|
|
147
|
+
/** Percentage points drained since tracking started */
|
|
148
|
+
drainSinceStart: number;
|
|
149
|
+
/** How many location fixes have been delivered since start() */
|
|
150
|
+
updateCount: number;
|
|
151
|
+
/** Total seconds since start() was called */
|
|
152
|
+
trackingElapsedSeconds: number;
|
|
153
|
+
/** Seconds the GPS chip was actively running (not in idle/low-power mode) */
|
|
154
|
+
gpsActiveSeconds: number;
|
|
155
|
+
/** Location updates per minute (averaged over entire session) */
|
|
156
|
+
updatesPerMinute: number;
|
|
157
|
+
/** Battery drain rate in percentage points per hour */
|
|
158
|
+
drainRatePerHour: number;
|
|
159
|
+
}
|
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.2",
|
|
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",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"lib",
|
|
9
9
|
"android",
|
|
10
10
|
"ios",
|
|
11
|
-
"react-native-geo-service.podspec"
|
|
11
|
+
"react-native-geo-service.podspec",
|
|
12
|
+
"debug-panel.js",
|
|
13
|
+
"debug-panel.d.ts"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"build": "tsc",
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
"react-native": ">=0.60.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
33
|
+
"@types/react": "^18.3.28",
|
|
31
34
|
"@types/react-native": "0.73.0",
|
|
32
35
|
"typescript": "5.9.3"
|
|
33
36
|
},
|
|
@@ -38,5 +41,8 @@
|
|
|
38
41
|
"bugs": {
|
|
39
42
|
"url": "https://github.com/tsachit/react-native-geo-service/issues"
|
|
40
43
|
},
|
|
41
|
-
"homepage": "https://github.com/tsachit/react-native-geo-service#readme"
|
|
44
|
+
"homepage": "https://github.com/tsachit/react-native-geo-service#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"react-native-root-siblings": "^5.0.1"
|
|
47
|
+
}
|
|
42
48
|
}
|