@josuelmm/cordova-background-geolocation 3.2.0 → 4.2.2

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 (51) hide show
  1. package/.npmignore +4 -0
  2. package/CHANGELOG.md +290 -0
  3. package/CLAUDE.md +56 -0
  4. package/HISTORY.md +125 -0
  5. package/README.md +189 -4
  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 +310 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +127 -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 +42 -5
  14. package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
  15. package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
  16. package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
  17. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
  18. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
  19. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
  20. package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
  21. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +305 -6
  22. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
  23. package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
  24. package/android/dependencies.gradle +0 -3
  25. package/angular/background-geolocation-events.ts +21 -0
  26. package/angular/background-geolocation.service.ts +63 -0
  27. package/angular/dist/background-geolocation-events.d.ts +18 -1
  28. package/angular/dist/background-geolocation.service.d.ts +36 -0
  29. package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
  30. package/angular/dist/esm2022/background-geolocation.service.mjs +35 -1
  31. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +55 -0
  32. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
  33. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +312 -1
  34. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +22 -0
  35. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +400 -15
  36. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
  37. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
  38. package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
  39. package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
  40. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
  41. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
  42. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +97 -44
  43. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
  44. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
  45. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
  46. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
  47. package/package.json +41 -1
  48. package/plugin.xml +19 -8
  49. package/www/BackgroundGeolocation.d.ts +517 -3
  50. package/www/BackgroundGeolocation.js +54 -1
  51. package/RELEASE.MD +0 -16
@@ -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:
@@ -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
 
@@ -15,6 +15,7 @@
15
15
  #import "MAURPostLocationTask.h"
16
16
  #import "MAURSQLiteLocationDAO.h"
17
17
  #import "MAURSessionLocationDAO.h"
18
+ #import "MAURUrlTemplateResolver.h"
18
19
 
19
20
  static NSString * const TAG = @"MAURPostLocationTask";
20
21
 
@@ -80,15 +81,26 @@ static MAURLocationTransform s_locationTransform = nil;
80
81
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
81
82
 
82
83
  MAURLocation *location = inLocation;
83
-
84
+
84
85
  if (locationTransform != nil) {
85
86
  location = locationTransform(location);
86
-
87
+
87
88
  if (location == nil) {
88
89
  return;
89
90
  }
90
91
  }
91
-
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
+
92
104
  MAURSQLiteLocationDAO *locationDAO = [MAURSQLiteLocationDAO sharedInstance];
93
105
  // TODO: investigate location id always 0
94
106
  NSNumber *locationId = [locationDAO persistLocation:location limitRows:self.config.maxLocations.integerValue];
@@ -117,28 +129,47 @@ static MAURLocationTransform s_locationTransform = nil;
117
129
  });
118
130
  }
119
131
 
120
- - (BOOL) post:(MAURLocation*)location
121
- toUrl:(NSString*)url
122
- withTemplate:(id)locationTemplate
123
- withHttpHeaders:(NSMutableDictionary*)httpHeaders
132
+ - (BOOL) post:(MAURLocation*)location
133
+ toUrl:(NSString*)url
134
+ withTemplate:(id)locationTemplate
135
+ withHttpHeaders:(NSMutableDictionary*)httpHeaders
124
136
  error:(NSError * __autoreleasing *)outError
125
137
  {
126
- NSArray *locations = [[NSArray alloc] initWithObjects:[location toResultFromTemplate:locationTemplate], nil];
127
- NSData *data = [NSJSONSerialization dataWithJSONObject:locations options:0 error:outError];
128
- if (!data) {
129
- 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
+ }
130
160
  }
131
-
132
- NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
133
-
161
+
162
+ NSString *jsonStr = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
134
163
  NSString *contentType = [httpHeaders objectForKey:@"Content-Type"];
135
164
  if (!contentType) {
136
165
  contentType = @"application/json";
137
166
  }
