@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
@@ -113,6 +113,29 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
113
113
 
114
114
  public static final int MSG_ON_HTTP_AUTHORIZATION = 107;
115
115
 
116
+ /** v3.5 Phase 4: sync queue events. */
117
+ public static final int MSG_ON_SYNC_START = 108;
118
+ public static final int MSG_ON_SYNC_SUCCESS = 109;
119
+ public static final int MSG_ON_SYNC_ERROR = 110;
120
+ public static final int MSG_ON_SYNC_PROGRESS = 111;
121
+ public static final int MSG_ON_HEARTBEAT = 112;
122
+ /** v4.0 Phase 6 — driver insight events. */
123
+ public static final int MSG_ON_TRIP_START = 113;
124
+ public static final int MSG_ON_TRIP_END = 114;
125
+ public static final int MSG_ON_MOVING = 115;
126
+ public static final int MSG_ON_STOPPED = 116;
127
+ public static final int MSG_ON_SPEEDING = 117;
128
+ public static final int MSG_ON_PROVIDER_CHANGE = 118;
129
+ public static final int MSG_ON_SOS = 119;
130
+ /** v4.1 — sensor-like GPS-derived driving events. */
131
+ public static final int MSG_ON_HARD_BRAKE = 120;
132
+ public static final int MSG_ON_RAPID_ACCELERATION = 121;
133
+ public static final int MSG_ON_SHARP_TURN = 122;
134
+ public static final int MSG_ON_POSSIBLE_CRASH = 123;
135
+ /** v4.2 — sensor-fusion-only events. {@code MSG_ON_POSSIBLE_CRASH} is reused
136
+ * by the sensor pipeline; phone-usage is a brand-new event. */
137
+ public static final int MSG_ON_PHONE_USAGE_WHILE_DRIVING = 124;
138
+
116
139
  /** notification id */
117
140
  private static int NOTIFICATION_ID = 1;
118
141
 
@@ -141,6 +164,21 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
141
164
 
142
165
  /** Last time we received a location (for watchdog). */
143
166
  private volatile long mLastLocationTime = 0L;
167
+ /** v3.5 Phase 4: latest received location, used as heartbeat payload. */
168
+ private volatile BackgroundLocation mLastReceivedLocation;
169
+ /** v4.0 Phase 6: static accessor for {@link com.marianhello.bgloc.BackgroundGeolocationFacade#triggerSOS}. */
170
+ private static volatile BackgroundLocation sLastReceivedLocation;
171
+ public static BackgroundLocation getLastReceivedLocation() { return sLastReceivedLocation; }
172
+ /** v3.5 Phase 4: heartbeat scheduler. */
173
+ private java.util.concurrent.ScheduledExecutorService mHeartbeatExecutor;
174
+ private java.util.concurrent.ScheduledFuture<?> mHeartbeatTask;
175
+
176
+ /** v4.0 Phase 6: driver-insights detector. Created lazily when config has drivingEvents.enabled. */
177
+ private com.marianhello.bgloc.driving.DrivingEventsDetector mDrivingDetector;
178
+ /** v4.2 Phase 8: real sensor-fusion detector. Created when drivingEvents.sensorFusion=true. */
179
+ private com.marianhello.bgloc.sensor.SensorFusionDetector mSensorFusion;
180
+ /** v4.2 Phase 8: cached tripActive state so hot-reload can re-inject it. */
181
+ private volatile boolean mDrivingTripActive = false;
144
182
  private static final long WATCHDOG_INTERVAL_MS = 60_000L;
145
183
  private final Handler mMainHandler = new Handler(Looper.getMainLooper());
