@josuelmm/cordova-background-geolocation 3.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.npmignore +4 -0
  2. package/CHANGELOG.md +313 -0
  3. package/CLAUDE.md +56 -0
  4. package/HISTORY.md +124 -0
  5. package/README.md +198 -6
  6. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +90 -0
  7. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +362 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +153 -0
  9. package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +27 -11
  10. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +268 -0
  11. package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +86 -26
  12. package/android/common/src/main/java/com/marianhello/bgloc/PluginDelegate.java +26 -0
  13. package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +48 -5
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/SessionLocationDAO.java +18 -0
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +8 -1
  16. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java +74 -0
  17. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java +169 -0
  18. package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
  19. package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
  20. package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
  21. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
  22. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
  23. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
  24. package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
  25. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +310 -7
  26. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
  27. package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
  28. package/android/dependencies.gradle +0 -3
  29. package/angular/background-geolocation-events.ts +21 -0
  30. package/angular/background-geolocation.service.ts +91 -0
  31. package/angular/dist/background-geolocation-events.d.ts +18 -1
  32. package/angular/dist/background-geolocation.service.d.ts +40 -0
  33. package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
  34. package/angular/dist/esm2022/background-geolocation.service.mjs +47 -1
  35. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +67 -0
  36. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
  37. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.h +4 -0
  38. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +352 -1
  39. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +26 -0
  40. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +421 -15
  41. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
  42. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
  43. package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
  44. package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
  45. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
  46. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +12 -3
  47. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
  48. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +102 -44
  49. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
  50. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
  51. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.h +29 -0
  52. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.m +31 -0
  53. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.h +25 -0
  54. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.m +153 -0
  55. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
  56. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
  57. package/package.json +36 -1
  58. package/plugin.xml +26 -8
  59. package/www/BackgroundGeolocation.d.ts +559 -3
  60. package/www/BackgroundGeolocation.js +78 -1
  61. package/RELEASE.MD +0 -16
@@ -9,6 +9,12 @@
9
9
  #import "MAURLogging.h"
10
10
  #import "MAURBackgroundSync.h"
11
11
  #import "MAURSQLiteLocationDAO.h"
12
+ #import <objc/runtime.h>
13
+
14
+ NSString * const MAURBackgroundSyncDidStartNotification = @"MAURBackgroundSyncDidStart";
15
+ NSString * const MAURBackgroundSyncDidSucceedNotification = @"MAURBackgroundSyncDidSucceed";
16
+ NSString * const MAURBackgroundSyncDidFailNotification = @"MAURBackgroundSyncDidFail";
17
+ NSString * const MAURBackgroundSyncDidProgressNotification = @"MAURBackgroundSyncDidProgress";
12
18
 
13
19
  @interface MAURBackgroundSync () <NSURLSessionDelegate, NSURLSessionTaskDelegate>
14
20
  {
@@ -22,11 +28,15 @@
22
28
  - (instancetype) init
23
29
  {
24
30
  if(!(self = [super init])) return nil;
25
-
31
+
32
+ // v3.5 Phase 4: previously `tasks` was never allocated; addObject/removeObject/cancel/status
33
+ // silently no-op'd on nil. Allocate now so cancel and status actually work.
34
+ tasks = [[NSMutableArray alloc] init];
35
+
26
36
  NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.marianhello.session"];
27
37
  conf.allowsCellularAccess = YES;
28
38
  urlSession = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:[NSOperationQueue mainQueue]];
29
-
39
+
30
40
  return self;
31
41
  }
32
42
 
@@ -55,6 +65,11 @@
55
65
  }
56
66
 
57
67
  - (void) sync:(NSString * _Nonnull)url withTemplate:(id)locationTemplate withHttpHeaders:(NSMutableDictionary * _Nullable)httpHeaders
