@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
@@ -14,6 +14,7 @@ package com.tenforwardconsulting.bgloc.cordova;
14
14
  import android.app.Activity;
15
15
  import android.app.Application;
16
16
  import android.content.Context;
17
+ import android.os.Build;
17
18
 
18
19
  import com.marianhello.bgloc.BackgroundGeolocationFacade;
19
20
  import com.marianhello.bgloc.Config;
@@ -87,9 +88,15 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
87
88
  public static final String ACTION_OPEN_AUTOSTART_SETTINGS = "openAutoStartSettings";
88
89
  public static final String ACTION_GET_MANUFACTURER_HELP = "getManufacturerHelp";
89
90
  public static final String ACTION_TRIGGER_SOS = "triggerSOS";
91
+ // v4.5: runtime permission helpers — opt-in. The app drives the flow; the plugin
92
+ // simply asks the OS dialog (or returns the current state on iOS where Apple does
93
+ // not surface separate runtime gates for background location / activity recognition).
94
+ public static final String ACTION_REQUEST_BACKGROUND_PERMISSION = "requestBackgroundLocationPermission";
95
+ public static final String ACTION_REQUEST_ACTIVITY_PERMISSION = "requestActivityRecognitionPermission";
96
+ public static final String ACTION_REQUEST_NOTIFICATION_PERMISSION = "requestNotificationPermission";
90
97
 
91
98
  /** Plugin version; keep in sync with plugin.xml. */
92
- public static final String PLUGIN_VERSION = "4.2.3";
99
+ public static final String PLUGIN_VERSION = "4.5.2";
93
100
 
94
101
  private BackgroundGeolocationFacade facade;
95
102
 
@@ -507,11 +514,64 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
507
514
  callbackContext.sendPluginResult(ErrorPluginResult.from("triggerSOS failed", e, PluginException.SERVICE_ERROR));
508
515
  }
509
516
  return true;
517
+ } else if (ACTION_REQUEST_BACKGROUND_PERMISSION.equals(action)) {
518
+ // Android 10+ (API 29+). Returns {granted: bool}.
519
+ return requestPermissionAction(callbackContext,
520
+ Build.VERSION.SDK_INT >= 29 ? android.Manifest.permission.ACCESS_BACKGROUND_LOCATION : android.Manifest.permission.ACCESS_FINE_LOCATION);
521
+ } else if (ACTION_REQUEST_ACTIVITY_PERMISSION.equals(action)) {
522
+ // Android 10+ (API 29+) needs runtime grant for activity recognition.
523
+ return requestPermissionAction(callbackContext,
524
+ Build.VERSION.SDK_INT >= 29 ? "android.permission.ACTIVITY_RECOGNITION" : null);
525
+ } else if (ACTION_REQUEST_NOTIFICATION_PERMISSION.equals(action)) {
526
+ // Android 13+ (API 33+) requires POST_NOTIFICATIONS at runtime.
527
+ return requestPermissionAction(callbackContext,
528
+ Build.VERSION.SDK_INT >= 33 ? "android.permission.POST_NOTIFICATIONS" : null);
510
529
  }
511
530
 
512
531
  return false;
513
532
  }
514
533
 
534
+ /** v4.5: shared helper — request a single runtime permission via PermissionManager.
535
+ * Returns {granted: true} if already granted (or unsupported on this OS version).
536
+ * Returns {granted: false, denied: [name]} if user denies.
537
+ */
538
+ private boolean requestPermissionAction(final CallbackContext cb, final String permission) {
539
+ if (permission == null) {
540
+ // OS version where this permission does not exist: act as already granted.
541
+ try {
542
+ JSONObject r = new JSONObject();
543
+ r.put("granted", true);
544
+ r.put("notRequired", true);
545
+ cb.success(r);
546
+ } catch (JSONException e) { cb.success(); }
547
+ return true;
548
+ }
549
+ Context ctx = cordova.getActivity().getApplicationContext();
550
+ if (hasPermission(ctx, permission)) {
551
+ try { JSONObject r = new JSONObject(); r.put("granted", true); cb.success(r); }
552
+ catch (JSONException e) { cb.success(); }
553
+ return true;
554
+ }
555
+ com.intentfilter.androidpermissions.PermissionManager pm =
556
+ com.intentfilter.androidpermissions.PermissionManager.getInstance(ctx);
557
+ pm.checkPermissions(java.util.Arrays.asList(permission),
558
+ new com.intentfilter.androidpermissions.PermissionManager.PermissionRequestListener() {
559
+ @Override public void onPermissionGranted() {
560
+ try { JSONObject r = new JSONObject(); r.put("granted", true); cb.success(r); }
561
+ catch (JSONException e) { cb.success(); }
562
+ }
563
+ @Override public void onPermissionDenied(com.intentfilter.androidpermissions.models.DeniedPermissions deniedPermissions) {
564
+ try {
565
+ JSONObject r = new JSONObject();
566
+ r.put("granted", false);
567
+ r.put("denied", new org.json.JSONArray().put(permission));
568
+ cb.success(r);
569
+ } catch (JSONException e) { cb.success(); }
570
+ }
571
+ });
572
+ return true;
573
+ }
574
+
515
575
  /** v3.5 Phase 4: extended diagnostics. */
