This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

22
node_modules/expo-location/ios/EXLocation.podspec generated vendored Normal file
View File

@ -0,0 +1,22 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'EXLocation'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platform = :ios, '10.0'
s.source = { git: 'https://github.com/expo/expo.git' }
s.source_files = 'EXLocation/**/*.{h,m}'
s.preserve_paths = 'EXLocation/**/*.{h,m}'
s.requires_arc = true
s.dependency 'UMCore'
s.dependency 'UMPermissionsInterface'
s.dependency 'UMTaskManagerInterface'
end

39
node_modules/expo-location/ios/EXLocation/EXLocation.h generated vendored Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocation.h>
#import <CoreLocation/CLLocationManager.h>
#import <UMCore/UMEventEmitter.h>
#import <UMCore/UMExportedModule.h>
#import <UMCore/UMModuleRegistryConsumer.h>
// Location accuracies
typedef NS_ENUM(NSUInteger, EXLocationAccuracy) {
EXLocationAccuracyLowest = 1,
EXLocationAccuracyLow = 2,
EXLocationAccuracyBalanced = 3,
EXLocationAccuracyHigh = 4,
EXLocationAccuracyHighest = 5,
EXLocationAccuracyBestForNavigation = 6,
};
// Geofencing event types
typedef NS_ENUM(NSUInteger, EXGeofencingEventType) {
EXGeofencingEventTypeEnter = 1,
EXGeofencingEventTypeExit = 2,
};
// Geofencing region states
typedef NS_ENUM(NSUInteger, EXGeofencingRegionState) {
EXGeofencingRegionStateUnknown = 0,
EXGeofencingRegionStateInside = 1,
EXGeofencingRegionStateOutside = 2,
};
@interface EXLocation : UMExportedModule <UMEventEmitter, UMModuleRegistryConsumer>
+ (NSDictionary *)exportLocation:(CLLocation *)location;
+ (CLLocationAccuracy)CLLocationAccuracyFromOption:(EXLocationAccuracy)accuracy;
+ (CLActivityType)CLActivityTypeFromOption:(NSInteger)activityType;
@end

563
node_modules/expo-location/ios/EXLocation/EXLocation.m generated vendored Normal file
View File

@ -0,0 +1,563 @@
// 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

View File

@ -0,0 +1,32 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <CoreLocation/CLHeading.h>
#import <CoreLocation/CLLocation.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXLocationDelegate : NSObject <CLLocationManagerDelegate>
@property (nonatomic, strong) NSNumber *watchId;
@property (nonatomic, strong) CLLocationManager *locMgr;
@property (nonatomic, strong) void (^onUpdateLocations)(NSArray<CLLocation *> *locations);
@property (nonatomic, strong) void (^onUpdateHeadings)(CLHeading *newHeading);
@property (nonatomic, strong) void (^onError)(NSError *error);
- (instancetype)initWithId:(nullable NSNumber *)watchId
withLocMgr:(CLLocationManager *)locMgr
onUpdateLocations:(nullable void (^)(NSArray<CLLocation *> *locations))onUpdateLocations
onUpdateHeadings:(nullable void (^)(CLHeading *newHeading))onUpdateHeadings
onError:(nullable void (^)(NSError *error))onError;
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading;
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(nonnull NSError *)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,46 @@
// Copyright 2015-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocationDelegate.h>
@implementation EXLocationDelegate
- (instancetype)initWithId:(nullable NSNumber *)watchId
withLocMgr:(CLLocationManager *)locMgr
onUpdateLocations:(nullable void (^)(NSArray<CLLocation *> *locations))onUpdateLocations
onUpdateHeadings:(nullable void (^)(CLHeading *newHeading))onUpdateHeadings
onError:(nullable void (^)(NSError *error))onError
{
if ((self = [super init])) {
_watchId = watchId;
_locMgr = locMgr;
_onUpdateLocations = onUpdateLocations;
_onUpdateHeadings = onUpdateHeadings;
_onError = onError;
}
return self;
}
// Delegate method called by CLLocationManager
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
if (_onUpdateLocations) {
_onUpdateLocations(locations);
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
if (_onUpdateHeadings) {
_onUpdateHeadings(newHeading);
}
}
// Delegate method called by CLLocationManager
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(nonnull NSError *)error
{
if (_onError) {
_onError(error);
}
}
@end

