UNPKG

7.13 kBPlain TextView Raw
1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <CoreLocation/CLCircularRegion.h>
4#import <CoreLocation/CLLocationManager.h>
5#import <CoreLocation/CLErrorDomain.h>
6
7#import <UMCore/UMUtilities.h>
8#import <EXLocation/EXLocation.h>
9#import <EXLocation/EXGeofencingTaskConsumer.h>
10#import <UMTaskManagerInterface/UMTaskInterface.h>
11
12@interface EXGeofencingTaskConsumer ()
13
14@property (nonatomic, strong) CLLocationManager *locationManager;
15@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *regionStates;
16@property (nonatomic, assign) BOOL backgroundOnly;
17
18@end
19
20@implementation EXGeofencingTaskConsumer
21
22- (void)dealloc
23{
24 [self reset];
25}
26
27# pragma mark - UMTaskConsumerInterface
28
29- (NSString *)taskType
30{
31 return @"geofencing";
32}
33
34- (void)setOptions:(nonnull NSDictionary *)options
35{
36 [self stopMonitoringAllRegions];
37 [self startMonitoringRegionsForTask:self->_task];
38}
39
40- (void)didRegisterTask:(id<UMTaskInterface>)task
41{
42 [self startMonitoringRegionsForTask:task];
43}
44
45- (void)didUnregister
46{
47 [self reset];
48}
49
50# pragma mark - helpers
51
52- (void)reset
53{
54 [self stopMonitoringAllRegions];
55 [UMUtilities performSynchronouslyOnMainThread:^{
56 self->_locationManager = nil;
57 self->_task = nil;
58 }];
59}
60
61- (void)startMonitoringRegionsForTask:(id<UMTaskInterface>)task
62{
63 [UMUtilities performSynchronouslyOnMainThread:^{
64 CLLocationManager *locationManager = [CLLocationManager new];
65 NSMutableDictionary *regionStates = [NSMutableDictionary new];
66 NSDictionary *options = [task options];
67 NSArray *regions = options[@"regions"];
68
69 self->_task = task;
70 self->_locationManager = locationManager;
71 self->_regionStates = regionStates;
72
73 locationManager.delegate = self;
74 locationManager.allowsBackgroundLocationUpdates = YES;
75 locationManager.pausesLocationUpdatesAutomatically = NO;
76
77 for (NSDictionary *regionDict in regions) {
78 NSString *identifier = regionDict[@"identifier"] ?: [[NSUUID UUID] UUIDString];
79 CLLocationDistance radius = [regionDict[@"radius"] doubleValue];
80 CLLocationCoordinate2D center = [self.class coordinateFromDictionary:regionDict];
81 BOOL notifyOnEntry = [self.class boolValueFrom:regionDict[@"notifyOnEntry"] defaultValue:YES];
82 BOOL notifyOnExit = [self.class boolValueFrom:regionDict[@"notifyOnExit"] defaultValue:YES];
83
84 CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center radius:radius identifier:identifier];
85
86 region.notifyOnEntry = notifyOnEntry;
87 region.notifyOnExit = notifyOnExit;
88
89 [regionStates setObject:@(CLRegionStateUnknown) forKey:identifier];
90 [locationManager startMonitoringForRegion:region];
91 [locationManager requestStateForRegion:region];
92 }
93 }];
94}
95
96- (void)stopMonitoringAllRegions
97{
98 [UMUtilities performSynchronouslyOnMainThread:^{
99 for (CLRegion *region in self->_locationManager.monitoredRegions) {
100 [self->_locationManager stopMonitoringForRegion:region];
101 }
102 }];
103}
104
105- (void)executeTaskWithRegion:(nonnull CLRegion *)region eventType:(EXGeofencingEventType)eventType
106{
107 if ([region isKindOfClass:[CLCircularRegion class]]) {
108 CLCircularRegion *circularRegion = (CLCircularRegion *)region;
109 CLRegionState regionState = [self regionStateForIdentifier:circularRegion.identifier];
110 NSDictionary *data = @{
111 @"eventType": @(eventType),
112 @"region": [[self class] exportRegion:circularRegion withState:regionState],
113 };
114
115 [_task executeWithData:data withError:nil];
116 }
117}
118
119# pragma mark - CLLocationManagerDelegate
120
121// There is a bug in iOS that causes didEnterRegion and didExitRegion to be called multiple times.
122// https://stackoverflow.com/questions/36807060/region-monitoring-method-getting-called-multiple-times-in-geo-fencing
123// To prevent this behavior, we execute tasks only when the state has changed.
124
125- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
126{
127 if ([self regionStateForIdentifier:region.identifier] != CLRegionStateInside) {
128 [self setRegionState:CLRegionStateInside forIdentifier:region.identifier];
129 [self executeTaskWithRegion:region eventType:EXGeofencingEventTypeEnter];
130 }
131}
132
133- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
134{
135 if ([self regionStateForIdentifier:region.identifier] != CLRegionStateOutside) {
136 [self setRegionState:CLRegionStateOutside forIdentifier:region.identifier];
137 [self executeTaskWithRegion:region eventType:EXGeofencingEventTypeExit];
138 }
139}
140
141- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
142{
143 [_task executeWithData:nil withError:error];
144}
145
146- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error
147{
148 if (error && error.domain == kCLErrorDomain) {
149 // This error might happen when the device is not able to find out the location. Try to restart monitoring this region.
150 [_locationManager stopMonitoringForRegion:region];
151 [_locationManager startMonitoringForRegion:region];
152 [_locationManager requestStateForRegion:region];
153 }
154}
155
156- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
157{
158 if ([self regionStateForIdentifier:region.identifier] != state) {
159 EXGeofencingEventType eventType = state == CLRegionStateInside ? EXGeofencingEventTypeEnter : EXGeofencingEventTypeExit;
160
161 [self setRegionState:state forIdentifier:region.identifier];
162 [self executeTaskWithRegion:region eventType:eventType];
163 }
164}
165
166# pragma mark - helpers
167
168- (CLRegionState)regionStateForIdentifier:(NSString *)identifier
169{
170 return [_regionStates[identifier] integerValue];
171}
172
173- (void)setRegionState:(CLRegionState)regionState forIdentifier:(NSString *)identifier
174{
175 [_regionStates setObject:@(regionState) forKey:identifier];
176}
177
178# pragma mark - static helpers
179
180+ (nonnull NSDictionary *)exportRegion:(nonnull CLCircularRegion *)region withState:(CLRegionState)regionState
181{
182 return @{
183 @"identifier": region.identifier,
184 @"state": @([self exportRegionState:regionState]),
185 @"radius": @(region.radius),
186 @"latitude": @(region.center.latitude),
187 @"longitude": @(region.center.longitude),
188 };
189}
190
191+ (EXGeofencingRegionState)exportRegionState:(CLRegionState)regionState
192{
193 switch (regionState) {
194 case CLRegionStateUnknown:
195 return EXGeofencingRegionStateUnknown;
196 case CLRegionStateInside:
197 return EXGeofencingRegionStateInside;
198 case CLRegionStateOutside:
199 return EXGeofencingRegionStateOutside;
200 }
201}
202
203+ (CLLocationCoordinate2D)coordinateFromDictionary:(nonnull NSDictionary *)dict
204{
205 CLLocationDegrees latitude = [dict[@"latitude"] doubleValue];
206 CLLocationDegrees longitude = [dict[@"longitude"] doubleValue];
207 return CLLocationCoordinate2DMake(latitude, longitude);
208}
209
210+ (BOOL)boolValueFrom:(id)pointer defaultValue:(BOOL)defaultValue
211{
212 return pointer == nil ? defaultValue : [pointer boolValue];
213}
214
215@end