516
576
  private JSONObject buildDiagnostics() throws JSONException {
517
577
  JSONObject d = new JSONObject();
@@ -58,7 +58,7 @@
58
58
  <uses-library android:name="org.apache.http.legacy" android:required="true" />
59
59
  </application>
60
60
 
61
- <uses-permission android:name="android.hardware.location" />
61
+ <uses-feature android:name="android.hardware.location" android:required="false" />
62
62
  <uses-permission android:name="android.permission.INTERNET"/>
63
63
  <uses-permission android:name="android.permission.WAKE_LOCK" />
64
64
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -17,6 +17,8 @@ import android.content.pm.PackageManager;
17
17
  import android.os.Build;
18
18
  import android.util.Log;
19
19
 
20
+ import androidx.core.content.ContextCompat;
21
+
20
22
  import com.marianhello.bgloc.data.ConfigurationDAO;
21
23
  import com.marianhello.bgloc.data.DAOFactory;
22
24
  import com.marianhello.bgloc.service.LocationServiceImpl;
@@ -32,6 +34,20 @@ public class BootCompletedReceiver extends BroadcastReceiver {
32
34
  @Override
33
35
  public void onReceive(Context context, Intent intent) {
34
36
  String action = intent != null ? intent.getAction() : null;
37
+
38
+ // v4.5.2 — hardening: ignore arbitrary broadcasts directed at this
39
+ // receiver. Without this, any explicit intent (e.g. a malicious app
40
+ // targeting our package) could trigger the service auto-start path.
41
+ // Accept only the canonical boot/package-replaced actions plus the
42
+ // OEM-specific quick-boot variants used by HTC and Samsung.
43
+ if (!Intent.ACTION_BOOT_COMPLETED.equals(action)
44
+ && !"android.intent.action.QUICKBOOT_POWERON".equals(action)
45
+ && !"com.htc.intent.action.QUICKBOOT_POWERON".equals(action)
46
+ && !Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
47
+ Log.w(TAG, "Ignoring unsupported broadcast: " + action);
48
+ return;
49
+ }
50
+
35
51
  Log.d(TAG, "Received boot/replace broadcast: " + action);
36
52
  ConfigurationDAO dao = DAOFactory.createConfigurationDAO(context);
37
53
  Config config = null;
@@ -76,11 +92,12 @@ public class BootCompletedReceiver extends BroadcastReceiver {
76
92
  }
77
93
 
78
94
  private static boolean hasLocationPermission(Context context) {
79
- return context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
80
- || context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
95
+ // v4.5.1 ContextCompat handles API < 23 (always granted at install time).
96
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
97
+ || ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
81
98
  }
82
99
 
83
100
  private static boolean hasBackgroundLocationPermission(Context context) {
84
- return context.checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
101
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED;
85
102
  }
86
103
  }
@@ -83,6 +83,22 @@ public class Config implements Parcelable
83
83
  private String mockLocationPolicy; // allow | flag | drop (default allow)
84
84
  // v4.0 (Phase 6): driver insights
85
85
  private DrivingEventsOptions drivingEvents;
86
+ // v4.4: include device battery in every location payload (default true).
87
+ private Boolean includeBattery;
88
+ // v4.5.1: battery-saving knobs.
89
+ /** WakeLock policy: 'none' | 'posting' | 'always'. Default 'posting'. */
90
+ private String wakeLockMode;
91
+ /** ms before declaring stationary. DistanceFilterLocationProvider default 5*60_000. */
92
+ private Integer stationaryTimeout;
93
+ /** Lazy poll interval while stationary (ms). Default 3*60_000. */
94
+ private Integer stationaryPollInterval;
95
+ /** Aggressive poll interval while stationary (ms). Default 60_000. */
96
+ private Integer stationaryPollFast;
97
+ // v4.5.2 — provider hardening
98
+ /** 0-100. Activity-recognition transitions below this confidence are ignored. Default 50. */
99
+ private Integer activityConfidenceThreshold;
100
+ /** Discard fixes whose `accuracy` (m) is worse than this. `null` (default) disables the filter. */
101
+ private Float maxAcceptedAccuracy;
86
102
 
87
103
  /** v4.0 Phase 6 + v4.1: driver-insights configuration. Plain holder; no Parcelable to keep this class diff small. */
88
104
  public static class DrivingEventsOptions {
@@ -149,6 +165,13 @@ public class Config implements Parcelable
149
165
  this.queryParams = CloneHelper.deepCopy(config.queryParams);
150
166
  this.heartbeatInterval = config.heartbeatInterval;
151
167
  this.mockLocationPolicy = config.mockLocationPolicy;
168
+ this.includeBattery = config.includeBattery;
169
+ this.wakeLockMode = config.wakeLockMode;
170
+ this.stationaryTimeout = config.stationaryTimeout;
171
+ this.stationaryPollInterval = config.stationaryPollInterval;
172
+ this.stationaryPollFast = config.stationaryPollFast;
173
+ this.activityConfidenceThreshold = config.activityConfidenceThreshold;
174
+ this.maxAcceptedAccuracy = config.maxAcceptedAccuracy;
152
175
  if (config.drivingEvents != null) {
153
176
  DrivingEventsOptions de = new DrivingEventsOptions();
154
177
  de.enabled = config.drivingEvents.enabled;
@@ -251,7 +274,19 @@ public class Config implements Parcelable
251
274
  de.phoneUsageCooldownMs = dePhoneUsageCooldown;
252
275
  this.drivingEvents = de;
253
276
  }
254
- Bundle bundle = in.readBundle();
277
+ // v4.4: includeBattery
278
+ setIncludeBattery((Boolean) in.readValue(null));
279
+ // v4.5.1: battery-saving knobs
280
+ setWakeLockMode(in.readString());
281
+ setStationaryTimeout((Integer) in.readValue(null));
282
+ setStationaryPollInterval((Integer) in.readValue(null));
283
+ setStationaryPollFast((Integer) in.readValue(null));
284
+ // v4.5.2 provider hardening
285
+ setActivityConfidenceThreshold((Integer) in.readValue(null));
286
+ setMaxAcceptedAccuracy((Float) in.readValue(null));
287
+ // v4.5.1 — pass the plugin's classloader so getSerializable() can deserialize
288
+ // LocationTemplate / HashMap subclasses across IPC boundaries (e.g. SyncService :sync process).
289
+ Bundle bundle = in.readBundle(Config.class.getClassLoader());
255
290
  setHttpHeaders((HashMap<String, String>) bundle.getSerializable("httpHeaders"));
256
291
  setQueryParams((HashMap<String, String>) bundle.getSerializable("queryParams"));
257
292
  setTemplate((LocationTemplate) bundle.getSerializable(AbstractLocationTemplate.BUNDLE_KEY));
@@ -298,6 +333,13 @@ public class Config implements Parcelable
298
333
  config.queryParams = null;
299
334
  config.heartbeatInterval = 0;
300
335
  config.mockLocationPolicy = "allow";
336
+ config.includeBattery = true; // v4.4: on by default
337
+ config.wakeLockMode = "posting"; // v4.5.1: hold wake lock only while posting/syncing
338
+ config.stationaryTimeout = 5 * 60 * 1000;
339
+ config.stationaryPollInterval = 3 * 60 * 1000;
340
+ config.stationaryPollFast = 60 * 1000;
341
+ config.activityConfidenceThreshold = 50; // v4.5.2: ignore <50% confidence transitions
342
+ config.maxAcceptedAccuracy = null; // v4.5.2: off by default (no JS regression)
301
343
 
302
344
  return config;
303
345
  }
@@ -365,6 +407,16 @@ public class Config implements Parcelable
365
407
  out.writeLong (de != null ? de.phoneUsageWindowMs : 4_000L);
366
408
  out.writeLong (de != null ? de.phoneUsageCooldownMs : 60_000L);
367
409
  out.writeInt (de != null ? 1 : 0);
410
+ // v4.4: includeBattery
411
+ out.writeValue(getIncludeBattery());
412
+ // v4.5.1
413
+ out.writeString(getWakeLockMode());
414
+ out.writeValue(getStationaryTimeout());
415
+ out.writeValue(getStationaryPollInterval());
416
+ out.writeValue(getStationaryPollFast());
417
+ // v4.5.2
418
+ out.writeValue(getActivityConfidenceThreshold());
419
+ out.writeValue(getMaxAcceptedAccuracy());
368
420
  Bundle bundle = new Bundle();
369
421
  bundle.putSerializable("httpHeaders", getHttpHeaders());
370
422
  bundle.putSerializable("queryParams", getQueryParams());
@@ -762,6 +814,28 @@ public class Config implements Parcelable
762
814
  this.enableWatchdog = enableWatchdog;
763
815
  }
764
816
 
817
+ @Nullable
818
+ public Boolean getIncludeBattery() {
819
+ return includeBattery;
820
+ }
821
+
822
+ public void setIncludeBattery(Boolean includeBattery) {
823
+ this.includeBattery = includeBattery;
824
+ }
825
+
826
+ @Nullable public String getWakeLockMode() { return wakeLockMode; }
827
+ public void setWakeLockMode(String mode) { this.wakeLockMode = mode; }
828
+ @Nullable public Integer getStationaryTimeout() { return stationaryTimeout; }
829
+ public void setStationaryTimeout(Integer ms) { this.stationaryTimeout = ms; }
830
+ @Nullable public Integer getStationaryPollInterval() { return stationaryPollInterval; }
831
+ public void setStationaryPollInterval(Integer ms) { this.stationaryPollInterval = ms; }
832
+ @Nullable public Integer getStationaryPollFast() { return stationaryPollFast; }
833
+ public void setStationaryPollFast(Integer ms) { this.stationaryPollFast = ms; }
834
+ @Nullable public Integer getActivityConfidenceThreshold() { return activityConfidenceThreshold; }
835
+ public void setActivityConfidenceThreshold(Integer v) { this.activityConfidenceThreshold = v; }
836
+ @Nullable public Float getMaxAcceptedAccuracy() { return maxAcceptedAccuracy; }
837
+ public void setMaxAcceptedAccuracy(Float v) { this.maxAcceptedAccuracy = v; }
838
+
765
839
  public boolean hasShowTime() {
766
840
  return showTime != null;
767
841
  }
@@ -1056,6 +1130,18 @@ public class Config implements Parcelable
1056
1130
  if (config2.drivingEvents != null) {
1057
1131
  merger.setDrivingEvents(config2.drivingEvents);
1058
1132
  }
1133
+ // v4.4.1 — was missing: configure({includeBattery: false}) was being ignored.
1134
+ if (config2.includeBattery != null) {
1135
+ merger.setIncludeBattery(config2.getIncludeBattery());
1136
+ }
1137
+ // v4.5.1 — battery-saving knobs.
1138
+ if (config2.wakeLockMode != null) merger.setWakeLockMode(config2.wakeLockMode);
1139
+ if (config2.stationaryTimeout != null) merger.setStationaryTimeout(config2.stationaryTimeout);
1140
+ if (config2.stationaryPollInterval != null) merger.setStationaryPollInterval(config2.stationaryPollInterval);
1141
+ if (config2.stationaryPollFast != null) merger.setStationaryPollFast(config2.stationaryPollFast);
1142
+ // v4.5.2
1143
+ if (config2.activityConfidenceThreshold != null) merger.setActivityConfidenceThreshold(config2.activityConfidenceThreshold);
1144
+ if (config2.maxAcceptedAccuracy != null) merger.setMaxAcceptedAccuracy(config2.maxAcceptedAccuracy);
1059
1145
 
1060
1146
  return merger;
1061
1147
  }
