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

216 lines
7.0 KiB
Objective-C

// 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