@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
@@ -29,6 +29,7 @@
29
29
  #import "MAURUncaughtExceptionLogger.h"
30
30
  #import "MAURPostLocationTask.h"
31
31
  #import "INTULocationManager.h"
32
+ #import "MAURSensorFusionDetector.h"
32
33
 
33
34
  // error messages
34
35
  #define CONFIGURE_ERROR_MSG "Configuration error."
@@ -46,21 +47,63 @@ static NSString * const TAG = @"BgGeo";
46
47
 
47
48
  FMDBLogger *sqliteLogger;
48
49
 
49
- @interface MAURBackgroundGeolocationFacade () <MAURProviderDelegate, MAURPostLocationTaskDelegate>
50
+ @interface MAURBackgroundGeolocationFacade () <MAURProviderDelegate, MAURPostLocationTaskDelegate, MAURSensorFusionListener>
50
51
  @end
51
52
 
53
+ // v3.5 Phase 4: notification name for heartbeat events. CDVBackgroundGeolocation observes
54
+ // it to forward into the JS event "heartbeat" with the latest known location.
55
+ NSString * const MAURHeartbeatNotification = @"MAURHeartbeatNotification";
56
+ // v4.0 Phase 6: driver-insight notifications.
57
+ NSString * const MAURTripStartNotification = @"MAURTripStartNotification";
58
+ NSString * const MAURTripEndNotification = @"MAURTripEndNotification";
59
+ NSString * const MAURMovingNotification = @"MAURMovingNotification";
60
+ NSString * const MAURStoppedNotification = @"MAURStoppedNotification";
61
+ NSString * const MAURSpeedingNotification = @"MAURSpeedingNotification";
62
+ NSString * const MAURProviderChangeNotification = @"MAURProviderChangeNotification";
63
+ NSString * const MAURSOSNotification = @"MAURSOSNotification";
64
+ // v4.1
65
+ NSString * const MAURHardBrakeNotification = @"MAURHardBrakeNotification";
66
+ NSString * const MAURRapidAccelerationNotification = @"MAURRapidAccelerationNotification";
67
+ NSString * const MAURSharpTurnNotification = @"MAURSharpTurnNotification";
68
+ NSString * const MAURPossibleCrashNotification = @"MAURPossibleCrashNotification";
69
+ // v4.2
70
+ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileDrivingNotification";
71
+
52
72
  @implementation MAURBackgroundGeolocationFacade {
53
73
  BOOL isStarted;
54
74
  MAUROperationalMode operationMode;
55
-
75
+
56
76
  UILocalNotification *localNotification;
57
-
77
+
58
78
  // configurable options
59
79
  MAURConfig *_config;
60
-
80
+
61
81
  MAURLocation *stationaryLocation;
82
+ MAURLocation *lastReceivedLocation; // v3.5 Phase 4: heartbeat payload
83
+ NSTimer *heartbeatTimer; // v3.5 Phase 4
62
84
  MAURAbstractLocationProvider<MAURLocationProvider> *locationProvider;
63
85
  MAURPostLocationTask *postLocationTask;
86
+
87
+ // v4.0 Phase 6: driver-insights state
88
+ BOOL drIsMoving;
89
+ BOOL drTripActive;
90
+ NSTimeInterval drTripStartedAt;
91
+ double drTripDistanceMeters;
92
+ BOOL drHasPrev;
93
+ double drPrevLat, drPrevLon;
94
+ NSTimeInterval drAboveTripSpeedSince;
95
+ NSTimeInterval drBelowMovingSince;
96
+ BOOL drWasSpeeding;
97
+ NSString *drLastProvider;
98
+ // v4.1 GPS-derived sensor-like state
99
+ double drPrevSpeed;
100
+ NSTimeInterval drPrevSpeedAt;
101
+ double drPrevBearing;
102
+ BOOL drHasPrevBearing;
103
+ NSTimeInterval drPrevBearingAt;
104
+ NSTimeInterval drLastHardBrakeAt, drLastRapidAccelAt, drLastSharpTurnAt, drLastCrashAt;
105
+ // v4.2 sensor fusion
106
+ MAURSensorFusionDetector *sensorFusion;
64
107
  }
65
108
 
66
109
 
