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
|