@josuelmm/cordova-background-geolocation 3.2.0 → 4.2.0

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 +284 -0
  3. package/CLAUDE.md +56 -0
  4. package/HISTORY.md +118 -0
  5. package/README.md +183 -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 +306 -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 +35 -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 +36 -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
@@ -21,6 +21,7 @@ import com.marianhello.bgloc.PluginDelegate;
21
21
  import com.marianhello.bgloc.PluginException;
22
22
  import com.marianhello.bgloc.cordova.ConfigMapper;
23
23
  import com.marianhello.bgloc.cordova.PluginRegistry;
24
+ import com.marianhello.bgloc.oem.BatteryOemHelper;
24
25
  import com.marianhello.bgloc.cordova.headless.JsEvaluatorTaskRunner;
25
26
  import com.marianhello.bgloc.data.BackgroundActivity;
26
27
  import com.marianhello.bgloc.data.BackgroundLocation;
@@ -78,9 +79,17 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
78
79
  public static final String ACTION_CLEAR_SESSION = "clearSession";
79
80
  public static final String ACTION_GET_SESSION_LOCATIONS_COUNT = "getSessionLocationsCount";
80
81
  public static final String ACTION_GET_PLUGIN_VERSION = "getPluginVersion";
82
+ public static final String ACTION_GET_DIAGNOSTICS = "getDiagnostics";
83
+ // v3.6 Phase 5
84
+ public static final String ACTION_IS_IGNORING_BATTERY_OPT = "isIgnoringBatteryOptimizations";
85
+ public static final String ACTION_REQUEST_IGNORE_BATTERY_OPT = "requestIgnoreBatteryOptimizations";
86
+ public static final String ACTION_OPEN_BATTERY_SETTINGS = "openBatterySettings";
87
+ public static final String ACTION_OPEN_AUTOSTART_SETTINGS = "openAutoStartSettings";
88
+ public static final String ACTION_GET_MANUFACTURER_HELP = "getManufacturerHelp";
89
+ public static final String ACTION_TRIGGER_SOS = "triggerSOS";
81
90
 
82
91
  /** Plugin version; keep in sync with plugin.xml. */
83
- public static final String PLUGIN_VERSION = "3.2.0";
92
+ public static final String PLUGIN_VERSION = "4.2.0";
84
93
 
85
94
  private BackgroundGeolocationFacade facade;
86
95
 