@@ -136,25 +179,41 @@ FMDBLogger *sqliteLogger;
136
179
  if (isStarted) {
137
180
  // Note: CLLocationManager must be created on a thread with an active run loop (main thread)
138
181
  [self runOnMainThread:^{
139
-
182
+
140
183
  // requesting new provider
141
184
  if (![currentConfig.locationProvider isEqual:_config.locationProvider]) {
142
185
  [locationProvider onDestroy]; // destroy current provider
143
186
  locationProvider = [self getProvider:_config.locationProvider.intValue error:&error];
144
187
  }
145
-
188
+
146
189
  if (locationProvider == nil) {
147
190
  return;
148
191
  }
149
-
192
+
150
193
  // trap configuration errors
151
194
  if (![locationProvider onConfigure:_config error:&error]) {
152
195
  return;
153
196
  }
154
-
197
+
155
198
  isStarted = [locationProvider onStart:&error];
156
199
  locationProvider.delegate = self;
157
200
  }];
201
+
202
+ // v4.1: hot-reload heartbeat scheduler if heartbeatInterval changed.
203
+ NSInteger prevHb = currentConfig.heartbeatInterval != nil ? [currentConfig.heartbeatInterval integerValue] : 0;
204
+ NSInteger newHb = _config.heartbeatInterval != nil ? [_config.heartbeatInterval integerValue] : 0;
205
+ if (prevHb != newHb) {
206
+ [self scheduleHeartbeat]; // cancels and reschedules; is a no-op if 0.
207
+ }
208
+ // Driver-insights detector reads `_config.drivingEvents` on every feed; no rebuild needed
209
+ // unless the dictionary identity changed in a way that toggles `enabled`. Always reset
210
+ // accumulators to apply the new thresholds cleanly from this point on.
211
+ if (![[currentConfig.drivingEvents description] isEqualToString:[_config.drivingEvents description]]) {
212
+ [self drivingDetectorReset];
213
+ // v4.2: re-evaluate sensor fusion as well (might have just been enabled/disabled).
214
+ [self configureSensorFusion];
215
+ if (isStarted) [sensorFusion start];
216
+ }
158
217
  }
159
218
 
160
219
  if (error != nil) {
@@ -213,10 +272,16 @@ FMDBLogger *sqliteLogger;
213
272
  if (outError != nil) {
214
273
  *outError = error;
215
274
  }
216
-
275
+
217
276
  return NO;
218
277
  }
219
-
278
+
279
+ // v3.5 Phase 4: schedule heartbeat once provider is up.
280
+ [self scheduleHeartbeat];
281
+ // v4.2 Phase 8: configure & start sensor fusion if requested.
282
+ [self configureSensorFusion];
283
+ [sensorFusion start];
284
+
220
285
  return isStarted;
221
286
  }
222
287
 
@@ -226,20 +291,334 @@ FMDBLogger *sqliteLogger;
226
291
  - (BOOL) stop:(NSError * __autoreleasing *)outError
227
292
  {
228
293
  DDLogInfo(@"%@ #stop", TAG);
229
-
294
+
230
295
  if (!isStarted) {
231
296
  return YES;
232
297
  }
233
-
298
+
299
+ // v3.5 Phase 4: cancel heartbeat scheduler.
300
+ [self cancelHeartbeat];
301
+ // v4.0 Phase 6: reset driver-insights state machine.
302
+ [self drivingDetectorReset];
303
+ // v4.2 Phase 8: stop sensor fusion sampling.
304
+ sensorFusion.tripActive = NO;
305
+ [sensorFusion stop];
306
+
234
307
  [postLocationTask stop];
235
-
308
+
236
309
  [self runOnMainThread:^{
237
310
  isStarted = ![locationProvider onStop:outError];
238
311
  }];
239
-
312
+
240
313
  return isStarted;
241
314
  }
242
315
 
