@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
@@ -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.2";
84
93
 
85
94
  private BackgroundGeolocationFacade facade;
86
95
 
@@ -449,11 +458,155 @@ 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
+ try {
523
+ d.put("locationServicesEnabled", facade.locationServicesEnabled());
524
+ } catch (PluginException e) {
525
+ d.put("locationServicesEnabled", JSONObject.NULL);
526
+ }
527
+
528
+ try {
529
+ Config cfg = facade.getConfig();
530
+ if (cfg != null) {
531
+ d.put("startOnBoot", cfg.getStartOnBoot());
532
+ }
533
+ } catch (Exception ignored) { /* config may not be persisted yet */ }
534
+
535
+ try {
536
+ d.put("pendingSyncCount", (int) Math.min(facade.getPendingSyncCount(), Integer.MAX_VALUE));
537
+ } catch (Exception ignored) { /* DAO might not be ready */ }
538
+
539
+ try {
540
+ BackgroundLocation last = facade.getStationaryLocation();
541
+ // Last *received* location is closer to lastBest; we expose stationary as a fallback signal.
542
+ d.put("lastLocationAt", last != null ? last.getTime() : JSONObject.NULL);
543
+ } catch (Exception ignored) {
544
+ d.put("lastLocationAt", JSONObject.NULL);
545
+ }
546
+
547
+ // Permissions
548
+ d.put("fineLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_FINE_LOCATION));
549
+ d.put("coarseLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_COARSE_LOCATION));
550
+ if (android.os.Build.VERSION.SDK_INT >= 29) {
551
+ d.put("backgroundLocationGranted", hasPermission(ctx, android.Manifest.permission.ACCESS_BACKGROUND_LOCATION));
552
+ } else {
553
+ d.put("backgroundLocationGranted", true);
554
+ }
555
+ if (android.os.Build.VERSION.SDK_INT >= 33) {
556
+ d.put("notificationPermissionGranted", hasPermission(ctx, "android.permission.POST_NOTIFICATIONS"));
557
+ } else {
558
+ d.put("notificationPermissionGranted", true);
559
+ }
560
+ if (android.os.Build.VERSION.SDK_INT >= 29) {
561
+ d.put("activityRecognitionGranted", hasPermission(ctx, "android.permission.ACTIVITY_RECOGNITION"));
562
+ } else {
563
+ d.put("activityRecognitionGranted", true);
564
+ }
565
+
566
+ // Battery / OEM
567
+ d.put("batteryOptimizationIgnored", isIgnoringBatteryOptimizations(ctx));
568
+ d.put("manufacturer", android.os.Build.MANUFACTURER != null ? android.os.Build.MANUFACTURER : "");
569
+
570
+ // Foreground service type read from manifest (only meaningful on API 34+; reported as-is otherwise)
571
+ d.put("foregroundServiceType", readForegroundServiceTypeFromManifest(ctx));
572
+
573
+ return d;
574
+ }
575
+
576
+ private static boolean hasPermission(Context ctx, String permission) {
577
+ try {
578
+ return ctx.getPackageManager().checkPermission(permission, ctx.getPackageName())
579
+ == android.content.pm.PackageManager.PERMISSION_GRANTED;
580
+ } catch (Exception e) {
581
+ return false;
582
+ }
583
+ }
584
+
585
+ private static boolean isIgnoringBatteryOptimizations(Context ctx) {
586
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return true;
587
+ try {
588
+ android.os.PowerManager pm = (android.os.PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
589
+ return pm != null && pm.isIgnoringBatteryOptimizations(ctx.getPackageName());
590
+ } catch (Exception e) {
591
+ return false;
592
+ }
593
+ }
594
+
595
+ private static int readForegroundServiceTypeFromManifest(Context ctx) {
596
+ if (android.os.Build.VERSION.SDK_INT < 34) return 0;
597
+ try {
598
+ android.content.ComponentName cn = new android.content.ComponentName(
599
+ ctx, com.marianhello.bgloc.service.LocationServiceImpl.class);
600
+ android.content.pm.ServiceInfo si = ctx.getPackageManager().getServiceInfo(
601
+ cn, android.content.pm.PackageManager.ComponentInfoFlags.of(0));
602
+ java.lang.reflect.Field f = android.content.pm.ServiceInfo.class.getField("foregroundServiceType");
603
+ Object v = f.get(si);
604
+ return (v instanceof Integer) ? (Integer) v : 0;
605
+ } catch (Throwable e) {
606
+ return 0;
607
+ }
608
+ }
609
+
457
610
  /**
458
611
  * Called when the system is about to start resuming a previous activity.
459
612
  *
@@ -693,4 +846,160 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
693
846
  public void onError(PluginException e) {
694
847
  sendError(e);
695
848
  }
849
+
850
+ // v3.5 Phase 4: sync queue events
851
+ @Override
852
+ public void onSyncStart() {
853
+ sendEvent("syncStart");
854
+ }
855
+
856
+ @Override
857
+ public void onSyncSuccess(int locationsSent) {
858
+ try {
859
+ JSONObject payload = new JSONObject();
860
+ payload.put("sent", locationsSent);
861
+ sendEvent("syncSuccess", payload);
862
+ } catch (JSONException e) {
863
+ sendEvent("syncSuccess");
864
+ }
865
+ }
866
+
867
+ @Override
868
+ public void onSyncError(int httpStatus, String message) {
869
+ try {
870
+ JSONObject payload = new JSONObject();
871
+ payload.put("httpStatus", httpStatus);
872
+ payload.put("message", message != null ? message : "");
873
+ sendEvent("syncError", payload);
874
+ } catch (JSONException e) {
875
+ sendEvent("syncError");
876
+ }
877
+ }
878
+
879
+ @Override
880
+ public void onSyncProgress(int progress) {
881
+ sendEvent("syncProgress", Integer.valueOf(progress));
882
+ }
883
+
884
+ @Override
885
+ public void onHeartbeat(BackgroundLocation location) {
886
+ if (location == null) {
887
+ sendEvent("heartbeat");
888
+ return;
889
+ }
890
+ try {
891
+ sendEvent("heartbeat", location.toJSONObjectWithId());
892
+ } catch (JSONException e) {
893
+ sendEvent("heartbeat");
894
+ }
895
+ }
896
+
897
+ // v4.0 Phase 6 — driver-insight events
898
+ @Override
899
+ public void onTripStart(BackgroundLocation location) {
900
+ sendLocationEvent("tripStart", location);
901
+ }
902
+
903
+ @Override
904
+ public void onTripEnd(BackgroundLocation location, double distance, long durationMs) {
905
+ try {
906
+ JSONObject p = new JSONObject();
907
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
908
+ p.put("distance", distance);
909
+ p.put("durationMs", durationMs);
910
+ sendEvent("tripEnd", p);
911
+ } catch (JSONException e) { sendEvent("tripEnd"); }
912
+ }
913
+
914
+ @Override
915
+ public void onMoving(BackgroundLocation location) {
916
+ sendLocationEvent("moving", location);
917
+ }
918
+
919
+ @Override
920
+ public void onStopped(BackgroundLocation location) {
921
+ sendLocationEvent("stopped", location);
922
+ }
923
+
924
+ @Override
925
+ public void onSpeeding(BackgroundLocation location, double speedKmh, double limitKmh) {
926
+ try {
927
+ JSONObject p = new JSONObject();
928
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
929
+ p.put("speedKmh", speedKmh);
930
+ p.put("limitKmh", limitKmh);
931
+ sendEvent("speeding", p);
932
+ } catch (JSONException e) { sendEvent("speeding"); }
933
+ }
934
+
935
+ @Override
936
+ public void onProviderChange(String provider) {
937
+ try {
938
+ JSONObject p = new JSONObject();
939
+ p.put("provider", provider != null ? provider : "");
940
+ sendEvent("providerChange", p);
941
+ } catch (JSONException e) { sendEvent("providerChange"); }
942
+ }
943
+
944
+ @Override
945
+ public void onSOS(BackgroundLocation location, JSONObject userPayload) {
946
+ try {
947
+ JSONObject p = userPayload != null ? new JSONObject(userPayload.toString()) : new JSONObject();
948
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
949
+ sendEvent("sos", p);
950
+ } catch (JSONException e) { sendEvent("sos"); }
951
+ }
952
+
953
+ private void sendLocationEvent(String name, BackgroundLocation location) {
954
+ if (location == null) { sendEvent(name); return; }
955
+ try { sendEvent(name, location.toJSONObjectWithId()); }
956
+ catch (JSONException e) { sendEvent(name); }
957
+ }
958
+
959
+ // v4.1 GPS-derived sensor-like events
960
+ @Override
961
+ public void onHardBrake(BackgroundLocation location, double decelMps2) {
962
+ sendDrivingEvent("hardBrake", location, decelMps2);
963
+ }
964
+ @Override
965
+ public void onRapidAcceleration(BackgroundLocation location, double accelMps2) {
966
+ sendDrivingEvent("rapidAcceleration", location, accelMps2);
967
+ }
968
+ @Override
969
+ public void onSharpTurn(BackgroundLocation location, double degPerSec) {
970
+ sendDrivingEvent("sharpTurn", location, degPerSec);
971
+ }
972
+ @Override
973
+ public void onPossibleCrash(BackgroundLocation location, double velocityDropKmh) {
974
+ sendDrivingEvent("possibleCrash", location, velocityDropKmh);
975
+ }
976
+
977
+ // v4.2 sensor fusion: enriched possibleCrash with `source` ("gps"|"sensor") and phone-usage event.
978
+ @Override
979
+ public void onPossibleCrash(BackgroundLocation location, double value, String source) {
980
+ try {
981
+ JSONObject p = new JSONObject();
982
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
983
+ p.put("value", value);
984
+ p.put("source", source != null ? source : "gps");
985
+ sendEvent("possibleCrash", p);
986
+ } catch (JSONException e) {
987
+ sendEvent("possibleCrash");
988
+ }
989
+ }
990
+ @Override
991
+ public void onPhoneUsageWhileDriving(BackgroundLocation location) {
992
+ sendLocationEvent("phoneUsageWhileDriving", location);
993
+ }
994
+
995
+ private void sendDrivingEvent(String name, BackgroundLocation location, double value) {
996
+ try {
997
+ JSONObject p = new JSONObject();
998
+ p.put("location", location != null ? location.toJSONObjectWithId() : JSONObject.NULL);
999
+ p.put("value", value);
1000
+ sendEvent(name, p);
1001
+ } catch (JSONException e) {
1002
+ sendEvent(name);
1003
+ }
1004
+ }
696
1005
  }
@@ -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
  }