68
+ {
69
+ [self sync:url withTemplate:locationTemplate withHttpHeaders:httpHeaders withMethod:@"POST"];
70
+ }
71
+
72
+ - (void) sync:(NSString * _Nonnull)url withTemplate:(id)locationTemplate withHttpHeaders:(NSMutableDictionary * _Nullable)httpHeaders withMethod:(NSString * _Nullable)method
58
73
  {
59
74
  MAURSQLiteLocationDAO* locationDAO = [MAURSQLiteLocationDAO sharedInstance];
60
75
  NSArray *locations = [locationDAO getLocationsForSync];
@@ -77,7 +92,8 @@
77
92
  uint64_t bytesTotalForThisFile = [[[NSFileManager defaultManager] attributesOfItemAtPath:jsonUrl.path error:nil] fileSize];
78
93
 
79
94
  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
80
- [request setHTTPMethod:@"POST"];
95
+ NSString *resolvedMethod = (method != nil && method.length > 0) ? [method uppercaseString] : @"POST";
96
+ [request setHTTPMethod:resolvedMethod];
81
97
  [request setTimeoutInterval:120]; // Prevents sync from hanging indefinitely if server does not respond
82
98
  [request setValue:[NSString stringWithFormat:@"%llu", bytesTotalForThisFile] forHTTPHeaderField:@"Content-Length"];
83
99
  [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
@@ -92,8 +108,18 @@
92
108
  task.taskDescription = fileName;
93
109
  [tasks addObject:task];
94
110
  DDLogInfo(@"Started upload for %@ as task %zu/%@/%@", jsonUrl.lastPathComponent, (unsigned long)task.taskIdentifier, task.taskDescription, task);
111
+
112
+ // v3.5 Phase 4: emit syncStart now that we are about to push to the server.
113
+ if (self.delegate && [self.delegate respondsToSelector:@selector(backgroundSyncStarted:)]) {
114
+ [self.delegate backgroundSyncStarted:self];
115
+ }
116
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURBackgroundSyncDidStartNotification object:self];
117
+
118
+ // Stash count so didCompleteWithError can report it as syncSuccess payload.
119
+ objc_setAssociatedObject(task, "locationsSent", @([jsonArray count]), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
120
+
95
121
  [task resume];
96
-
122
+
97
123
  }
98
124
 
99
125
  // http://stackoverflow.com/a/572623/48125
@@ -133,6 +159,23 @@ NSString *stringFromFileSize(unsigned long long theSize)
133
159
 
134
160
 
135
161
  #pragma mark -
162
+ // v3.5 Phase 4: forward upload progress as syncProgress (0..100).
163
+ - (void)URLSession:(NSURLSession *)session
164
+ task:(NSURLSessionTask *)task
165
+ didSendBodyData:(int64_t)bytesSent
166
+ totalBytesSent:(int64_t)totalBytesSent
167
+ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
168
+ {
169
+ if (totalBytesExpectedToSend <= 0) return;
170
+ NSInteger progress = (NSInteger)((totalBytesSent * 100) / totalBytesExpectedToSend);
171
+ if (progress < 0) progress = 0;
172
+ if (progress > 100) progress = 100;
173
+ [[NSNotificationCenter defaultCenter]
174
+ postNotificationName:MAURBackgroundSyncDidProgressNotification
175
+ object:self
176
+ userInfo:@{@"progress": @(progress)}];
177
+ }
178
+
136
179
  - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
137
180
  {
138
181
  NSInteger statusCode = [(NSHTTPURLResponse *)task.response statusCode];
@@ -157,7 +200,7 @@ NSString *stringFromFileSize(unsigned long long theSize)
157
200
  }
158
201
 
159
202
  if (statusCode == 401)
160
- {
203
+ {
161
204
  dispatch_async(dispatch_get_main_queue(), ^{
162
205
  if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncHttpAuthorizationUpdates:)])
163
206
  {
@@ -165,6 +208,41 @@ NSString *stringFromFileSize(unsigned long long theSize)
165
208
  }
166
209
  });
167
210
  }
211
+
212
+ // v3.5 Phase 4: emit syncSuccess / syncError.
213
+ NSNumber *sentNum = objc_getAssociatedObject(task, "locationsSent");
214
+ NSInteger locationsSent = sentNum != nil ? [sentNum integerValue] : 0;
215
+ BOOL isStatusOkay = (statusCode >= 200 && statusCode < 300);
216
+
217
+ dispatch_async(dispatch_get_main_queue(), ^{
218
+ if (error != nil) {
219
+ NSString *msg = error.localizedDescription ?: @"";
220
+ if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncFailed:httpStatus:message:)]) {
221
+ [_delegate backgroundSyncFailed:self httpStatus:0 message:msg];
222
+ }
223
+ [[NSNotificationCenter defaultCenter]
224
+ postNotificationName:MAURBackgroundSyncDidFailNotification
225
+ object:self
226
+ userInfo:@{@"httpStatus": @0, @"message": msg}];
227
+ } else if (!isStatusOkay) {
228
+ NSString *msg = [NSString stringWithFormat:@"HTTP %ld", (long)statusCode];
229
+ if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncFailed:httpStatus:message:)]) {
230
+ [_delegate backgroundSyncFailed:self httpStatus:statusCode message:msg];
231
+ }
232
+ [[NSNotificationCenter defaultCenter]
233
+ postNotificationName:MAURBackgroundSyncDidFailNotification
234
+ object:self
235
+ userInfo:@{@"httpStatus": @(statusCode), @"message": msg}];
236
+ } else {
237
+ if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncSucceeded:locationsSent:)]) {
238
+ [_delegate backgroundSyncSucceeded:self locationsSent:locationsSent];
239
+ }
240
+ [[NSNotificationCenter defaultCenter]
241
+ postNotificationName:MAURBackgroundSyncDidSucceedNotification
242
+ object:self
243
+ userInfo:@{@"sent": @(locationsSent)}];
244
+ }
245
+ });
168
246
  }