316
+ // v3.5 Phase 4: heartbeat scheduler.
317
+ - (void) scheduleHeartbeat
318
+ {
319
+ [self cancelHeartbeat];
320
+ if (_config == nil || _config.heartbeatInterval == nil) return;
321
+ NSInteger ms = [_config.heartbeatInterval integerValue];
322
+ if (ms <= 0) return;
323
+ NSTimeInterval seconds = ms / 1000.0;
324
+ DDLogDebug(@"%@ scheduling heartbeat every %.2fs", TAG, seconds);
325
+ dispatch_async(dispatch_get_main_queue(), ^{
326
+ heartbeatTimer = [NSTimer scheduledTimerWithTimeInterval:seconds
327
+ target:self
328
+ selector:@selector(onHeartbeatTick:)
329
+ userInfo:nil
330
+ repeats:YES];
331
+ });
332
+ }
333
+
334
+ - (void) cancelHeartbeat
335
+ {
336
+ if (heartbeatTimer != nil) {
337
+ [heartbeatTimer invalidate];
338
+ heartbeatTimer = nil;
339
+ }
340
+ }
341
+
342
+ - (void) onHeartbeatTick:(NSTimer *)timer
343
+ {
344
+ NSDictionary *userInfo = lastReceivedLocation != nil
345
+ ? @{ @"location": lastReceivedLocation }
346
+ : @{};
347
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURHeartbeatNotification
348
+ object:self
349
+ userInfo:userInfo];
350
+ }
351
+
352
+ #pragma mark - v4.0 Phase 6 driver-insights state machine
353
+
354
+ - (void) drivingDetectorReset
355
+ {
356
+ drIsMoving = NO;
357
+ drTripActive = NO;
358
+ drTripStartedAt = 0;
359
+ drTripDistanceMeters = 0;
360
+ drHasPrev = NO;
361
+ drAboveTripSpeedSince = 0;
362
+ drBelowMovingSince = 0;
363
+ drWasSpeeding = NO;
364
+ drLastProvider = nil;
365
+ drPrevSpeed = 0;
366
+ drPrevSpeedAt = 0;
367
+ drPrevBearing = 0;
368
+ drHasPrevBearing = NO;
369
+ drPrevBearingAt = 0;
370
+ drLastHardBrakeAt = drLastRapidAccelAt = drLastSharpTurnAt = drLastCrashAt = 0;
371
+ }
372
+
373
+ #pragma mark - v4.2 Phase 8 sensor fusion
374
+
375
+ - (void) configureSensorFusion
376
+ {
377
+ NSDictionary *de = _config.drivingEvents;
378
+ BOOL want = [de isKindOfClass:[NSDictionary class]]
379
+ && [de[@"enabled"] boolValue]
380
+ && [de[@"sensorFusion"] boolValue];
381
+ if (!want) {
382
+ [sensorFusion stop];
383
+ sensorFusion = nil;
384
+ return;
385
+ }
386
+ if (sensorFusion == nil) {
387
+ sensorFusion = [[MAURSensorFusionDetector alloc] init];
388
+ sensorFusion.listener = self;
389
+ }
390
+ sensorFusion.enabled = YES;
391
+ if (de[@"crashImpactG"]) sensorFusion.crashImpactG = [de[@"crashImpactG"] doubleValue];
392
+ if (de[@"sensorCrashCooldownMs"]) sensorFusion.crashCooldownMs = [de[@"sensorCrashCooldownMs"] doubleValue];
393
+ if (de[@"phoneUsageWindowMs"]) sensorFusion.phoneUsageWindowMs = [de[@"phoneUsageWindowMs"] doubleValue];
394
+ if (de[@"phoneUsageCooldownMs"]) sensorFusion.phoneUsageCooldownMs = [de[@"phoneUsageCooldownMs"] doubleValue];
395
+ // v4.2 hot-reload: re-inject current trip state + last location so a config change
396
+ // mid-trip starts the sensor pipeline in the correct mode.
397
+ sensorFusion.tripActive = drTripActive;
398
+ sensorFusion.lastLocation = lastReceivedLocation;
399
+ }
400
+
401
+ // MAURSensorFusionListener
402
+ - (void) onSensorCrashWithImpactG:(double)impactG location:(MAURLocation *)location
403
+ {
404
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
405
+ if (location != nil) userInfo[@"location"] = location;
406
+ userInfo[@"value"] = @(impactG);
407
+ userInfo[@"source"] = @"sensor";
408
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURPossibleCrashNotification
409
+ object:self
410
+ userInfo:userInfo];
411
+ }
412
+ - (void) onPhoneUsageWhileDriving:(MAURLocation *)location
413
+ {
414
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
415
+ if (location != nil) userInfo[@"location"] = location;
416
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURPhoneUsageWhileDrivingNotification
417
+ object:self
418
+ userInfo:userInfo];
419
+ }
420
+
421
+ - (void) drivingDetectorFeed:(MAURLocation *)loc
422
+ {
423
+ if (loc == nil || _config == nil) return;
424
+ BOOL enabled = NO;
425
+ double speedLimit = 0;
426
+ double minMovingSpeed = 1.0;
427
+ NSTimeInterval stoppedDuration = 60.0;
428
+ double minTripSpeed = 3.0;
429
+ NSTimeInterval minTripDuration = 30.0;
430
+ NSDictionary *de = [_config valueForKey:@"drivingEvents"]; // see MAURConfig: provided as NSDictionary
431
+ if ([de isKindOfClass:[NSDictionary class]]) {
432
+ enabled = [[de objectForKey:@"enabled"] boolValue];
433
+ speedLimit = [[de objectForKey:@"speedLimit"] doubleValue];
434
+ if ([de objectForKey:@"minMovingSpeed"]) minMovingSpeed = [[de objectForKey:@"minMovingSpeed"] doubleValue];
435
+ if ([de objectForKey:@"stoppedDuration"]) stoppedDuration = [[de objectForKey:@"stoppedDuration"] doubleValue] / 1000.0;
436
+ if ([de objectForKey:@"minTripSpeed"]) minTripSpeed = [[de objectForKey:@"minTripSpeed"] doubleValue];
437
+ if ([de objectForKey:@"minTripDuration"]) minTripDuration = [[de objectForKey:@"minTripDuration"] doubleValue] / 1000.0;
438
+ }
439
+ if (!enabled) return;
440
+
441
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
442
+ double speed = loc.speed != nil ? [loc.speed doubleValue] : 0.0;
443
+ if (speed < 0) speed = 0;
444
+
445
+ // Provider change
446
+ NSString *provider = loc.provider;
447
+ if (provider != nil && ![provider isEqualToString:drLastProvider]) {
448
+ drLastProvider = provider;
449
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURProviderChangeNotification
450
+ object:self
451
+ userInfo:@{@"provider": provider}];
452
+ }
453
+
454
+ double curLat = [loc.latitude doubleValue];
455
+ double curLon = [loc.longitude doubleValue];
456
+ if (drHasPrev && drTripActive) {
457
+ drTripDistanceMeters += [self drHaversineFromLat:drPrevLat lon:drPrevLon toLat:curLat lon:curLon];
458
+ }
459
+ drPrevLat = curLat;
460
+ drPrevLon = curLon;
461
+ drHasPrev = YES;
462
+
463
+ BOOL nowMoving = speed >= minMovingSpeed;
464
+ if (nowMoving) {
465
+ drBelowMovingSince = 0;
466
+ if (!drIsMoving) {
467
+ drIsMoving = YES;
468
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURMovingNotification
469
+ object:self
470
+ userInfo:@{@"location": loc}];
471
+ }
472
+ if (!drTripActive) {
473
+ if (speed >= minTripSpeed) {
474
+ if (drAboveTripSpeedSince == 0) drAboveTripSpeedSince = now;
475
+ if (now - drAboveTripSpeedSince >= minTripDuration) {
476
+ drTripActive = YES;
477
+ drTripStartedAt = now;
478
+ drTripDistanceMeters = 0;
479
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURTripStartNotification
480
+ object:self
481
+ userInfo:@{@"location": loc}];
482
+ sensorFusion.tripActive = YES;
483
+ }
484
+ } else {
485
+ drAboveTripSpeedSince = 0;
486
+ }
487
+ }
488
+ } else {
489
+ drAboveTripSpeedSince = 0;
490
+ if (drBelowMovingSince == 0) drBelowMovingSince = now;
491
+ if (drIsMoving && (now - drBelowMovingSince) >= stoppedDuration) {
492
+ drIsMoving = NO;
493
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURStoppedNotification
494
+ object:self
495
+ userInfo:@{@"location": loc}];
496
+ if (drTripActive) {
497
+ NSTimeInterval durMs = (now - drTripStartedAt) * 1000.0;
498
+ double dist = drTripDistanceMeters;
499
+ drTripActive = NO;
500
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURTripEndNotification
501
+ object:self
502
+ userInfo:@{
503
+ @"location": loc,
504
+ @"distance": @(dist),
505
+ @"durationMs": @((long long)durMs)
506
+ }];
507
+ sensorFusion.tripActive = NO;
508
+ }
509
+ }
510
+ }
511
+
512
+ if (speedLimit > 0) {
513
+ double kmh = speed * 3.6;
514
+ if (kmh > speedLimit) {
515
+ if (!drWasSpeeding) {
516
+ drWasSpeeding = YES;
517
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURSpeedingNotification
518
+ object:self
519
+ userInfo:@{
520
+ @"location": loc,
521
+ @"speedKmh": @(kmh),
522
+ @"limitKmh": @(speedLimit)
523
+ }];
524
+ }
525
+ } else {
526
+ drWasSpeeding = NO;
527
+ }
528
+ }
529
+
530
+ // v4.1 GPS-derived sensor-like events
531
+ double hardBrakeMps2 = 3.5, rapidAccelMps2 = 3.5, sharpTurnDegPerSec = 30, crashImpactKmh = 25;
532
+ NSTimeInterval crashWindow = 2.0;
533
+ if ([de isKindOfClass:[NSDictionary class]]) {
534
+ if ([de objectForKey:@"hardBrakeMps2"]) hardBrakeMps2 = [[de objectForKey:@"hardBrakeMps2"] doubleValue];
535
+ if ([de objectForKey:@"rapidAccelMps2"]) rapidAccelMps2 = [[de objectForKey:@"rapidAccelMps2"] doubleValue];
536
+ if ([de objectForKey:@"sharpTurnDegPerSec"]) sharpTurnDegPerSec = [[de objectForKey:@"sharpTurnDegPerSec"] doubleValue];
537
+ if ([de objectForKey:@"crashImpactKmh"]) crashImpactKmh = [[de objectForKey:@"crashImpactKmh"] doubleValue];
538
+ if ([de objectForKey:@"crashWindowMs"]) crashWindow = [[de objectForKey:@"crashWindowMs"] doubleValue] / 1000.0;
539
+ }
540
+ static const NSTimeInterval kCooldown = 4.0;
541
+
542
+ if (drTripActive && drPrevSpeedAt > 0) {
543
+ NSTimeInterval dt = now - drPrevSpeedAt;
544
+ if (dt > 0 && dt <= 5.0) {
545
+ double dv = speed - drPrevSpeed;
546
+ double accel = dv / dt;
547
+ if (hardBrakeMps2 > 0 && accel <= -hardBrakeMps2 && (now - drLastHardBrakeAt) >= kCooldown) {
548
+ drLastHardBrakeAt = now;
549
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURHardBrakeNotification
550
+ object:self
551
+ userInfo:@{@"location": loc, @"value": @(accel)}];
552
+ }
553
+ if (rapidAccelMps2 > 0 && accel >= rapidAccelMps2 && (now - drLastRapidAccelAt) >= kCooldown) {
554
+ drLastRapidAccelAt = now;
555
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURRapidAccelerationNotification
556
+ object:self
557
+ userInfo:@{@"location": loc, @"value": @(accel)}];
558
+ }
559
+ if (crashImpactKmh > 0 && dt <= crashWindow) {
560
+ double dropKmh = (drPrevSpeed - speed) * 3.6;
561
+ if (dropKmh >= crashImpactKmh
562
+ && speed < 1.5
563
+ && drPrevSpeed * 3.6 >= crashImpactKmh
564
+ && (now - drLastCrashAt) >= kCooldown) {
565
+ drLastCrashAt = now;
566
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURPossibleCrashNotification
567
+ object:self
568
+ userInfo:@{@"location": loc, @"value": @(dropKmh), @"source": @"gps"}];
569
+ }
570
+ }
571
+ }
572
+ }
573
+
574
+ // Sharp turn (bearing rate)
575
+ if (sharpTurnDegPerSec > 0 && loc.heading != nil && speed >= 5.0 && drHasPrevBearing) {
576
+ NSTimeInterval dt = now - drPrevBearingAt;
577
+ if (dt > 0 && dt <= 5.0) {
578
+ double bearing = [loc.heading doubleValue];
579
+ double diff = fabs(bearing - drPrevBearing);
580
+ if (diff > 180) diff = 360 - diff;
581
+ double rate = diff / dt;
582
+ if (rate >= sharpTurnDegPerSec && (now - drLastSharpTurnAt) >= kCooldown) {
583
+ drLastSharpTurnAt = now;
584
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURSharpTurnNotification
585
+ object:self
586
+ userInfo:@{@"location": loc, @"value": @(rate)}];
587
+ }
588
+ }
589
+ drPrevBearing = [loc.heading doubleValue];
590
+ drPrevBearingAt = now;
591
+ } else if (loc.heading != nil) {
592
+ drPrevBearing = [loc.heading doubleValue];
593
+ drPrevBearingAt = now;
594
+ drHasPrevBearing = YES;
595
+ }
596
+
597
+ drPrevSpeed = speed;
598
+ drPrevSpeedAt = now;
599
+ }
600
+
601
+ - (double) drHaversineFromLat:(double)lat1 lon:(double)lon1 toLat:(double)lat2 lon:(double)lon2
602
+ {
603
+ const double R = 6371000.0;
604
+ double dLat = (lat2 - lat1) * M_PI / 180.0;
605
+ double dLon = (lon2 - lon1) * M_PI / 180.0;
606
+ double a = sin(dLat/2) * sin(dLat/2)
607
+ + cos(lat1 * M_PI / 180.0) * cos(lat2 * M_PI / 180.0)
608
+ * sin(dLon/2) * sin(dLon/2);
609
+ return 2 * R * asin(sqrt(a));
610
+ }
611
+
612
+ - (void) triggerSOS:(NSDictionary *)payload
613
+ {
614
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
615
+ if (lastReceivedLocation != nil) userInfo[@"location"] = lastReceivedLocation;
616
+ userInfo[@"payload"] = payload != nil ? payload : @{};
617
+ [[NSNotificationCenter defaultCenter] postNotificationName:MAURSOSNotification
618
+ object:self
619
+ userInfo:userInfo];
620
+ }
621
+
243
622
  /**
244
623
  * toggle between foreground and background operation mode
245
624
  */