View File

@ -0,0 +1,7 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <UMPermissionsInterface/UMPermissionsInterface.h>
@interface EXLocationPermissionRequester : NSObject<UMPermissionsRequester>
@end

View File

@ -0,0 +1,177 @@
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXLocation/EXLocationPermissionRequester.h>
#import <UMCore/UMUtilities.h>
#import <objc/message.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
static SEL alwaysAuthorizationSelector;
static SEL whenInUseAuthorizationSelector;
@interface EXLocationPermissionRequester () <CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locMgr;
@property (nonatomic, strong) UMPromiseResolveBlock resolve;
@property (nonatomic, strong) UMPromiseRejectBlock reject;
@end
@implementation EXLocationPermissionRequester
+ (NSString *)permissionType
{
return @"location";
}
+ (void)load
{
alwaysAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"AlwaysAuthorization"]);
whenInUseAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"WhenInUseAuthorization"]);
}
- (NSDictionary *)getPermissions
{
UMPermissionStatus status;
NSString *scope = @"none";
CLAuthorizationStatus systemStatus;
if (![[self class] isConfiguredForAlwaysAuthorization] && ![[self class] isConfiguredForWhenInUseAuthorization]) {
UMFatal(UMErrorWithMessage(@"This app is missing usage descriptions, so location services will fail. Add one of the `NSLocation*UsageDescription` keys to your bundle's Info.plist. See https://bit.ly/2P5fEbG (https://docs.expo.io/versions/latest/guides/app-stores.html#system-permissions-dialogs-on-ios) for more information."));
systemStatus = kCLAuthorizationStatusDenied;
} else {
systemStatus = [CLLocationManager authorizationStatus];
}
switch (systemStatus) {
case kCLAuthorizationStatusAuthorizedWhenInUse: {
status = UMPermissionStatusGranted;
scope = @"whenInUse";
break;
}
case kCLAuthorizationStatusAuthorizedAlways: {
status = UMPermissionStatusGranted;
scope = @"always";
break;
}
case kCLAuthorizationStatusDenied: case kCLAuthorizationStatusRestricted: {
status = UMPermissionStatusDenied;
break;
}
case kCLAuthorizationStatusNotDetermined: default: {
status = UMPermissionStatusUndetermined;
break;
}
}
return @{
@"status": @(status),
@"scope": scope
};
}
- (void)requestPermissionsWithResolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject
{
NSDictionary *existingPermissions = [self getPermissions];
if (existingPermissions && [existingPermissions[@"status"] intValue] != UMPermissionStatusUndetermined) {
// since permissions are already determined, the iOS request methods will be no-ops.
// just resolve with whatever existing permissions.
resolve(existingPermissions);
} else {
_resolve = resolve;
_reject = reject;
UM_WEAKIFY(self)
[UMUtilities performSynchronouslyOnMainThread:^{
UM_ENSURE_STRONGIFY(self)
self.locMgr = [[CLLocationManager alloc] init];
self.locMgr.delegate = self;
}];
// 1. Why do we call CLLocationManager methods by those dynamically created selectors?
//
// Most probably application code submitted to Apple Store is statically analyzed
// paying special attention to camelcase(request_always_location) being called on CLLocationManager.
// This lets Apple warn developers when it notices that location authorization may be requested
// while there is no NSLocationUsageDescription in Info.plist. Since we want to neither
// make Expo developers receive this kind of messages nor add our own default usage description,
// we try to fool the static analyzer and construct the selector in runtime.
// This way behavior of this requester is governed by provided NSLocationUsageDescriptions.
//
// 2. Why there's no way to call specifically whenInUse or always authorization?
//
// The requester sets itself as the delegate of the CLLocationManager, so when the user responds
// to a permission requesting dialog, manager calls `locationManager:didChangeAuthorizationStatus:` method.
// To be precise, manager calls this method in two circumstances:
// - right when `request*Authorization` method is called,
// - when `authorizationStatus` changes.
// With this behavior we aren't able to support the following use case:
// - app requests `whenInUse` authorization
// - user allows `whenInUse` authorization
// - `authorizationStatus` changes from `undetermined` to `whenInUse`, callback is called, promise is resolved
// - app wants to escalate authorization to `always`
// - user selects `whenInUse` authorization (iOS 11+)
// - `authorizationStatus` doesn't change, so callback is not called and requester can't know whether
// user responded to the dialog selecting `whenInUse` or is still deciding
// To support this use case we will have to change the way location authorization is requested
// from promise-based to listener-based.
if ([[self class] isConfiguredForAlwaysAuthorization] && [_locMgr respondsToSelector:alwaysAuthorizationSelector]) {
((void (*)(id, SEL))objc_msgSend)(_locMgr, alwaysAuthorizationSelector);
} else if ([[self class] isConfiguredForWhenInUseAuthorization] && [_locMgr respondsToSelector:whenInUseAuthorizationSelector]) {
((void (*)(id, SEL))objc_msgSend)(_locMgr, whenInUseAuthorizationSelector);
} else {
_reject(@"E_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil);
}
}
}
# pragma mark - internal
+ (BOOL)isConfiguredForWhenInUseAuthorization
{
return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil;
}
+ (BOOL)isConfiguredForAlwaysAuthorization
{
if (@available(iOS 11.0, *)) {
return [self isConfiguredForWhenInUseAuthorization] && [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"];
}
// iOS 10 fallback
return [self isConfiguredForWhenInUseAuthorization] && [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"];
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
if (_reject) {
_reject(@"E_LOCATION_ERROR_UNKNOWN", error.localizedDescription, error);
_resolve = nil;
_reject = nil;
}
}
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
// TODO: Permissions.LOCATION issue (search by this phrase)
// if Permissions.LOCATION is being called for the first time on iOS devide and prompts for user action it might not call this callback at all
// it happens if user requests more that one permission at the same time via Permissions.askAsync(...) and LOCATION dialog is not being called first
// to reproduce this find NCL code testing that
if (status == kCLAuthorizationStatusNotDetermined) {
// CLLocationManager calls this delegate method once on start with kCLAuthorizationNotDetermined even before the user responds
// to the "Don't Allow" / "Allow" dialog box. This isn't the event we care about so we skip it. See:
// http://stackoverflow.com/questions/30106341/swift-locationmanager-didchangeauthorizationstatus-always-called/30107511#30107511
return;
}
if (_resolve) {
_resolve([self getPermissions]);
_resolve = nil;
_reject = nil;
}
}
@end

View File

@ -0,0 +1,14 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <UMTaskManagerInterface/UMTaskConsumerInterface.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXGeofencingTaskConsumer : NSObject <UMTaskConsumerInterface, CLLocationManagerDelegate>
@property (nonatomic, strong) id<UMTaskInterface> task;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,215 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLCircularRegion.h>
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLErrorDomain.h>
#import <UMCore/UMUtilities.h>
#import <EXLocation/EXLocation.h>
#import <EXLocation/EXGeofencingTaskConsumer.h>
#import <UMTaskManagerInterface/UMTaskInterface.h>
@interface EXGeofencingTaskConsumer ()
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *regionStates;
@property (nonatomic, assign) BOOL backgroundOnly;
@end
@implementation EXGeofencingTaskConsumer
- (void)dealloc
{
[self reset];
}
# pragma mark - UMTaskConsumerInterface
- (NSString *)taskType
{
return @"geofencing";
}
- (void)setOptions:(nonnull NSDictionary *)options
{
[self stopMonitoringAllRegions];
[self startMonitoringRegionsForTask:self->_task];
}
- (void)didRegisterTask:(id<UMTaskInterface>)task
{
[self startMonitoringRegionsForTask:task];
}
- (void)didUnregister
{
[self reset];
}
# pragma mark - helpers
- (void)reset
{
[self stopMonitoringAllRegions];
[UMUtilities performSynchronouslyOnMainThread:^{
self->_locationManager = nil;
self->_task = nil;
}];
}
- (void)startMonitoringRegionsForTask:(id<UMTaskInterface>)task
{
[UMUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = [CLLocationManager new];
NSMutableDictionary *regionStates = [NSMutableDictionary new];
NSDictionary *options = [task options];
NSArray *regions = options[@"regions"];
self->_task = task;
self->_locationManager = locationManager;
self->_regionStates = regionStates;
locationManager.delegate = self;
locationManager.allowsBackgroundLocationUpdates = YES;
locationManager.pausesLocationUpdatesAutomatically = NO;
for (NSDictionary *regionDict in regions) {
NSString *identifier = regionDict[@"identifier"] ?: [[NSUUID UUID] UUIDString];
CLLocationDistance radius = [regionDict[@"radius"] doubleValue];
CLLocationCoordinate2D center = [self.class coordinateFromDictionary:regionDict];
BOOL notifyOnEntry = [self.class boolValueFrom:regionDict[@"notifyOnEntry"] defaultValue:YES];
BOOL notifyOnExit = [self.class boolValueFrom:regionDict[@"notifyOnExit"] defaultValue:YES];
CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center radius:radius identifier:identifier];
region.notifyOnEntry = notifyOnEntry;
region.notifyOnExit = notifyOnExit;
[regionStates setObject:@(CLRegionStateUnknown) forKey:identifier];
[locationManager startMonitoringForRegion:region];
[locationManager requestStateForRegion:region];
}
}];
}
- (void)stopMonitoringAllRegions
{
[UMUtilities performSynchronouslyOnMainThread:^{
for (CLRegion *region in self->_locationManager.monitoredRegions) {
[self->_locationManager stopMonitoringForRegion:region];
}
}];
}
- (void)executeTaskWithRegion:(nonnull CLRegion *)region eventType:(EXGeofencingEventType)eventType
{
if ([region isKindOfClass:[CLCircularRegion class]]) {
CLCircularRegion *circularRegion = (CLCircularRegion *)region;
CLRegionState regionState = [self regionStateForIdentifier:circularRegion.identifier];
NSDictionary *data = @{
@"eventType": @(eventType),
@"region": [[self class] exportRegion:circularRegion withState:regionState],
};
[_task executeWithData:data withError:nil];
}
}
# pragma mark - CLLocationManagerDelegate
// There is a bug in iOS that causes didEnterRegion and didExitRegion to be called multiple times.
// https://stackoverflow.com/questions/36807060/region-monitoring-method-getting-called-multiple-times-in-geo-fencing
// To prevent this behavior, we execute tasks only when the state has changed.
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != CLRegionStateInside) {
[self setRegionState:CLRegionStateInside forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:EXGeofencingEventTypeEnter];
}
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != CLRegionStateOutside) {
[self setRegionState:CLRegionStateOutside forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:EXGeofencingEventTypeExit];
}
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
[_task executeWithData:nil withError:error];
}
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
{
if (error && error.domain == kCLErrorDomain) {
// This error might happen when the device is not able to find out the location. Try to restart monitoring this region.
[_locationManager stopMonitoringForRegion:region];
[_locationManager startMonitoringForRegion:region];
[_locationManager requestStateForRegion:region];
}
}
- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
if ([self regionStateForIdentifier:region.identifier] != state) {
EXGeofencingEventType eventType = state == CLRegionStateInside ? EXGeofencingEventTypeEnter : EXGeofencingEventTypeExit;
[self setRegionState:state forIdentifier:region.identifier];
[self executeTaskWithRegion:region eventType:eventType];
}
}
# pragma mark - helpers
- (CLRegionState)regionStateForIdentifier:(NSString *)identifier
{
return [_regionStates[identifier] integerValue];
}
- (void)setRegionState:(CLRegionState)regionState forIdentifier:(NSString *)identifier
{
[_regionStates setObject:@(regionState) forKey:identifier];
}
# pragma mark - static helpers
+ (nonnull NSDictionary *)exportRegion:(nonnull CLCircularRegion *)region withState:(CLRegionState)regionState
{
return @{
@"identifier": region.identifier,
@"state": @([self exportRegionState:regionState]),
@"radius": @(region.radius),
@"latitude": @(region.center.latitude),
@"longitude": @(region.center.longitude),
};
}
+ (EXGeofencingRegionState)exportRegionState:(CLRegionState)regionState
{
switch (regionState) {
case CLRegionStateUnknown:
return EXGeofencingRegionStateUnknown;
case CLRegionStateInside:
return EXGeofencingRegionStateInside;
case CLRegionStateOutside:
return EXGeofencingRegionStateOutside;
}
}
+ (CLLocationCoordinate2D)coordinateFromDictionary:(nonnull NSDictionary *)dict
{
CLLocationDegrees latitude = [dict[@"latitude"] doubleValue];
CLLocationDegrees longitude = [dict[@"longitude"] doubleValue];
return CLLocationCoordinate2DMake(latitude, longitude);
}
+ (BOOL)boolValueFrom:(id)pointer defaultValue:(BOOL)defaultValue
{
return pointer == nil ? defaultValue : [pointer boolValue];
}
@end

