@josuelmm/cordova-background-geolocation 4.2.3 → 4.5.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 (103) hide show
  1. package/.npmignore +11 -0
  2. package/CHANGELOG.md +261 -0
  3. package/README.md +306 -115
  4. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +34 -0
  5. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +61 -1
  6. package/android/common/src/main/AndroidManifest.xml +1 -1
  7. package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +20 -3
  8. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +87 -1
  9. package/android/common/src/main/java/com/marianhello/bgloc/data/BackgroundLocation.java +94 -0
  10. package/android/common/src/main/java/com/marianhello/bgloc/data/ConfigJsonMapper.java +211 -0
  11. package/android/common/src/main/java/com/marianhello/bgloc/data/LocationTemplateFactory.java +6 -0
  12. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +5 -1
  13. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +32 -1
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationContract.java +12 -2
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +33 -2
  16. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +15 -1
  17. package/android/common/src/main/java/com/marianhello/bgloc/provider/AbstractLocationProvider.java +48 -1
  18. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +105 -6
  19. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +336 -250
  20. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +69 -19
  21. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +246 -21
  22. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +5 -2
  23. package/android/common/src/main/java/com/marianhello/bgloc/sync/BatchManager.java +46 -13
  24. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +23 -1
  25. package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.m +208 -70
  26. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +132 -5
  27. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +20 -0
  28. package/ios/common/BackgroundGeolocation/MAURConfig.h +7 -0
  29. package/ios/common/BackgroundGeolocation/MAURConfig.m +37 -2
  30. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +3 -0
  31. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +3 -1
  32. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +10 -1
  33. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +15 -1
  34. package/ios/common/BackgroundGeolocation/MAURLocation.h +12 -0
  35. package/ios/common/BackgroundGeolocation/MAURLocation.m +33 -4
  36. package/ios/common/BackgroundGeolocation/MAURLocationContract.h +4 -0
  37. package/ios/common/BackgroundGeolocation/MAURLocationContract.m +5 -1
  38. package/ios/common/BackgroundGeolocation/MAURLocationManager.m +19 -1
  39. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +9 -0
  40. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +59 -1
  41. package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.m +10 -1
  42. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +54 -4
  43. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +12 -0
  44. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +125 -5
  45. package/package.json +31 -1
  46. package/plugin.xml +3 -10
  47. package/www/BackgroundGeolocation.d.ts +143 -3
  48. package/www/BackgroundGeolocation.js +11 -4
  49. package/CLAUDE.md +0 -56
  50. package/HISTORY.md +0 -871
  51. package/android/CDVBackgroundGeolocation/src/test/java/com/marianhello/ConfigMapperTest.java +0 -220
  52. package/android/common/src/androidTest/java/com/marianhello/bgloc/BackgroundGeolocationFacadeTest.java +0 -45
  53. package/android/common/src/androidTest/java/com/marianhello/bgloc/BatchManagerTest.java +0 -570
  54. package/android/common/src/androidTest/java/com/marianhello/bgloc/ConfigTest.java +0 -76
  55. package/android/common/src/androidTest/java/com/marianhello/bgloc/ContentProviderLocationDAOTest.java +0 -437
  56. package/android/common/src/androidTest/java/com/marianhello/bgloc/DBLogReaderTest.java +0 -95
  57. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationContentProviderTest.java +0 -159
  58. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceProxyTest.java +0 -161
  59. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceTest.java +0 -247
  60. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteConfigurationDAOTest.java +0 -200
  61. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOTest.java +0 -457
  62. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOThreadTest.java +0 -96
  63. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteOpenHelperTest.java +0 -225
  64. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestPluginDelegate.java +0 -46
  65. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestResourceResolver.java +0 -14
  66. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/MockLocationProvider.java +0 -50
  67. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/TestLocationProviderFactory.java +0 -17
  68. package/android/common/src/androidTest/java/com/marianhello/bgloc/sqlite/SQLiteOpenHelper10.java +0 -92
  69. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/LocationProviderTestCase.java +0 -107
  70. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/TestConstants.java +0 -5
  71. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ArrayListLocationTemplateTest.java +0 -82
  72. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/BackgroundLocationTest.java +0 -128
  73. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ConfigTest.java +0 -191
  74. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/DBLogReaderTest.java +0 -37
  75. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HashMapLocationTemplateTest.java +0 -216
  76. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HttpPostServiceTest.java +0 -223
  77. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/LocationTemplateFactoryTest.java +0 -50
  78. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/PostLocationTaskTest.java +0 -180
  79. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/TestHelper.java +0 -16
  80. package/ios/common/BackgroundGeolocation/SOMotionDetector/CHANGELOG.md +0 -2
  81. package/ios/common/BackgroundGeolocation/SOMotionDetector/LICENSE +0 -21
  82. package/ios/common/BackgroundGeolocation/SOMotionDetector/README.md +0 -135
  83. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOLocationManager.h +0 -80
  84. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOLocationManager.m +0 -147
  85. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionActivity.h +0 -30
  86. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionActivity.m +0 -42
  87. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionDetector.h +0 -99
  88. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionDetector.m +0 -327
  89. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOStepDetector.h +0 -44
  90. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOStepDetector.m +0 -94
  91. package/ios/common/BackgroundGeolocationTests/Info.plist +0 -24
  92. package/ios/common/BackgroundGeolocationTests/MAURBackgroundLocationTest.m +0 -185
  93. package/ios/common/BackgroundGeolocationTests/MAURConfigTest.m +0 -161
  94. package/ios/common/BackgroundGeolocationTests/MAURGeolocationOpenHelperTest.m +0 -102
  95. package/ios/common/BackgroundGeolocationTests/MAURLocationTest.m +0 -216
  96. package/ios/common/BackgroundGeolocationTests/MAURLocationUploaderTest.m +0 -55
  97. package/ios/common/BackgroundGeolocationTests/MAURLogReaderTest.m +0 -43
  98. package/ios/common/BackgroundGeolocationTests/MAURSQLiteConfigurationDAOTest.m +0 -102
  99. package/ios/common/BackgroundGeolocationTests/MAURSQLiteHelperTest.m +0 -41
  100. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOTests.m +0 -240
  101. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOThreadTest.m +0 -84
  102. package/ios/common/BackgroundGeolocationTests/MAURSQLiteOpenHelperTest.m +0 -144
  103. package/ios/common/scripts/xcode-refactor.js +0 -184
