@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.
- package/.npmignore +4 -0
- package/CHANGELOG.md +290 -0
- package/CLAUDE.md +56 -0
- package/HISTORY.md +125 -0
- package/README.md +189 -4
- package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +90 -0
- package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +310 -1
- package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +127 -0
- package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +27 -11
- package/android/common/src/main/java/com/marianhello/bgloc/Config.java +268 -0
- package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +86 -26
- package/android/common/src/main/java/com/marianhello/bgloc/PluginDelegate.java +26 -0
- package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +42 -5
- package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
- package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
- package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
- package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
- package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
- package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
- package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +305 -6
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
- package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
- package/android/dependencies.gradle +0 -3
- package/angular/background-geolocation-events.ts +21 -0
- package/angular/background-geolocation.service.ts +63 -0
- package/angular/dist/background-geolocation-events.d.ts +18 -1
- package/angular/dist/background-geolocation.service.d.ts +36 -0
- package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
- package/angular/dist/esm2022/background-geolocation.service.mjs +35 -1
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +55 -0
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
- package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +312 -1
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +22 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +400 -15
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
- package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
- package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
- package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +97 -44
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
- package/package.json +41 -1
- package/plugin.xml +19 -8
- package/www/BackgroundGeolocation.d.ts +517 -3
- package/www/BackgroundGeolocation.js +54 -1
- 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 (
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:
|
|
140
|
-
[request
|
|
141
|
-
|
|
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 (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
NSString *
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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 */
|