View File

@ -0,0 +1,14 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManagerDelegate.h>
#import <UMTaskManagerInterface/UMTaskConsumerInterface.h>
NS_ASSUME_NONNULL_BEGIN
@interface EXLocationTaskConsumer : NSObject <UMTaskConsumerInterface, CLLocationManagerDelegate>
@property (nonatomic, strong) id<UMTaskInterface> task;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,190 @@
// Copyright 2018-present 650 Industries. All rights reserved.
#import <CoreLocation/CLLocationManager.h>
#import <CoreLocation/CLErrorDomain.h>
#import <UMCore/UMUtilities.h>
#import <EXLocation/EXLocation.h>
#import <EXLocation/EXLocationTaskConsumer.h>
#import <UMTaskManagerInterface/UMTaskInterface.h>
@interface EXLocationTaskConsumer ()
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray<CLLocation *> *deferredLocations;
@property (nonatomic, strong) CLLocation *lastReportedLocation;
@property (nonatomic, assign) CLLocationDistance deferredDistance;
@end
@implementation EXLocationTaskConsumer
- (instancetype)init
{
if (self = [super init]) {
_deferredLocations = [NSMutableArray new];
_deferredDistance = 0.0;
}
return self;
}
- (void)dealloc
{
[self reset];
}
# pragma mark - UMTaskConsumerInterface
- (NSString *)taskType
{
return @"location";
}
- (void)didRegisterTask:(id<UMTaskInterface>)task
{
[UMUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = [CLLocationManager new];
self->_task = task;
self->_locationManager = locationManager;
locationManager.delegate = self;
locationManager.allowsBackgroundLocationUpdates = YES;
// Set options-specific things in location manager.
[self setOptions:task.options];
}];
}
- (void)didUnregister
{
[self reset];
}
- (void)setOptions:(NSDictionary *)options
{
[UMUtilities performSynchronouslyOnMainThread:^{
CLLocationManager *locationManager = self->_locationManager;
EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
locationManager.desiredAccuracy = [EXLocation CLLocationAccuracyFromOption:accuracy];
locationManager.distanceFilter = [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone;
locationManager.activityType = [EXLocation CLActivityTypeFromOption:[options[@"activityType"] integerValue]];
locationManager.pausesLocationUpdatesAutomatically = [options[@"pausesUpdatesAutomatically"] boolValue];
if (@available(iOS 11.0, *)) {
locationManager.showsBackgroundLocationIndicator = [options[@"showsBackgroundLocationIndicator"] boolValue];
}
[locationManager startUpdatingLocation];
[locationManager startMonitoringSignificantLocationChanges];
}];
}
# pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
{
if (_task != nil && locations.count > 0) {
[self deferLocations:locations];
[self maybeReportDeferredLocations];
}
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
if (error.domain == kCLErrorDomain) {
// This error might happen when the device is not able to find out the location. Try to restart monitoring location.
[manager stopUpdatingLocation];
[manager stopMonitoringSignificantLocationChanges];
[manager startUpdatingLocation];
[manager startMonitoringSignificantLocationChanges];
} else {
[_task executeWithData:nil withError:error];
}
}
# pragma mark - internal
- (void)reset
{
[UMUtilities performSynchronouslyOnMainThread:^{
[self->_locationManager stopUpdatingLocation];
[self->_locationManager stopMonitoringSignificantLocationChanges];
[self->_deferredLocations removeAllObjects];
self->_lastReportedLocation = nil;
self->_deferredDistance = 0.0;
self->_locationManager = nil;
self->_task = nil;
}];
}
- (void)executeTaskWithDeferredLocations
{
// Execute task with deferred locations.
NSDictionary *data = @{ @"locations": [EXLocationTaskConsumer _exportLocations:_deferredLocations] };
[_task executeWithData:data withError:nil];
// Reset deferring state.
_lastReportedLocation = _deferredLocations.lastObject;
_deferredDistance = 0.0;
[_deferredLocations removeAllObjects];
}
- (void)maybeReportDeferredLocations
{
if ([self shouldReportDeferredLocations]) {
[self executeTaskWithDeferredLocations];
}
}
- (void)deferLocations:(NSArray<CLLocation *> *)locations
{
CLLocation *lastLocation = _deferredLocations.lastObject ?: _lastReportedLocation;
for (CLLocation *location in locations) {
if (lastLocation) {
_deferredDistance += [location distanceFromLocation:lastLocation];
}
lastLocation = location;
}
[_deferredLocations addObjectsFromArray:locations];
}
- (BOOL)shouldReportDeferredLocations
{
if (_deferredLocations.count <= 0) {
return NO;
}
UIApplicationState appState = [[UIApplication sharedApplication] applicationState];
if (appState == UIApplicationStateActive) {
// Don't defer location updates when app is in foreground state.
return YES;
}
CLLocation *oldestLocation = _lastReportedLocation ?: _deferredLocations.firstObject;
CLLocation *newestLocation = _deferredLocations.lastObject;
NSDictionary *options = _task.options;
CLLocationDistance distance = [self numberToDouble:options[@"deferredUpdatesDistance"] defaultValue:0];
NSTimeInterval interval = [self numberToDouble:options[@"deferredUpdatesInterval"] defaultValue:0];
return [newestLocation.timestamp timeIntervalSinceDate:oldestLocation.timestamp] >= interval / 1000.0 && _deferredDistance >= distance;
}
- (double)numberToDouble:(NSNumber *)number defaultValue:(double)defaultValue
{
return number == nil ? defaultValue : [number doubleValue];
}
+ (NSArray<NSDictionary *> *)_exportLocations:(NSArray<CLLocation *> *)locations
{
NSMutableArray<NSDictionary *> *result = [NSMutableArray new];
for (CLLocation *location in locations) {
[result addObject:[EXLocation exportLocation:location]];
}
return result;
}
@end