This repository has been archived on 2022-03-12. You can view files and clone it, but cannot push or open issues or pull requests.
2021-04-02 02:24:13 +03:00

564 lines
21 KiB
Objective-C

// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocation.h>
#import <EXLocation/EXLocationDelegate.h>
#import <EXLocation/EXLocationTaskConsumer.h>
#import <EXLocation/EXGeofencingTaskConsumer.h>
#import <EXLocation/EXLocationPermissionRequester.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <CoreLocation/CLHeading.h>
#import <CoreLocation/CLGeocoder.h>
#import <CoreLocation/CLPlacemark.h>
#import <CoreLocation/CLError.h>
#import <CoreLocation/CLCircularRegion.h>
#import <UMCore/UMEventEmitterService.h>
#import <UMPermissionsInterface/UMPermissionsInterface.h>
#import <UMPermissionsInterface/UMPermissionsMethodsDelegate.h>
#import <UMTaskManagerInterface/UMTaskManagerInterface.h>
NS_ASSUME_NONNULL_BEGIN
NSString * const EXLocationChangedEventName = @"Expo.locationChanged";
NSString * const EXHeadingChangedEventName = @"Expo.headingChanged";
@interface EXLocation ()
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, EXLocationDelegate*> *delegates;
@property (nonatomic, strong) NSMutableSet<EXLocationDelegate *> *retainedDelegates;
@property (nonatomic, weak) id<UMEventEmitterService> eventEmitter;
@property (nonatomic, weak) id<UMPermissionsInterface> permissionsManager;
@property (nonatomic, weak) id<UMTaskManagerInterface> tasksManager;
@end
@implementation EXLocation
UM_EXPORT_MODULE(ExpoLocation);
- (instancetype)init
{
if (self = [super init]) {
_delegates = [NSMutableDictionary dictionary];
_retainedDelegates = [NSMutableSet set];
}
return self;
}
- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
{
_eventEmitter = [moduleRegistry getModuleImplementingProtocol:@protocol(UMEventEmitterService)];
_permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(UMPermissionsInterface)];
[UMPermissionsMethodsDelegate registerRequesters:@[[EXLocationPermissionRequester new]] withPermissionsManager:_permissionsManager];
_tasksManager = [moduleRegistry getModuleImplementingProtocol:@protocol(UMTaskManagerInterface)];
}
- (dispatch_queue_t)methodQueue
{
// Location managers must be created on the main thread
return dispatch_get_main_queue();
}
# pragma mark - UMEventEmitter
- (NSArray<NSString *> *)supportedEvents
{
return @[EXLocationChangedEventName, EXHeadingChangedEventName];
}
- (void)startObserving {}
- (void)stopObserving {}
# pragma mark - Exported methods
UM_EXPORT_METHOD_AS(getProviderStatusAsync,
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
resolve(@{
@"locationServicesEnabled": @([CLLocationManager locationServicesEnabled]),
@"backgroundModeEnabled": @([_tasksManager hasBackgroundModeEnabled:@"location"]),
});
}
UM_EXPORT_METHOD_AS(getCurrentPositionAsync,
options:(NSDictionary *)options
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
if (![self checkPermissions:reject]) {
return;
}
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
__weak typeof(self) weakSelf = self;
__block EXLocationDelegate *delegate;
delegate = [[EXLocationDelegate alloc] initWithId:nil withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> * _Nonnull locations) {
if (delegate != nil) {
if (locations.lastObject != nil) {
resolve([EXLocation exportLocation:locations.lastObject]);
} else {
reject(@"E_LOCATION_NOT_FOUND", @"Current location not found.", nil);
}
[weakSelf.retainedDelegates removeObject:delegate];
delegate = nil;
}
} onUpdateHeadings:nil onError:^(NSError *error) {
reject(@"E_LOCATION_UNAVAILABLE", [@"Cannot obtain current location: " stringByAppendingString:error.description], nil);
}];
// retain location manager delegate so it will not dealloc until onUpdateLocations gets called
[_retainedDelegates addObject:delegate];
locMgr.delegate = delegate;
[locMgr requestLocation];
}
UM_EXPORT_METHOD_AS(watchPositionImplAsync,
watchId:(nonnull NSNumber *)watchId
options:(NSDictionary *)options
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
if (![self checkPermissions:reject]) {
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [self locationManagerWithOptions:options];
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations:^(NSArray<CLLocation *> *locations) {
if (locations.lastObject != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
CLLocation *loc = locations.lastObject;
NSDictionary *body = @{
@"watchId": watchId,
@"location": [EXLocation exportLocation:loc],
};
[strongSelf->_eventEmitter sendEventWithName:EXLocationChangedEventName body:body];
}
} onUpdateHeadings:nil onError:^(NSError *error) {
// TODO: report errors
// (ben) error could be (among other things):
// - kCLErrorDenied - we should use the same UNAUTHORIZED behavior as elsewhere
// - kCLErrorLocationUnknown - we can actually ignore this error and keep tracking
// location (I think -- my knowledge might be a few months out of date)
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingLocation];
resolve([NSNull null]);
}
UM_EXPORT_METHOD_AS(getLastKnownPositionAsync,
getLastKnownPositionWithOptions:(NSDictionary *)options
resolve:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
if (![self checkPermissions:reject]) {
return;
}
CLLocation *location = [[self locationManagerWithOptions:nil] location];
if ([self.class isLocation:location validWithOptions:options]) {
resolve([EXLocation exportLocation:location]);
} else {
resolve([NSNull null]);
}
}
// Watch method for getting compass updates
UM_EXPORT_METHOD_AS(watchDeviceHeading,
watchHeadingWithWatchId:(nonnull NSNumber *)watchId
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject) {
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXLocationPermissionRequester class]]) {
reject(@"E_LOCATION_UNAUTHORIZED", @"Not authorized to use location services", nil);
return;
}
__weak typeof(self) weakSelf = self;
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.distanceFilter = kCLDistanceFilterNone;
locMgr.desiredAccuracy = kCLLocationAccuracyBest;
locMgr.allowsBackgroundLocationUpdates = NO;
EXLocationDelegate *delegate = [[EXLocationDelegate alloc] initWithId:watchId withLocMgr:locMgr onUpdateLocations: nil onUpdateHeadings:^(CLHeading *newHeading) {
if (newHeading != nil && weakSelf != nil) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSNumber *accuracy;
// Convert iOS heading accuracy to Android system
// 3: high accuracy, 2: medium, 1: low, 0: none
if (newHeading.headingAccuracy > 50 || newHeading.headingAccuracy < 0) {
accuracy = @(0);
} else if (newHeading.headingAccuracy > 35) {
accuracy = @(1);
} else if (newHeading.headingAccuracy > 20) {
accuracy = @(2);
} else {
accuracy = @(3);
}
NSDictionary *body = @{@"watchId": watchId,
@"heading": @{
@"trueHeading": @(newHeading.trueHeading),
@"magHeading": @(newHeading.magneticHeading),
@"accuracy": accuracy,
},
};
[strongSelf->_eventEmitter sendEventWithName:EXHeadingChangedEventName body:body];
}
} onError:^(NSError *error) {
// Error getting updates
}];
_delegates[delegate.watchId] = delegate;
locMgr.delegate = delegate;
[locMgr startUpdatingHeading];
resolve([NSNull null]);
}
UM_EXPORT_METHOD_AS(removeWatchAsync,
watchId:(nonnull NSNumber *)watchId
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
EXLocationDelegate *delegate = _delegates[watchId];
if (delegate) {
// Unsuscribe from both location and heading updates
[delegate.locMgr stopUpdatingLocation];
[delegate.locMgr stopUpdatingHeading];
delegate.locMgr.delegate = nil;
[_delegates removeObjectForKey:watchId];
}
resolve([NSNull null]);
}
UM_EXPORT_METHOD_AS(geocodeAsync,
address:(nonnull NSString *)address
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:address completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
CLLocation *location = placemark.location;
[results addObject:@{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
}];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_GEOCODING_FAILED", @"Error while geocoding an address", error);
}
}];
}
UM_EXPORT_METHOD_AS(reverseGeocodeAsync,
locationMap:(nonnull NSDictionary *)locationMap
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:[locationMap[@"latitude"] floatValue] longitude:[locationMap[@"longitude"] floatValue]];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray* placemarks, NSError* error){
if (!error) {
NSMutableArray *results = [NSMutableArray arrayWithCapacity:placemarks.count];
for (CLPlacemark* placemark in placemarks) {
NSDictionary *address = @{
@"city": UMNullIfNil(placemark.locality),
@"district": UMNullIfNil(placemark.subLocality),
@"street": UMNullIfNil(placemark.thoroughfare),
@"region": UMNullIfNil(placemark.administrativeArea),
@"subregion": UMNullIfNil(placemark.subAdministrativeArea),
@"country": UMNullIfNil(placemark.country),
@"postalCode": UMNullIfNil(placemark.postalCode),
@"name": UMNullIfNil(placemark.name),
@"isoCountryCode": UMNullIfNil(placemark.ISOcountryCode),
@"timezone": UMNullIfNil(placemark.timeZone.name),
};
[results addObject:address];
}
resolve(results);
} else if (error.code == kCLErrorGeocodeFoundNoResult || error.code == kCLErrorGeocodeFoundPartialResult) {
resolve(@[]);
} else if (error.code == kCLErrorNetwork) {
reject(@"E_RATE_EXCEEDED", @"Rate limit exceeded - too many requests", error);
} else {
reject(@"E_REVGEOCODING_FAILED", @"Error while reverse-geocoding a location", error);
}
}];
}
UM_EXPORT_METHOD_AS(getPermissionsAsync,
getPermissionsAsync:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
[UMPermissionsMethodsDelegate getPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
UM_EXPORT_METHOD_AS(requestPermissionsAsync,
requestPermissionsAsync:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
[UMPermissionsMethodsDelegate askForPermissionWithPermissionsManager:_permissionsManager
withRequester:[EXLocationPermissionRequester class]
resolve:resolve
reject:reject];
}
UM_EXPORT_METHOD_AS(hasServicesEnabledAsync,
hasServicesEnabled:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
BOOL servicesEnabled = [CLLocationManager locationServicesEnabled];
resolve(@(servicesEnabled));
}
# pragma mark - Background location
UM_EXPORT_METHOD_AS(startLocationUpdatesAsync,
startLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkPermissions:reject] || ![self checkTaskManagerExists:reject] || ![self checkBackgroundServices:reject]) {
return;
}
if (![CLLocationManager significantLocationChangeMonitoringAvailable]) {
return reject(@"E_SIGNIFICANT_CHANGES_UNAVAILABLE", @"Significant location changes monitoring is not available.", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXLocationTaskConsumer class] options:options];
}
@catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
UM_EXPORT_METHOD_AS(stopLocationUpdatesAsync,
stopLocationUpdatesForTaskWithName:(NSString *)taskName
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXLocationTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
UM_EXPORT_METHOD_AS(hasStartedLocationUpdatesAsync,
hasStartedLocationUpdatesForTaskWithName:(nonnull NSString *)taskName
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXLocationTaskConsumer class]]));
}
# pragma mark - Geofencing
UM_EXPORT_METHOD_AS(startGeofencingAsync,
startGeofencingWithTaskName:(nonnull NSString *)taskName
withOptions:(nonnull NSDictionary *)options
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkPermissions:reject] || ![self checkTaskManagerExists:reject]) {
return;
}
if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
return reject(@"E_GEOFENCING_UNAVAILABLE", @"Geofencing is not available", nil);
}
@try {
[_tasksManager registerTaskWithName:taskName consumer:[EXGeofencingTaskConsumer class] options:options];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
UM_EXPORT_METHOD_AS(stopGeofencingAsync,
stopGeofencingWithTaskName:(nonnull NSString *)taskName
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
@try {
[_tasksManager unregisterTaskWithName:taskName consumerClass:[EXGeofencingTaskConsumer class]];
} @catch (NSException *e) {
return reject(e.name, e.reason, nil);
}
resolve(nil);
}
UM_EXPORT_METHOD_AS(hasStartedGeofencingAsync,
hasStartedGeofencingForTaskWithName:(NSString *)taskName
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject)
{
if (![self checkTaskManagerExists:reject]) {
return;
}
resolve(@([_tasksManager taskWithName:taskName hasConsumerOfClass:[EXGeofencingTaskConsumer class]]));
}
# pragma mark - helpers
- (CLLocationManager *)locationManagerWithOptions:(nullable NSDictionary *)options
{
CLLocationManager *locMgr = [[CLLocationManager alloc] init];
locMgr.allowsBackgroundLocationUpdates = NO;
if (options) {
locMgr.distanceFilter = options[@"distanceInterval"] ? [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone : kCLLocationAccuracyHundredMeters;
if (options[@"accuracy"]) {
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
locMgr.desiredAccuracy = [self.class CLLocationAccuracyFromOption:accuracy];
}
}
return locMgr;
}
- (BOOL)checkPermissions:(UMPromiseRejectBlock)reject
{
if (![CLLocationManager locationServicesEnabled]) {
reject(@"E_LOCATION_SERVICES_DISABLED", @"Location services are disabled", nil);
return NO;
}
if (![_permissionsManager hasGrantedPermissionUsingRequesterClass:[EXLocationPermissionRequester class]]) {
reject(@"E_NO_PERMISSIONS", @"LOCATION permission is required to do this operation.", nil);
return NO;
}
return YES;
}
- (BOOL)checkTaskManagerExists:(UMPromiseRejectBlock)reject
{
if (_tasksManager == nil) {
reject(@"E_TASKMANAGER_NOT_FOUND", @"`expo-task-manager` module is required to use background services.", nil);
return NO;
}
return YES;
}
- (BOOL)checkBackgroundServices:(UMPromiseRejectBlock)reject
{
if (![_tasksManager hasBackgroundModeEnabled:@"location"]) {
reject(@"E_BACKGROUND_SERVICES_DISABLED", @"Background Location has not been configured. To enable it, add `location` to `UIBackgroundModes` in Info.plist file.", nil);
return NO;
}
return YES;
}
# pragma mark - static helpers
+ (NSDictionary *)exportLocation:(CLLocation *)location
{
return @{
@"coords": @{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
@"accuracy": @(location.horizontalAccuracy),
@"altitudeAccuracy": @(location.verticalAccuracy),
@"heading": @(location.course),
@"speed": @(location.speed),
},
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
};
}
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy
{
switch (accuracy) {
case EXLocationAccuracyLowest:
return kCLLocationAccuracyThreeKilometers;
case EXLocationAccuracyLow:
return kCLLocationAccuracyKilometer;
case EXLocationAccuracyBalanced:
return kCLLocationAccuracyHundredMeters;
case EXLocationAccuracyHigh:
return kCLLocationAccuracyNearestTenMeters;
case EXLocationAccuracyHighest:
return kCLLocationAccuracyBest;
case EXLocationAccuracyBestForNavigation:
return kCLLocationAccuracyBestForNavigation;
default:
return kCLLocationAccuracyHundredMeters;
}
}
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType
{
if (activityType >= CLActivityTypeOther && activityType <= CLActivityTypeOtherNavigation) {
return activityType;
}
if (@available(iOS 12.0, *)) {
if (activityType == CLActivityTypeAirborne) {
return activityType;
}
}
return CLActivityTypeOther;
}
+ (BOOL)isLocation:(nullable CLLocation *)location validWithOptions:(nullable NSDictionary *)options
{
if (location == nil) {
return NO;
}
NSTimeInterval maxAge = options[@"maxAge"] ? [options[@"maxAge"] doubleValue] : DBL_MAX;
CLLocationAccuracy requiredAccuracy = options[@"requiredAccuracy"] ? [options[@"requiredAccuracy"] doubleValue] : DBL_MAX;
NSTimeInterval timeDiff = -location.timestamp.timeIntervalSinceNow;
return location != nil && timeDiff * 1000 <= maxAge && location.horizontalAccuracy <= requiredAccuracy;
}
@end
NS_ASSUME_NONNULL_END