1 | // Copyright 2016-present 650 Industries. All rights reserved.
|
2 |
|
3 | #import <EXLocation/EXLocation.h>
|
4 | #import <EXLocation/EXLocationDelegate.h>
|
5 | #import <EXLocation/EXLocationTaskConsumer.h>
|
6 | #import <EXLocation/EXGeofencingTaskConsumer.h>
|
7 | #import <EXLocation/EXLocationPermissionRequester.h>
|
8 |
|
9 | #import <CoreLocation/CLLocationManager.h>
|
10 | #import <CoreLocation/CLLocationManagerDelegate.h>
|
11 | #import <CoreLocation/CLHeading.h>
|
12 | #import <CoreLocation/CLGeocoder.h>
|
13 | #import <CoreLocation/CLPlacemark.h>
|
14 | #import <CoreLocation/CLError.h>
|
15 | #import <CoreLocation/CLCircularRegion.h>
|
16 |
|
17 | #import <UMCore/UMEventEmitterService.h>
|
18 |
|
19 | #import <UMPermissionsInterface/UMPermissionsInterface.h>
|
20 | #import <UMPermissionsInterface/UMPermissionsMethodsDelegate.h>
|
21 |
|
22 | #import <UMTaskManagerInterface/UMTaskManagerInterface.h>
|
23 |
|
24 | NS_ASSUME_NONNULL_BEGIN
|
25 |
|
26 | NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
|
27 | NSString * const EXHeadingChangedEventName = @"Expo.headingChanged";
|
28 |
|
29 | @interface EXLocation ()
|
30 |
|
31 | @property (nonatomic, strong) NSMutableDictionary<NSNumber *, EXLocationDelegate*> *delegates;
|
32 | @property (nonatomic, strong) NSMutableSet<EXLocationDelegate *> *retainedDelegates;
|
33 | @property (nonatomic, weak) id<UMEventEmitterService> eventEmitter;
|
34 | @property (nonatomic, weak) id<UMPermissionsInterface> permissionsManager;
|
35 | @property (nonatomic, weak) id<UMTaskManagerInterface> tasksManager;
|
36 |
|
37 | @end
|
38 |
|
39 | @implementation EXLocation
|
40 |
|
41 | UM_EXPORT_MODULE(ExpoLocation);
|
42 |
|
43 | - (instancetype)init
|
44 | {
|
45 | if (self = [super init]) {
|
46 | _delegates = [NSMutableDictionary dictionary];
|
47 | _retainedDelegates = [NSMutableSet set];
|
48 | }
|
49 | return self;
|
50 | }
|
51 |
|
52 | - (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
|
53 | {
|
54 | _eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(UMEventEmitterService)];
|
55 | _permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(UMPermissionsInterface)];
|
56 | [UMPermissionsMethodsDelegate registerRequesters:@[[EXLocationPermissionRequester new]] withPermissionsManager:_permissionsManager];
|
57 | _tasksManager = [moduleRegistry getModuleImplementingProtocol:@protocol(UMTaskManagerInterface)];
|
58 | }
|
59 |
|
60 | - (dispatch_queue_t)methodQueue
|
61 | {
|
62 | // Location managers must be created on the main thread
|
63 | return dispatch_get_main_queue();
|
64 | }
|
65 |
|
66 | # pragma mark - UMEventEmitter
|
67 |
|
68 | - (NSArray<NSString *> *)supportedEvents
|
69 | {
|
70 | return @[EXLocationChangedEventName, EXHeadingChangedEventName];
|
71 | }
|
72 |
|
73 | - (void)startObserving {}
|
74 | - (void)stopObserving {}
|
75 |
|
76 | # pragma mark - Exported methods
|
77 |
|
78 | UM_EXPORT_METHOD_AS(getProviderStatusAsync,
|
79 | resolver:(UMPromiseResolveBlock)resolve
|
80 | rejecter:(UMPromiseRejectBlock)reject)
|
81 | {
|
82 | resolve(@{
|
83 | @"locationServicesEnabled": @([CLLocationManager locationServicesEnabled]),
|
84 | @"backgroundModeEnabled": @([_tasksManager hasBackgroundModeEnabled:@"location"]),
|
85 | });
|
86 | }
|
87 |
|
88 |
|
89 | UM_EXPORT_METHOD_AS(getCurrentPositionAsync,
|
90 | options:(NSDictionary *)options
|
91 | resolver:(UMPromiseResolveBlock)resolve
|
92 | rejecter:(UMPromiseRejectBlock)reject)
|
93 | {
|
94 | if (![self checkPermissions:reject]) {
|
95 | return;
|
96 | }
|
97 |
|
98 | CLLocationManager *locMgr = [self locationManagerWithOptions:options];
|
99 |
|
100 | __weak typeof(self) weakSelf = self;
|
101 | __block EXLocationDelegate *delegate;
|
102 |
|
103 | delegate = [[EXLocationDelegate alloc] initWithId:nil withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> * _Nonnull locations) {
|
104 | if (delegate != nil) {
|
105 | if (locations.lastObject != nil) {
|
106 | resolve([EXLocation exportLocation:locations.lastObject]);
|
107 | } else {
|
108 | reject(@"E_LOCATION_NOT_FOUND", @"Current location not found.", nil);
|
109 | }
|
110 | [weakSelf.retainedDelegates removeObject:delegate];
|
111 | delegate = nil;
|
112 | }
|
113 | } onUpdateHeadings:nil onError:^(NSError *error) {
|
114 | reject(@"E_LOCATION_UNAVAILABLE", [@"Cannot obtain current location: " stringByAppendingString:error.description], nil);
|
115 | }];
|
116 |
|
117 | // retain location manager delegate so it will not dealloc until onUpdateLocations gets called
|
118 | [_retainedDelegates addObject:delegate];
|
119 |
|
120 | locMgr.delegate = delegate;
|
121 | [locMgr requestLocation];
|
122 | }
|
123 |
|
124 | UM_EXPORT_METHOD_AS(watchPositionImplAsync,
|
125 | watchId:(nonnull NSNumber *)watchId
|
126 | options:(NSDictionary *)options
|
127 | resolver:(UMPromiseResolveBlock)resolve
|
128 | rejecter:(UMPromiseRejectBlock)reject)
|
129 | {
|
130 | if (![self checkPermissions:reject]) {
|
131 | return;
|
132 | }
|
133 |
|
134 | __weak typeof(self) weakSelf = self;
|
135 | CLLocationManager *locMgr = [self locationManagerWithOptions:options];
|
136 |
|
137 | EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> *locations) {
|
138 | if (locations.lastObject != nil && weakSelf != nil) {
|
139 | __strong typeof(weakSelf) strongSelf = weakSelf;
|
140 |
|
141 | CLLocation *loc = locations.lastObject;
|
142 | NSDictionary *body = @{
|
143 | @"watchId": watchId,
|
144 | @"location": [EXLocation exportLocation:loc],
|
145 | };
|
146 |
|
147 | [strongSelf->_eventEmitter sendEventWithName:EXLocationChangedEventName body:body];
|
148 | }
|
149 | } onUpdateHeadings:nil onError:^(NSError *error) {
|
150 | // TODO: report errors
|
151 | // (ben) error could be (among other things):
|
152 | // - kCLErrorDenied - we should use the same UNAUTHORIZED behavior as elsewhere
|
153 | // - kCLErrorLocationUnknown - we can actually ignore this error and keep tracking
|
154 | // location (I think -- my knowledge might be a few months out of date)
|
155 | }];
|
156 |
|
157 | _delegates[delegate.watchId] = delegate;
|
158 | locMgr.delegate = delegate;
|
159 | [locMgr startUpdatingLocation];
|
160 | resolve([NSNull null]);
|
161 | }
|
162 |
|
163 | UM_EXPORT_METHOD_AS(getLastKnownPositionAsync,
|
164 | getLastKnownPositionWithOptions:(NSDictionary *)options
|
165 | resolve:(UMPromiseResolveBlock)resolve
|
166 | rejecter:(UMPromiseRejectBlock)reject)
|
167 | {
|
168 | if (![self checkPermissions:reject]) {
|
169 | return;
|
170 | }
|
171 | CLLocation *location = [[self locationManagerWithOptions:nil] location];
|
172 |
|
173 | if ([self.class isLocation:location validWithOptions:options]) {
|
174 | resolve([EXLocation exportLocation:location]);
|
175 | } else {
|
176 | resolve([NSNull null]);
|
177 | }
|
178 | }
|
179 |
|
180 | // Watch method for getting compass updates
|
181 | UM_EXPORT_METHOD_AS(watchDeviceHeading,
|
182 | watchHeadingWithWatchId:(nonnull NSNumber *)watchId
|
183 | resolve:(UMPromiseResolveBlock)resolve
|
184 | reject:(UMPromiseRejectBlock)reject) {
|
185 | if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXLocationPermissionRequester class]]) {
|
186 | reject(@"E_LOCATION_UNAUTHORIZED", @"Not authorized to use location services", nil);
|
187 | return;
|
188 | }
|
189 |
|
190 | __weak typeof(self) weakSelf = self;
|
191 | CLLocationManager *locMgr = [[CLLocationManager alloc] init];
|
192 |
|
193 | locMgr.distanceFilter = kCLDistanceFilterNone;
|
194 | locMgr.desiredAccuracy = kCLLocationAccuracyBest;
|
195 | locMgr.allowsBackgroundLocationUpdates = NO;
|
196 |
|
197 | EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations: nil onUpdateHeadings:^(CLHeading *newHeading) {
|
198 | if (newHeading != nil && weakSelf != nil) {
|
199 | __strong typeof(weakSelf) strongSelf = weakSelf;
|
200 | NSNumber *accuracy;
|
201 |
|
202 | // Convert iOS heading accuracy to Android system
|
203 | // 3: high accuracy, 2: medium, 1: low, 0: none
|
204 | if (newHeading.headingAccuracy > 50 || newHeading.headingAccuracy < 0) {
|
205 | accuracy = @(0);
|
206 | } else if (newHeading.headingAccuracy > 35) {
|
207 | accuracy = @(1);
|
208 | } else if (newHeading.headingAccuracy > 20) {
|
209 | accuracy = @(2);
|
210 | } else {
|
211 | accuracy = @(3);
|
212 | }
|
213 | NSDictionary *body = @{@"watchId": watchId,
|
214 | @"heading": @{
|
215 | @"trueHeading": @(newHeading.trueHeading),
|
216 | @"magHeading": @(newHeading.magneticHeading),
|
217 | @"accuracy": accuracy,
|
218 | },
|
219 | };
|
220 | [strongSelf->_eventEmitter sendEventWithName:EXHeadingChangedEventName body:body];
|
221 | }
|
222 | } onError:^(NSError *error) {
|
223 | // Error getting updates
|
224 | }];
|
225 |
|
226 | _delegates[delegate.watchId] = delegate;
|
227 | locMgr.delegate = delegate;
|
228 | [locMgr startUpdatingHeading];
|
229 | resolve([NSNull null]);
|
230 | }
|
231 |
|
232 | UM_EXPORT_METHOD_AS(removeWatchAsync,
|
233 | watchId:(nonnull NSNumber *)watchId
|
234 | resolver:(UMPromiseResolveBlock)resolve
|
235 | rejecter:(UMPromiseRejectBlock)reject)
|
236 | {
|
237 | EXLocationDelegate *delegate = _delegates[watchId];
|
238 |
|
239 | if (delegate) {
|
240 | // Unsuscribe from both location and heading updates
|
241 | [delegate.locMgr stopUpdatingLocation];
|
242 | [delegate.locMgr stopUpdatingHeading];
|
243 | delegate.locMgr.delegate = nil;
|
244 | [_delegates removeObjectForKey:watchId];
|
245 | }
|
246 | resolve([NSNull null]);
|
247 | }
|
248 |
|
249 | UM_EXPORT_METHOD_AS(geocodeAsync,
|
250 | address:(nonnull NSString *)address
|
251 | resolver:(UMPromiseResolveBlock)resolve
|
252 | rejecter:(UMPromiseRejectBlock)reject)
|
253 | {
|
254 | CLGeocoder *geocoder = [[CLGeocoder alloc] init];
|
255 |
|
256 | [geocoder geocodeAddressString:address completionHandler:^(NSArray* placemarks, NSError* error){
|
257 | if (!error) {
|
258 | NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
|
259 | for (CLPlacemark* placemark in placemarks) {
|
260 | CLLocation *location = placemark.location;
|
261 | [results addObject:@{
|
262 | @"latitude": @(location.coordinate.latitude),
|
263 | @"longitude": @(location.coordinate.longitude),
|
264 | @"altitude": @(location.altitude),
|
265 | @"accuracy": @(location.horizontalAccuracy),
|
266 | }];
|
267 | }
|
268 | resolve(results);
|
269 | } else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
|
270 | resolve(@[]);
|
271 | } else if (error.code == kCLErrorNetwork) {
|
272 | reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
|
273 | } else {
|
274 | reject(@"E_GEOCODING_FAILED", @"Error while geocoding an address", error);
|
275 | }
|
276 | }];
|
277 | }
|
278 |
|
279 | UM_EXPORT_METHOD_AS(reverseGeocodeAsync,
|
280 | locationMap:(nonnull NSDictionary *)locationMap
|
281 | resolver:(UMPromiseResolveBlock)resolve
|
282 | rejecter:(UMPromiseRejectBlock)reject)
|
283 | {
|
284 | CLGeocoder *geocoder = [[CLGeocoder alloc] init];
|
285 | CLLocation *location = [[CLLocation alloc] initWithLatitude:[locationMap[@"latitude"] floatValue] longitude:[locationMap[@"longitude"] floatValue]];
|
286 |
|
287 | [geocoder reverseGeocodeLocation:location completionHandler:^(NSArray* placemarks, NSError* error){
|
288 | if (!error) {
|
289 | NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
|
290 | for (CLPlacemark* placemark in placemarks) {
|
291 | NSDictionary *address = @{
|
292 | @"city": UMNullIfNil(placemark.locality),
|
293 | @"district": UMNullIfNil(placemark.subLocality),
|
294 | @"street": UMNullIfNil(placemark.thoroughfare),
|
295 | @"region": UMNullIfNil(placemark.administrativeArea),
|
296 | @"subregion": UMNullIfNil(placemark.subAdministrativeArea),
|
297 | @"country": UMNullIfNil(placemark.country),
|
298 | @"postalCode": UMNullIfNil(placemark.postalCode),
|
299 | @"name": UMNullIfNil(placemark.name),
|
300 | @"isoCountryCode": UMNullIfNil(placemark.ISOcountryCode),
|
301 | @"timezone": UMNullIfNil(placemark.timeZone.name),
|
302 | };
|
303 | [results addObject:address];
|
304 | }
|
305 | resolve(results);
|
306 | } else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
|
307 | resolve(@[]);
|
308 | } else if (error.code == kCLErrorNetwork) {
|
309 | reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
|
310 | } else {
|
311 | reject(@"E_REVGEOCODING_FAILED", @"Error while reverse-geocoding a location", error);
|
312 | }
|
313 | }];
|
314 | }
|
315 |
|
316 | UM_EXPORT_METHOD_AS(getPermissionsAsync,
|
317 | getPermissionsAsync:(UMPromiseResolveBlock)resolve
|
318 | rejecter:(UMPromiseRejectBlock)reject)
|
319 | {
|
320 | [UMPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
|
321 | withRequester:[EXLocationPermissionRequester class]
|
322 | resolve:resolve
|
323 | reject:reject];
|
324 | }
|
325 |
|
326 | UM_EXPORT_METHOD_AS(requestPermissionsAsync,
|
327 | requestPermissionsAsync:(UMPromiseResolveBlock)resolve
|
328 | rejecter:(UMPromiseRejectBlock)reject)
|
329 | {
|
330 | [UMPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
|
331 | withRequester:[EXLocationPermissionRequester class]
|
332 | resolve:resolve
|
333 | reject:reject];
|
334 | }
|
335 |
|
336 | UM_EXPORT_METHOD_AS(hasServicesEnabledAsync,
|
337 | hasServicesEnabled:(UMPromiseResolveBlock)resolve
|
338 | reject:(UMPromiseRejectBlock)reject)
|
339 | {
|
340 | BOOL servicesEnabled = [CLLocationManager locationServicesEnabled];
|
341 | resolve(@(servicesEnabled));
|
342 | }
|
343 |
|
344 | # pragma mark - Background location
|
345 |
|
346 | UM_EXPORT_METHOD_AS(startLocationUpdatesAsync,
|
347 | startLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
|
348 | withOptions:(nonnull NSDictionary *)options
|
349 | resolve:(UMPromiseResolveBlock)resolve
|
350 | reject:(UMPromiseRejectBlock)reject)
|
351 | {
|
352 | if (![self checkPermissions:reject] || ![self checkTaskManagerExists:reject] || ![self checkBackgroundServices:reject]) {
|
353 | return;
|
354 | }
|
355 | if (![CLLocationManager significantLocationChangeMonitoringAvailable]) {
|
356 | return reject(@"E_SIGNIFICANT_CHANGES_UNAVAILABLE", @"Significant location changes monitoring is not available.", nil);
|
357 | }
|
358 |
|
359 | @try {
|
360 | [_tasksManager registerTaskWithName:taskName consumer:[EXLocationTaskConsumer class] options:options];
|
361 | }
|
362 | @catch (NSException *e) {
|
363 | return reject(e.name, e.reason, nil);
|
364 | }
|
365 | resolve(nil);
|
366 | }
|
367 |
|
368 | UM_EXPORT_METHOD_AS(stopLocationUpdatesAsync,
|
369 | stopLocationUpdatesForTaskWithName:(NSString *)taskName
|
370 | resolve:(UMPromiseResolveBlock)resolve
|
371 | reject:(UMPromiseRejectBlock)reject)
|
372 | {
|
373 | if (![self checkTaskManagerExists:reject]) {
|
374 | return;
|
375 | }
|
376 |
|
377 | @try {
|
378 | [_tasksManager unregisterTaskWithName:taskName consumerClass:[EXLocationTaskConsumer class]];
|
379 | } @catch (NSException *e) {
|
380 | return reject(e.name, e.reason, nil);
|
381 | }
|
382 | resolve(nil);
|
383 | }
|
384 |
|
385 | UM_EXPORT_METHOD_AS(hasStartedLocationUpdatesAsync,
|
386 | hasStartedLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
|
387 | resolve:(UMPromiseResolveBlock)resolve
|
388 | reject:(UMPromiseRejectBlock)reject)
|
389 | {
|
390 | if (![self checkTaskManagerExists:reject]) {
|
391 | return;
|
392 | }
|
393 |
|
394 | resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXLocationTaskConsumer class]]));
|
395 | }
|
396 |
|
397 | # pragma mark - Geofencing
|
398 |
|
399 | UM_EXPORT_METHOD_AS(startGeofencingAsync,
|
400 | startGeofencingWithTaskName:(nonnull NSString *)taskName
|
401 | withOptions:(nonnull NSDictionary *)options
|
402 | resolve:(UMPromiseResolveBlock)resolve
|
403 | reject:(UMPromiseRejectBlock)reject)
|
404 | {
|
405 | if (![self checkPermissions:reject] || ![self checkTaskManagerExists:reject]) {
|
406 | return;
|
407 | }
|
408 | if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
|
409 | return reject(@"E_GEOFENCING_UNAVAILABLE", @"Geofencing is not available", nil);
|
410 | }
|
411 |
|
412 | @try {
|
413 | [_tasksManager registerTaskWithName:taskName consumer:[EXGeofencingTaskConsumer class] options:options];
|
414 | } @catch (NSException *e) {
|
415 | return reject(e.name, e.reason, nil);
|
416 | }
|
417 | resolve(nil);
|
418 | }
|
419 |
|
420 | UM_EXPORT_METHOD_AS(stopGeofencingAsync,
|
421 | stopGeofencingWithTaskName:(nonnull NSString *)taskName
|
422 | resolve:(UMPromiseResolveBlock)resolve
|
423 | reject:(UMPromiseRejectBlock)reject)
|
424 | {
|
425 | if (![self checkTaskManagerExists:reject]) {
|
426 | return;
|
427 | }
|
428 |
|
429 | @try {
|
430 | [_tasksManager unregisterTaskWithName:taskName consumerClass:[EXGeofencingTaskConsumer class]];
|
431 | } @catch (NSException *e) {
|
432 | return reject(e.name, e.reason, nil);
|
433 | }
|
434 | resolve(nil);
|
435 | }
|
436 |
|
437 | UM_EXPORT_METHOD_AS(hasStartedGeofencingAsync,
|
438 | hasStartedGeofencingForTaskWithName:(NSString *)taskName
|
439 | resolve:(UMPromiseResolveBlock)resolve
|
440 | reject:(UMPromiseRejectBlock)reject)
|
441 | {
|
442 | if (![self checkTaskManagerExists:reject]) {
|
443 | return;
|
444 | }
|
445 |
|
446 | resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXGeofencingTaskConsumer class]]));
|
447 | }
|
448 |
|
449 | # pragma mark - helpers
|
450 |
|
451 | - (CLLocationManager *)locationManagerWithOptions:(nullable NSDictionary *)options
|
452 | {
|
453 | CLLocationManager *locMgr = [[CLLocationManager alloc] init];
|
454 | locMgr.allowsBackgroundLocationUpdates = NO;
|
455 |
|
456 | if (options) {
|
457 | locMgr.distanceFilter = options[@"distanceInterval"] ? [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone : kCLLocationAccuracyHundredMeters;
|
458 |
|
459 | if (options[@"accuracy"]) {
|
460 | EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
|
461 | locMgr.desiredAccuracy = [self.class CLLocationAccuracyFromOption:accuracy];
|
462 | }
|
463 | }
|
464 | return locMgr;
|
465 | }
|
466 |
|
467 | - (BOOL)checkPermissions:(UMPromiseRejectBlock)reject
|
468 | {
|
469 | if (![CLLocationManager locationServicesEnabled]) {
|
470 | reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
|
471 | return NO;
|
472 | }
|
473 | if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXLocationPermissionRequester class]]) {
|
474 | reject(@"E_NO_PERMISSIONS", @"LOCATION permission is required to do this operation.", nil);
|
475 | return NO;
|
476 | }
|
477 | return YES;
|
478 | }
|
479 |
|
480 | - (BOOL)checkTaskManagerExists:(UMPromiseRejectBlock)reject
|
481 | {
|
482 | if (_tasksManager == nil) {
|
483 | reject(@"E_TASKMANAGER_NOT_FOUND", @"`expo-task-manager` module is required to use background services.", nil);
|
484 | return NO;
|
485 | }
|
486 | return YES;
|
487 | }
|
488 |
|
489 | - (BOOL)checkBackgroundServices:(UMPromiseRejectBlock)reject
|
490 | {
|
491 | if (![_tasksManager hasBackgroundModeEnabled:@"location"]) {
|
492 | reject(@"E_BACKGROUND_SERVICES_DISABLED", @"Background Location has not been configured. To enable it, add `location` to `UIBackgroundModes` in Info.plist file.", nil);
|
493 | return NO;
|
494 | }
|
495 | return YES;
|
496 | }
|
497 |
|
498 | # pragma mark - static helpers
|
499 |
|
500 | + (NSDictionary *)exportLocation:(CLLocation *)location
|
501 | {
|
502 | return @{
|
503 | @"coords": @{
|
504 | @"latitude": @(location.coordinate.latitude),
|
505 | @"longitude": @(location.coordinate.longitude),
|
506 | @"altitude": @(location.altitude),
|
507 | @"accuracy": @(location.horizontalAccuracy),
|
508 | @"altitudeAccuracy": @(location.verticalAccuracy),
|
509 | @"heading": @(location.course),
|
510 | @"speed": @(location.speed),
|
511 | },
|
512 | @"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
|
513 | };
|
514 | }
|
515 |
|
516 | + (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy
|
517 | {
|
518 | switch (accuracy) {
|
519 | case EXLocationAccuracyLowest:
|
520 | return kCLLocationAccuracyThreeKilometers;
|
521 | case EXLocationAccuracyLow:
|
522 | return kCLLocationAccuracyKilometer;
|
523 | case EXLocationAccuracyBalanced:
|
524 | return kCLLocationAccuracyHundredMeters;
|
525 | case EXLocationAccuracyHigh:
|
526 | return kCLLocationAccuracyNearestTenMeters;
|
527 | case EXLocationAccuracyHighest:
|
528 | return kCLLocationAccuracyBest;
|
529 | case EXLocationAccuracyBestForNavigation:
|
530 | return kCLLocationAccuracyBestForNavigation;
|
531 | default:
|
532 | return kCLLocationAccuracyHundredMeters;
|
533 | }
|
534 | }
|
535 |
|
536 | + (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType
|
537 | {
|
538 | if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) {
|
539 | return activityType;
|
540 | }
|
541 | if (@available(iOS 12.0, *)) {
|
542 | if (activityType == CLActivityTypeAirborne) {
|
543 | return activityType;
|
544 | }
|
545 | }
|
546 | return CLActivityTypeOther;
|
547 | }
|
548 |
|
549 | + (BOOL)isLocation:(nullable CLLocation *)location validWithOptions:(nullable NSDictionary *)options
|
550 | {
|
551 | if (location == nil) {
|
552 | return NO;
|
553 | }
|
554 | NSTimeInterval maxAge = options[@"maxAge"] ? [options[@"maxAge"] doubleValue] : DBL_MAX;
|
555 | CLLocationAccuracy requiredAccuracy = options[@"requiredAccuracy"] ? [options[@"requiredAccuracy"] doubleValue] : DBL_MAX;
|
556 | NSTimeInterval timeDiff = -location.timestamp.timeIntervalSinceNow;
|
557 |
|
558 | return location != nil && timeDiff * 1000 <= maxAge && location.horizontalAccuracy <= requiredAccuracy;
|
559 | }
|
560 |
|
561 | @end
|
562 |
|
563 | NS_ASSUME_NONNULL_END
|