169
247
 
170
248
  - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
@@ -31,6 +31,19 @@ enum {
31
31
  @property NSNumber *syncThreshold;
32
32
  @property NSNumber *syncEnabled;
33
33
  @property NSMutableDictionary* httpHeaders;
34
+ // v3.3 Phase 2: backend-agnostic HTTP transport
35
+ @property NSString *httpMethod; // POST | GET | PUT | PATCH (default POST)
36
+ @property NSString *syncHttpMethod; // POST | GET | PUT | PATCH (default POST)
37
+ @property NSString *httpMode; // batch | single (default batch)
38
+ @property NSString *syncMode; // batch | single (default batch)
39
+ @property NSMutableDictionary* queryParams; // static placeholder values for URL templating
40
+ // v3.4 Phase 3: location API modernization
41
+ @property NSNumber *_showsBackgroundLocationIndicator; // iOS 11+: show blue bar when app uses location in background
42
+ // v3.5 Phase 4: diagnostics
43
+ @property NSNumber *heartbeatInterval; // ms; 0 disables (default)
44
+ @property NSString *mockLocationPolicy; // allow | flag | drop (default allow)
45
+ // v4.0 Phase 6: driver insights — passed through as a dictionary; the facade reads keys at runtime.
46
+ @property NSDictionary *drivingEvents;
34
47
  @property NSNumber *_saveBatteryOnBackground;
35
48
  @property NSNumber *maxLocations;
36
49
  @property NSNumber *_pauseLocationUpdates;
@@ -57,6 +70,8 @@ enum {
57
70
  - (BOOL) syncEnabled;
58
71
  - (BOOL) hasHttpHeaders;
59
72
  - (BOOL) hasSaveBatteryOnBackground;
73
+ - (BOOL) hasShowsBackgroundLocationIndicator;
74
+ - (BOOL) showsBackgroundLocationIndicator;
60
75
  - (BOOL) hasMaxLocations;
61
76
  - (BOOL) hasPauseLocationUpdates;
62
77
  - (BOOL) hasLocationProvider;
@@ -12,7 +12,7 @@
12
12
 
13
13
  @implementation MAURConfig
14
14
 
15
- @synthesize stationaryRadius, distanceFilter, desiredAccuracy, _debug, activityType, activitiesInterval, _stopOnTerminate, url, syncUrl, syncThreshold, syncEnabled, httpHeaders, _saveBatteryOnBackground, maxLocations, _pauseLocationUpdates, locationProvider, _template;
15
+ @synthesize stationaryRadius, distanceFilter, desiredAccuracy, _debug, activityType, activitiesInterval, _stopOnTerminate, url, syncUrl, syncThreshold, syncEnabled, httpHeaders, httpMethod, syncHttpMethod, httpMode, syncMode, queryParams, _showsBackgroundLocationIndicator, heartbeatInterval, mockLocationPolicy, drivingEvents, _saveBatteryOnBackground, maxLocations, _pauseLocationUpdates, locationProvider, _template;
16
16
 
17
17
  -(instancetype) initWithDefaults {
18
18
  self = [super init];
@@ -34,8 +34,14 @@
34
34
  syncEnabled = [NSNumber numberWithBool:YES];
35
35
  _pauseLocationUpdates = [NSNumber numberWithBool:NO];
36
36
  locationProvider = [NSNumber numberWithInt:DISTANCE_FILTER_PROVIDER];
37
+ httpMethod = @"POST";
38
+ syncHttpMethod = @"POST";
39
+ httpMode = @"batch";
40
+ syncMode = @"batch";
41
+ heartbeatInterval = [NSNumber numberWithInt:0];
42
+ mockLocationPolicy = @"allow";
37
43
  // template =
38
-
44
+
39
45
  return self;
40
46
  }
41
47
 
@@ -58,7 +64,7 @@
58
64
  if (isNotNull(config[@"activityType"])) {
59
65
  instance.activityType = config[@"activityType"];
60
66
  }
61
- if (isNull(config[@"activitiesInterval"])) {
67
+ if (isNotNull(config[@"activitiesInterval"])) {
62
68
  instance.activitiesInterval = config[@"activitiesInterval"];
63
69
  }
64
70
  if (isNotNull(config[@"stopOnTerminate"])) {
@@ -79,6 +85,38 @@
79
85
  if (config[@"httpHeaders"] != nil) {
80
86
  instance.httpHeaders = config[@"httpHeaders"];
81
87
  }
88
+ // headers (alias of httpHeaders)
89
+ if (config[@"headers"] != nil) {
90
+ instance.httpHeaders = config[@"headers"];
91
+ }
92
+ // v3.3 Phase 2: HTTP transport
93
+ if (isNotNull(config[@"httpMethod"])) {
94
+ instance.httpMethod = [(NSString*)config[@"httpMethod"] uppercaseString];
95
+ }
96
+ if (isNotNull(config[@"syncHttpMethod"])) {
97
+ instance.syncHttpMethod = [(NSString*)config[@"syncHttpMethod"] uppercaseString];
98
+ }
99
+ if (isNotNull(config[@"httpMode"])) {
100
+ instance.httpMode = [(NSString*)config[@"httpMode"] lowercaseString];
101
+ }
102
+ if (isNotNull(config[@"syncMode"])) {
103
+ instance.syncMode = [(NSString*)config[@"syncMode"] lowercaseString];
104
+ }
105
+ if (config[@"queryParams"] != nil) {
106
+ instance.queryParams = config[@"queryParams"];
107
+ }
108
+ if (isNotNull(config[@"showsBackgroundLocationIndicator"])) {
109
+ instance._showsBackgroundLocationIndicator = config[@"showsBackgroundLocationIndicator"];
110
+ }
111
+ if (isNotNull(config[@"heartbeatInterval"])) {
112
+ instance.heartbeatInterval = config[@"heartbeatInterval"];
113
+ }
114
+ if (isNotNull(config[@"mockLocationPolicy"])) {
115
+ instance.mockLocationPolicy = [(NSString*)config[@"mockLocationPolicy"] lowercaseString];
116
+ }
117
+ if ([config[@"drivingEvents"] isKindOfClass:[NSDictionary class]]) {
118
+ instance.drivingEvents = config[@"drivingEvents"];
119
+ }
82
120
  if (isNotNull(config[@"saveBatteryOnBackground"])) {
83
121
  instance._saveBatteryOnBackground = config[@"saveBatteryOnBackground"];
84
122
  }
@@ -94,6 +132,10 @@
94
132
  if (config[@"postTemplate"] != nil) {
95
133
  instance._template = config[@"postTemplate"];
96
134
  }
135
+ // bodyTemplate (alias of postTemplate)
136
+ if (config[@"bodyTemplate"] != nil) {
137
+ instance._template = config[@"bodyTemplate"];
138
+ }
97
139
 
98
140
  return instance;
99
141
  }
@@ -146,6 +188,33 @@
146
188
  if ([newConfig hasHttpHeaders]) {
147
189
  merger.httpHeaders = newConfig.httpHeaders;
148
190
  }
191
+ if (newConfig.httpMethod != nil) {
192
+ merger.httpMethod = newConfig.httpMethod;
193
+ }
194
+ if (newConfig.syncHttpMethod != nil) {
195
+ merger.syncHttpMethod = newConfig.syncHttpMethod;
196
+ }
197
+ if (newConfig.httpMode != nil) {
198
+ merger.httpMode = newConfig.httpMode;
199
+ }
200
+ if (newConfig.syncMode != nil) {
201
+ merger.syncMode = newConfig.syncMode;
202
+ }
203
+ if (newConfig.queryParams != nil) {
204
+ merger.queryParams = newConfig.queryParams;
205
+ }
206
+ if ([newConfig hasShowsBackgroundLocationIndicator]) {
207
+ merger._showsBackgroundLocationIndicator = newConfig._showsBackgroundLocationIndicator;
208
+ }
209
+ if (newConfig.heartbeatInterval != nil) {
210
+ merger.heartbeatInterval = newConfig.heartbeatInterval;
211
+ }
212
+ if (newConfig.mockLocationPolicy != nil) {
213
+ merger.mockLocationPolicy = newConfig.mockLocationPolicy;
214
+ }
215
+ if (newConfig.drivingEvents != nil) {
216
+ merger.drivingEvents = newConfig.drivingEvents;
217
+ }
149
218
  if ([newConfig hasSaveBatteryOnBackground]) {
150
219
  merger._saveBatteryOnBackground = newConfig._saveBatteryOnBackground;
151
220
  }
@@ -181,6 +250,15 @@
181
250
  copy.syncThreshold = syncThreshold;
182
251
  copy.syncEnabled = syncEnabled;
183
252
  copy.httpHeaders = httpHeaders;
253
+ copy.httpMethod = httpMethod;
254
+ copy.syncHttpMethod = syncHttpMethod;
255
+ copy.httpMode = httpMode;
256
+ copy.syncMode = syncMode;
257
+ copy.queryParams = queryParams;
258
+ copy._showsBackgroundLocationIndicator = _showsBackgroundLocationIndicator;
259
+ copy.heartbeatInterval = heartbeatInterval;
260
+ copy.mockLocationPolicy = mockLocationPolicy;
261
+ copy.drivingEvents = drivingEvents;
184
262
  copy._saveBatteryOnBackground = _saveBatteryOnBackground;
185
263
  copy.maxLocations = maxLocations;
186
264
  copy._pauseLocationUpdates = _pauseLocationUpdates;
@@ -322,6 +400,16 @@
322
400
  return _saveBatteryOnBackground != nil;
323
401
  }
324
402
 
403
+ - (BOOL) hasShowsBackgroundLocationIndicator
404
+ {
405
+ return _showsBackgroundLocationIndicator != nil;
406
+ }
407
+
408
+ - (BOOL) showsBackgroundLocationIndicator
409
+ {
410
+ return _showsBackgroundLocationIndicator != nil ? [_showsBackgroundLocationIndicator boolValue] : NO;
411
+ }
412
+
325
413
  - (BOOL) hasMaxLocations
326
414
  {
327
415
  return maxLocations != nil;
@@ -479,6 +567,15 @@
479
567
  if ([self hasUrl]) [dict setObject:self.url forKey:@"url"];
480
568
  if ([self hasSyncUrl]) [dict setObject:self.syncUrl forKey:@"syncUrl"];
481
569
  if ([self hasHttpHeaders]) [dict setObject:self.httpHeaders forKey:@"httpHeaders"];
570
+ if (self.httpMethod != nil) [dict setObject:self.httpMethod forKey:@"httpMethod"];
571
+ if (self.syncHttpMethod != nil) [dict setObject:self.syncHttpMethod forKey:@"syncHttpMethod"];
572
+ if (self.httpMode != nil) [dict setObject:self.httpMode forKey:@"httpMode"];
573
+ if (self.syncMode != nil) [dict setObject:self.syncMode forKey:@"syncMode"];
574
+ if (self.queryParams != nil) [dict setObject:self.queryParams forKey:@"queryParams"];
575
+ if ([self hasShowsBackgroundLocationIndicator]) [dict setObject:self._showsBackgroundLocationIndicator forKey:@"showsBackgroundLocationIndicator"];
576
+ if (self.heartbeatInterval != nil) [dict setObject:self.heartbeatInterval forKey:@"heartbeatInterval"];
577
+ if (self.mockLocationPolicy != nil) [dict setObject:self.mockLocationPolicy forKey:@"mockLocationPolicy"];
578
+ if (self.drivingEvents != nil) [dict setObject:self.drivingEvents forKey:@"drivingEvents"];
482
579
  if ([self hasStationaryRadius]) [dict setObject:self.stationaryRadius forKey:@"stationaryRadius"];
483
580
  if ([self hasDistanceFilter]) [dict setObject:self.distanceFilter forKey:@"distanceFilter"];
484
581
  if ([self hasDesiredAccuracy]) [dict setObject:self.desiredAccuracy forKey:@"desiredAccuracy"];
@@ -87,6 +87,12 @@ enum {
87
87
  _config = config;
88
88
 
89
89
  locationManager.pausesLocationUpdatesAutomatically = [_config pauseLocationUpdates];
90
+ // v3.4 Phase 3: showsBackgroundLocationIndicator (iOS 11+).
91
+ if (@available(iOS 11.0, *)) {
92
+ if ([_config hasShowsBackgroundLocationIndicator]) {
93
+ locationManager.showsBackgroundLocationIndicator = [_config showsBackgroundLocationIndicator];
94
+ }
95
+ }
90
96
  locationManager.activityType = [_config decodeActivityType];
91
97
  locationManager.distanceFilter = _config.distanceFilter.integerValue; // meters
92
98
  locationManager.desiredAccuracy = [_config decodeDesiredAccuracy];
@@ -376,13 +382,34 @@ enum {
376
382
  }
377
383
  }
378
384
 
385
+ // v3.4 Phase 3: iOS 14+ delegate callback. Replaces the legacy `didChangeAuthorizationStatus:`
386
+ // (which is deprecated in iOS 14 but still delivered). On iOS 14+ this is the canonical entry
387
+ // point and exposes accuracyAuthorization (Precise vs Reduced).
388
+ - (void) locationManagerDidChangeAuthorization:(CLLocationManager *)manager API_AVAILABLE(ios(14.0))
389
+ {
390
+ CLAuthorizationStatus status = manager.authorizationStatus;
391
+ DDLogInfo(@"LocationManager didChangeAuthorization (iOS 14+) status=%d accuracy=%ld",
392
+ (int)status, (long)manager.accuracyAuthorization);
393
+ [self handleAuthorizationStatusChange:status];
394
+ }
395
+
379
396
  - (void) locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
380
397
  {
381
- DDLogInfo(@"LocationManager didChangeAuthorizationStatus %u", status);
398
+ // On iOS 14+ the system also delivers `locationManagerDidChangeAuthorization:`; ignore this
399
+ // legacy callback there to avoid double-notifying delegates.
400
+ if (@available(iOS 14.0, *)) {
401
+ return;
402
+ }
403
+ DDLogInfo(@"LocationManager didChangeAuthorizationStatus (legacy) %u", status);
404
+ [self handleAuthorizationStatusChange:status];
405
+ }
406
+
407
+ - (void) handleAuthorizationStatusChange:(CLAuthorizationStatus)status
408
+ {
382
409
  if ([_config isDebugging]) {
383
410
  [self notify:[NSString stringWithFormat:@"Authorization status changed %u", status]];
384
411
  }
385
-
412
+
386
413
  switch(status) {
387
414
  case kCLAuthorizationStatusRestricted:
388
415
  case kCLAuthorizationStatusDenied:
@@ -10,11 +10,12 @@
10
10
  #import "MAURGeolocationOpenHelper.h"
11
11
  #import "MAURLocationContract.h"
12
12
  #import "MAURConfigurationContract.h"
13
+ #import "MAURSessionLocationContract.h"
13
14
 
14
15
  @implementation MAURGeolocationOpenHelper
15
16
 
16
17
  static NSString *const kDatabaseName = @"cordova_bg_geolocation.db";
17
- static NSInteger const kDatabaseVersion = 4;
18
+ static NSInteger const kDatabaseVersion = 5;
18
19
 
19
20
  - (instancetype)init
20
21
  {
@@ -39,7 +40,9 @@ static NSInteger const kDatabaseVersion = 4;
39
40
  NSString *sql = [@[
40
41
  [MAURLocationContract createTableSQL],
41
42
  [MAURConfigurationContract createTableSQL],
42
- @"CREATE INDEX recorded_at_idx ON " @LC_TABLE_NAME @" (" @LC_COLUMN_NAME_RECORDED_AT @")"
43
+ @"CREATE INDEX recorded_at_idx ON " @LC_TABLE_NAME @" (" @LC_COLUMN_NAME_RECORDED_AT @")",
44
+ [MAURSessionLocationContract createTableSQL],
45
+ @"CREATE INDEX session_recorded_at_idx ON " @LSC_TABLE_NAME @" (" @LSC_COLUMN_NAME_RECORDED_AT @")"
43
46
  ] componentsJoinedByString:@";"];
44
47
  if (![database executeStatements:sql]) {
45
48
  NSLog(@"%@ failed code: %d: message: %@", sql, [database lastErrorCode], [database lastErrorMessage]);
@@ -52,7 +55,8 @@ static NSInteger const kDatabaseVersion = 4;
52
55
  NSLog(@"Downgrading geolocation db oldVersion: %ld, newVersion: %ld", oldVersion, newVersion);
53
56
 
54
57
  NSString *sql = [@[
55
- @"DROP TABLE IF EXISTS " @LC_TABLE_NAME
58
+ @"DROP TABLE IF EXISTS " @LC_TABLE_NAME,
59
+ @"DROP TABLE IF EXISTS " @LSC_TABLE_NAME
56
60
  ] componentsJoinedByString:@";"];
57
61
 
58
62
  [queue inDatabase:^(FMDatabase *database) {
@@ -85,6 +89,11 @@ static NSInteger const kDatabaseVersion = 4;
85
89
  [sql addObjectsFromArray: @[
86
90
  [NSString stringWithFormat:@"ALTER TABLE %s ADD COLUMN %s INTEGER", CC_TABLE_NAME, CC_COLUMN_NAME_SYNC_ENABLED]
87
91
  ]];
92
+ case 4:
93
+ [sql addObjectsFromArray: @[
94
+ [MAURSessionLocationContract createTableSQL],
95
+ [NSString stringWithFormat:@"CREATE INDEX session_recorded_at_idx ON %@ (%@)", @LSC_TABLE_NAME, @LSC_COLUMN_NAME_RECORDED_AT]
96
+ ]];
88
97
  break; // break only for previous db version (cascade statements)
89
98
  default:
90
99
  return;
@@ -19,6 +19,10 @@
19
19
  @optional
20
20
  - (void)postLocationTaskRequestedAbortUpdates:(MAURPostLocationTask * _Nonnull)task;
21
21
  - (void)postLocationTaskHttpAuthorizationUpdates:(MAURPostLocationTask * _Nonnull)task;
22
+ // v3.5 Phase 4
23
+ - (void)postLocationTaskSyncStarted:(MAURPostLocationTask * _Nonnull)task;
24
+ - (void)postLocationTaskSyncSucceeded:(MAURPostLocationTask * _Nonnull)task locationsSent:(NSInteger)locationsSent;
25
+ - (void)postLocationTaskSyncFailed:(MAURPostLocationTask * _Nonnull)task httpStatus:(NSInteger)httpStatus message:(NSString * _Nullable)message;
22
26
 
23
27
  @end
24
28
 
@@ -14,6 +14,8 @@
14
14
  #import "MAURLogging.h"
15
15
  #import "MAURPostLocationTask.h"
16
16
  #import "MAURSQLiteLocationDAO.h"
17
+ #import "MAURSessionLocationDAO.h"
18
+ #import "MAURUrlTemplateResolver.h"
17
19
 
18
20
  static NSString * const TAG = @"MAURPostLocationTask";
19
21
 
@@ -79,19 +81,34 @@ static MAURLocationTransform s_locationTransform = nil;
79
81
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
80
82
 
81
83
  MAURLocation *location = inLocation;
82
-
84
+
83
85
  if (locationTransform != nil) {
84
86
  location = locationTransform(location);
85
-
87
+
86
88
  if (location == nil) {
87
89
  return;
88
90
  }
89
91
  }
90
-
92
+
93
+ // v3.5 Phase 4: mock location policy. Detection already exists in MAURLocation.simulated.
94
+ if (location.simulated != nil && [location.simulated boolValue]) {
95
+ NSString *policy = self.config.mockLocationPolicy ?: @"allow";
96
+ if ([@"drop" isEqualToString:policy]) {
97
+ DDLogInfo(@"%@ Simulated/mock location dropped (mockLocationPolicy=drop)", TAG);
98
+ return;
99
+ }
100
+ // "flag": leave it. The simulated NSNumber is already on the model and propagates via toResultFromTemplate.
101
+ // "allow": no-op.
102
+ }
103
+
91
104
  MAURSQLiteLocationDAO *locationDAO = [MAURSQLiteLocationDAO sharedInstance];
92
105
  // TODO: investigate location id always 0
93
106
  NSNumber *locationId = [locationDAO persistLocation:location limitRows:self.config.maxLocations.integerValue];
94
107
 
108
+ if ([[MAURSessionLocationDAO sharedInstance] isSessionActive]) {
109
+ [[MAURSessionLocationDAO sharedInstance] persistSessionLocation:location];
110
+ }
111
+
95
112
  if (hasConnectivity && [self.config hasValidUrl]) {
96
113
  NSError *error = nil;
97
114
  if ([self post:location toUrl:self.config.url withTemplate:self.config._template withHttpHeaders:self.config.httpHeaders error:&error]) {
@@ -112,28 +129,47 @@ static MAURLocationTransform s_locationTransform = nil;
112
129
  });
113
130
  }
114
131
 
115
- - (BOOL) post:(MAURLocation*)location
116
- toUrl:(NSString*)url
117
- withTemplate:(id)locationTemplate
118
- withHttpHeaders:(NSMutableDictionary*)httpHeaders
132
+ - (BOOL) post:(MAURLocation*)location
133
+ toUrl:(NSString*)url
134
+ withTemplate:(id)locationTemplate
135
+ withHttpHeaders:(NSMutableDictionary*)httpHeaders
119
136
  error:(NSError * __autoreleasing *)outError
120
137
  {
121
- NSArray *locations = [[NSArray alloc] initWithObjects:[location toResultFromTemplate:locationTemplate], nil];
122
- NSData *data = [NSJSONSerialization dataWithJSONObject:locations options:0 error:outError];
123
- if (!data) {
124
- return NO;
138
+ // v3.3 Phase 2: backend-agnostic transport.
139
+ // Resolve URL template using current location + queryParams (for both single and batch modes).
140
+ NSString *resolvedUrl = [MAURUrlTemplateResolver resolve:url location:location queryParams:self.config.queryParams];
141
+
142
+ NSString *method = self.config.httpMethod ?: @"POST";
143
+ NSString *mode = self.config.httpMode ?: @"batch";
144
+ BOOL isBodyless = [@"GET" isEqualToString:method];
145
+ BOOL singleMode = isBodyless || [@"single" isEqualToString:mode];
146
+
147
+ NSData *data = nil;
148
+ if (!isBodyless) {
149
+ // For single mode (or body methods that prefer one location per request) send a JSONObject;
150
+ // for batch send the array (current behaviour).
151
+ if (singleMode) {
152
+ data = [NSJSONSerialization dataWithJSONObject:[location toResultFromTemplate:locationTemplate] options:0 error:outError];
153
+ } else {
154
+ NSArray *locations = [[NSArray alloc] initWithObjects:[location toResultFromTemplate:locationTemplate], nil];
155
+ data = [NSJSONSerialization dataWithJSONObject:locations options:0 error:outError];
156
+ }
157
+ if (!data) {
158
+ return NO;
159
+ }
125
160
  }
126
-
127
- NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
128
-
161
+
162
+ NSString *jsonStr = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
129
163
  NSString *contentType = [httpHeaders objectForKey:@"Content-Type"];
130
164
  if (!contentType) {
131
165
  contentType = @"application/json";
132
166
  }
133
-
134
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
135
- [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
136
- [request setHTTPMethod:@"POST"];
167
+
168
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:resolvedUrl]];
169
+ [request setHTTPMethod:method];
170
+ if (!isBodyless) {
171
+ [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
172
+ }
137
173
  if (httpHeaders != nil) {
138
174
  for (id key in httpHeaders) {
139
175
  if (![key isEqualToString:@"Content-Type"]) {
@@ -142,36 +178,54 @@ static MAURLocationTransform s_locationTransform = nil;
142
178
  }
143
179
  }
144
180
  }
145
-
146
- if ([contentType isEqualToString:@"application/x-www-form-urlencoded"]) {
147
- id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:outError];
148
- NSDictionary *dict = nil;
149
- if ([jsonObject isKindOfClass:[NSArray class]] && [jsonObject count] == 1) {
150
- dict = [jsonObject firstObject];
151
- } else if ([jsonObject isKindOfClass:[NSDictionary class]]) {
152
- dict = jsonObject;
153
- }
154
- if (dict) {
155
- NSMutableArray *parts = [NSMutableArray array];
156
- for (NSString *key in dict) {
157
- NSString *value = [NSString stringWithFormat:@"%@", dict[key]];
158
- NSString *encodedKey = [key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
159
- NSString *encodedValue = [value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
160
- NSString *part = [NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue];
161
- [parts addObject:part];
181
+
182
+ if (!isBodyless) {
183
+ if ([contentType isEqualToString:@"application/x-www-form-urlencoded"]) {
184
+ id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:outError];
185
+ NSDictionary *dict = nil;
186
+ if ([jsonObject isKindOfClass:[NSArray class]] && [jsonObject count] == 1) {
187
+ dict = [jsonObject firstObject];
188
+ } else if ([jsonObject isKindOfClass:[NSDictionary class]]) {
189
+ dict = jsonObject;
190
+ }
191
+ if (dict) {
192
+ NSMutableArray *parts = [NSMutableArray array];
193
+ for (NSString *key in dict) {
194
+ NSString *value = [NSString stringWithFormat:@"%@", dict[key]];
195
+ NSString *encodedKey = [key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
196
+ NSString *encodedValue = [value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
197
+ NSString *part = [NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue];
198
+ [parts addObject:part];
199
+ }
200
+ NSString *encodedString = [parts componentsJoinedByString:@"&"];
201
+ [request setHTTPBody:[encodedString dataUsingEncoding:NSUTF8StringEncoding]];
202
+ } else {
203
+ [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
162
204
  }
163
- NSString *encodedString = [parts componentsJoinedByString:@"&"];
164
- [request setHTTPBody:[encodedString dataUsingEncoding:NSUTF8StringEncoding]];
165
205
  } else {
166
206
  [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
167
207
  }
168
- } else {
169
- [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
170
208
  }
171
-
172
- NSHTTPURLResponse* urlResponse = nil;
173
- [NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:outError];
174
-
209
+
210
+ // v3.4: NSURLSession (iOS 7+) replaces deprecated [NSURLConnection sendSynchronousRequest:].
211
+ // We run on a background queue (see -add: dispatch_async) so a semaphore-based wait is safe.
212
+ __block NSHTTPURLResponse *urlResponse = nil;
213
+ __block NSError *taskError = nil;
214
+ dispatch_semaphore_t sema = dispatch_semaphore_create(0);
215
+ NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession]
216
+ dataTaskWithRequest:request
217
+ completionHandler:^(NSData * _Nullable d, NSURLResponse * _Nullable response, NSError * _Nullable err) {
218
+ urlResponse = (NSHTTPURLResponse *)response;
219
+ taskError = err;
220
+ dispatch_semaphore_signal(sema);
221
+ }];
222
+ [dataTask resume];
223
+ // 120s ceiling to mirror the previous synchronous timeout; URLSession also enforces its own.
224
+ dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(120 * NSEC_PER_SEC)));
225
+ if (taskError != nil && outError != NULL) {
226
+ *outError = taskError;
227
+ }
228
+
175
229
  NSInteger statusCode = urlResponse.statusCode;
176
230
 
177
231
  if (statusCode == 285)
@@ -215,7 +269,11 @@ static MAURLocationTransform s_locationTransform = nil;
215
269
  if (![self.config syncEnabled] || ![self.config hasValidSyncUrl]) {
216
270
  return;
217
271
  }
218
- [uploader sync:self.config.syncUrl withTemplate:self.config._template withHttpHeaders:self.config.httpHeaders];
272
+ // For sync (batch) only static queryParams placeholders apply; per-location templating
273
+ // belongs in real-time post (httpMode="single" + httpMethod=GET) instead.
274
+ NSString *resolvedSyncUrl = [MAURUrlTemplateResolver resolve:self.config.syncUrl location:nil queryParams:self.config.queryParams];
275
+ NSString *syncMethod = self.config.syncHttpMethod ?: @"POST";
276
+ [uploader sync:resolvedSyncUrl withTemplate:self.config._template withHttpHeaders:self.config.httpHeaders withMethod:syncMethod];
219
277
  }
220
278
 
221
279
  #pragma mark - Location transform