146
184
  private final Runnable mWatchdogRunnable = new Runnable() {
@@ -469,6 +507,170 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
469
507
  bundle.putInt("action", MSG_ON_SERVICE_STARTED);
470
508
  bundle.putLong("serviceId", mServiceId);
471
509
  broadcastMessage(bundle);
510
+
511
+ // v3.5 Phase 4: kick off heartbeat scheduler when the service starts.
512
+ scheduleHeartbeat();
513
+
514
+ // v4.0 Phase 6: build driver-insights detector if enabled in config.
515
+ configureDrivingDetector();
516
+ }
517
+
518
+ /** v4.0 Phase 6: instantiate / reconfigure the GPS-based driver-insights detector. */
519
+ private void configureDrivingDetector() {
520
+ if (mConfig == null) return;
521
+ com.marianhello.bgloc.Config.DrivingEventsOptions opts = mConfig.getDrivingEvents();
522
+ if (opts == null || !opts.enabled) {
523
+ if (mDrivingDetector != null) mDrivingDetector.reset();
524
+ mDrivingDetector = null;
525
+ return;
526
+ }
527
+ com.marianhello.bgloc.driving.DrivingEventsDetector.Config c =
528
+ new com.marianhello.bgloc.driving.DrivingEventsDetector.Config();
529
+ c.enabled = true;
530
+ c.speedLimitKmh = opts.speedLimitKmh;
531
+ c.minMovingSpeedMps = opts.minMovingSpeedMps;
532
+ c.stoppedDurationMs = opts.stoppedDurationMs;
533
+ c.minTripSpeedMps = opts.minTripSpeedMps;
534
+ c.minTripDurationMs = opts.minTripDurationMs;
535
+
536
+ mDrivingDetector = new com.marianhello.bgloc.driving.DrivingEventsDetector(
537
+ new com.marianhello.bgloc.driving.DrivingEventsDetector.Listener() {
538
+ @Override public void onMoving(BackgroundLocation l) {
539
+ Bundle b = new Bundle();
540
+ b.putInt("action", MSG_ON_MOVING);
541
+ if (l != null) b.putParcelable("payload", l);
542
+ broadcastMessage(b);
543
+ }
544
+ @Override public void onStopped(BackgroundLocation l) {
545
+ Bundle b = new Bundle();
546
+ b.putInt("action", MSG_ON_STOPPED);
547
+ if (l != null) b.putParcelable("payload", l);
548
+ broadcastMessage(b);
549
+ }
550
+ @Override public void onTripStart(BackgroundLocation l) {
551
+ Bundle b = new Bundle();
552
+ b.putInt("action", MSG_ON_TRIP_START);
553
+ if (l != null) b.putParcelable("payload", l);
554
+ broadcastMessage(b);
555
+ mDrivingTripActive = true;
556
+ if (mSensorFusion != null) mSensorFusion.setTripActive(true);
557
+ }
558
+ @Override public void onTripEnd(BackgroundLocation l, double distance, long durationMs) {
559
+ Bundle b = new Bundle();
560
+ b.putInt("action", MSG_ON_TRIP_END);
561
+ if (l != null) b.putParcelable("payload", l);
562
+ b.putDouble("distance", distance);
563
+ b.putLong("durationMs", durationMs);
564
+ broadcastMessage(b);
565
+ mDrivingTripActive = false;
566
+ if (mSensorFusion != null) mSensorFusion.setTripActive(false);
567
+ }
568
+ @Override public void onSpeeding(BackgroundLocation l, double speedKmh, double limitKmh) {
569
+ Bundle b = new Bundle();
570
+ b.putInt("action", MSG_ON_SPEEDING);
571
+ if (l != null) b.putParcelable("payload", l);
572
+ b.putDouble("speedKmh", speedKmh);
573
+ b.putDouble("limitKmh", limitKmh);
574
+ broadcastMessage(b);
575
+ }
576
+ @Override public void onProviderChange(String provider) {
577
+ Bundle b = new Bundle();
578
+ b.putInt("action", MSG_ON_PROVIDER_CHANGE);
579
+ b.putString("provider", provider != null ? provider : "");
580
+ broadcastMessage(b);
581
+ }
582
+ @Override public void onHardBrake(BackgroundLocation l, double decelMps2) {
583
+ Bundle b = new Bundle();
584
+ b.putInt("action", MSG_ON_HARD_BRAKE);
585
+ if (l != null) b.putParcelable("payload", l);
586
+ b.putDouble("value", decelMps2);
587
+ broadcastMessage(b);
588
+ }
589
+ @Override public void onRapidAcceleration(BackgroundLocation l, double accelMps2) {
590
+ Bundle b = new Bundle();
591
+ b.putInt("action", MSG_ON_RAPID_ACCELERATION);
592
+ if (l != null) b.putParcelable("payload", l);
593
+ b.putDouble("value", accelMps2);
594
+ broadcastMessage(b);
595
+ }
596
+ @Override public void onSharpTurn(BackgroundLocation l, double degPerSec) {
597
+ Bundle b = new Bundle();
598
+ b.putInt("action", MSG_ON_SHARP_TURN);
599
+ if (l != null) b.putParcelable("payload", l);
600
+ b.putDouble("value", degPerSec);
601
+ broadcastMessage(b);
602
+ }
603
+ @Override public void onPossibleCrash(BackgroundLocation l, double velocityDropKmh) {
604
+ Bundle b = new Bundle();
605
+ b.putInt("action", MSG_ON_POSSIBLE_CRASH);
606
+ if (l != null) b.putParcelable("payload", l);
607
+ b.putDouble("value", velocityDropKmh);
608
+ b.putString("source", "gps");
609
+ broadcastMessage(b);
610
+ }
611
+ });
612
+ // Pass v4.1 thresholds from app config (with defaults from c).
613
+ com.marianhello.bgloc.Config.DrivingEventsOptions optsRef = mConfig.getDrivingEvents();
614
+ if (optsRef != null) {
615
+ c.hardBrakeMps2 = optsRef.hardBrakeMps2;
616
+ c.rapidAccelMps2 = optsRef.rapidAccelMps2;
617
+ c.sharpTurnDegPerSec = optsRef.sharpTurnDegPerSec;
618
+ c.crashImpactKmh = optsRef.crashImpactKmh;
619
+ c.crashWindowMs = optsRef.crashWindowMs;
620
+ }
621
+ mDrivingDetector.setConfig(c);
622
+ configureSensorFusion();
623
+ }
624
+
625
+ /** v4.2 Phase 8: instantiate / reconfigure the real sensor-fusion detector. */
626
+ private void configureSensorFusion() {
627
+ if (mConfig == null) {
628
+ if (mSensorFusion != null) { mSensorFusion.stop(); mSensorFusion = null; }
629
+ return;
630
+ }
631
+ com.marianhello.bgloc.Config.DrivingEventsOptions opts = mConfig.getDrivingEvents();
632
+ boolean wantSF = opts != null && opts.enabled && opts.sensorFusion;
633
+ if (!wantSF) {
634
+ if (mSensorFusion != null) { mSensorFusion.stop(); mSensorFusion = null; }
635
+ return;
636
+ }
637
+
638
+ com.marianhello.bgloc.sensor.SensorFusionDetector.Listener l =
639
+ new com.marianhello.bgloc.sensor.SensorFusionDetector.Listener() {
640
+ @Override public void onSensorCrash(BackgroundLocation lastLocation, double impactG) {
641
+ Bundle b = new Bundle();
642
+ b.putInt("action", MSG_ON_POSSIBLE_CRASH);
643
+ if (lastLocation != null) b.putParcelable("payload", lastLocation);
644
+ b.putDouble("value", impactG);
645
+ b.putString("source", "sensor");
646
+ broadcastMessage(b);
647
+ }
648
+ @Override public void onPhoneUsageWhileDriving(BackgroundLocation lastLocation) {
649
+ Bundle b = new Bundle();
650
+ b.putInt("action", MSG_ON_PHONE_USAGE_WHILE_DRIVING);
651
+ if (lastLocation != null) b.putParcelable("payload", lastLocation);
652
+ broadcastMessage(b);
653
+ }
654
+ };
655
+
656
+ if (mSensorFusion == null) {
657
+ mSensorFusion = new com.marianhello.bgloc.sensor.SensorFusionDetector(this, l);
658
+ }
659
+ com.marianhello.bgloc.sensor.SensorFusionDetector.Config sfc =
660
+ new com.marianhello.bgloc.sensor.SensorFusionDetector.Config();
661
+ sfc.enabled = true;
662
+ sfc.crashImpactG = opts.crashImpactG;
663
+ sfc.crashCooldownMs = opts.sensorCrashCooldownMs;
664
+ sfc.phoneUsageWindowMs = opts.phoneUsageWindowMs;
665
+ sfc.phoneUsageCooldownMs = opts.phoneUsageCooldownMs;
666
+ mSensorFusion.setConfig(sfc);
667
+ // v4.2 hot-reload: re-inject current tripActive state and last location so the
668
+ // sensor pipeline starts in the right mode (e.g. config arrives mid-trip).
669
+ mSensorFusion.setTripActive(mDrivingTripActive);
670
+ if (mLastReceivedLocation != null) {
671
+ mSensorFusion.setLastLocation(mLastReceivedLocation);
672
+ }
673
+ if (sIsRunning) mSensorFusion.start();
472
674
  }