@@ -8,9 +8,14 @@ import android.os.Bundle;
8
8
 
9
9
  import com.marianhello.bgloc.Config;
10
10
 
11
+ import java.util.ArrayList;
12
+ import java.util.List;
13
+
11
14
  public class RawLocationProvider extends AbstractLocationProvider implements LocationListener {
12
15
  private LocationManager locationManager;
13
16
  private boolean isStarted = false;
17
+ // v4.5.2: providers we actively subscribed to (so we can unsubscribe cleanly).
18
+ private final List<String> activeProviders = new ArrayList<>(2);
14
19
 
15
20
  public RawLocationProvider(Context context) {
16
21
  super(context);
@@ -37,31 +42,67 @@ public class RawLocationProvider extends AbstractLocationProvider implements Loc
37
42
  logger.warn("RawLocationProvider started without config");
38
43
  return;
39
44
  }
40
- // v3.4 Phase 3: explicit GPS/Network selection. Drops the deprecated Criteria + getBestProvider.
41
- String provider = pickProvider();
42
- if (provider == null) {
45
+ // v4.5.2: honor desiredAccuracy and subscribe to all suitable providers
46
+ // simultaneously (GPS + Network when available). Previously RAW only
47
+ // used GPS-or-Network and ignored desiredAccuracy.
48
+ List<String> providers = pickProviders();
49
+ if (providers.isEmpty()) {
43
50
  logger.warn("No location provider available (GPS and Network disabled)");
44
51
  return;
45
52
  }
46
- try {
47
- logger.info("Requesting location updates from provider {}", provider);
48
- locationManager.requestLocationUpdates(provider, mConfig.getInterval(), mConfig.getDistanceFilter(), this);
49
- isStarted = true;
50
- } catch (SecurityException e) {
51
- logger.error("Security exception: {}", e.getMessage());
52
- this.handleSecurityException(e);
53
+ activeProviders.clear();
54
+ for (String provider : providers) {
55
+ try {
56
+ logger.info("Requesting location updates from provider {}", provider);
57
+ locationManager.requestLocationUpdates(provider, mConfig.getInterval(), mConfig.getDistanceFilter(), this);
58
+ activeProviders.add(provider);
59
+ } catch (SecurityException e) {
60
+ logger.error("Security exception requesting {} updates: {}", provider, e.getMessage());
61
+ this.handleSecurityException(e);
62
+ } catch (IllegalArgumentException e) {
63
+ logger.warn("requestLocationUpdates({}) failed: {}", provider, e.getMessage());
64
+ }
53
65
  }
66
+ isStarted = !activeProviders.isEmpty();
54
67
  }
55
68
 
56
- /** GPS first, fall back to Network. */
57
- private String pickProvider() {
58
- if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
59
- return LocationManager.GPS_PROVIDER;
69
+ /**
70
+ * v4.5.2: choose providers based on desiredAccuracy.
71
+ * <ul>
72
+ * <li>&lt; 1000 m → include GPS when enabled (HIGH / BALANCED)</li>
73
+ * <li>≥ 10 m → include Network when enabled (covers indoor and quick fixes)</li>
74
+ * <li>≥ 1000 m → Network-only (LOW_POWER)</li>
75
+ * </ul>
76
+ * Falls back to whatever is enabled if the preferred set is empty.
77
+ */
78
+ private List<String> pickProviders() {
79
+ List<String> result = new ArrayList<>(2);
80
+ if (locationManager == null) return result;
81
+
82
+ Integer da = mConfig != null ? mConfig.getDesiredAccuracy() : null;
83
+ int desired = (da != null) ? da : 100; // default BALANCED
84
+
85
+ boolean wantGps = desired < 1000;
86
+ boolean wantNet = desired >= 10;
87
+
88
+ boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
89
+ boolean netEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
90
+
91
+ if (wantGps && gpsEnabled) result.add(LocationManager.GPS_PROVIDER);
92
+ if (wantNet && netEnabled) result.add(LocationManager.NETWORK_PROVIDER);
93
+
94
+ // Fallback: at least one of the available providers if our preferred set was empty.
95
+ if (result.isEmpty()) {
96
+ if (gpsEnabled) result.add(LocationManager.GPS_PROVIDER);
97
+ else if (netEnabled) result.add(LocationManager.NETWORK_PROVIDER);
60
98
  }
61
- if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
62
- return LocationManager.NETWORK_PROVIDER;
63
- }
64
- return null;
99
+ return result;
100
+ }
101
+
102
+ /** Backwards-compatible single-provider picker used by onProviderDisabled to check fallback. */
103
+ private String pickProvider() {
104
+ List<String> ps = pickProviders();
105
+ return ps.isEmpty() ? null : ps.get(0);
65
106
  }
66
107
 
67
108
  @Override
@@ -70,11 +111,14 @@ public class RawLocationProvider extends AbstractLocationProvider implements Loc
70
111
  return;
71
112
  }
72
113
  try {
114
+ // v4.5.2: removeUpdates(this) detaches us from every provider we
115
+ // subscribed to via the same LocationListener.
73
116
  locationManager.removeUpdates(this);
74
117
  } catch (SecurityException e) {
75
118
  logger.error("Security exception: {}", e.getMessage());
76
119
  this.handleSecurityException(e);
77
120
  } finally {
121
+ activeProviders.clear();
78
122
  isStarted = false;
79
123
  }
80
124
  }