138
-
139
- NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
140
- [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
141
- [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
+ }
142
173
  if (httpHeaders != nil) {
143
174
  for (id key in httpHeaders) {
144
175
  if (![key isEqualToString:@"Content-Type"]) {
@@ -147,36 +178,54 @@ static MAURLocationTransform s_locationTransform = nil;
147
178
  }
148
179
  }
149
180
  }
150
-
151
- if ([contentType isEqualToString:@"application/x-www-form-urlencoded"]) {
152
- id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:outError];
153
- NSDictionary *dict = nil;
154
- if ([jsonObject isKindOfClass:[NSArray class]] && [jsonObject count] == 1) {
155
- dict = [jsonObject firstObject];
156
- } else if ([jsonObject isKindOfClass:[NSDictionary class]]) {
157
- dict = jsonObject;
158
- }
159
- if (dict) {
160
- NSMutableArray *parts = [NSMutableArray array];
161
- for (NSString *key in dict) {
162
- NSString *value = [NSString stringWithFormat:@"%@", dict[key]];
163
- NSString *encodedKey = [key stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
164
- NSString *encodedValue = [value stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
165
- NSString *part = [NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue];
166
- [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]];
167
204
  }
168
- NSString *encodedString = [parts componentsJoinedByString:@"&"];
169
- [request setHTTPBody:[encodedString dataUsingEncoding:NSUTF8StringEncoding]];
170
205
  } else {
171
206
  [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
172
207
  }
173
- } else {
174
- [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
175
208
  }
176
-
177
- NSHTTPURLResponse* urlResponse = nil;
178
- [NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:outError];
179
-
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
+
180
229
  NSInteger statusCode = urlResponse.statusCode;
181
230
 
182
231
  if (statusCode == 285)
@@ -220,7 +269,11 @@ static MAURLocationTransform s_locationTransform = nil;
220
269
  if (![self.config syncEnabled] || ![self.config hasValidSyncUrl]) {
221
270
  return;
222
271
  }
223
- [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];
224
277
  }
225
278
 
226
279
  #pragma mark - Location transform
@@ -0,0 +1,41 @@
1
+ //
2
+ // MAURSensorFusionDetector.h
3
+ // BackgroundGeolocation
4
+ //
5
+ // v4.2 Phase 8 — Real sensor fusion detector for iOS.
6
+ // Uses CMMotionManager to sample userAcceleration (gravity removed) and rotationRate.
7
+ // Refines possibleCrash via accelerometer impact and detects phoneUsageWhileDriving.
8
+ //
9
+
10
+ #ifndef MAURSensorFusionDetector_h
11
+ #define MAURSensorFusionDetector_h
12
+
13
+ #import <Foundation/Foundation.h>
14
+
15
+ @class MAURLocation;
16
+
17
+ @protocol MAURSensorFusionListener <NSObject>
18
+ - (void)onSensorCrashWithImpactG:(double)impactG location:(MAURLocation *)location;
19
+ - (void)onPhoneUsageWhileDriving:(MAURLocation *)location;
20
+ @end
21
+
22
+ @interface MAURSensorFusionDetector : NSObject
23
+
24
+ @property (nonatomic, weak) id<MAURSensorFusionListener> listener;
25
+ @property (nonatomic, assign) BOOL enabled;
26
+ @property (nonatomic, assign) double crashImpactG; // default 3.0
27
+ @property (nonatomic, assign) NSTimeInterval crashCooldownMs; // default 10000
28
+ @property (nonatomic, assign) NSTimeInterval phoneUsageWindowMs; // default 4000
29
+ @property (nonatomic, assign) NSTimeInterval phoneUsageCooldownMs; // default 60000
30
+
31
+ @property (nonatomic, assign) BOOL tripActive;
32
+ @property (nonatomic, strong) MAURLocation *lastLocation;
33
+
34
+ - (instancetype)init;
35
+ - (BOOL)isAvailable;
36
+ - (void)start;
37
+ - (void)stop;
38
+
39
+ @end
40
+
41
+ #endif /* MAURSensorFusionDetector_h */
@@ -0,0 +1,137 @@
1
+ //
2
+ // MAURSensorFusionDetector.m
3
+ // BackgroundGeolocation
4
+ //
5
+ // v4.2 Phase 8 — sensor fusion detector implementation.
6
+ //
7
+
8
+ #import "MAURSensorFusionDetector.h"
9
+ #import <CoreMotion/CoreMotion.h>
10
+ #import <UIKit/UIKit.h>
11
+
12
+ static const double kJitterGyroRadS = 0.7; // ~40 deg/s
13
+ static const double kJitterAccelMps2 = 0.5;
14
+
15
+ @interface MAURSensorFusionDetector ()
16
+ @property (nonatomic, strong) CMMotionManager *motion;
17
+ @property (nonatomic, strong) NSOperationQueue *queue;
18
+ @property (nonatomic, assign) BOOL started;
19
+ @property (nonatomic, assign) NSTimeInterval lastCrashAt;
20
+ @property (nonatomic, assign) NSTimeInterval lastPhoneUsageAt;
21
+ @property (nonatomic, assign) NSTimeInterval jitterAboveSince;
22
+ @end
23
+
24
+ @implementation MAURSensorFusionDetector
25
+
26
+ - (instancetype)init {
27
+ if ((self = [super init])) {
28
+ _motion = [[CMMotionManager alloc] init];
29
+ _motion.deviceMotionUpdateInterval = 1.0 / 50.0; // 50 Hz
30
+ _queue = [[NSOperationQueue alloc] init];
31
+ _queue.name = @"MAURSensorFusionQueue";
32
+ _queue.maxConcurrentOperationCount = 1;
33
+ _enabled = NO;
34
+ _crashImpactG = 3.0;
35
+ _crashCooldownMs = 10000;
36
+ _phoneUsageWindowMs = 4000;
37
+ _phoneUsageCooldownMs = 60000;
38
+ _started = NO;
39
+ _lastCrashAt = 0;
40
+ _lastPhoneUsageAt = 0;
41
+ _jitterAboveSince = 0;
42
+ }
43
+ return self;
44
+ }
45
+
46
+ - (BOOL)isAvailable {
47
+ return self.motion.isDeviceMotionAvailable;
48
+ }
49
+
50
+ - (void)start {
51
+ @synchronized (self) {
52
+ if (self.started || !self.enabled) return;
53
+ if (![self.motion isDeviceMotionAvailable]) return;
54
+ __weak typeof(self) weakSelf = self;
55
+ [self.motion startDeviceMotionUpdatesToQueue:self.queue
56
+ withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {
57
+ if (!motion || error) return;
58
+ [weakSelf processMotion:motion];
59
+ }];
60
+ self.started = YES;
61
+ }
62
+ }
63
+
64
+ - (void)stop {
65
+ @synchronized (self) {
66
+ if (!self.started) return;
67
+ [self.motion stopDeviceMotionUpdates];
68
+ self.started = NO;
69
+ self.jitterAboveSince = 0;
70
+ }
71
+ }
72
+
73
+ - (void)processMotion:(CMDeviceMotion *)motion {
74
+ if (!self.enabled) return;
75
+ NSTimeInterval nowMs = [[NSDate date] timeIntervalSince1970] * 1000.0;
76
+
77
+ // userAcceleration is in g (gravity removed); convert magnitude to g and to m/s².
78
+ double ax = motion.userAcceleration.x;
79
+ double ay = motion.userAcceleration.y;
80
+ double az = motion.userAcceleration.z;
81
+ double accelMagG = sqrt(ax*ax + ay*ay + az*az); // g
82
+ double accelMagMs = accelMagG * 9.80665; // m/s²
83
+
84
+ double gx = motion.rotationRate.x;
85
+ double gy = motion.rotationRate.y;
86
+ double gz = motion.rotationRate.z;
87
+ double gyroMag = sqrt(gx*gx + gy*gy + gz*gz); // rad/s
88
+
89
+ BOOL tripActiveNow = self.tripActive;
90
+ MAURLocation *loc = self.lastLocation;
91
+ id<MAURSensorFusionListener> l = self.listener;
92
+
93
+ // Crash detection
94
+ if (tripActiveNow && self.crashImpactG > 0 && accelMagG >= self.crashImpactG
95
+ && (nowMs - self.lastCrashAt) >= self.crashCooldownMs) {
96
+ self.lastCrashAt = nowMs;
97
+ if ([l respondsToSelector:@selector(onSensorCrashWithImpactG:location:)]) {
98
+ [l onSensorCrashWithImpactG:accelMagG location:loc];
99
+ }
100
+ }
101
+
102
+ // phoneUsageWhileDriving
103
+ if (!tripActiveNow) { self.jitterAboveSince = 0; return; }
104
+ BOOL screenOn = [self isScreenOnApprox];
105
+ if (!screenOn) { self.jitterAboveSince = 0; return; }
106
+
107
+ BOOL above = (accelMagMs >= kJitterAccelMps2) || (gyroMag >= kJitterGyroRadS);
108
+ if (above) {
109
+ if (self.jitterAboveSince == 0) self.jitterAboveSince = nowMs;
110
+ if ((nowMs - self.jitterAboveSince) >= self.phoneUsageWindowMs
111
+ && (nowMs - self.lastPhoneUsageAt) >= self.phoneUsageCooldownMs) {
112
+ self.lastPhoneUsageAt = nowMs;
113
+ self.jitterAboveSince = 0;
114
+ if ([l respondsToSelector:@selector(onPhoneUsageWhileDriving:)]) {
115
+ [l onPhoneUsageWhileDriving:loc];
116
+ }
117
+ }
118
+ } else {
119
+ self.jitterAboveSince = 0;
120
+ }
121
+ }
122
+
123
+ - (BOOL)isScreenOnApprox {
124
+ // Heuristic: app is foreground active => screen is on. Background sampling does
125
+ // not constitute phone usage while driving (passenger may have screen off too).
126
+ __block UIApplicationState state = UIApplicationStateBackground;
127
+ if ([NSThread isMainThread]) {
128
+ state = [UIApplication sharedApplication].applicationState;
129
+ } else {
130
+ dispatch_sync(dispatch_get_main_queue(), ^{
131
+ state = [UIApplication sharedApplication].applicationState;
132
+ });
133
+ }
134
+ return state == UIApplicationStateActive;
135
+ }
136
+
137
+ @end
@@ -0,0 +1,31 @@
1
+ //
2
+ // MAURUrlTemplateResolver.h
3
+ // BackgroundGeolocation
4
+ //
5
+ // Resolves placeholders like {lat}, {lon}, {timestamp_iso}, {device_id}, ...
6
+ // in a URL template using a single MAURLocation and an optional queryParams dictionary.
7
+ //
8
+ // Placeholders not found in the location/queryParams are left as-is so partial templates
9
+ // (e.g. only static keys for batch mode) keep working.
10
+ //
11
+
12
+ #ifndef MAURUrlTemplateResolver_h
13
+ #define MAURUrlTemplateResolver_h
14
+
15
+ #import <Foundation/Foundation.h>
16
+
17
+ @class MAURLocation;
18
+
19
+ @interface MAURUrlTemplateResolver : NSObject
20
+
21
+ /**
22
+ * Resolve placeholders in `template` using values from `location` and `queryParams`.
23
+ * Either may be nil. Returns the resolved URL (or the original template if no placeholders).
24
+ */
25
+ + (NSString *)resolve:(NSString *)urlTemplate
26
+ location:(MAURLocation *)location
27
+ queryParams:(NSDictionary *)queryParams;
28
+
29
+ @end
30
+
31
+ #endif /* MAURUrlTemplateResolver_h */