@josuelmm/cordova-background-geolocation 3.1.1 → 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 (61) hide show
  1. package/.npmignore +4 -0
  2. package/CHANGELOG.md +313 -0
  3. package/CLAUDE.md +56 -0
  4. package/HISTORY.md +124 -0
  5. package/README.md +198 -6
  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 +362 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +153 -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 +48 -5
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/SessionLocationDAO.java +18 -0
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +8 -1
  16. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionContract.java +74 -0
  17. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteSessionLocationDAO.java +169 -0
  18. package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
  19. package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
  20. package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
  21. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
  22. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
  23. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
  24. package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
  25. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +310 -7
  26. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
  27. package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
  28. package/android/dependencies.gradle +0 -3
  29. package/angular/background-geolocation-events.ts +21 -0
  30. package/angular/background-geolocation.service.ts +91 -0
  31. package/angular/dist/background-geolocation-events.d.ts +18 -1
  32. package/angular/dist/background-geolocation.service.d.ts +40 -0
  33. package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
  34. package/angular/dist/esm2022/background-geolocation.service.mjs +47 -1
  35. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +67 -0
  36. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
  37. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.h +4 -0
  38. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +352 -1
  39. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +26 -0
  40. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +421 -15
  41. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
  42. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
  43. package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
  44. package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
  45. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
  46. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +12 -3
  47. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
  48. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +102 -44
  49. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
  50. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
  51. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.h +29 -0
  52. package/ios/common/BackgroundGeolocation/MAURSessionLocationContract.m +31 -0
  53. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.h +25 -0
  54. package/ios/common/BackgroundGeolocation/MAURSessionLocationDAO.m +153 -0
  55. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
  56. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
  57. package/package.json +36 -1
  58. package/plugin.xml +26 -8
  59. package/www/BackgroundGeolocation.d.ts +559 -3
  60. package/www/BackgroundGeolocation.js +78 -1
  61. 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;
@@ -73,10 +74,22 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
73
74
  public static final String ACTION_FORCE_SYNC = "forceSync";
74
75
  public static final String ACTION_CLEAR_SYNC = "clearSync";
75
76
  public static final String ACTION_GET_PENDING_SYNC_COUNT = "getPendingSyncCount";
77
+ public static final String ACTION_START_SESSION = "startSession";
78
+ public static final String ACTION_GET_SESSION_LOCATIONS = "getSessionLocations";
79
+ public static final String ACTION_CLEAR_SESSION = "clearSession";
80
+ public static final String ACTION_GET_SESSION_LOCATIONS_COUNT = "getSessionLocationsCount";
76
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";
77
90
 
78
91
  /** Plugin version; keep in sync with plugin.xml. */
79
- public static final String PLUGIN_VERSION = "3.1.0";
92
+ public static final String PLUGIN_VERSION = "4.2.0";
80
93
 
81
94
  private BackgroundGeolocationFacade facade;
82
95
 
@@ -399,14 +412,197 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
399
412
  }
400
413
  });
401
414
  return true;
415
+ } else if (ACTION_START_SESSION.equals(action)) {
416
+ runOnWebViewThread(new Runnable() {
417
+ @Override
418
+ public void run() {
419
+ facade.startSession();
420
+ callbackContext.success();
421
+ }
422
+ });
423
+ return true;
424
+ } else if (ACTION_GET_SESSION_LOCATIONS.equals(action)) {
425
+ runOnWebViewThread(new Runnable() {
426
+ @Override
427
+ public void run() {
428
+ try {
429
+ callbackContext.success(getSessionLocations());
430
+ } catch (JSONException e) {
431
+ callbackContext.sendPluginResult(ErrorPluginResult.from("getSessionLocations failed", e, PluginException.JSON_ERROR));
432
+ }
433
+ }
434
+ });
435
+ return true;
436
+ } else if (ACTION_CLEAR_SESSION.equals(action)) {
437
+ runOnWebViewThread(new Runnable() {
438
+ @Override
439
+ public void run() {
440
+ facade.clearSession();
441
+ callbackContext.success();
442
+ }
443
+ });
444
+ return true;
445
+ } else if (ACTION_GET_SESSION_LOCATIONS_COUNT.equals(action)) {
446
+ runOnWebViewThread(new Runnable() {
447
+ @Override
448
+ public void run() {
449
+ try {
450
+ int count = facade.getSessionLocationsCount();
451
+ callbackContext.success(count);
452
+ } catch (Exception e) {
453
+ callbackContext.sendPluginResult(ErrorPluginResult.from("getSessionLocationsCount failed", e, PluginException.SERVICE_ERROR));
454
+ }
455
+ }
456
+ });
457
+ return true;
402
458
  } else if (ACTION_GET_PLUGIN_VERSION.equals(action)) {
403
459
  callbackContext.success(PLUGIN_VERSION);
404
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;
405
510
  }