@@ -449,11 +458,151 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
449
458
  } else if (ACTION_GET_PLUGIN_VERSION.equals(action)) {
450
459
  callbackContext.success(PLUGIN_VERSION);
451
460
  return true;
461
+ } else if (ACTION_GET_DIAGNOSTICS.equals(action)) {
462
+ runOnWebViewThread(new Runnable() {
463
+ @Override
464
+ public void run() {
465
+ try {
466
+ callbackContext.success(buildDiagnostics());
467
+ } catch (Exception e) {
468
+ callbackContext.sendPluginResult(ErrorPluginResult.from("getDiagnostics failed", e, PluginException.SERVICE_ERROR));
469
+ }
470
+ }
471
+ });
472
+ return true;
473
+ } else if (ACTION_IS_IGNORING_BATTERY_OPT.equals(action)) {
474
+ Context ctx = cordova.getActivity().getApplicationContext();
475
+ callbackContext.success(BatteryOemHelper.isIgnoringBatteryOptimizations(ctx) ? 1 : 0);
476
+ return true;
477
+ } else if (ACTION_REQUEST_IGNORE_BATTERY_OPT.equals(action)) {
478
+ BatteryOemHelper.requestIgnoreBatteryOptimizations(cordova.getActivity());
479
+ // Resolve with the (possibly unchanged) current state; the user accepts the dialog asynchronously.
480
+ Context ctx = cordova.getActivity().getApplicationContext();
481
+ callbackContext.success(BatteryOemHelper.isIgnoringBatteryOptimizations(ctx) ? 1 : 0);
482
+ return true;
483
+ } else if (ACTION_OPEN_BATTERY_SETTINGS.equals(action)) {
484
+ BatteryOemHelper.openBatterySettings(cordova.getActivity());
485
+ callbackContext.success();
486
+ return true;
487
+ } else if (ACTION_OPEN_AUTOSTART_SETTINGS.equals(action)) {
488
+ try {
489
+ callbackContext.success(BatteryOemHelper.openAutoStartSettings(cordova.getActivity()));
490
+ } catch (Exception e) {
491
+ callbackContext.sendPluginResult(ErrorPluginResult.from("openAutoStartSettings failed", e, PluginException.SERVICE_ERROR));
492
+ }
493
+ return true;
494
+ } else if (ACTION_GET_MANUFACTURER_HELP.equals(action)) {
495
+ try {
496
+ callbackContext.success(BatteryOemHelper.getManufacturerHelp());
497
+ } catch (Exception e) {
498
+ callbackContext.sendPluginResult(ErrorPluginResult.from("getManufacturerHelp failed", e, PluginException.SERVICE_ERROR));
499
+ }
500
+ return true;
501
+ } else if (ACTION_TRIGGER_SOS.equals(action)) {
502
+ try {
503
+ JSONObject payload = data.optJSONObject(0);
504
+ facade.triggerSOS(payload);
505
+ callbackContext.success();
506
+ } catch (Exception e) {
507
+ callbackContext.sendPluginResult(ErrorPluginResult.from("triggerSOS failed", e, PluginException.SERVICE_ERROR));
508
+ }
509
+ return true;
452
510
  }
453
511
 
454
512
  return false;
455
513
  }
456
514
 