473
675
 
474
676
  @Override
@@ -503,6 +705,17 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
503
705
  stopForeground(true);
504
706
  stopSelf();
505
707
 
708
+ // v3.5 Phase 4: stop heartbeat scheduler.
709
+ cancelHeartbeat();
710
+ // v4.0 Phase 6: reset driver-insights state machine.
711
+ if (mDrivingDetector != null) mDrivingDetector.reset();
712
+ // v4.2 Phase 8: stop sensor fusion sampling.
713
+ mDrivingTripActive = false;
714
+ if (mSensorFusion != null) {
715
+ mSensorFusion.setTripActive(false);
716
+ mSensorFusion.stop();
717
+ }
718
+
506
719
  broadcastMessage(MSG_ON_SERVICE_STOPPED);
507
720
  sIsRunning = false;
508
721
  }
@@ -516,13 +729,10 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
516
729
  || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
517
730
  }
518
731
 
519
- /** FOREGROUND_SERVICE_TYPE_LOCATION = 4 when compileSdk >= 34. */
520
- private static final int FOREGROUND_SERVICE_TYPE_LOCATION = 4;
521
-
522
732
  /**
523
733
  * Reads this service's foregroundServiceType from the merged AndroidManifest (API 34+).
524
734
  * Uses ComponentInfoFlags.of(0) (not GET_META_DATA) so getServiceInfo returns complete ServiceInfo.
525
- * Returns the real value; never invents 0x4. If unknown, returns 0 so callers must not call startForeground.
735
+ * Returns the real value; never invents a hardcoded type. If unknown, returns 0 so callers must not call startForeground.
526
736
  * Requires compileSdk 33+ (ComponentInfoFlags); 34+ for ServiceInfo.foregroundServiceType.
527
737
  */