@@ -113,7 +157,13 @@ public class RawLocationProvider extends AbstractLocationProvider implements Loc
113
157
 
114
158
  @Override
115
159
  public void onProviderDisabled(String provider) {
116
- logger.debug("Provider {} was disabled", provider);
160
+ logger.warn("Provider {} was disabled", provider);
161
+ // v4.5.2: emit SERVICE error when no fallback provider is available so
162
+ // the JS layer can re-prompt the user. Matches DISTANCE_FILTER provider
163
+ // behavior.
164
+ if (locationManager != null && pickProvider() == null) {
165
+ handleServiceError("Location provider '" + provider + "' disabled and no fallback available.");
166
+ }
117
167
  }
118
168
 
119
169
  @Override
@@ -36,6 +36,7 @@ import android.os.Message;
36
36
  import android.os.PowerManager;
37
37
  import android.os.Process;
38
38
  import androidx.annotation.Nullable;
39
+ import androidx.core.content.ContextCompat;
39
40
  import androidx.localbroadcastmanager.content.LocalBroadcastManager;
40
41
 
41
42
  import com.marianhello.bgloc.Config;
@@ -179,6 +180,13 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
179
180
  private com.marianhello.bgloc.sensor.SensorFusionDetector mSensorFusion;
180
181
  /** v4.2 Phase 8: cached tripActive state so hot-reload can re-inject it. */
181
182
  private volatile boolean mDrivingTripActive = false;
183
+ /** v4.3: events fired without a simultaneous fix (providerChange, sensor crash, phone usage,
184
+ * manual SOS) buffered here and flushed onto the next location's `events` array.
185
+ * v4.4.1: capped at PENDING_DRIVING_EVENTS_MAX entries (oldest evicted) and entries older
186
+ * than PENDING_DRIVING_EVENTS_TTL_MS are dropped at flush time. */
187
+ private final org.json.JSONArray mPendingDrivingEvents = new org.json.JSONArray();
188
+ private static final int PENDING_DRIVING_EVENTS_MAX = 20;
189
+ private static final long PENDING_DRIVING_EVENTS_TTL_MS = 60_000L;
182
190
  private static final long WATCHDOG_INTERVAL_MS = 60_000L;
183
191
  private final Handler mMainHandler = new Handler(Looper.getMainLooper());