515
+ /** v3.5 Phase 4: extended diagnostics. */
516
+ private JSONObject buildDiagnostics() throws JSONException {
517
+ JSONObject d = new JSONObject();
518
+ Context ctx = cordova.getActivity().getApplicationContext();
519
+
520
+ // Common
521
+ d.put("isRunning", facade.isRunning());
522
+ d.put("locationServicesEnabled", facade.locationServicesEnabled());
523
+
524
+ try {
525
+ Config cfg = facade.getConfig();
526
+ if (cfg != null) {
527
+ d.put("startOnBoot", cfg.getStartOnBoot());
528
+ }
529
+ } catch (Exception ignored) { /* config may not be persisted yet */ }
530
+
531
+ try {
532
+ d.put("pendingSyncCount", (int) Math.min(facade.getPendingSyncCount(), Integer.MAX_VALUE));
533
+ } catch (Exception ignored) { /* DAO might not be ready */ }
534
+
535
+ try {
536
+ BackgroundLocation last = facade.getStationaryLocation();
537
+ // Last *received* location is closer to lastBest; we expose stationary as a fallback signal.
538
+ d.put("lastLocationAt", last != null ? last.getTime() : JSONObject.NULL);
539
+ } catch (Exception ignored) {
540
+ d.put("lastLocationAt", JSONObject.NULL);
541
+ }
542
+
543
+ // Permissions
544
+ d.put("fineLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_FINE_LOCATION));
545
+ d.put("coarseLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_COARSE_LOCATION));
546
+ if (android.os.Build.VERSION.SDK_INT >= 29) {
547
+ d.put("backgroundLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_BACKGROUND_LOCATION));
548
+ } else {
549
+ d.put("backgroundLocationGranted", true);
550
+ }
551
+ if (android.os.Build.VERSION.SDK_INT >= 33) {
552
+ d.put("notificationPermissionGranted", hasPermission(ctx, "android.permission.POST_NOTIFICATIONS"));
553
+ } else {
554
+ d.put("notificationPermissionGranted", true);
555
+ }
556
+ if (android.os.Build.VERSION.SDK_INT >= 29) {
557
+ d.put("activityRecognitionGranted", hasPermission(ctx, "android.permission.ACTIVITY_RECOGNITION"));
558
+ } else {
559
+ d.put("activityRecognitionGranted", true);
560
+ }
561
+
562
+ // Battery / OEM
563
+ d.put("batteryOptimizationIgnored", isIgnoringBatteryOptimizations(ctx));
564
+ d.put("manufacturer", android.os.Build.MANUFACTURER != null ? android.os.Build.MANUFACTURER : "");
565
+
566
+ // Foreground service type read from manifest (only meaningful on API 34+; reported as-is otherwise)
567
+ d.put("foregroundServiceType", readForegroundServiceTypeFromManifest(ctx));
568
+
569
+ return d;
570
+ }
571
+
572
+ private static boolean hasPermission(Context ctx, String permission) {
573
+ try {
574
+ return ctx.getPackageManager().checkPermission(permission, ctx.getPackageName())
575
+ == android.content.pm.PackageManager.PERMISSION_GRANTED;
576
+ } catch (Exception e) {
577
+ return false;
578
+ }
579
+ }
580
+
581
+ private static boolean isIgnoringBatteryOptimizations(Context ctx) {
582
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return true;
583
+ try {
584
+ android.os.PowerManager pm = (android.os.PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
585
+ return pm != null && pm.isIgnoringBatteryOptimizations(ctx.getPackageName());
586
+ } catch (Exception e) {
587
+ return false;
588
+ }
589
+ }
590
+
591
+ private static int readForegroundServiceTypeFromManifest(Context ctx) {
592
+ if (android.os.Build.VERSION.SDK_INT < 34) return 0;
593
+ try {
594
+ android.content.ComponentName cn = new android.content.ComponentName(
595
+ ctx, com.marianhello.bgloc.service.LocationServiceImpl.class);
596
+ android.content.pm.ServiceInfo si = ctx.getPackageManager().getServiceInfo(
597
+ cn, android.content.pm.PackageManager.ComponentInfoFlags.of(0));
598
+ java.lang.reflect.Field f = android.content.pm.ServiceInfo.class.getField("foregroundServiceType");
599
+ Object v = f.get(si);
600
+ return (v instanceof Integer) ? (Integer) v : 0;
601
+ } catch (Throwable e) {
602
+ return 0;
603
+ }
604
+ }
605
+
457
606
  /**
458
607
  * Called when the system is about to start resuming a previous activity.
459
608
  *
@@ -693,4 +842,160 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
693
842
  public void onError(PluginException e) {
694
843
  sendError(e);
695
844
  }
845
+
846
+ // v3.5 Phase 4: sync queue events
847
+ @Override
848
+ public void onSyncStart() {
849
+ sendEvent("syncStart");
850
+ }
851
+
852
+ @Override
853
+ public void onSyncSuccess(int locationsSent) {
854
+ try {
855
+ JSONObject payload = new JSONObject();
856
+ payload.put("sent", locationsSent);
857
+ sendEvent("syncSuccess", payload);
858
+ } catch (JSONException e) {
859
+ sendEvent("syncSuccess");
860
+ }
861
+ }
862
+
863
+ @Override
864
+ public void onSyncError(int httpStatus, String message) {
865
+ try {
866
+ JSONObject payload = new JSONObject();
867
+ payload.put("httpStatus", httpStatus);
868
+ payload.put("message", message != null ? message : "");
869
+ sendEvent("syncError", payload);
870
+ } catch (JSONException e) {
871
+ sendEvent("syncError");
872
+ }
873
+ }
874
+
875
+ @Override
876
+ public void onSyncProgress(int progress) {
877
+ sendEvent("syncProgress", Integer.valueOf(progress));
878
+ }
879
+
880
+ @Override
881
+ public void onHeartbeat(BackgroundLocation location) {
882
+ if (location == null) {
883
+ sendEvent("heartbeat");
884
+ return;
885
+ }
886
+ try {
887
+ sendEvent("heartbeat", location.toJSONObjectWithId());
888
+ } catch (JSONException e) {
889
+ sendEvent("heartbeat");
890
+ }
891
+ }
892
+
893
+ // v4.0 Phase 6 — driver-insight events
894
+ @Override
895
+ public void onTripStart(BackgroundLocation location) {
896
+ sendLocationEvent("tripStart", location);
897
+ }
898
+
899
+ @Override
900
+ public void onTripEnd(BackgroundLocation location, double distance, long durationMs) {
901
+ try {
902
+ JSONObject p = new JSONObject();
903
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
904
+ p.put("distance", distance);
905
+ p.put("durationMs", durationMs);
906
+ sendEvent("tripEnd", p);
907
+ } catch (JSONException e) { sendEvent("tripEnd"); }
908
+ }
909
+
910
+ @Override
911
+ public void onMoving(BackgroundLocation location) {
912
+ sendLocationEvent("moving", location);
913
+ }
914
+
915
+ @Override
916
+ public void onStopped(BackgroundLocation location) {
917
+ sendLocationEvent("stopped", location);
918
+ }
919
+
920
+ @Override
921
+ public void onSpeeding(BackgroundLocation location, double speedKmh, double limitKmh) {
922
+ try {
923
+ JSONObject p = new JSONObject();
924
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
925
+ p.put("speedKmh", speedKmh);
926
+ p.put("limitKmh", limitKmh);
927
+ sendEvent("speeding", p);
928
+ } catch (JSONException e) { sendEvent("speeding"); }
929
+ }
930
+
931
+ @Override
932
+ public void onProviderChange(String provider) {
933
+ try {
934
+ JSONObject p = new JSONObject();
935
+ p.put("provider", provider != null ? provider : "");
936
+ sendEvent("providerChange", p);
937
+ } catch (JSONException e) { sendEvent("providerChange"); }
938
+ }
939
+
940
+ @Override
941
+ public void onSOS(BackgroundLocation location, JSONObject userPayload) {
942
+ try {
943
+ JSONObject p = userPayload != null ? new JSONObject(userPayload.toString()) : new JSONObject();
944
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
945
+ sendEvent("sos", p);
946
+ } catch (JSONException e) { sendEvent("sos"); }
947
+ }
948
+
949
+ private void sendLocationEvent(String name, BackgroundLocation location) {
950
+ if (location == null) { sendEvent(name); return; }
951
+ try { sendEvent(name, location.toJSONObjectWithId()); }
952
+ catch (JSONException e) { sendEvent(name); }
953
+ }
954
+
955
+ // v4.1 GPS-derived sensor-like events
956
+ @Override
957
+ public void onHardBrake(BackgroundLocation location, double decelMps2) {
958
+ sendDrivingEvent("hardBrake", location, decelMps2);
959
+ }
960
+ @Override
961
+ public void onRapidAcceleration(BackgroundLocation location, double accelMps2) {
962
+ sendDrivingEvent("rapidAcceleration", location, accelMps2);
963
+ }
964
+ @Override
965
+ public void onSharpTurn(BackgroundLocation location, double degPerSec) {
966
+ sendDrivingEvent("sharpTurn", location, degPerSec);
967
+ }
968
+ @Override
969
+ public void onPossibleCrash(BackgroundLocation location, double velocityDropKmh) {
970
+ sendDrivingEvent("possibleCrash", location, velocityDropKmh);
971
+ }
972
+
973
+ // v4.2 sensor fusion: enriched possibleCrash with `source` ("gps"|"sensor") and phone-usage event.
974
+ @Override
975
+ public void onPossibleCrash(BackgroundLocation location, double value, String source) {
976
+ try {
977
+ JSONObject p = new JSONObject();
978
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
979
+ p.put("value", value);
980
+ p.put("source", source != null ? source : "gps");
981
+ sendEvent("possibleCrash", p);
982
+ } catch (JSONException e) {
983
+ sendEvent("possibleCrash");
984
+ }
985
+ }
986
+ @Override
987
+ public void onPhoneUsageWhileDriving(BackgroundLocation location) {
988
+ sendLocationEvent("phoneUsageWhileDriving", location);
989
+ }
990
+
991
+ private void sendDrivingEvent(String name, BackgroundLocation location, double value) {
992
+ try {
993
+ JSONObject p = new JSONObject();
994
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
995
+ p.put("value", value);
996
+ sendEvent(name, p);
997
+ } catch (JSONException e) {
998
+ sendEvent(name);
999
+ }
1000
+ }
696
1001
  }
@@ -174,6 +174,117 @@ public class BackgroundGeolocationFacade {
174
174
 
175
175
  return;
176
176
  }
177
+
178
+ case LocationServiceImpl.MSG_ON_SYNC_START: {
179
+ logger.debug("Received MSG_ON_SYNC_START");
180
+ if (mDelegate != null) mDelegate.onSyncStart();
181
+ return;
182
+ }
183
+
184
+ case LocationServiceImpl.MSG_ON_SYNC_SUCCESS: {
185
+ int sent = bundle != null ? bundle.getInt("sent", 0) : 0;
186
+ logger.debug("Received MSG_ON_SYNC_SUCCESS sent={}", sent);
187
+ if (mDelegate != null) mDelegate.onSyncSuccess(sent);
188
+ return;
189
+ }
190
+
191
+ case LocationServiceImpl.MSG_ON_SYNC_ERROR: {
192
+ int status = bundle != null ? bundle.getInt("httpStatus", 0) : 0;
193
+ String msg = bundle != null ? bundle.getString("message", "") : "";
194
+ logger.debug("Received MSG_ON_SYNC_ERROR status={} message={}", status, msg);
195
+ if (mDelegate != null) mDelegate.onSyncError(status, msg);
196
+ return;
197
+ }
198
+
199
+ case LocationServiceImpl.MSG_ON_SYNC_PROGRESS: {
200
+ int progress = bundle != null ? bundle.getInt("progress", 0) : 0;
201
+ if (mDelegate != null) mDelegate.onSyncProgress(progress);
202
+ return;
203
+ }
204
+
205
+ case LocationServiceImpl.MSG_ON_HEARTBEAT: {
206
+ if (bundle != null) {
207
+ bundle.setClassLoader(LocationServiceImpl.class.getClassLoader());
208
+ }
209
+ BackgroundLocation hb = bundle != null ? (BackgroundLocation) bundle.getParcelable("payload") : null;
210
+ if (mDelegate != null) mDelegate.onHeartbeat(hb);
211
+ return;
212
+ }
213
+
214
+ // v4.0 Phase 6: driver-insights events
215
+ case LocationServiceImpl.MSG_ON_TRIP_START:
216
+ case LocationServiceImpl.MSG_ON_TRIP_END:
217
+ case LocationServiceImpl.MSG_ON_MOVING:
218
+ case LocationServiceImpl.MSG_ON_STOPPED:
219
+ case LocationServiceImpl.MSG_ON_SPEEDING:
220
+ case LocationServiceImpl.MSG_ON_SOS: {
221
+ if (bundle != null) bundle.setClassLoader(LocationServiceImpl.class.getClassLoader());
222
+ BackgroundLocation loc = bundle != null ? (BackgroundLocation) bundle.getParcelable("payload") : null;
223
+ if (mDelegate == null) return;
224
+ switch (action) {
225
+ case LocationServiceImpl.MSG_ON_TRIP_START:
226
+ mDelegate.onTripStart(loc); break;
227
+ case LocationServiceImpl.MSG_ON_TRIP_END:
228
+ double dist = bundle != null ? bundle.getDouble("distance", 0.0) : 0.0;
229
+ long durMs = bundle != null ? bundle.getLong("durationMs", 0L) : 0L;
230
+ mDelegate.onTripEnd(loc, dist, durMs); break;
231
+ case LocationServiceImpl.MSG_ON_MOVING:
232
+ mDelegate.onMoving(loc); break;
233
+ case LocationServiceImpl.MSG_ON_STOPPED:
234
+ mDelegate.onStopped(loc); break;
235
+ case LocationServiceImpl.MSG_ON_SPEEDING:
236
+ double sKmh = bundle != null ? bundle.getDouble("speedKmh", 0.0) : 0.0;
237
+ double lKmh = bundle != null ? bundle.getDouble("limitKmh", 0.0) : 0.0;
238
+ mDelegate.onSpeeding(loc, sKmh, lKmh); break;
239
+ case LocationServiceImpl.MSG_ON_SOS:
240
+ org.json.JSONObject sosPayload = null;
241
+ if (bundle != null) {
242
+ String s = bundle.getString("sosPayload");
243
+ if (s != null) {
244
+ try { sosPayload = new org.json.JSONObject(s); }
245
+ catch (org.json.JSONException ignored) { sosPayload = new org.json.JSONObject(); }
246
+ }
247
+ }
248
+ mDelegate.onSOS(loc, sosPayload); break;
249
+ }
250
+ return;
251
+ }
252
+
253
+ case LocationServiceImpl.MSG_ON_PROVIDER_CHANGE: {
254
+ String provider = bundle != null ? bundle.getString("provider", "") : "";
255
+ if (mDelegate != null) mDelegate.onProviderChange(provider);
256
+ return;
257
+ }
258
+
259
+ // v4.1 GPS-derived sensor-like events (and v4.2 sensor-driven possibleCrash)
260
+ case LocationServiceImpl.MSG_ON_HARD_BRAKE:
261
+ case LocationServiceImpl.MSG_ON_RAPID_ACCELERATION:
262
+ case LocationServiceImpl.MSG_ON_SHARP_TURN:
263
+ case LocationServiceImpl.MSG_ON_POSSIBLE_CRASH: {
264
+ if (bundle != null) bundle.setClassLoader(LocationServiceImpl.class.getClassLoader());
265
+ BackgroundLocation drvLoc = bundle != null ? (BackgroundLocation) bundle.getParcelable("payload") : null;
266
+ double drvVal = bundle != null ? bundle.getDouble("value", 0.0) : 0.0;
267
+ String drvSrc = bundle != null ? bundle.getString("source", "gps") : "gps";
268
+ if (mDelegate == null) return;
269
+ switch (action) {
270
+ case LocationServiceImpl.MSG_ON_HARD_BRAKE:
271
+ mDelegate.onHardBrake(drvLoc, drvVal); break;
272
+ case LocationServiceImpl.MSG_ON_RAPID_ACCELERATION:
273
+ mDelegate.onRapidAcceleration(drvLoc, drvVal); break;
274
+ case LocationServiceImpl.MSG_ON_SHARP_TURN:
275
+ mDelegate.onSharpTurn(drvLoc, drvVal); break;
276
+ case LocationServiceImpl.MSG_ON_POSSIBLE_CRASH:
277
+ mDelegate.onPossibleCrash(drvLoc, drvVal, drvSrc); break;
278
+ }
279
+ return;
280
+ }
281
+ // v4.2 sensor fusion: phone usage while driving
282
+ case LocationServiceImpl.MSG_ON_PHONE_USAGE_WHILE_DRIVING: {
283
+ if (bundle != null) bundle.setClassLoader(LocationServiceImpl.class.getClassLoader());
284
+ BackgroundLocation puLoc = bundle != null ? (BackgroundLocation) bundle.getParcelable("payload") : null;
285
+ if (mDelegate != null) mDelegate.onPhoneUsageWhileDriving(puLoc);
286
+ return;
287
+ }
177
288
  }
178
289
  }
179
290
  };
@@ -448,6 +559,22 @@ public class BackgroundGeolocationFacade {
448
559
  SyncService.sync(syncAccount, resolver.getAuthority(), true);
449
560
  }
450
561
 
562
+ /**
563
+ * v4.0 Phase 6 — Trigger an SOS event. The plugin emits a single `sos` JS event
564
+ * carrying the latest known location and the user-supplied JSON payload.
565
+ */
566
+ public void triggerSOS(org.json.JSONObject payload) {
567
+ Bundle b = new Bundle();
568
+ b.putInt("action", LocationServiceImpl.MSG_ON_SOS);
569
+ BackgroundLocation last = LocationServiceImpl.getLastReceivedLocation();
570
+ if (last != null) b.putParcelable("payload", last);
571
+ b.putString("sosPayload", payload != null ? payload.toString() : "{}");
572
+ Intent intent = new Intent(LocationServiceImpl.ACTION_BROADCAST);
573
+ intent.putExtras(b);
574
+ androidx.localbroadcastmanager.content.LocalBroadcastManager
575
+ .getInstance(getContext().getApplicationContext()).sendBroadcast(intent);
576
+ }
577
+
451
578
  /**
452
579
  * Returns the number of locations pending to be synced (not yet sent to syncUrl).
453
580
  */
@@ -31,7 +31,8 @@ public class BootCompletedReceiver extends BroadcastReceiver {
31
31
 
32
32
  @Override
33
33
  public void onReceive(Context context, Intent intent) {
34
- Log.d(TAG, "Received boot completed");
34
+ String action = intent != null ? intent.getAction() : null;
35
+ Log.d(TAG, "Received boot/replace broadcast: " + action);
35
36
  ConfigurationDAO dao = DAOFactory.createConfigurationDAO(context);
36
37
  Config config = null;
37
38
 
@@ -43,23 +44,34 @@ public class BootCompletedReceiver extends BroadcastReceiver {
43
44
 
44
45
  if (config == null) { return; }
45
46
 
46
- Log.d(TAG, "Boot completed " + config.toString());
47
+ Log.d(TAG, "Boot/replace handler " + config.toString());
47
48
 
48
- if (config.getStartOnBoot()) {
49
- if (!hasLocationPermission(context)) {
50
- Log.w(TAG, "Skipping start on boot: ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION not granted");
51
- return;
52
- }
53
- Log.i(TAG, "Starting service after boot");
54
- Intent locationServiceIntent = new Intent(context, LocationServiceImpl.class);
55
- locationServiceIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
56
- locationServiceIntent.putExtra("config", config);
49
+ if (!config.getStartOnBoot()) { return; }
50
+
51
+ if (!hasLocationPermission(context)) {
52
+ Log.w(TAG, "Skipping start on boot: ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION not granted");
53
+ return;
54
+ }
55
+ if (Build.VERSION.SDK_INT >= 29 && !hasBackgroundLocationPermission(context)) {
56
+ Log.w(TAG, "Skipping start on boot: ACCESS_BACKGROUND_LOCATION not granted (Android 10+)");
57
+ return;
58
+ }
57
59
 
60
+ Log.i(TAG, "Starting service after boot/replace");
61
+ Intent locationServiceIntent = new Intent(context, LocationServiceImpl.class);
62
+ locationServiceIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
63
+ locationServiceIntent.putExtra("config", config);
64
+
65
+ try {
58
66
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
59
67
  context.startForegroundService(locationServiceIntent);
60
68
  } else {
61
69
  context.startService(locationServiceIntent);
62
70
  }
71
+ } catch (Exception e) {
72
+ // Android 12+ may throw ForegroundServiceStartNotAllowedException.
73
+ // Log and exit; do NOT fall back to a non-foreground service for tracking.
74
+ Log.e(TAG, "Start on boot blocked: " + e.getClass().getSimpleName() + ": " + e.getMessage(), e);
63
75
  }
64
76
  }
65
77
 
@@ -67,4 +79,8 @@ public class BootCompletedReceiver extends BroadcastReceiver {
67
79
  return context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
68
80
  || context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
69
81
  }
82
+
83
+ private static boolean hasBackgroundLocationPermission(Context context) {
84
+ return context.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
85
+ }
70
86
  }