528
738
  private int getManifestForegroundServiceType() {
@@ -581,9 +791,14 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
581
791
  mProvider.onCommand(LocationProvider.CMD_SWITCH_MODE,
582
792
  LocationProvider.FOREGROUND_MODE);
583
793
  }
584
- // Android 14+ (API 34): type must match the merged manifest. If we get 0, do not start (avoid crash from invented type).
794
+ // Android 14+ (API 34): type must match the merged manifest. Read it dynamically; if unknown, do not start.
585
795
  if (Build.VERSION.SDK_INT >= 34) {
586
- super.startForeground(NOTIFICATION_ID, notification, 0x8);
796
+ int type = getManifestForegroundServiceType();
797
+ if (type == 0) {
798
+ logger.error("Cannot start foreground: manifest foregroundServiceType missing or unreadable");
799
+ return;
800
+ }
801
+ super.startForeground(NOTIFICATION_ID, notification, type);
587
802
  } else {
588
803
  super.startForeground(NOTIFICATION_ID, notification);
589
804
  }
@@ -724,10 +939,49 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
724
939
  } else {
725
940
  mProvider.onConfigure(mConfig);
726
941
  }
942
+
943
+ // v4.1: re-evaluate hot-reload features when config changes while service is running.
944
+ if (sIsRunning) {
945
+ Integer prevHb = currentConfig.getHeartbeatInterval();
946
+ Integer newHb = mConfig.getHeartbeatInterval();
947
+ if (prevHb == null) prevHb = 0;
948
+ if (newHb == null) newHb = 0;
949
+ if (!prevHb.equals(newHb)) {
950
+ scheduleHeartbeat(); // cancels and reschedules with the new interval (or stops if 0)
951
+ }
952
+ // Driver-insights detector: rebuild if the config dict changed.
953
+ Config.DrivingEventsOptions prevDe = currentConfig.getDrivingEvents();
954
+ Config.DrivingEventsOptions newDe = mConfig.getDrivingEvents();
955
+ if (!equalsDrivingEvents(prevDe, newDe)) {
956
+ configureDrivingDetector();
957
+ }
958
+ }
727
959
  }
