@tsachit/react-native-geo-service 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +48 -0
- package/android/src/main/java/com/geoservice/BootReceiver.kt +73 -0
- package/android/src/main/java/com/geoservice/GeoServiceConfig.kt +56 -0
- package/android/src/main/java/com/geoservice/GeoServiceModule.kt +291 -0
- package/android/src/main/java/com/geoservice/GeoServicePackage.kt +14 -0
- package/android/src/main/java/com/geoservice/HeadlessLocationTask.kt +76 -0
- package/android/src/main/java/com/geoservice/LocationService.kt +265 -0
- package/ios/RNGeoService.h +19 -0
- package/ios/RNGeoService.m +311 -0
- package/lib/index.d.ts +73 -0
- package/lib/index.js +155 -0
- package/lib/types.d.ts +139 -0
- package/lib/types.js +2 -0
- package/package.json +42 -0
- package/react-native-geo-service.podspec +19 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#import "RNGeoService.h"
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// CLActivityType helper
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
static CLActivityType activityTypeFromString(NSString *type) {
|
|
8
|
+
if ([type isEqualToString:@"automotiveNavigation"]) return CLActivityTypeAutomotiveNavigation;
|
|
9
|
+
if ([type isEqualToString:@"fitness"]) return CLActivityTypeFitness;
|
|
10
|
+
if ([type isEqualToString:@"otherNavigation"]) return CLActivityTypeOtherNavigation;
|
|
11
|
+
if (@available(iOS 12.0, *)) {
|
|
12
|
+
if ([type isEqualToString:@"airborne"]) return CLActivityTypeAirborne;
|
|
13
|
+
}
|
|
14
|
+
return CLActivityTypeOther;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// CLLocationAccuracy helper
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
static CLLocationAccuracy accuracyFromString(NSString *accuracy) {
|
|
21
|
+
if ([accuracy isEqualToString:@"navigation"]) return kCLLocationAccuracyBestForNavigation;
|
|
22
|
+
if ([accuracy isEqualToString:@"high"]) return kCLLocationAccuracyBest;
|
|
23
|
+
if ([accuracy isEqualToString:@"low"]) return kCLLocationAccuracyKilometer;
|
|
24
|
+
// "balanced" → nearest 100 metres — good trade-off between precision and battery
|
|
25
|
+
return kCLLocationAccuracyHundredMeters;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// RNGeoService implementation
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
@interface RNGeoService ()
|
|
32
|
+
|
|
33
|
+
@property (nonatomic, strong) CLLocationManager *locationManager;
|
|
34
|
+
@property (nonatomic, strong) NSDictionary *config;
|
|
35
|
+
|
|
36
|
+
@property (nonatomic, assign) BOOL isTracking;
|
|
37
|
+
@property (nonatomic, assign) BOOL hasListeners;
|
|
38
|
+
@property (nonatomic, assign) BOOL coarseTracking;
|
|
39
|
+
@property (nonatomic, assign) BOOL debugMode;
|
|
40
|
+
|
|
41
|
+
// Adaptive accuracy state
|
|
42
|
+
@property (nonatomic, assign) BOOL adaptiveAccuracy;
|
|
43
|
+
@property (nonatomic, assign) float idleSpeedThreshold;
|
|
44
|
+
@property (nonatomic, assign) NSInteger idleSampleCount;
|
|
45
|
+
@property (nonatomic, assign) NSInteger slowReadingCount;
|
|
46
|
+
@property (nonatomic, assign) BOOL isIdle;
|
|
47
|
+
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
@implementation RNGeoService
|
|
51
|
+
|
|
52
|
+
RCT_EXPORT_MODULE();
|
|
53
|
+
|
|
54
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
55
|
+
return YES;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (dispatch_queue_t)methodQueue {
|
|
59
|
+
return dispatch_get_main_queue();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Supported events
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
66
|
+
return @[@"onLocation", @"onError"];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)startObserving { self.hasListeners = YES; }
|
|
70
|
+
- (void)stopObserving { self.hasListeners = NO; }
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Lazy CLLocationManager
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
- (CLLocationManager *)locationManager {
|
|
76
|
+
if (!_locationManager) {
|
|
77
|
+
_locationManager = [[CLLocationManager alloc] init];
|
|
78
|
+
_locationManager.delegate = self;
|
|
79
|
+
}
|
|
80
|
+
return _locationManager;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// configure()
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
RCT_EXPORT_METHOD(configure:(NSDictionary *)options
|
|
87
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
88
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
89
|
+
self.config = options;
|
|
90
|
+
self.coarseTracking = [options[@"coarseTracking"] boolValue];
|
|
91
|
+
self.debugMode = [options[@"debug"] boolValue];
|
|
92
|
+
self.adaptiveAccuracy = options[@"adaptiveAccuracy"] ? [options[@"adaptiveAccuracy"] boolValue] : YES;
|
|
93
|
+
self.idleSpeedThreshold = options[@"idleSpeedThreshold"] ? [options[@"idleSpeedThreshold"] floatValue] : 0.5f;
|
|
94
|
+
self.idleSampleCount = options[@"idleSampleCount"] ? [options[@"idleSampleCount"] integerValue] : 3;
|
|
95
|
+
self.slowReadingCount = 0;
|
|
96
|
+
self.isIdle = NO;
|
|
97
|
+
|
|
98
|
+
[self applyConfigToLocationManager];
|
|
99
|
+
|
|
100
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Config applied: %@", options);
|
|
101
|
+
resolve(nil);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)applyConfigToLocationManager {
|
|
105
|
+
NSDictionary *cfg = self.config ?: @{};
|
|
106
|
+
|
|
107
|
+
NSString *accuracy = cfg[@"accuracy"] ?: @"balanced";
|
|
108
|
+
self.locationManager.desiredAccuracy = accuracyFromString(accuracy);
|
|
109
|
+
|
|
110
|
+
double minDist = [cfg[@"minDistanceMeters"] doubleValue];
|
|
111
|
+
self.locationManager.distanceFilter = (minDist > 0) ? minDist : kCLDistanceFilterNone;
|
|
112
|
+
|
|
113
|
+
NSString *motionActivity = cfg[@"motionActivity"] ?: @"other";
|
|
114
|
+
self.locationManager.activityType = activityTypeFromString(motionActivity);
|
|
115
|
+
|
|
116
|
+
BOOL autoPause = [cfg[@"autoPauseUpdates"] boolValue];
|
|
117
|
+
self.locationManager.pausesLocationUpdatesAutomatically = autoPause;
|
|
118
|
+
|
|
119
|
+
if (@available(iOS 11.0, *)) {
|
|
120
|
+
BOOL bgIndicator = [cfg[@"showBackgroundIndicator"] boolValue];
|
|
121
|
+
self.locationManager.showsBackgroundLocationIndicator = bgIndicator;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
self.locationManager.allowsBackgroundLocationUpdates = YES;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// start()
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
RCT_EXPORT_METHOD(start:(RCTPromiseResolveBlock)resolve
|
|
131
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
132
|
+
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
|
|
133
|
+
|
|
134
|
+
if (status == kCLAuthorizationStatusDenied ||
|
|
135
|
+
status == kCLAuthorizationStatusRestricted) {
|
|
136
|
+
reject(@"PERMISSION_DENIED", @"Location permission denied. Request 'Always' permission before calling start().", nil);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (status == kCLAuthorizationStatusNotDetermined) {
|
|
141
|
+
[self.locationManager requestAlwaysAuthorization];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[self applyConfigToLocationManager];
|
|
145
|
+
|
|
146
|
+
if (self.coarseTracking) {
|
|
147
|
+
[self.locationManager startMonitoringSignificantLocationChanges];
|
|
148
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Coarse (significant-change) tracking started");
|
|
149
|
+
} else {
|
|
150
|
+
[self.locationManager startUpdatingLocation];
|
|
151
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Standard tracking started");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
self.isTracking = YES;
|
|
155
|
+
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"GeoServiceIsTracking"];
|
|
156
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
157
|
+
|
|
158
|
+
resolve(nil);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// stop()
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
RCT_EXPORT_METHOD(stop:(RCTPromiseResolveBlock)resolve
|
|
165
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
166
|
+
if (self.coarseTracking) {
|
|
167
|
+
[self.locationManager stopMonitoringSignificantLocationChanges];
|
|
168
|
+
} else {
|
|
169
|
+
[self.locationManager stopUpdatingLocation];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
self.isTracking = NO;
|
|
173
|
+
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"GeoServiceIsTracking"];
|
|
174
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
175
|
+
|
|
176
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Tracking stopped");
|
|
177
|
+
resolve(nil);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// isTracking() / getCurrentLocation()
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
RCT_EXPORT_METHOD(isTracking:(RCTPromiseResolveBlock)resolve
|
|
184
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
185
|
+
resolve(@(self.isTracking));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
RCT_EXPORT_METHOD(getCurrentLocation:(RCTPromiseResolveBlock)resolve
|
|
189
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
190
|
+
CLLocation *last = self.locationManager.location;
|
|
191
|
+
if (last) {
|
|
192
|
+
resolve([self locationToDictionary:last]);
|
|
193
|
+
} else {
|
|
194
|
+
reject(@"NO_LOCATION", @"No cached location available. Call start() first.", nil);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// CLLocationManagerDelegate
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
- (void)locationManager:(CLLocationManager *)manager
|
|
202
|
+
didUpdateLocations:(NSArray<CLLocation *> *)locations {
|
|
203
|
+
CLLocation *location = [locations lastObject];
|
|
204
|
+
if (!location) return;
|
|
205
|
+
|
|
206
|
+
if (self.debugMode) {
|
|
207
|
+
RCTLogInfo(@"[RNGeoService] Location: %f, %f (±%.0fm) speed=%.1fm/s",
|
|
208
|
+
location.coordinate.latitude,
|
|
209
|
+
location.coordinate.longitude,
|
|
210
|
+
location.horizontalAccuracy,
|
|
211
|
+
location.speed);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (self.adaptiveAccuracy && !self.coarseTracking) {
|
|
215
|
+
[self evaluateMotionState:location];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (self.hasListeners) {
|
|
219
|
+
[self sendEventWithName:@"onLocation" body:[self locationToDictionary:location]];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
- (void)evaluateMotionState:(CLLocation *)location {
|
|
224
|
+
// speed is -1 when unavailable (e.g. first fix from cache) — treat as moving
|
|
225
|
+
float speed = (location.speed >= 0) ? (float)location.speed : 1.0f;
|
|
226
|
+
|
|
227
|
+
if (speed < self.idleSpeedThreshold) {
|
|
228
|
+
self.slowReadingCount++;
|
|
229
|
+
if (!self.isIdle && self.slowReadingCount >= self.idleSampleCount) {
|
|
230
|
+
self.isIdle = YES;
|
|
231
|
+
self.slowReadingCount = 0;
|
|
232
|
+
// Reduce accuracy — CoreLocation stops requesting GPS
|
|
233
|
+
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
|
|
234
|
+
self.locationManager.distanceFilter = 50.0;
|
|
235
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Device idle — GPS off");
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
if (self.isIdle) {
|
|
239
|
+
self.isIdle = NO;
|
|
240
|
+
self.slowReadingCount = 0;
|
|
241
|
+
[self applyConfigToLocationManager];
|
|
242
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Movement detected — accuracy restored");
|
|
243
|
+
} else {
|
|
244
|
+
self.slowReadingCount = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
- (void)locationManager:(CLLocationManager *)manager
|
|
250
|
+
didFailWithError:(NSError *)error {
|
|
251
|
+
// kCLErrorLocationUnknown (code 0) is transient — CoreLocation couldn't get a
|
|
252
|
+
// fix yet but will keep trying automatically. Silently ignore it so we don't
|
|
253
|
+
// surface a noisy error to the app before the GPS has warmed up.
|
|
254
|
+
if ([error.domain isEqualToString:kCLErrorDomain] && error.code == kCLErrorLocationUnknown) {
|
|
255
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Location unknown (transient) — waiting for GPS fix");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// kCLErrorDenied means the user revoked location permission — this is a real
|
|
260
|
+
// error worth surfacing, and we should stop tracking to avoid repeated failures.
|
|
261
|
+
if ([error.domain isEqualToString:kCLErrorDomain] && error.code == kCLErrorDenied) {
|
|
262
|
+
RCTLogWarn(@"[RNGeoService] Location permission denied — stopping tracking");
|
|
263
|
+
[self.locationManager stopUpdatingLocation];
|
|
264
|
+
[self.locationManager stopMonitoringSignificantLocationChanges];
|
|
265
|
+
} else {
|
|
266
|
+
RCTLogError(@"[RNGeoService] Location error: %@", error.localizedDescription);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (self.hasListeners) {
|
|
270
|
+
[self sendEventWithName:@"onError" body:@{
|
|
271
|
+
@"code": @(error.code),
|
|
272
|
+
@"message": error.localizedDescription ?: @"Unknown location error"
|
|
273
|
+
}];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
- (void)locationManager:(CLLocationManager *)manager
|
|
278
|
+
didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
|
|
279
|
+
if (self.debugMode) RCTLogInfo(@"[RNGeoService] Auth status: %d", status);
|
|
280
|
+
|
|
281
|
+
// Resume tracking after background relaunch (e.g. significant location change)
|
|
282
|
+
if (status == kCLAuthorizationStatusAuthorizedAlways &&
|
|
283
|
+
[[NSUserDefaults standardUserDefaults] boolForKey:@"GeoServiceIsTracking"]) {
|
|
284
|
+
[self applyConfigToLocationManager];
|
|
285
|
+
if (self.coarseTracking) {
|
|
286
|
+
[self.locationManager startMonitoringSignificantLocationChanges];
|
|
287
|
+
} else {
|
|
288
|
+
[self.locationManager startUpdatingLocation];
|
|
289
|
+
}
|
|
290
|
+
self.isTracking = YES;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Helpers
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
- (NSDictionary *)locationToDictionary:(CLLocation *)location {
|
|
298
|
+
return @{
|
|
299
|
+
@"latitude": @(location.coordinate.latitude),
|
|
300
|
+
@"longitude": @(location.coordinate.longitude),
|
|
301
|
+
@"accuracy": @(location.horizontalAccuracy),
|
|
302
|
+
@"altitude": @(location.altitude),
|
|
303
|
+
@"altitudeAccuracy": @(location.verticalAccuracy),
|
|
304
|
+
@"speed": @(location.speed),
|
|
305
|
+
@"bearing": @(location.course),
|
|
306
|
+
@"timestamp": @((long long)(location.timestamp.timeIntervalSince1970 * 1000)),
|
|
307
|
+
@"isStationary": @(self.isIdle)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@end
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { GeoServiceConfig, GeoSubscription, Location, LocationCallback, ErrorCallback } from './types';
|
|
2
|
+
export * from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Configure the geo service. Call this before start().
|
|
5
|
+
* Safe to call multiple times; subsequent calls update the config.
|
|
6
|
+
*/
|
|
7
|
+
declare function configure(config: GeoServiceConfig): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Start background location tracking.
|
|
10
|
+
* On Android, this starts a foreground service with a persistent notification.
|
|
11
|
+
* On iOS, this starts standard or significant-change location monitoring.
|
|
12
|
+
*/
|
|
13
|
+
declare function start(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Stop background location tracking.
|
|
16
|
+
*/
|
|
17
|
+
declare function stop(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Fetch the current device location as a one-time request.
|
|
20
|
+
*/
|
|
21
|
+
declare function getCurrentLocation(): Promise<Location>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns whether the geo service is currently tracking.
|
|
24
|
+
*/
|
|
25
|
+
declare function isTracking(): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to location updates.
|
|
28
|
+
* Returns a GeoSubscription — call .remove() to unsubscribe.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const sub = RNGeoService.onLocation((location) => {
|
|
32
|
+
* console.log(location.latitude, location.longitude);
|
|
33
|
+
* });
|
|
34
|
+
* // Later:
|
|
35
|
+
* sub.remove();
|
|
36
|
+
*/
|
|
37
|
+
declare function onLocation(callback: LocationCallback): GeoSubscription;
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to location errors.
|
|
40
|
+
*/
|
|
41
|
+
declare function onError(callback: ErrorCallback): GeoSubscription;
|
|
42
|
+
/**
|
|
43
|
+
* Register a headless task handler for Android background processing.
|
|
44
|
+
*
|
|
45
|
+
* When the app is not in the foreground, location updates are delivered via
|
|
46
|
+
* HeadlessJS. Register your handler here OR in your app's index.js using
|
|
47
|
+
* AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', ...).
|
|
48
|
+
*
|
|
49
|
+
* The handler receives a Location object and should return a Promise.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // In index.js (outside the App component, at the top level):
|
|
53
|
+
* import { AppRegistry } from 'react-native';
|
|
54
|
+
* AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
|
|
55
|
+
* console.log('[Headless] Location:', location);
|
|
56
|
+
* // Send to your server using a pre-stored auth token (e.g. SecureStore/Keychain).
|
|
57
|
+
* // Do not rely on in-memory app state — this context is headless and isolated.
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* @platform android
|
|
61
|
+
*/
|
|
62
|
+
declare function registerHeadlessTask(handler: (location: Location) => Promise<void>): void;
|
|
63
|
+
declare const RNGeoService: {
|
|
64
|
+
configure: typeof configure;
|
|
65
|
+
start: typeof start;
|
|
66
|
+
stop: typeof stop;
|
|
67
|
+
getCurrentLocation: typeof getCurrentLocation;
|
|
68
|
+
isTracking: typeof isTracking;
|
|
69
|
+
onLocation: typeof onLocation;
|
|
70
|
+
onError: typeof onError;
|
|
71
|
+
registerHeadlessTask: typeof registerHeadlessTask;
|
|
72
|
+
};
|
|
73
|
+
export default RNGeoService;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
17
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const react_native_1 = require("react-native");
|
|
27
|
+
__exportStar(require("./types"), exports);
|
|
28
|
+
// The native bridge module registered as "RNGeoService" on both platforms
|
|
29
|
+
const nativeModule = react_native_1.NativeModules.RNGeoService;
|
|
30
|
+
if (!nativeModule) {
|
|
31
|
+
throw new Error('[react-native-geo-service] Native module not found. ' +
|
|
32
|
+
'Make sure you have linked the native module correctly. ' +
|
|
33
|
+
'For iOS run `pod install`, for Android rebuild the project.');
|
|
34
|
+
}
|
|
35
|
+
const eventEmitter = new react_native_1.NativeEventEmitter(nativeModule);
|
|
36
|
+
const DEFAULT_CONFIG = {
|
|
37
|
+
minDistanceMeters: 10,
|
|
38
|
+
accuracy: 'balanced',
|
|
39
|
+
stopOnAppClose: false,
|
|
40
|
+
restartOnBoot: false,
|
|
41
|
+
updateIntervalMs: 5000,
|
|
42
|
+
minUpdateIntervalMs: 2000,
|
|
43
|
+
serviceTitle: 'Location Tracking',
|
|
44
|
+
serviceBody: 'Your location is being tracked in the background.',
|
|
45
|
+
backgroundTaskName: 'GeoServiceHeadlessTask',
|
|
46
|
+
motionActivity: 'other',
|
|
47
|
+
autoPauseUpdates: false,
|
|
48
|
+
showBackgroundIndicator: false,
|
|
49
|
+
coarseTracking: false,
|
|
50
|
+
adaptiveAccuracy: true,
|
|
51
|
+
idleSpeedThreshold: 0.5,
|
|
52
|
+
idleSampleCount: 3,
|
|
53
|
+
debug: false,
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Configure the geo service. Call this before start().
|
|
57
|
+
* Safe to call multiple times; subsequent calls update the config.
|
|
58
|
+
*/
|
|
59
|
+
function configure(config) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
const merged = Object.assign(Object.assign({}, DEFAULT_CONFIG), config);
|
|
62
|
+
return nativeModule.configure(merged);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Start background location tracking.
|
|
67
|
+
* On Android, this starts a foreground service with a persistent notification.
|
|
68
|
+
* On iOS, this starts standard or significant-change location monitoring.
|
|
69
|
+
*/
|
|
70
|
+
function start() {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
return nativeModule.start();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Stop background location tracking.
|
|
77
|
+
*/
|
|
78
|
+
function stop() {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
return nativeModule.stop();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fetch the current device location as a one-time request.
|
|
85
|
+
*/
|
|
86
|
+
function getCurrentLocation() {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
return nativeModule.getCurrentLocation();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns whether the geo service is currently tracking.
|
|
93
|
+
*/
|
|
94
|
+
function isTracking() {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
return nativeModule.isTracking();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Subscribe to location updates.
|
|
101
|
+
* Returns a GeoSubscription — call .remove() to unsubscribe.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const sub = RNGeoService.onLocation((location) => {
|
|
105
|
+
* console.log(location.latitude, location.longitude);
|
|
106
|
+
* });
|
|
107
|
+
* // Later:
|
|
108
|
+
* sub.remove();
|
|
109
|
+
*/
|
|
110
|
+
function onLocation(callback) {
|
|
111
|
+
return eventEmitter.addListener('onLocation', callback);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Subscribe to location errors.
|
|
115
|
+
*/
|
|
116
|
+
function onError(callback) {
|
|
117
|
+
return eventEmitter.addListener('onError', callback);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Register a headless task handler for Android background processing.
|
|
121
|
+
*
|
|
122
|
+
* When the app is not in the foreground, location updates are delivered via
|
|
123
|
+
* HeadlessJS. Register your handler here OR in your app's index.js using
|
|
124
|
+
* AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', ...).
|
|
125
|
+
*
|
|
126
|
+
* The handler receives a Location object and should return a Promise.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* // In index.js (outside the App component, at the top level):
|
|
130
|
+
* import { AppRegistry } from 'react-native';
|
|
131
|
+
* AppRegistry.registerHeadlessTask('GeoServiceHeadlessTask', () => async (location) => {
|
|
132
|
+
* console.log('[Headless] Location:', location);
|
|
133
|
+
* // Send to your server using a pre-stored auth token (e.g. SecureStore/Keychain).
|
|
134
|
+
* // Do not rely on in-memory app state — this context is headless and isolated.
|
|
135
|
+
* });
|
|
136
|
+
*
|
|
137
|
+
* @platform android
|
|
138
|
+
*/
|
|
139
|
+
function registerHeadlessTask(handler) {
|
|
140
|
+
if (react_native_1.Platform.OS !== 'android')
|
|
141
|
+
return;
|
|
142
|
+
const taskName = DEFAULT_CONFIG.backgroundTaskName;
|
|
143
|
+
react_native_1.AppRegistry.registerHeadlessTask(taskName, () => handler);
|
|
144
|
+
}
|
|
145
|
+
const RNGeoService = {
|
|
146
|
+
configure,
|
|
147
|
+
start,
|
|
148
|
+
stop,
|
|
149
|
+
getCurrentLocation,
|
|
150
|
+
isTracking,
|
|
151
|
+
onLocation,
|
|
152
|
+
onError,
|
|
153
|
+
registerHeadlessTask,
|
|
154
|
+
};
|
|
155
|
+
exports.default = RNGeoService;
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface GeoServiceConfig {
|
|
2
|
+
/**
|
|
3
|
+
* Minimum distance in meters the device must move before a location update is fired.
|
|
4
|
+
* Higher values = fewer updates = better battery life.
|
|
5
|
+
* Default: 10
|
|
6
|
+
*/
|
|
7
|
+
minDistanceMeters?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Location accuracy mode.
|
|
10
|
+
* - 'navigation': GPS-level accuracy (most battery)
|
|
11
|
+
* - 'high': High accuracy
|
|
12
|
+
* - 'balanced': City-block accuracy, uses cell/WiFi (recommended for most apps)
|
|
13
|
+
* - 'low': Approximate location, very low battery usage
|
|
14
|
+
* Default: 'balanced'
|
|
15
|
+
*/
|
|
16
|
+
accuracy?: 'navigation' | 'high' | 'balanced' | 'low';
|
|
17
|
+
/**
|
|
18
|
+
* Stop location tracking when the app is closed by the user.
|
|
19
|
+
* Set to false for always-on headless tracking.
|
|
20
|
+
* Default: false
|
|
21
|
+
*/
|
|
22
|
+
stopOnAppClose?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Automatically restart tracking on device reboot (Android only).
|
|
25
|
+
* Default: false
|
|
26
|
+
*/
|
|
27
|
+
restartOnBoot?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Target time interval between location updates in milliseconds (Android only).
|
|
30
|
+
* Default: 5000
|
|
31
|
+
*/
|
|
32
|
+
updateIntervalMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Minimum time between location updates in milliseconds (Android only).
|
|
35
|
+
* Updates will never arrive faster than this value.
|
|
36
|
+
* Default: 2000
|
|
37
|
+
*/
|
|
38
|
+
minUpdateIntervalMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Title of the persistent foreground service notification (Android only).
|
|
41
|
+
* Default: 'Location Tracking'
|
|
42
|
+
*/
|
|
43
|
+
serviceTitle?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Body text of the persistent foreground service notification (Android only).
|
|
46
|
+
* Default: 'Your location is being tracked in the background.'
|
|
47
|
+
*/
|
|
48
|
+
serviceBody?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Name of the HeadlessJS task to invoke when the app is not in the foreground (Android only).
|
|
51
|
+
* Register this task in your app's index.js using AppRegistry.registerHeadlessTask().
|
|
52
|
+
* Default: 'GeoServiceHeadlessTask'
|
|
53
|
+
*/
|
|
54
|
+
backgroundTaskName?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Hint to the OS about what kind of motion this location data is used for (iOS only).
|
|
57
|
+
* Allows CoreLocation to apply activity-specific power optimisations.
|
|
58
|
+
* Default: 'other'
|
|
59
|
+
*/
|
|
60
|
+
motionActivity?: 'other' | 'automotiveNavigation' | 'fitness' | 'otherNavigation' | 'airborne';
|
|
61
|
+
/**
|
|
62
|
+
* Allow iOS to automatically pause location updates when no movement is detected.
|
|
63
|
+
* Set to false to always receive updates.
|
|
64
|
+
* Default: false
|
|
65
|
+
*/
|
|
66
|
+
autoPauseUpdates?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Show the blue location indicator in the iOS status bar when tracking in background.
|
|
69
|
+
* Default: false
|
|
70
|
+
*/
|
|
71
|
+
showBackgroundIndicator?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Use Significant Location Changes instead of standard location updates (iOS only).
|
|
74
|
+
* Much more battery efficient — only fires when the device moves ~500m.
|
|
75
|
+
* Wakes the app even if it was terminated.
|
|
76
|
+
* Default: false
|
|
77
|
+
*/
|
|
78
|
+
coarseTracking?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Automatically drop to low-power mode when the device appears stationary,
|
|
81
|
+
* and restore the configured accuracy the moment movement is detected again.
|
|
82
|
+
*
|
|
83
|
+
* On Android this turns the GPS chip completely off while parked.
|
|
84
|
+
* On iOS this reduces accuracy to kCLLocationAccuracyKilometer while still.
|
|
85
|
+
*
|
|
86
|
+
* This is the single biggest battery saving for apps that track driving/walking —
|
|
87
|
+
* GPS stays off while the user is parked or sitting still.
|
|
88
|
+
* Default: true
|
|
89
|
+
*/
|
|
90
|
+
adaptiveAccuracy?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Speed in m/s below which a reading is counted as "idle/stationary".
|
|
93
|
+
* Default: 0.5 (~1.8 km/h)
|
|
94
|
+
*/
|
|
95
|
+
idleSpeedThreshold?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Number of consecutive idle readings required before entering low-power mode.
|
|
98
|
+
* Higher = fewer false positives but slower to power down.
|
|
99
|
+
* Default: 3
|
|
100
|
+
*/
|
|
101
|
+
idleSampleCount?: number;
|
|
102
|
+
/**
|
|
103
|
+
* Enable verbose native logging.
|
|
104
|
+
* Default: false
|
|
105
|
+
*/
|
|
106
|
+
debug?: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface Location {
|
|
109
|
+
latitude: number;
|
|
110
|
+
longitude: number;
|
|
111
|
+
/** Horizontal accuracy in meters */
|
|
112
|
+
accuracy: number;
|
|
113
|
+
altitude: number;
|
|
114
|
+
/** Vertical accuracy in meters (iOS only, -1 on Android) */
|
|
115
|
+
altitudeAccuracy: number;
|
|
116
|
+
/** Speed in meters per second, -1 if unavailable */
|
|
117
|
+
speed: number;
|
|
118
|
+
/** Bearing/heading in degrees (0–360), -1 if unavailable */
|
|
119
|
+
bearing: number;
|
|
120
|
+
/** Unix timestamp in milliseconds */
|
|
121
|
+
timestamp: number;
|
|
122
|
+
/** Whether this location came from a mock provider (Android only) */
|
|
123
|
+
isFromMockProvider?: boolean;
|
|
124
|
+
/** True when adaptive accuracy has detected the device is idle and GPS is off */
|
|
125
|
+
isStationary?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface LocationError {
|
|
128
|
+
code: number;
|
|
129
|
+
message: string;
|
|
130
|
+
}
|
|
131
|
+
export type LocationCallback = (location: Location) => void;
|
|
132
|
+
export type ErrorCallback = (error: LocationError) => void;
|
|
133
|
+
/**
|
|
134
|
+
* Returned by onLocation() and onError().
|
|
135
|
+
* Call .remove() to stop receiving updates and clean up the listener.
|
|
136
|
+
*/
|
|
137
|
+
export interface GeoSubscription {
|
|
138
|
+
remove(): void;
|
|
139
|
+
}
|
package/lib/types.js
ADDED