@@ -11,6 +11,7 @@ import androidx.core.util.TimeUtils;
11
11
 
12
12
  import com.marianhello.bgloc.data.sqlite.SQLiteLocationContract.LocationEntry;
13
13
 
14
+ import org.json.JSONArray;
14
15
  import org.json.JSONException;
15
16
  import org.json.JSONObject;
16
17
 
@@ -43,6 +44,20 @@ public class BackgroundLocation implements Parcelable {
43
44
  private int status = POST_PENDING;
44
45
  private Bundle extras = null;
45
46
 
47
+ /**
48
+ * v4.3 — Driving events anexados al fix actual.
49
+ * v4.5: ahora se persiste en SQLite (events_json TEXT) y se propaga vía Parcel para
50
+ * sobrevivir a la cola de sync. Si el POST en real-time falla, los eventos llegan al
51
+ * backend cuando la location se sincroniza más tarde.
52
+ */
53
+ private JSONArray drivingEvents;
54
+
55
+ /** v4.4 — Device battery percentage (0-100) at the time of this fix, or null if unknown
56
+ * or {@code includeBattery} is disabled. v4.5: persisted in SQLite + Parcel. */
57
+ private Integer batteryLevel;
58
+ /** v4.4 — Whether the device is charging at the time of this fix. v4.5: persisted. */
59
+ private Boolean isCharging;
60
+
46
61
  private static final long TWO_MINUTES_IN_NANOS = 1000000000L * 60 * 2;
47
62
 
48
63
  public BackgroundLocation() {}
@@ -106,6 +121,12 @@ public class BackgroundLocation implements Parcelable {
106
121
  mockFlags = l.mockFlags;
107
122
  status = l.status;
108
123
  extras = (l.extras == null) ? null : new Bundle(l.extras);
124
+ // v4.5: copy v4.3+ persisted fields
125
+ if (l.drivingEvents != null) {
126
+ try { drivingEvents = new JSONArray(l.drivingEvents.toString()); } catch (JSONException ignored) {}
127
+ }
128
+ batteryLevel = l.batteryLevel;
129
+ isCharging = l.isCharging;
109
130
  }
110
131
 
111
132
  private static BackgroundLocation fromParcel(Parcel in) {
@@ -134,6 +155,13 @@ public class BackgroundLocation implements Parcelable {
134
155
  l.mockFlags = in.readInt();
135
156
  l.status = in.readInt();
136
157
  l.extras = in.readBundle();
158
+ // v4.5: read driving events / battery / charging
159
+ String evJson = in.readString();
160
+ if (evJson != null) {
161
+ try { l.drivingEvents = new JSONArray(evJson); } catch (JSONException ignored) {}
162
+ }
163
+ l.batteryLevel = (Integer) in.readValue(null);
164
+ l.isCharging = (Boolean) in.readValue(null);
137
165
 
138
166
  return l;
139
167
  }
@@ -205,6 +233,18 @@ public class BackgroundLocation implements Parcelable {
205
233
  l.setStatus(c.getInt(c.getColumnIndex(LocationEntry.COLUMN_NAME_STATUS)));
206
234
  l.setLocationId(c.getLong(c.getColumnIndex(LocationEntry._ID)));
207
235
  l.setMockFlags(c.getInt((c.getColumnIndex(LocationEntry.COLUMN_NAME_MOCK_FLAGS))));
236
+ // v4.5: events / battery / charging — guarded for DBs that may have NULL after migration.
237
+ int idxEv = c.getColumnIndex(LocationEntry.COLUMN_NAME_EVENTS_JSON);
238
+ if (idxEv >= 0 && !c.isNull(idxEv)) {
239
+ String s = c.getString(idxEv);
240
+ if (s != null && !s.isEmpty()) {
241
+ try { l.drivingEvents = new JSONArray(s); } catch (JSONException ignored) {}
242
+ }
243
+ }
244
+ int idxBat = c.getColumnIndex(LocationEntry.COLUMN_NAME_BATTERY_LEVEL);
245
+ if (idxBat >= 0 && !c.isNull(idxBat)) l.batteryLevel = c.getInt(idxBat);
246
+ int idxChg = c.getColumnIndex(LocationEntry.COLUMN_NAME_IS_CHARGING);
247
+ if (idxChg >= 0 && !c.isNull(idxChg)) l.isCharging = (c.getInt(idxChg) == 1);
208
248
 
209
249
  return l;
210
250
  }
@@ -239,6 +279,10 @@ public class BackgroundLocation implements Parcelable {
239
279
  dest.writeInt(mockFlags);
240
280
  dest.writeInt(status);
241
281
  dest.writeBundle(extras);
282
+ // v4.5: persist driving events / battery / charging through Parcel.
283
+ dest.writeString(drivingEvents != null ? drivingEvents.toString() : null);
284
+ dest.writeValue(batteryLevel);
285
+ dest.writeValue(isCharging);
242
286
  }
243
287
 
244
288
  public static final Parcelable.Creator<BackgroundLocation> CREATOR
@@ -899,10 +943,37 @@ public class BackgroundLocation implements Parcelable {
899
943
  if (hasRadius) json.put("radius", radius);
900
944
  if (hasIsFromMockProvider()) json.put("isFromMockProvider", isFromMockProvider());
901
945
  if (hasMockLocationsEnabled()) json.put("mockLocationsEnabled", areMockLocationsEnabled());
946
+ // v4.3: driving events anexados a este fix (si los hay).
947
+ if (drivingEvents != null && drivingEvents.length() > 0) {
948
+ json.put("events", drivingEvents);
949
+ }
950
+ // v4.4: device battery snapshot.
951
+ if (batteryLevel != null) json.put("battery", batteryLevel);
952
+ if (isCharging != null) json.put("isCharging", isCharging);
902
953
 
903
954
  return json;
904
955
  }
905
956
 
957
+ // v4.3 — driving event helpers
958
+ /** Append a driving event to this location. The event survives only until the next
959
+ * serialization in real-time POST. NOT persisted in SQLite. */
960
+ public void addDrivingEvent(JSONObject event) {
961
+ if (event == null) return;
962
+ if (drivingEvents == null) drivingEvents = new JSONArray();
963
+ drivingEvents.put(event);
964
+ }
965
+ public JSONArray getDrivingEvents() { return drivingEvents; }
966
+ public boolean hasDrivingEvents() { return drivingEvents != null && drivingEvents.length() > 0; }
967
+ public void clearDrivingEvents() { drivingEvents = null; }
968
+ /** v4.5: bulk setter used by SQLite hydration to restore the persisted events array. */
969
+ public void setDrivingEvents(JSONArray events) { this.drivingEvents = events; }
970
+
971
+ // v4.4 — battery helpers
972
+ public void setBatteryLevel(Integer level) { this.batteryLevel = level; }
973
+ public Integer getBatteryLevel() { return batteryLevel; }
974
+ public void setCharging(Boolean charging) { this.isCharging = charging; }
975
+ public Boolean isCharging() { return isCharging; }
976
+
906
977
  /**
907
978
  * Returns location as JSON object containing location id
908
979
  * Note: Location id is not unique and is usually being recycled when
@@ -942,6 +1013,18 @@ public class BackgroundLocation implements Parcelable {
942
1013
  values.put(LocationEntry.COLUMN_NAME_STATUS, status);
943
1014
  values.put(LocationEntry.COLUMN_NAME_BATCH_START_MILLIS, batchStartMillis);
944
1015
  values.put(LocationEntry.COLUMN_NAME_MOCK_FLAGS, mockFlags);
1016
+ // v4.5.1 — always write these columns (with NULL when absent) so that recycled rows
1017
+ // in ContentProviderLocationDAO's max-rows UPDATE path do not inherit stale events,
1018
+ // battery or charging state from the location previously stored at that _id.
1019
+ if (drivingEvents != null && drivingEvents.length() > 0) {
1020
+ values.put(LocationEntry.COLUMN_NAME_EVENTS_JSON, drivingEvents.toString());
1021
+ } else {
1022
+ values.putNull(LocationEntry.COLUMN_NAME_EVENTS_JSON);
1023
+ }
1024
+ if (batteryLevel != null) values.put(LocationEntry.COLUMN_NAME_BATTERY_LEVEL, batteryLevel);
1025
+ else values.putNull(LocationEntry.COLUMN_NAME_BATTERY_LEVEL);
1026
+ if (isCharging != null) values.put(LocationEntry.COLUMN_NAME_IS_CHARGING, isCharging ? 1 : 0);
1027
+ else values.putNull(LocationEntry.COLUMN_NAME_IS_CHARGING);
945
1028
  return values;
946
1029
  }
947
1030
 
@@ -988,6 +1071,17 @@ public class BackgroundLocation implements Parcelable {
988
1071
  if ("@mockLocationsEnabled".equals(key)) {
989
1072
  return hasMockLocationsEnabled() ? areMockLocationsEnabled() : JSONObject.NULL;
990
1073
  }
1074
+ // v4.3 — driving events array (only present if events were attached during this fix).
1075
+ if ("@events".equals(key)) {
1076
+ return drivingEvents != null ? drivingEvents : JSONObject.NULL;
1077
+ }
1078
+ // v4.4 — battery snapshot
1079
+ if ("@battery".equals(key)) {
1080
+ return batteryLevel != null ? batteryLevel : JSONObject.NULL;
1081
+ }
1082
+ if ("@isCharging".equals(key)) {
1083
+ return isCharging != null ? isCharging : JSONObject.NULL;
1084
+ }
991
1085
 
992
1086
  return null;
993
1087
  }
@@ -0,0 +1,211 @@
1
+ package com.marianhello.bgloc.data;
2
+
3
+ import com.marianhello.bgloc.Config;
4
+
5
+ import org.json.JSONException;
6
+ import org.json.JSONObject;
7
+
8
+ import java.util.HashMap;
9
+ import java.util.Iterator;
10
+
11
+ /**
12
+ * v4.4.1 — JSON serializer/deserializer for the full {@link Config} state.
13
+ *
14
+ * Lives in {@code common} so both the SQLite DAO (also common) and the Cordova
15
+ * {@code ConfigMapper} (cordova) can reuse it without creating a common→cordova
16
+ * dependency. Used to persist a single {@code config_json} TEXT column instead of
17
+ * adding one schema column per new field on every release.
18
+ *
19
+ * Round-trip: every JS-configurable key the plugin understands is preserved.
20
+ * Anything not present in the input JSON keeps the {@link Config} default.
21
+ */
22
+ public final class ConfigJsonMapper {
23
+
24
+ private ConfigJsonMapper() {}
25
+
26
+ /** Serialize the current Config state to a JSONObject suitable for storage.
27
+ * String fields use {@link JSONObject#NULL} when the user explicitly cleared them
28
+ * (i.e. equals {@link Config#NullString}) so the sentinel survives the round-trip. */
29
+ public static JSONObject toJSONObject(Config c) throws JSONException {
30
+ JSONObject j = new JSONObject();
31
+ if (c == null) return j;
32
+ j.put("stationaryRadius", c.getStationaryRadius());
33
+ j.put("distanceFilter", c.getDistanceFilter());
34
+ j.put("desiredAccuracy", c.getDesiredAccuracy());
35
+ j.put("debug", c.isDebugging());
36
+ j.put("notificationTitle", nullable(c.getNotificationTitle()));
37
+ j.put("notificationText", nullable(c.getNotificationText()));
38
+ j.put("notificationSyncTitle", nullable(c.getNotificationSyncTitle()));
39
+ j.put("notificationSyncText", nullable(c.getNotificationSyncText()));
40
+ j.put("notificationSyncCompletedText", nullable(c.getNotificationSyncCompletedText()));
41
+ j.put("notificationSyncFailedText", nullable(c.getNotificationSyncFailedText()));
42
+ j.put("notificationIconLarge", nullable(c.getLargeNotificationIcon()));
43
+ j.put("notificationIconSmall", nullable(c.getSmallNotificationIcon()));
44
+ j.put("notificationIconColor", nullable(c.getNotificationIconColor()));
45
+ j.put("locationProvider", c.getLocationProvider());
46
+ j.put("interval", c.getInterval());
47
+ j.put("fastestInterval", c.getFastestInterval());
48
+ j.put("activitiesInterval", c.getActivitiesInterval());
49
+ j.put("stopOnTerminate", c.getStopOnTerminate());
50
+ j.put("startOnBoot", c.getStartOnBoot());
51
+ j.put("startForeground", c.getStartForeground());
52
+ j.put("notificationsEnabled", c.getNotificationsEnabled());
53
+ j.put("stopOnStillActivity", c.getStopOnStillActivity());
54
+ j.put("url", nullable(c.getUrl()));
55
+ j.put("syncUrl", nullable(c.getSyncUrl()));
56
+ j.put("syncThreshold", c.getSyncThreshold());
57
+ j.put("syncEnabled", c.getSyncEnabled());
58
+ j.put("maxLocations", c.getMaxLocations());
59
+ j.put("enableWatchdog", c.getEnableWatchdog());
60
+ j.put("showTime", c.getShowTime());
61
+ j.put("showDistance", c.getShowDistance());
62
+ j.put("httpMethod", c.getHttpMethod());
63
+ j.put("syncHttpMethod", c.getSyncHttpMethod());
64
+ j.put("httpMode", c.getHttpMode());
65
+ j.put("syncMode", c.getSyncMode());
66
+ j.put("heartbeatInterval", c.getHeartbeatInterval());
67
+ j.put("mockLocationPolicy", c.getMockLocationPolicy());
68
+ j.put("includeBattery", c.getIncludeBattery());
69
+ // v4.5.1: battery knobs
70
+ j.put("wakeLockMode", c.getWakeLockMode());
71
+ j.put("stationaryTimeout", c.getStationaryTimeout());
72
+ j.put("stationaryPollInterval", c.getStationaryPollInterval());
73
+ j.put("stationaryPollFast", c.getStationaryPollFast());
74
+ // v4.5.2: provider hardening knobs
75
+ j.put("activityConfidenceThreshold", c.getActivityConfidenceThreshold());
76
+ j.put("maxAcceptedAccuracy", c.getMaxAcceptedAccuracy());
77
+
78
+ if (c.getHttpHeaders() != null) {
79
+ j.put("httpHeaders", new JSONObject(c.getHttpHeaders()));
80
+ }
81
+ if (c.getQueryParams() != null) {
82
+ j.put("queryParams", new JSONObject(c.getQueryParams()));
83
+ }
84
+
85
+ Config.DrivingEventsOptions de = c.getDrivingEvents();
86
+ if (de != null) {
87
+ JSONObject deJson = new JSONObject();
88
+ deJson.put("enabled", de.enabled);
89
+ deJson.put("speedLimit", de.speedLimitKmh);
90
+ deJson.put("minMovingSpeed", de.minMovingSpeedMps);
91
+ deJson.put("stoppedDuration", de.stoppedDurationMs);
92
+ deJson.put("minTripSpeed", de.minTripSpeedMps);
93
+ deJson.put("minTripDuration", de.minTripDurationMs);
94
+ deJson.put("hardBrakeMps2", de.hardBrakeMps2);
95
+ deJson.put("rapidAccelMps2", de.rapidAccelMps2);
96
+ deJson.put("sharpTurnDegPerSec", de.sharpTurnDegPerSec);
97
+ deJson.put("crashImpactKmh", de.crashImpactKmh);
98
+ deJson.put("crashWindowMs", de.crashWindowMs);
99
+ deJson.put("sensorFusion", de.sensorFusion);
100
+ deJson.put("crashImpactG", de.crashImpactG);
101
+ deJson.put("sensorCrashCooldownMs", de.sensorCrashCooldownMs);
102
+ deJson.put("phoneUsageWindowMs", de.phoneUsageWindowMs);
103
+ deJson.put("phoneUsageCooldownMs", de.phoneUsageCooldownMs);
104
+ j.put("drivingEvents", deJson);
105
+ }
106
+ return j;
107
+ }
108
+
109
+ /** Deserialize a previously serialized config JSON. Missing keys are skipped (defaults preserved). */
110
+ public static Config fromJSONObject(JSONObject j) throws JSONException {
111
+ Config c = Config.getDefault();
112
+ if (j == null) return c;
113
+ if (j.has("stationaryRadius")) c.setStationaryRadius((float) j.getDouble("stationaryRadius"));
114
+ if (j.has("distanceFilter")) c.setDistanceFilter(j.getInt("distanceFilter"));
115
+ if (j.has("desiredAccuracy")) c.setDesiredAccuracy(j.getInt("desiredAccuracy"));
116
+ if (j.has("debug")) c.setDebugging(j.getBoolean("debug"));
117
+ if (j.has("notificationTitle")) c.setNotificationTitle(readNullable(j, "notificationTitle"));
118
+ if (j.has("notificationText")) c.setNotificationText(readNullable(j, "notificationText"));
119
+ if (j.has("notificationSyncTitle")) c.setNotificationSyncTitle(readNullable(j, "notificationSyncTitle"));
120
+ if (j.has("notificationSyncText")) c.setNotificationSyncText(readNullable(j, "notificationSyncText"));
121
+ if (j.has("notificationSyncCompletedText")) c.setNotificationSyncCompletedText(readNullable(j, "notificationSyncCompletedText"));
122
+ if (j.has("notificationSyncFailedText")) c.setNotificationSyncFailedText(readNullable(j, "notificationSyncFailedText"));
123
+ if (j.has("notificationIconLarge")) c.setLargeNotificationIcon(readNullable(j, "notificationIconLarge"));
124
+ if (j.has("notificationIconSmall")) c.setSmallNotificationIcon(readNullable(j, "notificationIconSmall"));
125
+ if (j.has("notificationIconColor")) c.setNotificationIconColor(readNullable(j, "notificationIconColor"));
126
+ if (j.has("locationProvider")) c.setLocationProvider(j.getInt("locationProvider"));
127
+ if (j.has("interval")) c.setInterval(j.getInt("interval"));
128
+ if (j.has("fastestInterval")) c.setFastestInterval(j.getInt("fastestInterval"));
129
+ if (j.has("activitiesInterval")) c.setActivitiesInterval(j.getInt("activitiesInterval"));
130
+ if (j.has("stopOnTerminate")) c.setStopOnTerminate(j.getBoolean("stopOnTerminate"));
131
+ if (j.has("startOnBoot")) c.setStartOnBoot(j.getBoolean("startOnBoot"));
132
+ if (j.has("startForeground")) c.setStartForeground(j.getBoolean("startForeground"));
133
+ if (j.has("notificationsEnabled")) c.setNotificationsEnabled(j.getBoolean("notificationsEnabled"));
134
+ if (j.has("stopOnStillActivity")) c.setStopOnStillActivity(j.getBoolean("stopOnStillActivity"));
135
+ if (j.has("url")) c.setUrl(readNullable(j, "url"));
136
+ if (j.has("syncUrl")) c.setSyncUrl(readNullable(j, "syncUrl"));
137
+ if (j.has("syncThreshold")) c.setSyncThreshold(j.getInt("syncThreshold"));
138
+ if (j.has("syncEnabled")) c.setSyncEnabled(j.getBoolean("syncEnabled"));
139
+ if (j.has("maxLocations")) c.setMaxLocations(j.getInt("maxLocations"));
140
+ if (j.has("enableWatchdog")) c.setEnableWatchdog(j.getBoolean("enableWatchdog"));
141
+ if (j.has("showTime")) c.setShowTime(j.getBoolean("showTime"));
142
+ if (j.has("showDistance")) c.setShowDistance(j.getBoolean("showDistance"));
143
+ if (has(j, "httpMethod")) c.setHttpMethod(j.getString("httpMethod"));
144
+ if (has(j, "syncHttpMethod")) c.setSyncHttpMethod(j.getString("syncHttpMethod"));
145
+ if (has(j, "httpMode")) c.setHttpMode(j.getString("httpMode"));
146
+ if (has(j, "syncMode")) c.setSyncMode(j.getString("syncMode"));
147
+ if (j.has("heartbeatInterval")) c.setHeartbeatInterval(j.getInt("heartbeatInterval"));
148
+ if (has(j, "mockLocationPolicy")) c.setMockLocationPolicy(j.getString("mockLocationPolicy"));
149
+ if (j.has("includeBattery")) c.setIncludeBattery(j.getBoolean("includeBattery"));
150
+ // v4.5.1: battery knobs
151
+ if (has(j, "wakeLockMode")) c.setWakeLockMode(j.getString("wakeLockMode"));
152
+ if (j.has("stationaryTimeout") && !j.isNull("stationaryTimeout")) c.setStationaryTimeout(j.getInt("stationaryTimeout"));
153
+ if (j.has("stationaryPollInterval") && !j.isNull("stationaryPollInterval")) c.setStationaryPollInterval(j.getInt("stationaryPollInterval"));
154
+ if (j.has("stationaryPollFast") && !j.isNull("stationaryPollFast")) c.setStationaryPollFast(j.getInt("stationaryPollFast"));
155
+ // v4.5.2
156
+ if (j.has("activityConfidenceThreshold") && !j.isNull("activityConfidenceThreshold")) c.setActivityConfidenceThreshold(j.getInt("activityConfidenceThreshold"));
157
+ if (j.has("maxAcceptedAccuracy") && !j.isNull("maxAcceptedAccuracy")) c.setMaxAcceptedAccuracy((float) j.getDouble("maxAcceptedAccuracy"));
158
+
159
+ if (has(j, "httpHeaders")) c.setHttpHeaders(jsonToHashMap(j.getJSONObject("httpHeaders")));
160
+ if (has(j, "queryParams")) c.setQueryParams(jsonToHashMap(j.getJSONObject("queryParams")));
161
+
162
+ if (has(j, "drivingEvents")) {
163
+ JSONObject de = j.getJSONObject("drivingEvents");
164
+ Config.DrivingEventsOptions o = new Config.DrivingEventsOptions();
165
+ if (de.has("enabled")) o.enabled = de.getBoolean("enabled");
166
+ if (de.has("speedLimit")) o.speedLimitKmh = de.getDouble("speedLimit");
167
+ if (de.has("minMovingSpeed")) o.minMovingSpeedMps = de.getDouble("minMovingSpeed");
168
+ if (de.has("stoppedDuration")) o.stoppedDurationMs = de.getLong("stoppedDuration");
169
+ if (de.has("minTripSpeed")) o.minTripSpeedMps = de.getDouble("minTripSpeed");
170
+ if (de.has("minTripDuration")) o.minTripDurationMs = de.getLong("minTripDuration");
171
+ if (de.has("hardBrakeMps2")) o.hardBrakeMps2 = de.getDouble("hardBrakeMps2");
172
+ if (de.has("rapidAccelMps2")) o.rapidAccelMps2 = de.getDouble("rapidAccelMps2");
173
+ if (de.has("sharpTurnDegPerSec")) o.sharpTurnDegPerSec = de.getDouble("sharpTurnDegPerSec");
174
+ if (de.has("crashImpactKmh")) o.crashImpactKmh = de.getDouble("crashImpactKmh");
175
+ if (de.has("crashWindowMs")) o.crashWindowMs = de.getLong("crashWindowMs");
176
+ if (de.has("sensorFusion")) o.sensorFusion = de.getBoolean("sensorFusion");
177
+ if (de.has("crashImpactG")) o.crashImpactG = de.getDouble("crashImpactG");
178
+ if (de.has("sensorCrashCooldownMs")) o.sensorCrashCooldownMs = de.getLong("sensorCrashCooldownMs");
179
+ if (de.has("phoneUsageWindowMs")) o.phoneUsageWindowMs = de.getLong("phoneUsageWindowMs");
180
+ if (de.has("phoneUsageCooldownMs")) o.phoneUsageCooldownMs = de.getLong("phoneUsageCooldownMs");
181
+ c.setDrivingEvents(o);
182
+ }
183
+ return c;
184
+ }
185
+
186
+ private static HashMap<String, String> jsonToHashMap(JSONObject obj) throws JSONException {
187
+ HashMap<String, String> map = new HashMap<>();
188
+ Iterator<String> keys = obj.keys();
189
+ while (keys.hasNext()) {
190
+ String k = keys.next();
191
+ map.put(k, String.valueOf(obj.get(k)));
192
+ }
193
+ return map;
194
+ }
195
+
196
+ private static boolean has(JSONObject j, String key) {
197
+ return j.has(key) && !j.isNull(key);
198
+ }
199
+
200
+ /** Map {@link Config#NullString} or null to JSONObject.NULL so the sentinel survives. */
201
+ private static Object nullable(String s) {
202
+ if (s == null || s == Config.NullString) return JSONObject.NULL;
203
+ return s;
204
+ }
205
+
206
+ /** Inverse of {@link #nullable(String)}. JSON null → {@link Config#NullString}. */
207
+ private static String readNullable(JSONObject j, String key) throws JSONException {
208
+ if (j.isNull(key)) return Config.NullString;
209
+ return j.getString(key);
210
+ }
211
+ }