@@ -565,7 +944,13 @@ FMDBLogger *sqliteLogger;
565
944
  {
566
945
  DDLogDebug(@"%@ #onLocationChanged %@", TAG, location);
567
946
  stationaryLocation = nil;
568
-
947
+ lastReceivedLocation = location; // v3.5 Phase 4: cached for heartbeat payload
948
+
949
+ // v4.0 Phase 6: feed driver-insights state machine.
950
+ [self drivingDetectorFeed:location];
951
+ // v4.2 Phase 8: keep sensor pipeline aware of the latest fix.
952
+ sensorFusion.lastLocation = location;
953
+
569
954
  [postLocationTask add:location];
570
955
 
571
956
  MAURConfig *config = [self getConfig];
@@ -12,11 +12,22 @@
12
12
 
13
13
  @class MAURBackgroundSync;
14
14
 
15
+ // v3.5 Phase 4: notification names for sync events. The plugin layer observes them
16
+ // via NSNotificationCenter to forward into JS as syncStart / syncSuccess / syncError / syncProgress.
17
+ extern NSString * _Nonnull const MAURBackgroundSyncDidStartNotification;
18
+ extern NSString * _Nonnull const MAURBackgroundSyncDidSucceedNotification;
19
+ extern NSString * _Nonnull const MAURBackgroundSyncDidFailNotification;
20
+ extern NSString * _Nonnull const MAURBackgroundSyncDidProgressNotification;
21
+
15
22
  @protocol MAURBackgroundSyncDelegate <NSObject>
16
23
 
17
24
  @optional
18
25
  - (void)backgroundSyncRequestedAbortUpdates:(MAURBackgroundSync * _Nonnull)task;
19
26
  - (void)backgroundSyncHttpAuthorizationUpdates:(MAURBackgroundSync * _Nonnull)task;
27
+ // v3.5 Phase 4
28
+ - (void)backgroundSyncStarted:(MAURBackgroundSync * _Nonnull)task;
29
+ - (void)backgroundSyncSucceeded:(MAURBackgroundSync * _Nonnull)task locationsSent:(NSInteger)locationsSent;
30
+ - (void)backgroundSyncFailed:(MAURBackgroundSync * _Nonnull)task httpStatus:(NSInteger)httpStatus message:(NSString * _Nullable)message;
20
31
 
21
32
  @end
22
33
 
@@ -27,6 +38,7 @@
27
38
  - (instancetype) init;
28
39
  - (NSString*) status;
29
40
  - (void) sync:(NSString * _Nonnull)url withTemplate:(id)locationTemplate withHttpHeaders:(NSMutableDictionary * _Nullable)httpHeaders;
41
+ - (void) sync:(NSString * _Nonnull)url withTemplate:(id)locationTemplate withHttpHeaders:(NSMutableDictionary * _Nullable)httpHeaders withMethod:(NSString * _Nullable)method;
30
42
  - (void) cancel;
31
43
 
32
44
  @end
@@ -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