728
960
  });
729
961
  }
730
962
 
963
+ /** Shallow value equality for DrivingEventsOptions; avoids needless detector rebuilds. */
964
+ private static boolean equalsDrivingEvents(Config.DrivingEventsOptions a, Config.DrivingEventsOptions b) {
965
+ if (a == b) return true;
966
+ if (a == null || b == null) return false;
967
+ return a.enabled == b.enabled
968
+ && a.speedLimitKmh == b.speedLimitKmh
969
+ && a.minMovingSpeedMps == b.minMovingSpeedMps
970
+ && a.stoppedDurationMs == b.stoppedDurationMs
971
+ && a.minTripSpeedMps == b.minTripSpeedMps
972
+ && a.minTripDurationMs == b.minTripDurationMs
973
+ && a.hardBrakeMps2 == b.hardBrakeMps2
974
+ && a.rapidAccelMps2 == b.rapidAccelMps2
975
+ && a.sharpTurnDegPerSec == b.sharpTurnDegPerSec
976
+ && a.crashImpactKmh == b.crashImpactKmh
977
+ && a.crashWindowMs == b.crashWindowMs
978
+ && a.sensorFusion == b.sensorFusion
979
+ && a.crashImpactG == b.crashImpactG
980
+ && a.sensorCrashCooldownMs == b.sensorCrashCooldownMs
981
+ && a.phoneUsageWindowMs == b.phoneUsageWindowMs
982
+ && a.phoneUsageCooldownMs == b.phoneUsageCooldownMs;
983
+ }
984
+
731
985
  @Override
