564 lines
21 KiB
Objective-C
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
|