406
511
 
407
512
  return false;
408
513
  }
409
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
+
410
606
  /**
411
607
  * Called when the system is about to start resuming a previous activity.
412
608
  *
@@ -557,6 +753,15 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
557
753
  return jsonLocationsArray;
558
754
  }
559
755
 
756
+ private JSONArray getSessionLocations() throws JSONException {
757
+ JSONArray jsonLocationsArray = new JSONArray();
758
+ Collection<BackgroundLocation> locations = facade.getSessionLocations();
759
+ for (BackgroundLocation location : locations) {
760
+ jsonLocationsArray.put(location.toJSONObjectWithId());
761
+ }
762
+ return jsonLocationsArray;
763
+ }
764
+
560
765
  private JSONArray getLogs(Integer limit, int offset, String minLevel) throws Exception {
561
766
  JSONArray jsonLogsArray = new JSONArray();
562
767
  Collection<LogEntry> logEntries = facade.getLogEntries(limit, offset, minLevel);
@@ -637,4 +842,160 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
637
842
  public void onError(PluginException e) {
638
843
  sendError(e);
639
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
+ }
640
1001
  }
@@ -26,11 +26,13 @@ import com.marianhello.bgloc.data.BackgroundLocation;
26
26
  import com.marianhello.bgloc.data.ConfigurationDAO;
27
27
  import com.marianhello.bgloc.data.DAOFactory;
28
28
  import com.marianhello.bgloc.data.LocationDAO;
29
+ import com.marianhello.bgloc.data.SessionLocationDAO;
29
30
  import com.marianhello.bgloc.provider.LocationProvider;
30
31
  import com.marianhello.bgloc.service.LocationService;
31
32
  import com.marianhello.bgloc.service.LocationServiceImpl;
32
33
  import com.marianhello.bgloc.service.LocationServiceProxy;
33
34
  import com.marianhello.bgloc.data.LocationTransform;
35
+ import com.marianhello.bgloc.data.sqlite.SQLiteSessionLocationDAO;
34
36
  import com.marianhello.bgloc.sync.AccountHelper;
35
37
  import com.marianhello.bgloc.sync.NotificationHelper;
36
38
  import com.marianhello.bgloc.sync.SyncService;
@@ -172,6 +174,117 @@ public class BackgroundGeolocationFacade {
172
174
 
173
175
  return;
174
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
+ }
175
288
  }
176
289
  }
177
290
  };
@@ -294,6 +407,30 @@ public class BackgroundGeolocationFacade {
294
407
  return dao.getValidLocationsAndDelete();
295
408
  }
296
409
 
410
+ /** Clear session table and start storing all new locations in session. Call when user starts a route. */
411
+ public void startSession() {
412
+ SessionLocationDAO dao = new SQLiteSessionLocationDAO(getContext());
413
+ dao.startSession();
414
+ }
415
+
416
+ /** Return all locations stored in the current session (ordered by time). */
417
+ public Collection<BackgroundLocation> getSessionLocations() {
418
+ SessionLocationDAO dao = new SQLiteSessionLocationDAO(getContext());
419
+ return dao.getSessionLocations();
420
+ }
421
+
422
+ /** Clear session table and stop storing. Call when route is finished and sync OK. */
423
+ public void clearSession() {
424
+ SessionLocationDAO dao = new SQLiteSessionLocationDAO(getContext());
425
+ dao.clearSession();
426
+ }
427
+
428
+ /** Number of locations in the current session. */
429
+ public int getSessionLocationsCount() {
430
+ SessionLocationDAO dao = new SQLiteSessionLocationDAO(getContext());
431
+ return dao.getSessionLocationsCount();
432
+ }
433
+
297
434
  public BackgroundLocation getStationaryLocation() {
298
435
  return mStationaryLocation;
299
436
  }
@@ -422,6 +559,22 @@ public class BackgroundGeolocationFacade {
422
559
  SyncService.sync(syncAccount, resolver.getAuthority(), true);
423
560
  }
424
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
+
425
578
  /**
426
579
  * Returns the number of locations pending to be synced (not yet sent to syncUrl).
427
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
  }