@tsachit/react-native-geo-service 1.0.0 → 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 +11 -5
package/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
# react-native-geo-service
|
|
1
|
+
# @tsachit/react-native-geo-service
|
|
2
2
|
|
|
3
|
-
Battery-efficient background geolocation for React Native.
|
|
3
|
+
Battery-efficient background geolocation for React Native — a lightweight, free alternative to commercial packages.
|
|
4
4
|
|
|
5
5
|
- Tracks location as the user moves and fires a JS listener
|
|
6
|
-
- Keeps tracking
|
|
7
|
-
- Uses `FusedLocationProviderClient` on Android and `CLLocationManager` on iOS
|
|
8
|
-
- **Adaptive accuracy
|
|
9
|
-
-
|
|
6
|
+
- Keeps tracking when the app is backgrounded or killed (headless mode)
|
|
7
|
+
- Uses `FusedLocationProviderClient` on Android and `CLLocationManager` on iOS
|
|
8
|
+
- **Adaptive accuracy** — GPS turns off automatically when the device is idle and wakes the moment movement is detected
|
|
9
|
+
- **Debug panel** — draggable floating overlay that mounts automatically when `debug: true`, showing live metrics, GPS activity, and battery saving suggestions with no component needed in the app tree
|
|
10
|
+
- Fully configurable from JavaScript — no API keys, no license required
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
|
|
17
|
+
yarn add @tsachit/react-native-geo-service
|
|
17
18
|
# or
|
|
18
|
-
|
|
19
|
+
npm install @tsachit/react-native-geo-service
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
### iOS
|
|
@@ -24,7 +25,7 @@ yarn add react-native-geo-service
|
|
|
24
25
|
cd ios && pod install
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
Add
|
|
28
|
+
Add to your `Info.plist`:
|
|
28
29
|
|
|
29
30
|
```xml
|
|
30
31
|
<!-- Required — explain why you need location -->
|
|
@@ -41,23 +42,18 @@ Add the following to your `Info.plist`:
|
|
|
41
42
|
</array>
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
#### iOS
|
|
45
|
+
#### iOS — AppDelegate (headless relaunch)
|
|
45
46
|
|
|
46
|
-
When
|
|
47
|
-
`coarseTracking: true` (fires when the device moves ~500 m) or standard background
|
|
48
|
-
location updates (requires the Always permission).
|
|
49
|
-
|
|
50
|
-
Add this to your **AppDelegate** so tracking resumes after a background relaunch:
|
|
47
|
+
When iOS relaunches a terminated app for a location event, add this so tracking resumes automatically:
|
|
51
48
|
|
|
52
49
|
```objc
|
|
53
50
|
// AppDelegate.m
|
|
54
51
|
- (BOOL)application:(UIApplication *)application
|
|
55
52
|
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
|
56
53
|
|
|
57
|
-
// If the app was relaunched in the background due to a location event,
|
|
58
|
-
// the location manager delegate will resume tracking automatically.
|
|
59
54
|
if (launchOptions[UIApplicationLaunchOptionsLocationKey]) {
|
|
60
|
-
//
|
|
55
|
+
// RNGeoService detects this automatically and resumes tracking
|
|
56
|
+
// from the config it persisted to NSUserDefaults before termination.
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
// ... rest of your setup
|
|
@@ -70,7 +66,7 @@ Add this to your **AppDelegate** so tracking resumes after a background relaunch
|
|
|
70
66
|
|
|
71
67
|
#### 1. Register the package
|
|
72
68
|
|
|
73
|
-
**`android/app/src/main/java/.../MainApplication.kt
|
|
69
|
+
**`android/app/src/main/java/.../MainApplication.kt`**:
|
|
74
70
|
|
|
75
71
|
```kotlin
|
|
76
72
|
import com.geoservice.GeoServicePackage
|
|
@@ -83,7 +79,7 @@ override fun getPackages(): List<ReactPackage> =
|
|
|
83
79
|
|
|
84
80
|
#### 2. Register the HeadlessJS task
|
|
85
81
|
|
|
86
|
-
In your app's **`index.js`** (
|
|
82
|
+
In your app's **`index.js`** (top level, outside any component):
|
|
87
83
|
|
|
88
84
|
```js
|
|
89
85
|
import { AppRegistry } from 'react-native';
|
|
@@ -91,99 +87,115 @@ import App from './App';
|
|
|
91
87
|
|
|
92
88
|
AppRegistry.registerComponent('YourApp', () => App);
|
|
93
89
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
90
|
+
// Handles location events when the React context is not active.
|
|
91
|
+
// Runs even when the app is killed (foreground service must be running).
|
|
96
92
|
AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
|
|
97
93
|
console.log('[Background] Location:', location);
|
|
98
|
-
// Send to your server using a pre-stored auth token (e.g.
|
|
99
|
-
//
|
|
94
|
+
// Send to your server using a pre-stored auth token (e.g. SecureStore/Keychain).
|
|
95
|
+
// Do not rely on in-memory state — this JS context is isolated.
|
|
100
96
|
});
|
|
101
97
|
```
|
|
102
98
|
|
|
103
99
|
#### 3. Add permissions to `AndroidManifest.xml`
|
|
104
100
|
|
|
105
|
-
The library's manifest already declares the permissions, but your **app's** manifest must
|
|
106
|
-
also include them (merging happens at build time):
|
|
107
|
-
|
|
108
101
|
```xml
|
|
109
102
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
110
103
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
111
|
-
<!-- Android 10+ — required for background access
|
|
104
|
+
<!-- Android 10+ — required for background access -->
|
|
112
105
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
113
106
|
```
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
user has already granted foreground location. Use
|
|
119
|
-
[`react-native-permissions`](https://github.com/zoontek/react-native-permissions) or the
|
|
120
|
-
built-in `PermissionsAndroid` API.
|
|
112
|
+
### Request permissions first
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
import { PermissionsAndroid, Platform } from 'react-native';
|
|
114
|
+
Always request OS permission before calling `start()`. We recommend [`react-native-permissions`](https://github.com/zoontek/react-native-permissions):
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
```ts
|
|
117
|
+
import { request, PERMISSIONS, RESULTS, Platform } from 'react-native-permissions';
|
|
118
|
+
|
|
119
|
+
async function requestLocationPermissions(): Promise<boolean> {
|
|
120
|
+
if (Platform.OS === 'ios') {
|
|
121
|
+
const result = await request(PERMISSIONS.IOS.LOCATION_ALWAYS);
|
|
122
|
+
return result === RESULTS.GRANTED;
|
|
123
|
+
}
|
|
127
124
|
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
);
|
|
125
|
+
const fine = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
|
|
126
|
+
if (fine !== RESULTS.GRANTED) return false;
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION
|
|
136
|
-
);
|
|
128
|
+
if (Number(Platform.Version) >= 29) {
|
|
129
|
+
const bg = await request(PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION);
|
|
130
|
+
return bg === RESULTS.GRANTED;
|
|
137
131
|
}
|
|
132
|
+
return true;
|
|
138
133
|
}
|
|
139
134
|
```
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
## Usage
|
|
136
|
+
### Start tracking
|
|
144
137
|
|
|
145
138
|
```ts
|
|
146
|
-
import RNGeoService from 'react-native-geo-service';
|
|
139
|
+
import RNGeoService from '@tsachit/react-native-geo-service';
|
|
147
140
|
|
|
148
|
-
// 1.
|
|
141
|
+
// 1. Request OS permission
|
|
142
|
+
const granted = await requestLocationPermissions();
|
|
143
|
+
if (!granted) return;
|
|
144
|
+
|
|
145
|
+
// 2. Configure (call once before start, safe to call again to update)
|
|
149
146
|
await RNGeoService.configure({
|
|
150
|
-
minDistanceMeters: 10,
|
|
151
|
-
accuracy: 'balanced',
|
|
152
|
-
stopOnAppClose: false,
|
|
153
|
-
restartOnBoot: true,
|
|
147
|
+
minDistanceMeters: 10,
|
|
148
|
+
accuracy: 'balanced',
|
|
149
|
+
stopOnAppClose: false,
|
|
150
|
+
restartOnBoot: true,
|
|
154
151
|
serviceTitle: 'Tracking active',
|
|
155
152
|
serviceBody: 'Your route is being recorded.',
|
|
156
153
|
});
|
|
157
154
|
|
|
158
|
-
//
|
|
155
|
+
// 3. Start tracking
|
|
159
156
|
await RNGeoService.start();
|
|
160
157
|
|
|
161
|
-
//
|
|
158
|
+
// 4. Listen for updates
|
|
162
159
|
const subscription = RNGeoService.onLocation((location) => {
|
|
163
160
|
console.log(location.latitude, location.longitude);
|
|
164
|
-
console.log('GPS
|
|
161
|
+
console.log('Idle (GPS off):', location.isStationary);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// 5. Listen for errors
|
|
165
|
+
const errorSub = RNGeoService.onError((error) => {
|
|
166
|
+
console.error('Location error:', error.code, error.message);
|
|
165
167
|
});
|
|
166
168
|
|
|
167
|
-
//
|
|
169
|
+
// 6. Stop
|
|
168
170
|
await RNGeoService.stop();
|
|
169
171
|
|
|
170
|
-
//
|
|
172
|
+
// 7. Clean up listeners
|
|
171
173
|
subscription.remove();
|
|
174
|
+
errorSub.remove();
|
|
172
175
|
```
|
|
173
176
|
|
|
174
177
|
### One-time location
|
|
175
178
|
|
|
176
179
|
```ts
|
|
177
180
|
const location = await RNGeoService.getCurrentLocation();
|
|
178
|
-
console.log(location.latitude, location.longitude);
|
|
179
181
|
```
|
|
180
182
|
|
|
181
|
-
### Check tracking
|
|
183
|
+
### Check if tracking
|
|
182
184
|
|
|
183
185
|
```ts
|
|
184
186
|
const tracking = await RNGeoService.isTracking();
|
|
185
187
|
```
|
|
186
188
|
|
|
189
|
+
### Register headless task via the module
|
|
190
|
+
|
|
191
|
+
Alternatively to `AppRegistry.registerHeadlessTask`, you can use the helper:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
RNGeoService.registerHeadlessTask(async (location) => {
|
|
195
|
+
await sendToServer(location);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
187
199
|
---
|
|
188
200
|
|
|
189
201
|
## Configuration reference
|
|
@@ -191,7 +203,7 @@ const tracking = await RNGeoService.isTracking();
|
|
|
191
203
|
| Option | Type | Default | Description |
|
|
192
204
|
|--------|------|---------|-------------|
|
|
193
205
|
| `minDistanceMeters` | `number` | `10` | Minimum metres of movement before a location update fires |
|
|
194
|
-
| `accuracy` | `'navigation' \| 'high' \| 'balanced' \| 'low'` | `'balanced'` | Location accuracy
|
|
206
|
+
| `accuracy` | `'navigation' \| 'high' \| 'balanced' \| 'low'` | `'balanced'` | Location accuracy — higher accuracy uses more battery |
|
|
195
207
|
| `stopOnAppClose` | `boolean` | `false` | Stop tracking when the app is killed |
|
|
196
208
|
| `restartOnBoot` | `boolean` | `false` | Resume tracking after device reboot *(Android only)* |
|
|
197
209
|
| `updateIntervalMs` | `number` | `5000` | Target ms between updates *(Android only)* |
|
|
@@ -199,49 +211,215 @@ const tracking = await RNGeoService.isTracking();
|
|
|
199
211
|
| `serviceTitle` | `string` | `'Location Tracking'` | Foreground service notification title *(Android only)* |
|
|
200
212
|
| `serviceBody` | `string` | `'Your location is being tracked...'` | Foreground service notification body *(Android only)* |
|
|
201
213
|
| `backgroundTaskName` | `string` | `'GeoServiceHeadlessTask'` | HeadlessJS task name *(Android only)* |
|
|
202
|
-
| `motionActivity` | `'other' \| 'automotiveNavigation' \| 'fitness' \| 'otherNavigation' \| 'airborne'` | `'other'` |
|
|
203
|
-
| `autoPauseUpdates` | `boolean` | `false` | Let iOS pause updates when no movement *(iOS only)* |
|
|
204
|
-
| `showBackgroundIndicator` | `boolean` | `false` | Show blue bar in status bar while tracking
|
|
205
|
-
| `coarseTracking` | `boolean` | `false` | Use significant-change monitoring — very battery-efficient, wakes app
|
|
206
|
-
| `adaptiveAccuracy` | `boolean` | `true` | Auto-drop to low-power when idle, restore on movement |
|
|
214
|
+
| `motionActivity` | `'other' \| 'automotiveNavigation' \| 'fitness' \| 'otherNavigation' \| 'airborne'` | `'other'` | Activity hint for iOS power optimisations *(iOS only)* |
|
|
215
|
+
| `autoPauseUpdates` | `boolean` | `false` | Let iOS pause updates when no movement detected *(iOS only)* |
|
|
216
|
+
| `showBackgroundIndicator` | `boolean` | `false` | Show blue location bar in status bar while tracking *(iOS only)* |
|
|
217
|
+
| `coarseTracking` | `boolean` | `false` | Use significant-change monitoring only — very battery-efficient, wakes terminated app *(iOS only)* |
|
|
218
|
+
| `adaptiveAccuracy` | `boolean` | `true` | Auto-drop to low-power when idle, restore on movement (biggest battery saver) |
|
|
207
219
|
| `idleSpeedThreshold` | `number` | `0.5` | Speed in m/s below which a reading counts as idle |
|
|
208
|
-
| `idleSampleCount` | `number` | `3` | Consecutive idle readings before entering low-power mode |
|
|
209
|
-
| `debug` | `boolean` | `false` | Enable verbose native logging |
|
|
220
|
+
| `idleSampleCount` | `number` | `3` | Consecutive idle readings required before entering low-power mode |
|
|
221
|
+
| `debug` | `boolean` | `false` | Enable verbose native logging + debug notification on Android + status bar indicator on iOS |
|
|
210
222
|
|
|
211
223
|
---
|
|
212
224
|
|
|
213
|
-
##
|
|
225
|
+
## API reference
|
|
226
|
+
|
|
227
|
+
### `configure(config)`
|
|
228
|
+
Apply configuration. Safe to call multiple times — subsequent calls update the running config.
|
|
229
|
+
|
|
230
|
+
### `start()`
|
|
231
|
+
Start background location tracking. Always call `requestLocationPermissions()` before this.
|
|
232
|
+
|
|
233
|
+
### `stop()`
|
|
234
|
+
Stop tracking and remove the foreground service (Android) / stop CLLocationManager (iOS).
|
|
235
|
+
|
|
236
|
+
### `isTracking(): Promise<boolean>`
|
|
237
|
+
Returns whether tracking is currently active.
|
|
238
|
+
|
|
239
|
+
### `getCurrentLocation(): Promise<Location>`
|
|
240
|
+
One-time location fetch from the last known position.
|
|
241
|
+
|
|
242
|
+
### `onLocation(callback): GeoSubscription`
|
|
243
|
+
Subscribe to location updates. Call `.remove()` on the returned subscription to unsubscribe.
|
|
244
|
+
|
|
245
|
+
### `onError(callback): GeoSubscription`
|
|
246
|
+
Subscribe to location errors (e.g. permission revoked mid-session).
|
|
247
|
+
|
|
248
|
+
### `registerHeadlessTask(handler)` *(Android only)*
|
|
249
|
+
Register a function to handle location events when the app is not in the foreground.
|
|
250
|
+
|
|
251
|
+
### `getBatteryInfo(): Promise<BatteryInfo>`
|
|
252
|
+
Returns battery and session tracking metrics. See [Debug mode](#debug-mode) below.
|
|
253
|
+
|
|
254
|
+
### `setLocationIndicator(show: boolean)` *(iOS only)*
|
|
255
|
+
Show or hide the blue location indicator in the status bar at runtime. No-op on Android.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Type reference
|
|
260
|
+
|
|
261
|
+
### `Location`
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
interface Location {
|
|
265
|
+
latitude: number;
|
|
266
|
+
longitude: number;
|
|
267
|
+
accuracy: number; // horizontal accuracy in metres
|
|
268
|
+
altitude: number;
|
|
269
|
+
altitudeAccuracy: number; // vertical accuracy in metres (iOS only, -1 on Android)
|
|
270
|
+
speed: number; // m/s, -1 if unavailable
|
|
271
|
+
bearing: number; // degrees 0–360, -1 if unavailable
|
|
272
|
+
timestamp: number; // Unix ms
|
|
273
|
+
isFromMockProvider?: boolean; // Android only
|
|
274
|
+
isStationary?: boolean; // true when adaptive accuracy has turned GPS off
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `BatteryInfo`
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
interface BatteryInfo {
|
|
282
|
+
level: number; // current battery level 0–100
|
|
283
|
+
isCharging: boolean;
|
|
284
|
+
levelAtStart: number; // battery level when start() was called
|
|
285
|
+
drainSinceStart: number; // total % dropped since start() (whole device)
|
|
286
|
+
|
|
287
|
+
updateCount: number; // location fixes received this session
|
|
288
|
+
trackingElapsedSeconds: number; // seconds since start() was called
|
|
289
|
+
gpsActiveSeconds: number; // seconds the GPS chip was actively running
|
|
290
|
+
updatesPerMinute: number; // average location fixes per minute
|
|
291
|
+
drainRatePerHour: number; // battery drain rate in %/hr (whole device)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### `GeoServiceConfig`
|
|
296
|
+
See [Configuration reference](#configuration-reference) above.
|
|
297
|
+
|
|
298
|
+
### `GeoSubscription`
|
|
214
299
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
300
|
+
```ts
|
|
301
|
+
interface GeoSubscription {
|
|
302
|
+
remove(): void;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Debug mode
|
|
309
|
+
|
|
310
|
+
Set `debug: true` in `configure()` to enable debug features:
|
|
311
|
+
|
|
312
|
+
- **iOS** — forces the blue location arrow in the status bar while tracking is active
|
|
313
|
+
- **Android** — notification title changes to `[DEBUG] <title>` so you can confirm the foreground service is running
|
|
314
|
+
- **Both** — verbose native logging via `console.log` / `Logcat`
|
|
315
|
+
- **Both** — a floating debug panel appears automatically showing live metrics and battery saving suggestions
|
|
316
|
+
|
|
317
|
+
### Setup (one-time)
|
|
318
|
+
|
|
319
|
+
Add one import to the **top** of your app's `index.js`, before `AppRegistry.registerComponent`. This registers the overlay host so the panel can mount itself automatically when `debug: true` — no component needed anywhere in the app.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
// index.js
|
|
323
|
+
import 'react-native-gesture-handler';
|
|
324
|
+
import '@tsachit/react-native-geo-service/debug-panel'; // ← add this once
|
|
325
|
+
import { AppRegistry } from 'react-native';
|
|
326
|
+
import App from './App';
|
|
327
|
+
import { name as appName } from './app.json';
|
|
328
|
+
|
|
329
|
+
AppRegistry.registerComponent(appName, () => App);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
> **Why must it be in `index.js`?** React Native's `AppRegistry.setWrapperComponentProvider` must be called before `registerComponent` — the same reason `react-native-gesture-handler` must also be imported there. Placing it anywhere else (e.g. inside a hook or screen) is too late; the app root has already mounted.
|
|
333
|
+
|
|
334
|
+
### Auto-mount debug panel
|
|
335
|
+
|
|
336
|
+
Once the setup import is in place, the panel mounts and unmounts automatically:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
// Panel appears automatically when tracking starts
|
|
340
|
+
await RNGeoService.configure({ debug: true, ... });
|
|
341
|
+
await RNGeoService.start(); // ← panel is now visible
|
|
342
|
+
|
|
343
|
+
// Panel is removed when tracking stops
|
|
344
|
+
await RNGeoService.stop();
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
No `<GeoDebugPanel />` or `<GeoDebugOverlay />` needed anywhere in the component tree.
|
|
348
|
+
|
|
349
|
+
### Debug panel behaviour
|
|
350
|
+
|
|
351
|
+
The panel is a **draggable, minimizable floating overlay** that starts minimized:
|
|
352
|
+
|
|
353
|
+
- **Tap the 📍 circle** to expand
|
|
354
|
+
- **Drag** by holding the striped header bar
|
|
355
|
+
- **Minimize** with the ⊖ button — collapses back to the 📍 circle
|
|
356
|
+
|
|
357
|
+
**Metrics shown:**
|
|
358
|
+
|
|
359
|
+
| Metric | Description |
|
|
360
|
+
|--------|-------------|
|
|
361
|
+
| Tracking for | How long the current session has been running |
|
|
362
|
+
| Updates | Total location fixes received |
|
|
363
|
+
| Updates/min | Average frequency of location updates |
|
|
364
|
+
| GPS active | % of session time the GPS chip was on vs idle |
|
|
365
|
+
| Battery now | Current device battery level |
|
|
366
|
+
| Drained | Total device battery % dropped since `start()` |
|
|
367
|
+
| Drain rate | Battery consumed per hour (total device, not just location) |
|
|
368
|
+
|
|
369
|
+
**Smart suggestions** are shown automatically:
|
|
370
|
+
|
|
371
|
+
- 🔴 Updates/min > 20 → increase `minDistanceMeters` or `updateIntervalMs`
|
|
372
|
+
- ⚠️ Updates/min 8–20 → consider reducing update frequency
|
|
373
|
+
- 🔴 GPS on > 80% of time → enable `adaptiveAccuracy` or use `coarseTracking`
|
|
374
|
+
- 🔴 Drain rate > 8%/hr → try `'balanced'` accuracy or longer update intervals
|
|
375
|
+
- ✅ All metrics in range → confirms settings are efficient
|
|
376
|
+
|
|
377
|
+
> **Note:** Battery drain is measured at the whole-device level since iOS and Android do not expose per-app battery consumption via public APIs. Use GPS active % and updates/min as the primary indicators of how much the package itself is contributing.
|
|
378
|
+
|
|
379
|
+
### Manual usage (optional)
|
|
380
|
+
|
|
381
|
+
If you prefer to control rendering yourself, `GeoDebugPanel` and `GeoDebugOverlay` are also exported for direct use — the `debug-panel` setup import is still required for them to render correctly.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
import { GeoDebugPanel, GeoDebugOverlay } from '@tsachit/react-native-geo-service';
|
|
385
|
+
|
|
386
|
+
// Renders anywhere in your tree — self-hides when tracking is inactive:
|
|
387
|
+
<GeoDebugOverlay />
|
|
388
|
+
|
|
389
|
+
// Always-visible panel with custom poll interval:
|
|
390
|
+
<GeoDebugPanel pollInterval={15000} />
|
|
391
|
+
```
|
|
225
392
|
|
|
226
393
|
---
|
|
227
394
|
|
|
228
395
|
## Headless mode explained
|
|
229
396
|
|
|
230
397
|
### Android
|
|
231
|
-
When the app is removed from recents
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
398
|
+
When the app is removed from recents, the foreground service keeps running. When a location arrives and the React JS context is inactive, the library calls `AppRegistry.startHeadlessTask` to spin up a lightweight JS runtime and invoke your registered handler.
|
|
399
|
+
|
|
400
|
+
A `WatchdogWorker` (WorkManager, 15-min interval) monitors whether the service is still alive. On OEM devices with aggressive battery optimisation (Xiaomi, Samsung, Huawei), it restarts the service if it was killed unexpectedly.
|
|
401
|
+
|
|
402
|
+
A `BootReceiver` restarts the service after device reboot if `restartOnBoot: true`.
|
|
235
403
|
|
|
236
404
|
### iOS
|
|
237
|
-
When the app is terminated, iOS
|
|
238
|
-
1.
|
|
239
|
-
2.
|
|
240
|
-
3.
|
|
241
|
-
|
|
242
|
-
Upon relaunch, `
|
|
243
|
-
|
|
244
|
-
|
|
405
|
+
When the app is terminated, iOS relaunches it silently when:
|
|
406
|
+
1. `UIBackgroundModes` contains `location`, **and**
|
|
407
|
+
2. `startMonitoringSignificantLocationChanges` is active (always on when tracking), **or**
|
|
408
|
+
3. Standard location updates are running with the _Always_ permission
|
|
409
|
+
|
|
410
|
+
Upon relaunch, the module detects `UIApplicationLaunchOptionsLocationKey`, restores config from `NSUserDefaults`, and resumes tracking before the JS bridge has fully mounted. Any location events that arrive before JS listeners attach are buffered and flushed once `onLocation` is subscribed.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Battery saving tips
|
|
415
|
+
|
|
416
|
+
- Use `accuracy: 'balanced'` unless you need GPS precision — cell/WiFi positioning uses far less power
|
|
417
|
+
- Increase `minDistanceMeters` to the minimum useful for your use case — fewer wakes = longer battery
|
|
418
|
+
- Leave `adaptiveAccuracy: true` (default) — this is the single biggest saving; GPS turns off completely when parked
|
|
419
|
+
- On iOS, use `coarseTracking: true` if ~500m granularity is acceptable — uses cell towers only
|
|
420
|
+
- On Android, increase `updateIntervalMs` (e.g. `10000`) to give FusedLocationProvider room to batch fixes
|
|
421
|
+
- Set `motionActivity: 'automotiveNavigation'` or `'fitness'` so iOS applies activity-specific optimisations
|
|
422
|
+
- Use the `GeoDebugPanel` to measure real-world impact and act on its suggestions
|
|
245
423
|
|
|
246
424
|
---
|
|
247
425
|
|
package/android/build.gradle
CHANGED
|
@@ -40,4 +40,5 @@ dependencies {
|
|
|
40
40
|
implementation 'com.facebook.react:react-android:+'
|
|
41
41
|
implementation 'com.google.android.gms:play-services-location:21.0.1'
|
|
42
42
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
|
43
|
+
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
|
43
44
|
}
|
|
@@ -5,13 +5,18 @@ import android.content.BroadcastReceiver
|
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.Intent
|
|
7
7
|
import android.content.IntentFilter
|
|
8
|
+
import android.os.BatteryManager
|
|
8
9
|
import android.os.Build
|
|
9
10
|
import android.util.Log
|
|
10
11
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
12
|
+
import androidx.work.ExistingPeriodicWorkPolicy
|
|
13
|
+
import androidx.work.PeriodicWorkRequestBuilder
|
|
14
|
+
import androidx.work.WorkManager
|
|
11
15
|
import com.facebook.react.bridge.*
|
|
12
16
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
13
17
|
import com.google.android.gms.location.LocationServices
|
|
14
18
|
import org.json.JSONObject
|
|
19
|
+
import java.util.concurrent.TimeUnit
|
|
15
20
|
|
|
16
21
|
class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
17
22
|
ReactContextBaseJavaModule(reactContext) {
|
|
@@ -30,6 +35,7 @@ class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
|
30
35
|
private var config: GeoServiceConfig = GeoServiceConfig()
|
|
31
36
|
private var listenerCount = 0
|
|
32
37
|
private var locationReceiver: BroadcastReceiver? = null
|
|
38
|
+
private var batteryLevelAtStart: Int = 0
|
|
33
39
|
|
|
34
40
|
init {
|
|
35
41
|
isReactContextActive = true
|
|
@@ -103,6 +109,9 @@ class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
|
103
109
|
registerLocationReceiver()
|
|
104
110
|
startLocationService()
|
|
105
111
|
saveTrackingState(true)
|
|
112
|
+
scheduleWatchdog()
|
|
113
|
+
val bm = reactContext.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager
|
|
114
|
+
batteryLevelAtStart = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: 0
|
|
106
115
|
log("Tracking started")
|
|
107
116
|
promise.resolve(null)
|
|
108
117
|
} catch (e: Exception) {
|
|
@@ -115,6 +124,7 @@ class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
|
115
124
|
try {
|
|
116
125
|
reactContext.stopService(Intent(reactContext, LocationService::class.java))
|
|
117
126
|
saveTrackingState(false)
|
|
127
|
+
cancelWatchdog()
|
|
118
128
|
log("Tracking stopped")
|
|
119
129
|
promise.resolve(null)
|
|
120
130
|
} catch (e: Exception) {
|
|
@@ -144,6 +154,45 @@ class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
156
|
|
|
157
|
+
@ReactMethod
|
|
158
|
+
fun getBatteryInfo(promise: Promise) {
|
|
159
|
+
try {
|
|
160
|
+
val bm = reactContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
|
161
|
+
val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
|
|
162
|
+
val status = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS)
|
|
163
|
+
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
|
164
|
+
status == BatteryManager.BATTERY_STATUS_FULL
|
|
165
|
+
val drain = if (batteryLevelAtStart > 0) maxOf((batteryLevelAtStart - level).toDouble(), 0.0) else 0.0
|
|
166
|
+
|
|
167
|
+
val elapsedMs = if (LocationService.trackingStartTimeMs > 0)
|
|
168
|
+
System.currentTimeMillis() - LocationService.trackingStartTimeMs else 0L
|
|
169
|
+
val elapsedSec = elapsedMs / 1000.0
|
|
170
|
+
val gpsActiveSec = LocationService.currentGpsActiveMs / 1000.0
|
|
171
|
+
val updatesPerMin = if (elapsedSec > 0) LocationService.updateCount / (elapsedSec / 60.0) else 0.0
|
|
172
|
+
val drainRatePerHour = if (elapsedSec > 0 && drain > 0) drain / (elapsedSec / 3600.0) else 0.0
|
|
173
|
+
|
|
174
|
+
promise.resolve(Arguments.createMap().apply {
|
|
175
|
+
putDouble("level", level.toDouble())
|
|
176
|
+
putBoolean("isCharging", isCharging)
|
|
177
|
+
putDouble("levelAtStart", batteryLevelAtStart.toDouble())
|
|
178
|
+
putDouble("drainSinceStart", drain)
|
|
179
|
+
putInt("updateCount", LocationService.updateCount.toInt())
|
|
180
|
+
putDouble("trackingElapsedSeconds", elapsedSec)
|
|
181
|
+
putDouble("gpsActiveSeconds", gpsActiveSec)
|
|
182
|
+
putDouble("updatesPerMinute", updatesPerMin)
|
|
183
|
+
putDouble("drainRatePerHour", maxOf(drainRatePerHour, 0.0))
|
|
184
|
+
})
|
|
185
|
+
} catch (e: Exception) {
|
|
186
|
+
promise.reject("BATTERY_ERROR", e.message, e)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@ReactMethod
|
|
191
|
+
fun setLocationIndicator(@Suppress("UNUSED_PARAMETER") show: Boolean, promise: Promise) {
|
|
192
|
+
// No-op on Android — the foreground notification already provides the status bar indicator
|
|
193
|
+
promise.resolve(null)
|
|
194
|
+
}
|
|
195
|
+
|
|
147
196
|
// --------------------------------------------------------------------------------------------
|
|
148
197
|
// Internal helpers
|
|
149
198
|
// --------------------------------------------------------------------------------------------
|
|
@@ -263,6 +312,21 @@ class GeoServiceModule(private val reactContext: ReactApplicationContext) :
|
|
|
263
312
|
.apply()
|
|
264
313
|
}
|
|
265
314
|
|
|
315
|
+
private fun scheduleWatchdog() {
|
|
316
|
+
val request = PeriodicWorkRequestBuilder<WatchdogWorker>(15, TimeUnit.MINUTES).build()
|
|
317
|
+
WorkManager.getInstance(reactContext).enqueueUniquePeriodicWork(
|
|
318
|
+
WatchdogWorker.WORK_NAME,
|
|
319
|
+
ExistingPeriodicWorkPolicy.KEEP,
|
|
320
|
+
request
|
|
321
|
+
)
|
|
322
|
+
log("Watchdog scheduled (15-min interval)")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private fun cancelWatchdog() {
|
|
326
|
+
WorkManager.getInstance(reactContext).cancelUniqueWork(WatchdogWorker.WORK_NAME)
|
|
327
|
+
log("Watchdog cancelled")
|
|
328
|
+
}
|
|
329
|
+
|
|
266
330
|
private fun log(msg: String) {
|
|
267
331
|
if (config.debug) Log.d(TAG, msg)
|
|
268
332
|
}
|