732
986
  public synchronized void registerHeadlessTask(String taskRunnerClass) {
733
987
  logger.debug("Registering headless task");
@@ -769,6 +1023,17 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
769
1023
  @Override
770
1024
  public void onLocation(BackgroundLocation location) {
771
1025
  mLastLocationTime = System.currentTimeMillis();
1026
+ mLastReceivedLocation = location;
1027
+ sLastReceivedLocation = location;
1028
+
1029
+ // v4.0 Phase 6: feed the driver-insights state machine.
1030
+ if (mDrivingDetector != null) {
1031
+ mDrivingDetector.onLocation(location);
1032
+ }
1033
+ // v4.2 Phase 8: keep sensor pipeline aware of the latest fix.
1034
+ if (mSensorFusion != null) {
1035
+ mSensorFusion.setLastLocation(location);
1036
+ }
772
1037
  if (Boolean.TRUE.equals(mConfig != null ? mConfig.getShowDistance() : null)) {
773
1038
  double lat = location.getLatitude();
774
1039
  double lon = location.getLongitude();
@@ -889,6 +1154,40 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
889
1154
  LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
890
1155
  }
891
1156
 
1157
+ /** v3.5 Phase 4: schedule periodic heartbeat broadcasts using {@link Config#getHeartbeatInterval()}. */
1158
+ private void scheduleHeartbeat() {
1159
+ cancelHeartbeat();
1160
+ if (mConfig == null) return;
1161
+ Integer interval = mConfig.getHeartbeatInterval();
1162
+ if (interval == null || interval <= 0) return;
1163
+ logger.debug("Scheduling heartbeat every {} ms", interval);
1164
+ mHeartbeatExecutor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
1165
+ mHeartbeatTask = mHeartbeatExecutor.scheduleAtFixedRate(new Runnable() {
1166
+ @Override public void run() {
1167
+ try {
1168
+ Bundle b = new Bundle();
1169
+ b.putInt("action", MSG_ON_HEARTBEAT);
1170
+ BackgroundLocation last = mLastReceivedLocation;
1171
+ if (last != null) b.putParcelable("payload", last);
1172
+ broadcastMessage(b);
1173
+ } catch (Throwable t) {
1174
+ logger.warn("Heartbeat tick failed: {}", t.getMessage());
1175
+ }
1176
+ }
1177
+ }, interval, interval, java.util.concurrent.TimeUnit.MILLISECONDS);
1178
+ }
1179
+
1180
+ private void cancelHeartbeat() {
1181
+ if (mHeartbeatTask != null) {
1182
+ mHeartbeatTask.cancel(false);
1183
+ mHeartbeatTask = null;
1184
+ }
1185
+ if (mHeartbeatExecutor != null) {
1186
+ mHeartbeatExecutor.shutdownNow();
1187
+ mHeartbeatExecutor = null;
1188
+ }
1189
+ }
1190
+
892
1191
  @Override
893
1192
  public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
894
1193
  return super.registerReceiver(receiver, filter, null, mServiceHandler, RECEIVER_NOT_EXPORTED);
@@ -5,10 +5,12 @@ import android.content.Context;
5
5
  import android.content.Intent;
6
6
  import android.content.pm.PackageManager;
7
7
  import android.os.Build;
8
+ import android.util.Log;
8
9
 
9
10
  import com.marianhello.bgloc.Config;
10
11
 
11
12
  public class LocationServiceProxy implements LocationService, LocationServiceInfo {
13
+ private static final String TAG = LocationServiceProxy.class.getSimpleName();
12
14
  private final Context mContext;
13
15
  private final LocationServiceIntentBuilder mIntentBuilder;
14
16
 
@@ -78,9 +80,19 @@ public class LocationServiceProxy implements LocationService, LocationServiceInf
78
80
  Intent intent = mIntentBuilder.setCommand(CommandId.START_FOREGROUND_SERVICE).build();
79
81
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
80
82
  if (!hasLocationPermission()) {
81
- mContext.startService(intent);
82
- } else {
83
+ // Do NOT fall back to startService(): would create a non-foreground service that crashes
84
+ // on first location update. Caller must request the permission first.
85
+ Log.w(TAG, "Cannot start foreground service: ACCESS_FINE_LOCATION/COARSE_LOCATION not granted");
86
+ return;
87
+ }
88
+ // Note: ACCESS_BACKGROUND_LOCATION is only required when the service is started from
89
+ // background (e.g. BootCompletedReceiver). When called from foreground, the OS allows
90
+ // a location-typed FGS to run with only fine/coarse location and inherit "while-in-use".
91
+ try {
83
92
  mContext.startForegroundService(intent);
93
+ } catch (Exception e) {
94
+ // Android 12+ may throw ForegroundServiceStartNotAllowedException.
95
+ Log.e(TAG, "startForegroundService blocked: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
84
96
  }
85
97
  } else {
86
98
  mContext.startService(intent);
@@ -140,7 +140,12 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
140
140
  }
141
141
  httpHeaders.put("x-batch-id", String.valueOf(batchStartMillis));
142
142
 
143
- if (uploadLocations(file, url, httpHeaders)) {
143
+ // For URL templating in sync mode we can only resolve static queryParams keys; per-location
144
+ // placeholders (like {lat}) cannot apply to a multi-location batch. If the user wants per-location
145
+ // URL substitution they should use httpMode="single" + url= ... (real-time) or syncMode="single".
146
+ String resolvedUrl = com.marianhello.bgloc.http.UrlTemplateResolver.resolve(url, null, config.getQueryParams());
147
+ String syncMethod = config.getSyncHttpMethod();
148
+ if (uploadLocations(file, resolvedUrl, httpHeaders, syncMethod)) {
144
149
  logger.info("Batch sync successful");
145
150
  batchManager.setBatchCompleted(batchStartMillis);
146
151
  if (file.delete()) {
@@ -154,7 +159,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
154
159
  }
155
160
  }
156
161
 
157
- private boolean uploadLocations(File file, String url, HashMap httpHeaders) {
162
+ private boolean uploadLocations(File file, String url, HashMap httpHeaders, String method) {
158
163
  NotificationCompat.Builder builder = null;
159
164
 
160
165
  if (notificationsEnabled) {
@@ -166,8 +171,29 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
166
171
  notificationManager.notify(NOTIFICATION_ID, builder.build());
167
172
  }
168
173
 
174
+ // v3.5 Phase 4: emit syncStart event.
175
+ Bundle syncStart = new Bundle();
176
+ syncStart.putInt("action", LocationServiceImpl.MSG_ON_SYNC_START);
177
+ broadcastMessage(syncStart);
178
+
179
+ // Count locations being uploaded (best-effort, API-21+ safe).
180
+ int locationsAttempted = 0;
181
+ java.io.FileInputStream fis = null;
169
182
  try {
170
- int responseCode = HttpPostService.postJSONFile(url, file, httpHeaders, this);
183
+ fis = new java.io.FileInputStream(file);
184
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
185
+ byte[] buf = new byte[4096];
186
+ int n;
187
+ while ((n = fis.read(buf)) > 0) baos.write(buf, 0, n);
188
+ org.json.JSONArray arr = new org.json.JSONArray(new String(baos.toByteArray(), "UTF-8"));
189
+ locationsAttempted = arr.length();
190
+ } catch (Throwable ignored) { /* best-effort; emit 0 if we cannot read */
191
+ } finally {
192
+ if (fis != null) try { fis.close(); } catch (Exception ignored) {}
193
+ }
194
+
195
+ try {
196
+ int responseCode = HttpPostService.postJSONFile(url, file, httpHeaders, this, method);
171
197
 
172
198
  // All 2xx statuses are okay
173
199
  boolean isStatusOkay = responseCode >= 200 && responseCode < 300;
@@ -198,6 +224,16 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
198
224
 
199
225
  if (!isStatusOkay) {
200
226
  logger.warn("Batch sync failed: server returned HTTP {} (check server logs or sync URL)", responseCode);
227
+ Bundle errBundle = new Bundle();
228
+ errBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_ERROR);
229
+ errBundle.putInt("httpStatus", responseCode);
230
+ errBundle.putString("message", "HTTP " + responseCode);
231
+ broadcastMessage(errBundle);
232
+ } else {
233
+ Bundle okBundle = new Bundle();
234
+ okBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_SUCCESS);
235
+ okBundle.putInt("sent", locationsAttempted);
236
+ broadcastMessage(okBundle);
201
237
  }
202
238
 
203
239
  return isStatusOkay;
@@ -208,6 +244,11 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
208
244
  if (builder != null) {
209
245
  builder.setContentText(currentSyncConfig.getNotificationSyncFailedText() + ": " + errMsg);
210
246
  }
247
+ Bundle errBundle = new Bundle();
248
+ errBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_ERROR);
249
+ errBundle.putInt("httpStatus", 0);
250
+ errBundle.putString("message", errMsg);
251
+ broadcastMessage(errBundle);
211
252
  } finally {
212
253
  logger.info("Syncing endAt: {}", System.currentTimeMillis());
213
254
 
@@ -244,6 +285,12 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements HttpPost
244
285
  builder.setProgress(100, progress, false);
245
286
  notificationManager.notify(NOTIFICATION_ID, builder.build());
246
287
  }
288
+
289
+ // v3.5 Phase 4: forward progress percentage to JS via syncProgress event.
290
+ Bundle progBundle = new Bundle();
291
+ progBundle.putInt("action", LocationServiceImpl.MSG_ON_SYNC_PROGRESS);
292
+ progBundle.putInt("progress", progress);
293
+ broadcastMessage(progBundle);
247
294
  }
248
295
 
249
296
  private void broadcastMessage(Bundle bundle) {
@@ -8,6 +8,3 @@ dependencies {
8
8
  implementation 'com.github.jparkie:promise:1.0.3'
9
9
  }
10
10
 
11
- android {
12
- useLibrary 'org.apache.http.legacy'
13
- }
@@ -14,4 +14,25 @@ export enum BackgroundGeolocationEvents {
14
14
  activity = 'activity',
15
15
  stationary = 'stationary',
16
16
  location = 'location',
17
+ // v3.5+
18
+ heartbeat = 'heartbeat',
19
+ syncStart = 'syncStart',
20
+ syncProgress = 'syncProgress',
21
+ syncSuccess = 'syncSuccess',
22
+ syncError = 'syncError',
23
+ // v4.0
24
+ tripStart = 'tripStart',
25
+ tripEnd = 'tripEnd',
26
+ moving = 'moving',
27
+ stopped = 'stopped',
28
+ speeding = 'speeding',
29
+ providerChange = 'providerChange',
30
+ sos = 'sos',
31
+ // v4.1
32
+ hardBrake = 'hardBrake',
33
+ rapidAcceleration = 'rapidAcceleration',
34
+ sharpTurn = 'sharpTurn',
35
+ possibleCrash = 'possibleCrash',
36
+ // v4.2 sensor fusion
37
+ phoneUsageWhileDriving = 'phoneUsageWhileDriving',
17
38
  }
@@ -83,6 +83,69 @@ export class BackgroundGeolocationService {
83
83
  return this.ensurePlugin().checkStatus(success, fail);
84
84
  }
85
85
 
86
+ /**
87
+ * Extended diagnostics. Returns permissions, battery optimisation state,
88
+ * last fix age, pending sync count, OEM info and (on iOS) precise location /
89
+ * background refresh / low power flags.
90
+ *
91
+ * @since 3.5.0
92
+ */
93
+ getDiagnostics(
94
+ success?: (diagnostics: any) => void,
95
+ fail?: (error: any) => void
96
+ ): Promise<any> {
97
+ return this.ensurePlugin().getDiagnostics(success, fail);
98
+ }
99
+
100
+ /** @since 3.6.0 */
101
+ isIgnoringBatteryOptimizations(
102
+ success?: (whitelisted: boolean) => void,
103
+ fail?: (error: any) => void
104
+ ): Promise<boolean> {
105
+ return this.ensurePlugin().isIgnoringBatteryOptimizations(success, fail);
106
+ }
107
+
108
+ /** @since 3.6.0 */
109
+ requestIgnoreBatteryOptimizations(
110
+ success?: (whitelisted: boolean) => void,
111
+ fail?: (error: any) => void
112
+ ): Promise<boolean> {
113
+ return this.ensurePlugin().requestIgnoreBatteryOptimizations(success, fail);
114
+ }
115
+
116
+ /** @since 3.6.0 */
117
+ openBatterySettings(
118
+ success?: () => void,
119
+ fail?: (error: any) => void
120
+ ): Promise<void> {
121
+ return this.ensurePlugin().openBatterySettings(success, fail);
122
+ }
123
+
124
+ /** @since 3.6.0 */
125
+ openAutoStartSettings(
126
+ success?: (info: { opened: boolean; manufacturer: string; screen: string }) => void,
127
+ fail?: (error: any) => void
128
+ ): Promise<{ opened: boolean; manufacturer: string; screen: string }> {
129
+ return this.ensurePlugin().openAutoStartSettings(success, fail);
130
+ }
131
+
132
+ /** @since 3.6.0 */
133
+ getManufacturerHelp(
134
+ success?: (info: { manufacturer: string; steps: string[] }) => void,
135
+ fail?: (error: any) => void
136
+ ): Promise<{ manufacturer: string; steps: string[] }> {
137
+ return this.ensurePlugin().getManufacturerHelp(success, fail);
138
+ }
139
+
140
+ /** @since 4.0.0 */
141
+ triggerSOS(
142
+ payload?: { [key: string]: any },
143
+ success?: () => void,
144
+ fail?: (error: any) => void
145
+ ): Promise<void> {
146
+ return this.ensurePlugin().triggerSOS(payload, success, fail);
147
+ }
148
+
86
149
  showAppSettings(): Promise<void> {
87
150
  return this.ensurePlugin().showAppSettings();
88
151
  }
@@ -13,5 +13,22 @@ export declare enum BackgroundGeolocationEvents {
13
13
  start = "start",
14
14
  activity = "activity",
15
15
  stationary = "stationary",
16
- location = "location"
16
+ location = "location",
17
+ heartbeat = "heartbeat",
18
+ syncStart = "syncStart",
19
+ syncProgress = "syncProgress",
20
+ syncSuccess = "syncSuccess",
21
+ syncError = "syncError",
22
+ tripStart = "tripStart",
23
+ tripEnd = "tripEnd",
24
+ moving = "moving",
25
+ stopped = "stopped",
26
+ speeding = "speeding",
27
+ providerChange = "providerChange",
28
+ sos = "sos",
29
+ hardBrake = "hardBrake",
30
+ rapidAcceleration = "rapidAcceleration",
31
+ sharpTurn = "sharpTurn",
32
+ possibleCrash = "possibleCrash",
33
+ phoneUsageWhileDriving = "phoneUsageWhileDriving"
17
34
  }