184
192
  private final Runnable mWatchdogRunnable = new Runnable() {
@@ -188,12 +196,23 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
188
196
  if (!Boolean.TRUE.equals(mConfig.getEnableWatchdog())) return;
189
197
  long now = System.currentTimeMillis();
190
198
  if (mLastLocationTime > 0 && (now - mLastLocationTime) > WATCHDOG_INTERVAL_MS) {
191
- logger.info("Location watchdog: no update in {}s, restarting provider", WATCHDOG_INTERVAL_MS / 1000);
192
- try {
193
- mProvider.onStop();
194
- mProvider.onStart();
195
- } catch (Exception e) {
196
- logger.warn("Watchdog restart failed", e);
199
+ // v4.5.1: when drivingEvents is enabled, treat "no fixes" while NOT tripActive as
200
+ // intentional stationary → don't restart (saves battery). When drivingEvents is
201
+ // disabled (the plugin has no notion of "trip"), keep the legacy behaviour of
202
+ // restarting on every stale window.
203
+ Config.DrivingEventsOptions de = mConfig.getDrivingEvents();
204
+ boolean drivingEnabled = de != null && de.enabled;
205
+ boolean shouldRestart = !drivingEnabled || mDrivingTripActive;
206
+ if (shouldRestart) {
207
+ logger.info("Location watchdog: no update in {}s, restarting provider", WATCHDOG_INTERVAL_MS / 1000);
208
+ try {
209
+ mProvider.onStop();
210
+ mProvider.onStart();
211
+ } catch (Exception e) {
212
+ logger.warn("Watchdog restart failed", e);
213
+ }
214
+ } else {
215
+ logger.debug("Location watchdog: stationary (no active trip); skipping restart");
197
216
  }
198
217
  }
199
218
  mMainHandler.postDelayed(this, WATCHDOG_INTERVAL_MS);
@@ -476,9 +495,12 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
476
495
  mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
477
496
  }
478
497
  }
