UNPKG

21.1 kBPlain TextView Raw
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
24NS_ASSUME_NONNULL_BEGIN
25
26NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
27NSString * 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
41UM_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
78UM_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
89UM_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
124UM_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
163UM_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
181UM_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
232UM_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
249UM_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
279UM_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
316UM_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
326UM_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
336UM_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
346UM_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
368UM_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
385UM_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
399UM_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
420UM_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
437UM_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
563NS_ASSUME_NONNULL_END