479
- if (mWakeLock != null && !mWakeLock.isHeld()) {
498
+ // v4.5.1: only hold a permanent CPU wake lock when wakeLockMode == 'always'.
499
+ // Default 'posting' acquires only briefly during onLocation/post; 'none' never.
500
+ String wlMode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
501
+ if ("always".equals(wlMode) && mWakeLock != null && !mWakeLock.isHeld()) {
480
502
  mWakeLock.acquire();
481
- logger.debug("Wake lock acquired");
503
+ logger.debug("Wake lock acquired (mode=always)");
482
504
  }
483
505
 
484
506
  if (Boolean.TRUE.equals(mConfig.getEnableWatchdog())) {
@@ -536,18 +558,21 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
536
558
  mDrivingDetector = new com.marianhello.bgloc.driving.DrivingEventsDetector(
537
559
  new com.marianhello.bgloc.driving.DrivingEventsDetector.Listener() {
538
560
  @Override public void onMoving(BackgroundLocation l) {
561
+ attachDrivingEvent(l, "moving", null);
539
562
  Bundle b = new Bundle();
540
563
  b.putInt("action", MSG_ON_MOVING);
541
564
  if (l != null) b.putParcelable("payload", l);
542
565
  broadcastMessage(b);
543
566
  }
544
567
  @Override public void onStopped(BackgroundLocation l) {
568
+ attachDrivingEvent(l, "stopped", null);
545
569
  Bundle b = new Bundle();
546
570
  b.putInt("action", MSG_ON_STOPPED);
547
571
  if (l != null) b.putParcelable("payload", l);
548
572
  broadcastMessage(b);
549
573
  }
550
574
  @Override public void onTripStart(BackgroundLocation l) {
575
+ attachDrivingEvent(l, "tripStart", null);
551
576
  Bundle b = new Bundle();
552
577
  b.putInt("action", MSG_ON_TRIP_START);
553
578
  if (l != null) b.putParcelable("payload", l);
@@ -556,6 +581,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
556
581
  if (mSensorFusion != null) mSensorFusion.setTripActive(true);
557
582
  }
558
583
  @Override public void onTripEnd(BackgroundLocation l, double distance, long durationMs) {
584
+ org.json.JSONObject extra = new org.json.JSONObject();
585
+ try { extra.put("distance", distance); extra.put("durationMs", durationMs); } catch (org.json.JSONException ignored) {}
586
+ attachDrivingEvent(l, "tripEnd", extra);
559
587
  Bundle b = new Bundle();
560
588
  b.putInt("action", MSG_ON_TRIP_END);
561
589
  if (l != null) b.putParcelable("payload", l);
@@ -566,6 +594,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
566
594
  if (mSensorFusion != null) mSensorFusion.setTripActive(false);
567
595
  }
568
596
  @Override public void onSpeeding(BackgroundLocation l, double speedKmh, double limitKmh) {
597
+ org.json.JSONObject extra = new org.json.JSONObject();
598
+ try { extra.put("speedKmh", speedKmh); extra.put("limitKmh", limitKmh); } catch (org.json.JSONException ignored) {}
599
+ attachDrivingEvent(l, "speeding", extra);
569
600
  Bundle b = new Bundle();
570
601
  b.putInt("action", MSG_ON_SPEEDING);
571
602
  if (l != null) b.putParcelable("payload", l);
@@ -574,12 +605,19 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
574
605
  broadcastMessage(b);
575
606
  }
576
607
  @Override public void onProviderChange(String provider) {
608
+ // No location associated; buffer for next fix.
609
+ org.json.JSONObject ev = new org.json.JSONObject();
610
+ try { ev.put("type", "providerChange"); ev.put("provider", provider != null ? provider : ""); ev.put("time", System.currentTimeMillis()); } catch (org.json.JSONException ignored) {}
611
+ enqueuePendingDrivingEvent(ev);
577
612
  Bundle b = new Bundle();
578
613
  b.putInt("action", MSG_ON_PROVIDER_CHANGE);
579
614
  b.putString("provider", provider != null ? provider : "");
580
615
  broadcastMessage(b);
581
616
  }
582
617
  @Override public void onHardBrake(BackgroundLocation l, double decelMps2) {
618
+ org.json.JSONObject extra = new org.json.JSONObject();
619
+ try { extra.put("value", decelMps2); } catch (org.json.JSONException ignored) {}
620
+ attachDrivingEvent(l, "hardBrake", extra);
583
621
  Bundle b = new Bundle();
584
622
  b.putInt("action", MSG_ON_HARD_BRAKE);
585
623
  if (l != null) b.putParcelable("payload", l);
@@ -587,6 +625,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
587
625
  broadcastMessage(b);
588
626
  }
589
627
  @Override public void onRapidAcceleration(BackgroundLocation l, double accelMps2) {
628
+ org.json.JSONObject extra = new org.json.JSONObject();
629
+ try { extra.put("value", accelMps2); } catch (org.json.JSONException ignored) {}
630
+ attachDrivingEvent(l, "rapidAcceleration", extra);
590
631
  Bundle b = new Bundle();
591
632
  b.putInt("action", MSG_ON_RAPID_ACCELERATION);
592
633
  if (l != null) b.putParcelable("payload", l);
@@ -594,6 +635,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
594
635
  broadcastMessage(b);
595
636
  }
596
637
  @Override public void onSharpTurn(BackgroundLocation l, double degPerSec) {
638
+ org.json.JSONObject extra = new org.json.JSONObject();
639
+ try { extra.put("value", degPerSec); } catch (org.json.JSONException ignored) {}
640
+ attachDrivingEvent(l, "sharpTurn", extra);
597
641
  Bundle b = new Bundle();
598
642
  b.putInt("action", MSG_ON_SHARP_TURN);
599
643
  if (l != null) b.putParcelable("payload", l);
@@ -601,6 +645,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
601
645
  broadcastMessage(b);
602
646
  }
603
647
  @Override public void onPossibleCrash(BackgroundLocation l, double velocityDropKmh) {
648
+ org.json.JSONObject extra = new org.json.JSONObject();
649
+ try { extra.put("value", velocityDropKmh); extra.put("source", "gps"); } catch (org.json.JSONException ignored) {}
650
+ attachDrivingEvent(l, "possibleCrash", extra);
604
651
  Bundle b = new Bundle();
605
652
  b.putInt("action", MSG_ON_POSSIBLE_CRASH);
606
653
  if (l != null) b.putParcelable("payload", l);
@@ -638,6 +685,15 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
638
685
  com.marianhello.bgloc.sensor.SensorFusionDetector.Listener l =
639
686
  new com.marianhello.bgloc.sensor.SensorFusionDetector.Listener() {
640
687
  @Override public void onSensorCrash(BackgroundLocation lastLocation, double impactG) {
688
+ // Buffer for next fix (sensor events fire async to GPS).
689
+ try {
690
+ org.json.JSONObject ev = new org.json.JSONObject();
691
+ ev.put("type", "possibleCrash");
692
+ ev.put("value", impactG);
693
+ ev.put("source", "sensor");
694
+ ev.put("time", System.currentTimeMillis());
695
+ enqueuePendingDrivingEvent(ev);
696
+ } catch (org.json.JSONException ignored) {}
641
697
  Bundle b = new Bundle();
642
698
  b.putInt("action", MSG_ON_POSSIBLE_CRASH);
643
699
  if (lastLocation != null) b.putParcelable("payload", lastLocation);
@@ -646,6 +702,12 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
646
702
  broadcastMessage(b);
647
703
  }
648
704
  @Override public void onPhoneUsageWhileDriving(BackgroundLocation lastLocation) {
705
+ try {
706
+ org.json.JSONObject ev = new org.json.JSONObject();
707
+ ev.put("type", "phoneUsageWhileDriving");
708
+ ev.put("time", System.currentTimeMillis());
709
+ enqueuePendingDrivingEvent(ev);
710
+ } catch (org.json.JSONException ignored) {}
649
711
  Bundle b = new Bundle();
650
712
  b.putInt("action", MSG_ON_PHONE_USAGE_WHILE_DRIVING);
651
713
  if (lastLocation != null) b.putParcelable("payload", lastLocation);
@@ -673,6 +735,89 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
673
735
  if (sIsRunning) mSensorFusion.start();
674
736
  }
675
737
 
738
+ /** v4.3 — append a {type, time, ...extra} entry to the location's events array. */
739
+ private void attachDrivingEvent(BackgroundLocation loc, String type, org.json.JSONObject extra) {
740
+ if (loc == null || type == null) return;
741
+ try {
742
+ org.json.JSONObject ev = new org.json.JSONObject();
743
+ ev.put("type", type);
744
+ ev.put("time", System.currentTimeMillis());
745
+ if (extra != null) {
746
+ java.util.Iterator<String> keys = extra.keys();
747
+ while (keys.hasNext()) {
748
+ String k = keys.next();
749
+ ev.put(k, extra.opt(k));
750
+ }
751
+ }
752
+ loc.addDrivingEvent(ev);
753
+ } catch (org.json.JSONException ignored) {}
754
+ }
755
+
756
+ /** v4.5.1 — acquire a short, time-bounded wake lock when wakeLockMode is 'posting'. */
757
+ private void acquireWakeLockForPosting() {
758
+ if (mWakeLock == null || mConfig == null) return;
759
+ String mode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
760
+ if (!"posting".equals(mode)) return;
761
+ try {
762
+ // Bounded: SQLite write + HTTP POST should finish well within 30 s.
763
+ if (!mWakeLock.isHeld()) mWakeLock.acquire(30_000L);
764
+ } catch (Throwable ignored) { /* best-effort */ }
765
+ }
766
+
767
+ /** v4.4 — read current device battery via sticky broadcast and stamp it onto the location.
768
+ * No permission required. Sticky broadcast returns instantly without blocking.
769
+ * v4.4.1: route through the application context to bypass our own registerReceiver()
770
+ * override (which forces RECEIVER_NOT_EXPORTED + handler — incompatible with sticky-only reads). */
771
+ private void attachBatterySnapshot(BackgroundLocation loc) {
772
+ if (loc == null) return;
773
+ try {
774
+ android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED);
775
+ android.content.Intent batteryStatus = getApplicationContext().registerReceiver(null, filter);
776
+ if (batteryStatus == null) return;
777
+ int level = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
778
+ int scale = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
779
+ if (level >= 0 && scale > 0) {
780
+ loc.setBatteryLevel((int) Math.round(level * 100.0 / scale));
781
+ }
782
+ int status = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_STATUS, -1);
783
+ boolean charging = (status == android.os.BatteryManager.BATTERY_STATUS_CHARGING
784
+ || status == android.os.BatteryManager.BATTERY_STATUS_FULL);
785
+ loc.setCharging(charging);
786
+ } catch (Throwable ignored) { /* best-effort; never fail the fix */ }
787
+ }
788
+
789
+ /** v4.3 — drain pending events (those fired without a simultaneous fix) onto this location.
790
+ * v4.4.1: drop entries older than PENDING_DRIVING_EVENTS_TTL_MS so we don't anexar an event
791
+ * whose context (location, speed, etc.) is no longer relevant. */
792
+ private void flushPendingDrivingEvents(BackgroundLocation loc) {
793
+ if (loc == null) return;
794
+ long now = System.currentTimeMillis();
795
+ synchronized (mPendingDrivingEvents) {
796
+ int n = mPendingDrivingEvents.length();
797
+ if (n == 0) return;
798
+ for (int i = 0; i < n; i++) {
799
+ org.json.JSONObject ev = mPendingDrivingEvents.optJSONObject(i);
800
+ if (ev == null) continue;
801
+ long t = ev.optLong("time", now);
802
+ if (now - t <= PENDING_DRIVING_EVENTS_TTL_MS) {
803
+ loc.addDrivingEvent(ev);
804
+ }
805
+ }
806
+ for (int i = n - 1; i >= 0; i--) mPendingDrivingEvents.remove(i);
807
+ }
808
+ }
809
+
810
+ /** v4.4.1 — append to pending events with cap (oldest evicted). */
811
+ private void enqueuePendingDrivingEvent(org.json.JSONObject ev) {
812
+ if (ev == null) return;
813
+ synchronized (mPendingDrivingEvents) {
814
+ while (mPendingDrivingEvents.length() >= PENDING_DRIVING_EVENTS_MAX) {
815
+ mPendingDrivingEvents.remove(0);
816
+ }
817
+ mPendingDrivingEvents.put(ev);
818
+ }
819
+ }
820
+
676
821
  @Override
677
822
  public synchronized void startForegroundService() {
678
823
  start();
@@ -725,8 +870,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
725
870
  * Required before starting a location foreground service on API 34+.
726
871
  */
727
872
  private boolean hasLocationPermission() {
728
- return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
729
- || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
873
+ // v4.5.1 ContextCompat handles API < 23 (permissions granted at install time).
874
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
875
+ || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
730
876
  }
731
877
 
732
878
  /**
@@ -791,16 +937,32 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
791
937
  mProvider.onCommand(LocationProvider.CMD_SWITCH_MODE,
792
938
  LocationProvider.FOREGROUND_MODE);
793
939
  }
794
- // Android 14+ (API 34): type must match the merged manifest. Read it dynamically; if unknown, do not start.
795
- if (Build.VERSION.SDK_INT >= 34) {
796
- int type = getManifestForegroundServiceType();
797
- if (type == 0) {
798
- logger.error("Cannot start foreground: manifest foregroundServiceType missing or unreadable");
940
+ // Android 14+ (API 34): type is required. Android 12-13 (API 31-33): type accepted (preferred).
941
+ // FOREGROUND_SERVICE_TYPE_LOCATION = 0x00000008. Resolve from merged manifest first;
942
+ // if reflection fails or manifest merge missed the attribute, fall back to LOCATION
943
+ // hardcoded so the FGS still promotes (otherwise: no notification, no background tracking).
944
+ try {
945
+ if (Build.VERSION.SDK_INT >= 30) {
946
+ int type = getManifestForegroundServiceType();
947
+ if (type == 0) {
948
+ // Defensive fallback: every consumer of this plugin requires location FGS.
949
+ // Logging at warn so the failure is visible without breaking the service.
950
+ logger.warn("Manifest foregroundServiceType unreadable; defaulting to LOCATION (0x8). "
951
+ + "Verify merged AndroidManifest has foregroundServiceType=\"location\"." );
952
+ type = 0x00000008; // ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
953
+ }
954
+ super.startForeground(NOTIFICATION_ID, notification, type);
955
+ } else {
956
+ super.startForeground(NOTIFICATION_ID, notification);
957
+ }
958
+ } catch (Throwable t) {
959
+ logger.error("startForeground threw {}; retrying without type", t.getMessage());
960
+ try {
961
+ super.startForeground(NOTIFICATION_ID, notification);
962
+ } catch (Throwable t2) {
963
+ logger.error("startForeground retry failed: {}", t2.getMessage());
799
964
  return;
800
965
  }
801
- super.startForeground(NOTIFICATION_ID, notification, type);
802
- } else {
803
- super.startForeground(NOTIFICATION_ID, notification);
804
966
  }
805
967
  mIsInForeground = true;
806
968
  scheduleNotificationUpdater();
@@ -955,6 +1117,26 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
955
1117
  if (!equalsDrivingEvents(prevDe, newDe)) {
956
1118
  configureDrivingDetector();
957
1119
  }
1120
+ // v4.5.1 — hot-reload wakeLockMode: when transitioning between always /
1121
+ // posting / none, the existing permanent lock (if any) must be released,
1122
+ // or a new permanent lock acquired. Without this, switching mode at runtime
1123
+ // either leaked CPU or left the service running without the requested lock.
1124
+ String prevWl = currentConfig.getWakeLockMode() != null ? currentConfig.getWakeLockMode() : "posting";
1125
+ String newWl = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
1126
+ if (!prevWl.equals(newWl) && mWakeLock != null) {
1127
+ if ("always".equals(newWl)) {
1128
+ if (!mWakeLock.isHeld()) {
1129
+ try { mWakeLock.acquire(); logger.debug("Wake lock acquired (hot-reload → always)"); }
1130
+ catch (Throwable t) { logger.warn("Wake lock acquire failed", t); }
1131
+ }
1132
+ } else {
1133
+ // 'posting' or 'none' — release any permanent lock; per-fix lock continues to work via acquireWakeLockForPosting().
1134
+ if (mWakeLock.isHeld()) {
1135
+ try { mWakeLock.release(); logger.debug("Wake lock released (hot-reload → {})", newWl); }
1136
+ catch (Throwable t) { logger.warn("Wake lock release failed", t); }
1137
+ }
1138
+ }
1139
+ }
958
1140
  }
959
1141
  }
960
1142
  });
@@ -1022,15 +1204,19 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1022
1204
 
1023
1205
  @Override
1024
1206
  public void onLocation(BackgroundLocation location) {
1207
+ // v4.5.1: in 'posting' wake-lock mode, hold the CPU briefly so SQLite writes + HTTP
1208
+ // POST finish before the system returns to deep sleep. 30s ceiling — plenty for a fix.
1209
+ acquireWakeLockForPosting();
1025
1210
  mLastLocationTime = System.currentTimeMillis();
1026
1211
  mLastReceivedLocation = location;
1027
1212
  sLastReceivedLocation = location;
1028
1213
 
1029
- // v4.0 Phase 6: feed the driver-insights state machine.
1214
+ // v4.0 Phase 6: feed the driver-insights state machine on the *raw* location so speed/bearing
1215
+ // come straight from the sensors. Listener attaches events to this same instance.
1030
1216
  if (mDrivingDetector != null) {
1031
1217
  mDrivingDetector.onLocation(location);
1032
1218
  }
1033
- // v4.2 Phase 8: keep sensor pipeline aware of the latest fix.
1219
+ // v4.2 Phase 8: keep sensor pipeline aware of the latest raw fix.
1034
1220
  if (mSensorFusion != null) {
1035
1221
  mSensorFusion.setLastLocation(location);
1036
1222
  }
@@ -1056,12 +1242,38 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1056
1242
  }
1057
1243
  logger.debug("New location {}", location.toString());
1058
1244
 
1245
+ // v4.5.1 — events were attached to the RAW location above (so detector heuristics see real
1246
+ // speed/bearing). If transformLocation() returns a NEW instance, we'd lose those events
1247
+ // and the battery snapshot. Solution: copy them across to the transformed instance below.
1248
+ org.json.JSONArray rawEvents = location.getDrivingEvents();
1249
+ Integer rawBatteryLevel = null;
1250
+ Boolean rawIsCharging = null;
1251
+
1059
1252
  location = transformLocation(location);
1060
1253
  if (location == null) {
1061
1254
  logger.debug("Skipping location as requested by the locationTransform");
1062
1255
  return;
1063
1256
  }
1064
1257
 
1258
+ // Re-attach events to the transformed location if the transform produced a new instance.
1259
+ if (rawEvents != null && rawEvents.length() > 0 && location.getDrivingEvents() != rawEvents) {
1260
+ try {
1261
+ org.json.JSONArray copy = new org.json.JSONArray(rawEvents.toString());
1262
+ for (int i = 0; i < copy.length(); i++) {
1263
+ org.json.JSONObject ev = copy.optJSONObject(i);
1264
+ if (ev != null) location.addDrivingEvent(ev);
1265
+ }
1266
+ } catch (org.json.JSONException ignored) {}
1267
+ }
1268
+ // v4.3: drain pending events (providerChange/sensor crash/phone usage) onto the post-transform
1269
+ // instance so they always reach the backend.
1270
+ flushPendingDrivingEvents(location);
1271
+ // v4.4: stamp device battery snapshot onto the *transformed* location so it survives any
1272
+ // user-supplied locationTransform that creates a new instance.
1273
+ if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
1274
+ attachBatterySnapshot(location);
1275
+ }
1276
+
1065
1277
  Bundle bundle = new Bundle();
1066
1278
  bundle.putInt("action", MSG_ON_LOCATION);
1067
1279
  bundle.putParcelable("payload", location);
@@ -1091,6 +1303,11 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1091
1303
  logger.debug("Skipping location as requested by the locationTransform");
1092
1304
  return;
1093
1305
  }
1306
+ // v4.5.1 — same enrichment as regular fixes: drain pending events and stamp battery.
1307
+ flushPendingDrivingEvents(location);
1308
+ if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
1309
+ attachBatterySnapshot(location);
1310
+ }
1094
1311
 
1095
1312
  Bundle bundle = new Bundle();
1096
1313
  bundle.putInt("action", MSG_ON_STATIONARY);
@@ -1190,7 +1407,15 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1190
1407
 
1191
1408
  @Override
1192
1409
  public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
1193
- return super.registerReceiver(receiver, filter, null, mServiceHandler, RECEIVER_NOT_EXPORTED);
1410
+ // v4.5.1 RECEIVER_NOT_EXPORTED flag is required on Android 13+ (API 33) for non-system
1411
+ // broadcasts and the 5-arg overload exists only from API 26. Guard for older OSs.
1412
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1413
+ return super.registerReceiver(receiver, filter, null, mServiceHandler, Context.RECEIVER_NOT_EXPORTED);
1414
+ }
1415
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1416
+ return super.registerReceiver(receiver, filter, null, mServiceHandler);
1417
+ }
1418
+ return super.registerReceiver(receiver, filter);
1194
1419
  }
1195
1420
 
1196
1421
  @Override
@@ -7,6 +7,8 @@ import android.content.pm.PackageManager;
7
7
  import android.os.Build;
8
8
  import android.util.Log;
9
9
 
10
+ import androidx.core.content.ContextCompat;
11
+
10
12
  import com.marianhello.bgloc.Config;
11
13
 
12
14
  public class LocationServiceProxy implements LocationService, LocationServiceInfo {
@@ -100,8 +102,9 @@ public class LocationServiceProxy implements LocationService, LocationServiceInf
100
102
  }
101
103
 
102
104
  private boolean hasLocationPermission() {
103
- return mContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
104
- || mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
105
+ // v4.5.1 ContextCompat handles API < 23 safely.
106
+ return ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
107
+ || ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
105
108
  }
106
109
 
107
110
  @Override