@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
@@ -24,6 +24,7 @@ import org.json.JSONObject;
24
24
 
25
25
  import java.util.HashMap;
26
26
  import java.util.Iterator;
27
+ import java.util.Locale;
27
28
 
28
29
  /**
29
30
  * Config class
@@ -71,6 +72,39 @@ public class Config implements Parcelable
71
72
  private Boolean enableWatchdog;
72
73
  private Boolean showTime;
73
74
  private Boolean showDistance;
75
+ // v3.3 (Phase 2): backend-agnostic HTTP transport
76
+ private String httpMethod; // POST | GET | PUT | PATCH (default POST)
77
+ private String syncHttpMethod; // POST | GET | PUT | PATCH (default POST)
78
+ private String httpMode; // batch | single (default batch)
79
+ private String syncMode; // batch | single (default batch)
80
+ private HashMap queryParams; // static placeholder values for URL templating
81
+ // v3.5 (Phase 4): diagnostics
82
+ private Integer heartbeatInterval; // ms; 0 disables heartbeat events
83
+ private String mockLocationPolicy; // allow | flag | drop (default allow)
84
+ // v4.0 (Phase 6): driver insights
85
+ private DrivingEventsOptions drivingEvents;
86
+
87
+ /** v4.0 Phase 6 + v4.1: driver-insights configuration. Plain holder; no Parcelable to keep this class diff small. */
88
+ public static class DrivingEventsOptions {
89
+ public boolean enabled = false;
90
+ public double speedLimitKmh = 0;
91
+ public double minMovingSpeedMps = 1.0;
92
+ public long stoppedDurationMs = 60_000L;
93
+ public double minTripSpeedMps = 3.0;
94
+ public long minTripDurationMs = 30_000L;
95
+ // v4.1 GPS-derived sensor-like events. 0 disables each one.
96
+ public double hardBrakeMps2 = 3.5;
97
+ public double rapidAccelMps2 = 3.5;
98
+ public double sharpTurnDegPerSec = 30;
99
+ public double crashImpactKmh = 25;
100
+ public long crashWindowMs = 2_000L;
101
+ // v4.2 sensor fusion (real accelerometer + gyroscope).
102
+ public boolean sensorFusion = false;
103
+ public double crashImpactG = 3.0; // |a| threshold for sensor crash, in g
104
+ public long sensorCrashCooldownMs = 10_000L;
105
+ public long phoneUsageWindowMs = 4_000L;
106
+ public long phoneUsageCooldownMs = 60_000L;
107
+ }
74
108
 
75
109
  public Config () {
76
110
  }
@@ -108,6 +142,33 @@ public class Config implements Parcelable
108
142
  this.enableWatchdog = config.enableWatchdog;
109
143
  this.showTime = config.showTime;
110
144
  this.showDistance = config.showDistance;
145
+ this.httpMethod = config.httpMethod;
146
+ this.syncHttpMethod = config.syncHttpMethod;
147
+ this.httpMode = config.httpMode;
148
+ this.syncMode = config.syncMode;
149
+ this.queryParams = CloneHelper.deepCopy(config.queryParams);
150
+ this.heartbeatInterval = config.heartbeatInterval;
151
+ this.mockLocationPolicy = config.mockLocationPolicy;
152
+ if (config.drivingEvents != null) {
153
+ DrivingEventsOptions de = new DrivingEventsOptions();
154
+ de.enabled = config.drivingEvents.enabled;
155
+ de.speedLimitKmh = config.drivingEvents.speedLimitKmh;
156
+ de.minMovingSpeedMps = config.drivingEvents.minMovingSpeedMps;
157
+ de.stoppedDurationMs = config.drivingEvents.stoppedDurationMs;
158
+ de.minTripSpeedMps = config.drivingEvents.minTripSpeedMps;
159
+ de.minTripDurationMs = config.drivingEvents.minTripDurationMs;
160
+ de.hardBrakeMps2 = config.drivingEvents.hardBrakeMps2;
161
+ de.rapidAccelMps2 = config.drivingEvents.rapidAccelMps2;
162
+ de.sharpTurnDegPerSec = config.drivingEvents.sharpTurnDegPerSec;
163
+ de.crashImpactKmh = config.drivingEvents.crashImpactKmh;
164
+ de.crashWindowMs = config.drivingEvents.crashWindowMs;
165
+ de.sensorFusion = config.drivingEvents.sensorFusion;
166
+ de.crashImpactG = config.drivingEvents.crashImpactG;
167
+ de.sensorCrashCooldownMs = config.drivingEvents.sensorCrashCooldownMs;
168
+ de.phoneUsageWindowMs = config.drivingEvents.phoneUsageWindowMs;
169
+ de.phoneUsageCooldownMs = config.drivingEvents.phoneUsageCooldownMs;
170
+ this.drivingEvents = de;
171
+ }
111
172
  if (config.template instanceof AbstractLocationTemplate) {
112
173
  this.template = ((AbstractLocationTemplate)config.template).clone();
113
174
  }
@@ -144,8 +205,55 @@ public class Config implements Parcelable
144
205
  setEnableWatchdog((Boolean) in.readValue(null));
145
206
  setShowTime((Boolean) in.readValue(null));
146
207
  setShowDistance((Boolean) in.readValue(null));
208
+ setHttpMethod(in.readString());
209
+ setSyncHttpMethod(in.readString());
210
+ setHttpMode(in.readString());
211
+ setSyncMode(in.readString());
212
+ setHeartbeatInterval((Integer) in.readValue(null));
213
+ setMockLocationPolicy(in.readString());
214
+ // v4.0 + v4.1: driver-insights options serialised as primitives.
215
+ boolean deEnabled = in.readInt() != 0;
216
+ double deSpeedLimit = in.readDouble();
217
+ double deMinMove = in.readDouble();
218
+ long deStoppedDur = in.readLong();
219
+ double deMinTrip = in.readDouble();
220
+ long deMinTripDur = in.readLong();
221
+ // v4.1
222
+ double deHardBrake = in.readDouble();
223
+ double deRapidAccel = in.readDouble();
224
+ double deSharpTurn = in.readDouble();
225
+ double deCrashKmh = in.readDouble();
226
+ long deCrashWin = in.readLong();
227
+ // v4.2 sensor fusion
228
+ boolean deSensorFusion = in.readInt() != 0;
229
+ double deCrashImpactG = in.readDouble();
230
+ long deSensorCrashCooldown = in.readLong();
231
+ long dePhoneUsageWindow = in.readLong();
232
+ long dePhoneUsageCooldown = in.readLong();
233
+ boolean deHasOptions = in.readInt() != 0;
234
+ if (deHasOptions) {
235
+ DrivingEventsOptions de = new DrivingEventsOptions();
236
+ de.enabled = deEnabled;
237
+ de.speedLimitKmh = deSpeedLimit;
238
+ de.minMovingSpeedMps = deMinMove;
239
+ de.stoppedDurationMs = deStoppedDur;
240
+ de.minTripSpeedMps = deMinTrip;
241
+ de.minTripDurationMs = deMinTripDur;
242
+ de.hardBrakeMps2 = deHardBrake;
243
+ de.rapidAccelMps2 = deRapidAccel;
244
+ de.sharpTurnDegPerSec = deSharpTurn;
245
+ de.crashImpactKmh = deCrashKmh;
246
+ de.crashWindowMs = deCrashWin;
247
+ de.sensorFusion = deSensorFusion;
248
+ de.crashImpactG = deCrashImpactG;
249
+ de.sensorCrashCooldownMs = deSensorCrashCooldown;
250
+ de.phoneUsageWindowMs = dePhoneUsageWindow;
251
+ de.phoneUsageCooldownMs = dePhoneUsageCooldown;
252
+ this.drivingEvents = de;
253
+ }
147
254
  Bundle bundle = in.readBundle();
148
255
  setHttpHeaders((HashMap<String, String>) bundle.getSerializable("httpHeaders"));
256
+ setQueryParams((HashMap<String, String>) bundle.getSerializable("queryParams"));
149
257
  setTemplate((LocationTemplate) bundle.getSerializable(AbstractLocationTemplate.BUNDLE_KEY));
150
258
  }
151
259
 
@@ -183,6 +291,13 @@ public class Config implements Parcelable
183
291
  config.enableWatchdog = false;
184
292
  config.showTime = false;
185
293
  config.showDistance = false;
294
+ config.httpMethod = "POST";
295
+ config.syncHttpMethod = "POST";
296
+ config.httpMode = "batch";
297
+ config.syncMode = "batch";
298
+ config.queryParams = null;
299
+ config.heartbeatInterval = 0;
300
+ config.mockLocationPolicy = "allow";
186
301
 
187
302
  return config;
188
303
  }
@@ -223,8 +338,36 @@ public class Config implements Parcelable
223
338
  out.writeValue(getEnableWatchdog());
224
339
  out.writeValue(getShowTime());
225
340
  out.writeValue(getShowDistance());
341
+ out.writeString(getHttpMethod());
342
+ out.writeString(getSyncHttpMethod());
343
+ out.writeString(getHttpMode());
344
+ out.writeString(getSyncMode());
345
+ out.writeValue(getHeartbeatInterval());
346
+ out.writeString(getMockLocationPolicy());
347
+ // v4.0 + v4.1: drivingEvents primitives (always written; "hasOptions" flag at end).
348
+ DrivingEventsOptions de = drivingEvents;
349
+ out.writeInt(de != null && de.enabled ? 1 : 0);
350
+ out.writeDouble(de != null ? de.speedLimitKmh : 0.0);
351
+ out.writeDouble(de != null ? de.minMovingSpeedMps : 1.0);
352
+ out.writeLong (de != null ? de.stoppedDurationMs : 60_000L);
353
+ out.writeDouble(de != null ? de.minTripSpeedMps : 3.0);
354
+ out.writeLong (de != null ? de.minTripDurationMs : 30_000L);
355
+ // v4.1
356
+ out.writeDouble(de != null ? de.hardBrakeMps2 : 3.5);
357
+ out.writeDouble(de != null ? de.rapidAccelMps2 : 3.5);
358
+ out.writeDouble(de != null ? de.sharpTurnDegPerSec: 30.0);
359
+ out.writeDouble(de != null ? de.crashImpactKmh : 25.0);
360
+ out.writeLong (de != null ? de.crashWindowMs : 2_000L);
361
+ // v4.2 sensor fusion
362
+ out.writeInt (de != null && de.sensorFusion ? 1 : 0);
363
+ out.writeDouble(de != null ? de.crashImpactG : 3.0);
364
+ out.writeLong (de != null ? de.sensorCrashCooldownMs : 10_000L);
365
+ out.writeLong (de != null ? de.phoneUsageWindowMs : 4_000L);
366
+ out.writeLong (de != null ? de.phoneUsageCooldownMs : 60_000L);
367
+ out.writeInt (de != null ? 1 : 0);
226
368
  Bundle bundle = new Bundle();
227
369
  bundle.putSerializable("httpHeaders", getHttpHeaders());
370
+ bundle.putSerializable("queryParams", getQueryParams());
228
371
  bundle.putSerializable(AbstractLocationTemplate.BUNDLE_KEY, (AbstractLocationTemplate) getTemplate());
229
372
  out.writeBundle(bundle);
230
373
  }
@@ -645,6 +788,102 @@ public class Config implements Parcelable
645
788
  this.showDistance = showDistance;
646
789
  }
647
790
 
791
+ /** HTTP method for the main `url`. Default POST. */
792
+ @Nullable
793
+ public String getHttpMethod() {
794
+ return httpMethod != null ? httpMethod : "POST";
795
+ }
796
+
797
+ public void setHttpMethod(@Nullable String httpMethod) {
798
+ this.httpMethod = (httpMethod == null || httpMethod.isEmpty()) ? null : httpMethod.toUpperCase(Locale.US);
799
+ }
800
+
801
+ /** HTTP method for the `syncUrl`. Default POST. */
802
+ @Nullable
803
+ public String getSyncHttpMethod() {
804
+ return syncHttpMethod != null ? syncHttpMethod : "POST";
805
+ }
806
+
807
+ public void setSyncHttpMethod(@Nullable String syncHttpMethod) {
808
+ this.syncHttpMethod = (syncHttpMethod == null || syncHttpMethod.isEmpty()) ? null : syncHttpMethod.toUpperCase(Locale.US);
809
+ }
810
+
811
+ /** Real-time post mode. "batch" (default) or "single". */
812
+ @Nullable
813
+ public String getHttpMode() {
814
+ return httpMode != null ? httpMode : "batch";
815
+ }
816
+
817
+ public void setHttpMode(@Nullable String httpMode) {
818
+ this.httpMode = (httpMode == null || httpMode.isEmpty()) ? null : httpMode.toLowerCase(Locale.US);
819
+ }
820
+
821
+ /** Sync queue mode. "batch" (default) or "single". */
822
+ @Nullable
823
+ public String getSyncMode() {
824
+ return syncMode != null ? syncMode : "batch";
825
+ }
826
+
827
+ public void setSyncMode(@Nullable String syncMode) {
828
+ this.syncMode = (syncMode == null || syncMode.isEmpty()) ? null : syncMode.toLowerCase(Locale.US);
829
+ }
830
+
831
+ public boolean hasQueryParams() {
832
+ return queryParams != null && !queryParams.isEmpty();
833
+ }
834
+
835
+ public HashMap<String, String> getQueryParams() {
836
+ return queryParams;
837
+ }
838
+
839
+ public void setQueryParams(HashMap queryParams) {
840
+ this.queryParams = queryParams;
841
+ }
842
+
843
+ public void setQueryParams(JSONObject queryParams) throws JSONException {
844
+ this.queryParams = new HashMap<String, String>();
845
+ if (queryParams == null) return;
846
+ Iterator<?> it = queryParams.keys();
847
+ while (it.hasNext()) {
848
+ String key = (String) it.next();
849
+ // queryParams accepts string | number (per d.ts). Convert numbers to string.
850
+ Object value = queryParams.get(key);
851
+ this.queryParams.put(key, value == null || value == JSONObject.NULL ? "" : String.valueOf(value));
852
+ }
853
+ }
854
+
855
+ /** Heartbeat emit interval in ms. 0 disables. */
856
+ @Nullable
857
+ public Integer getHeartbeatInterval() {
858
+ return heartbeatInterval != null ? heartbeatInterval : 0;
859
+ }
860
+
861
+ public void setHeartbeatInterval(Integer heartbeatInterval) {
862
+ this.heartbeatInterval = heartbeatInterval;
863
+ }
864
+
865
+ /** Mock location policy: "allow" | "flag" | "drop". Default "allow". */
866
+ @Nullable
867
+ public String getMockLocationPolicy() {
868
+ return mockLocationPolicy != null ? mockLocationPolicy : "allow";
869
+ }
870
+
871
+ public void setMockLocationPolicy(@Nullable String mockLocationPolicy) {
872
+ this.mockLocationPolicy = (mockLocationPolicy == null || mockLocationPolicy.isEmpty())
873
+ ? null
874
+ : mockLocationPolicy.toLowerCase(Locale.US);
875
+ }
876
+
877
+ /** v4.0 Phase 6: driver-insights options. */
878
+ @Nullable
879
+ public DrivingEventsOptions getDrivingEvents() {
880
+ return drivingEvents;
881
+ }
882
+
883
+ public void setDrivingEvents(@Nullable DrivingEventsOptions drivingEvents) {
884
+ this.drivingEvents = drivingEvents;
885
+ }
886
+
648
887
  @Override
649
888
  public String toString () {
650
889
  return new StringBuffer()
@@ -675,6 +914,11 @@ public class Config implements Parcelable
675
914
  .append(" postTemplate=").append(hasTemplate() ? getTemplate().toString() : null)
676
915
  .append(" showTime=").append(getShowTime())
677
916
  .append(" showDistance=").append(getShowDistance())
917
+ .append(" httpMethod=").append(getHttpMethod())
918
+ .append(" syncHttpMethod=").append(getSyncHttpMethod())
919
+ .append(" httpMode=").append(getHttpMode())
920
+ .append(" syncMode=").append(getSyncMode())
921
+ .append(" queryParams=").append(hasQueryParams() ? getQueryParams().toString() : null)
678
922
  .append("]")
679
923
  .toString();
680
924
  }
@@ -788,6 +1032,30 @@ public class Config implements Parcelable
788
1032
  if (config2.hasShowDistance()) {
789
1033
  merger.setShowDistance(config2.getShowDistance());
790
1034
  }
1035
+ if (config2.httpMethod != null) {
1036
+ merger.setHttpMethod(config2.getHttpMethod());
1037
+ }
1038
+ if (config2.syncHttpMethod != null) {
1039
+ merger.setSyncHttpMethod(config2.getSyncHttpMethod());
1040
+ }
1041
+ if (config2.httpMode != null) {
1042
+ merger.setHttpMode(config2.getHttpMode());
1043
+ }
1044
+ if (config2.syncMode != null) {
1045
+ merger.setSyncMode(config2.getSyncMode());
1046
+ }
1047
+ if (config2.hasQueryParams()) {
1048
+ merger.setQueryParams(config2.getQueryParams());
1049
+ }
1050
+ if (config2.heartbeatInterval != null) {
1051
+ merger.setHeartbeatInterval(config2.getHeartbeatInterval());
1052
+ }
1053
+ if (config2.mockLocationPolicy != null) {
1054
+ merger.setMockLocationPolicy(config2.getMockLocationPolicy());
1055
+ }
1056
+ if (config2.drivingEvents != null) {
1057
+ merger.setDrivingEvents(config2.drivingEvents);
1058
+ }
791
1059
 
792
1060
  return merger;
793
1061
  }
@@ -21,7 +21,6 @@ import org.json.JSONTokener;
21
21
 
22
22
  import java.net.URL;
23
23
  import java.net.HttpURLConnection;
24
- import java.io.OutputStreamWriter;
25
24
  import java.net.URLEncoder;
26
25
 
27
26
  public class HttpPostService {
@@ -32,6 +31,7 @@ public class HttpPostService {
32
31
  private static final int READ_TIMEOUT_MS = 120_000;
33
32
 
34
33
  private String mUrl;
34
+ private String mMethod = "POST";
35
35
  private HttpURLConnection mHttpURLConnection;
36
36
 
37
37
  public interface UploadingProgressListener {
@@ -42,10 +42,31 @@ public class HttpPostService {
42
42
  mUrl = url;
43
43
  }
44
44
 
45
+ public HttpPostService(String url, String method) {
46
+ mUrl = url;
47
+ mMethod = normalizeMethod(method);
48
+ }
49
+
45
50
  public HttpPostService(final HttpURLConnection httpURLConnection) {
46
51
  mHttpURLConnection = httpURLConnection;
47
52
  }
48
53
 
54
+ public void setMethod(String method) {
55
+ mMethod = normalizeMethod(method);
56
+ }
57
+
58
+ private static String normalizeMethod(String method) {
59
+ if (method == null || method.isEmpty()) return "POST";
60
+ String m = method.trim().toUpperCase();
61
+ if (m.equals("POST") || m.equals("GET") || m.equals("PUT") || m.equals("PATCH")) return m;
62
+ return "POST";
63
+ }
64
+
65
+ /** Returns true when the HTTP method has no request body (GET). */
66
+ private boolean isBodyless() {
67
+ return "GET".equals(mMethod);
68
+ }
69
+
49
70
  private HttpURLConnection openConnection() throws IOException {
50
71
  if (mHttpURLConnection == null) {
51
72
  mHttpURLConnection = (HttpURLConnection) new URL(mUrl).openConnection();
@@ -84,7 +105,7 @@ public class HttpPostService {
84
105
  if (headers == null) {
85
106
  headers = new HashMap();
86
107
  }
87
-
108
+
88
109
  String contentType = null;
89
110
  for (Object keyObj : headers.keySet()) {
90
111
  String key = (String) keyObj;
@@ -96,7 +117,29 @@ public class HttpPostService {
96
117
  if (contentType == null) {
97
118
  contentType = "application/json";
98
119
  }
99
- // Prepare body according to Content-Type so header and body always match
120
+
121
+ HttpURLConnection conn = this.openConnection();
122
+ conn.setRequestMethod(mMethod);
123
+
124
+ // Set headers (including Content-Type) up-front; needed for both bodyless and body requests.
125
+ if (!isBodyless()) {
126
+ conn.setRequestProperty("Content-Type", contentType);
127
+ }
128
+ Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator();
129
+ while (it.hasNext()) {
130
+ Map.Entry<String, String> pair = it.next();
131
+ if (!pair.getKey().equalsIgnoreCase("Content-Type")) {
132
+ conn.setRequestProperty(pair.getKey(), pair.getValue());
133
+ }
134
+ }
135
+
136
+ // GET: no body; data is expected to live in the URL (URL templating).
137
+ if (isBodyless()) {
138
+ conn.setDoOutput(false);
139
+ return conn.getResponseCode();
140
+ }
141
+
142
+ // Prepare body according to Content-Type so header and body always match.
100
143
  String finalBody = body;
101
144
  if (contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) {
102
145
  try {
@@ -105,25 +148,17 @@ public class HttpPostService {
105
148
  finalBody = body;
106
149
  }
107
150
  }
108
-
109
- HttpURLConnection conn = this.openConnection();
151
+
152
+ // Use byte length, not String.length(), so multi-byte UTF-8 characters
153
+ // (ñ, é, emoji, ...) match the Content-Length the server expects.
154
+ byte[] outputBytes = finalBody.getBytes(StandardCharsets.UTF_8);
110
155
  conn.setDoOutput(true);
111
- conn.setFixedLengthStreamingMode(finalBody.length());
112
- conn.setRequestMethod("POST");
113
- conn.setRequestProperty("Content-Type", contentType);
114
-
115
- Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator();
116
- while (it.hasNext()) {
117
- Map.Entry<String, String> pair = it.next();
118
- if (!pair.getKey().equalsIgnoreCase("Content-Type")) {
119
- conn.setRequestProperty(pair.getKey(), pair.getValue());
120
- }
121
- }
122
-
123
- OutputStreamWriter os = null;
156
+ conn.setFixedLengthStreamingMode(outputBytes.length);
157
+
158
+ java.io.OutputStream os = null;
124
159
  try {
125
- os = new OutputStreamWriter(conn.getOutputStream());
126
- os.write(finalBody);
160
+ os = conn.getOutputStream();
161
+ os.write(outputBytes);
127
162
  } finally {
128
163
  if (os != null) {
129
164
  os.flush();
@@ -251,6 +286,20 @@ public class HttpPostService {
251
286
  }
252
287
 
253
288
  HttpURLConnection conn = this.openConnection();
289
+ conn.setRequestMethod(mMethod);
290
+ if (isBodyless()) {
291
+ conn.setDoOutput(false);
292
+ // No headers loop / body for GET; we just consume the response.
293
+ Iterator<Map.Entry<String, String>> hit = headers.entrySet().iterator();
294
+ while (hit.hasNext()) {
295
+ Map.Entry<String, String> pair = hit.next();
296
+ if (!pair.getKey().equalsIgnoreCase("Content-Type")) {
297
+ conn.setRequestProperty(pair.getKey(), pair.getValue());
298
+ }
299
+ }
300
+ if (listener != null) listener.onProgress(100);
301
+ return conn.getResponseCode();
302
+ }
254
303
  conn.setDoInput(false);
255
304
  conn.setDoOutput(true);
256
305
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
@@ -258,7 +307,6 @@ public class HttpPostService {
258
307
  } else {
259
308
  conn.setChunkedStreamingMode(0);
260
309
  }
261
- conn.setRequestMethod("POST");
262
310
  conn.setRequestProperty("Content-Type", contentType);
263
311
  Iterator<Map.Entry<String, String>> it = headers.entrySet().iterator();
264
312
  while (it.hasNext()) {
@@ -286,17 +334,29 @@ public class HttpPostService {
286
334
  }
287
335
 
288
336
  public static int postJSON(String url, JSONObject json, Map headers) throws IOException {
289
- HttpPostService service = new HttpPostService(url);
290
- return service.postJSON(json, headers);
337
+ return postJSON(url, json, headers, "POST");
291
338
  }
292
339
 
293
340
  public static int postJSON(String url, JSONArray json, Map headers) throws IOException {
294
- HttpPostService service = new HttpPostService(url);
295
- return service.postJSON(json, headers);
341
+ return postJSON(url, json, headers, "POST");
296
342
  }
297
343
 
298
344
  public static int postJSONFile(String url, File file, Map headers, UploadingProgressListener listener) throws IOException {
299
- HttpPostService service = new HttpPostService(url);
345
+ return postJSONFile(url, file, headers, listener, "POST");
346
+ }
347
+
348
+ public static int postJSON(String url, JSONObject json, Map headers, String method) throws IOException {
349
+ HttpPostService service = new HttpPostService(url, method);
350
+ return service.postJSON(json, headers);
351
+ }
352
+
353
+ public static int postJSON(String url, JSONArray json, Map headers, String method) throws IOException {
354
+ HttpPostService service = new HttpPostService(url, method);
355
+ return service.postJSON(json, headers);
356
+ }
357
+
358
+ public static int postJSONFile(String url, File file, Map headers, UploadingProgressListener listener, String method) throws IOException {
359
+ HttpPostService service = new HttpPostService(url, method);
300
360
  return service.postJSONFile(file, headers, listener);
301
361
  }
302
362
  }
@@ -16,4 +16,30 @@ public interface PluginDelegate {
16
16
  void onAbortRequested();
17
17
  void onHttpAuthorization();
18
18
  void onError(PluginException error);
19
+ /** v3.5 Phase 4: sync queue events. Default no-op so existing implementations keep compiling. */
20
+ default void onSyncStart() {}
21
+ default void onSyncSuccess(int locationsSent) {}
22
+ default void onSyncError(int httpStatus, String message) {}
23
+ default void onSyncProgress(int progress) {}
24
+ default void onHeartbeat(BackgroundLocation location) {}
25
+ // v4.0 Phase 6: driver insights
26
+ default void onTripStart(BackgroundLocation location) {}
27
+ default void onTripEnd(BackgroundLocation location, double distance, long durationMs) {}
28
+ default void onMoving(BackgroundLocation location) {}
29
+ default void onStopped(BackgroundLocation location) {}
30
+ default void onSpeeding(BackgroundLocation location, double speedKmh, double limitKmh) {}
31
+ default void onProviderChange(String provider) {}
32
+ default void onSOS(BackgroundLocation location, org.json.JSONObject payload) {}
33
+ // v4.1 GPS-derived sensor-like events
34
+ default void onHardBrake(BackgroundLocation location, double decelMps2) {}
35
+ default void onRapidAcceleration(BackgroundLocation location, double accelMps2) {}
36
+ default void onSharpTurn(BackgroundLocation location, double degPerSec) {}
37
+ default void onPossibleCrash(BackgroundLocation location, double velocityDropKmh) {}
38
+ /** v4.2: same event, but enriched with the source ("gps" | "sensor") and impact value. */
39
+ default void onPossibleCrash(BackgroundLocation location, double value, String source) {
40
+ // Backward-compat default: forward to the legacy 2-arg overload.
41
+ onPossibleCrash(location, value);
42
+ }
43
+ /** v4.2 sensor fusion: emitted when device interaction is detected during an active trip. */
44
+ default void onPhoneUsageWhileDriving(BackgroundLocation location) {}
19
45
  }
@@ -3,10 +3,12 @@ package com.marianhello.bgloc;
3
3
  import com.marianhello.bgloc.data.BackgroundLocation;
4
4
  import com.marianhello.bgloc.data.LocationDAO;
5
5
  import com.marianhello.bgloc.data.SessionLocationDAO;
6
+ import com.marianhello.bgloc.http.UrlTemplateResolver;
6
7
  import com.marianhello.logging.LoggerManager;
7
8
 
8
9
  import org.json.JSONArray;
9
10
  import org.json.JSONException;
11
+ import org.json.JSONObject;
10
12
 
11
13
  import java.util.concurrent.ExecutorService;
12
14
  import java.util.concurrent.Executors;
@@ -90,6 +92,18 @@ public class PostLocationTask {
90
92
  return;
91
93
  }
92
94
 
95
+ // v3.5 Phase 4: mock location policy. Detection is already in BackgroundLocation
96
+ // (isFromMockProvider). Here we apply the policy.
97
+ if (location != null && location.isFromMockProvider()) {
98
+ String policy = mConfig.getMockLocationPolicy(); // "allow" | "flag" | "drop"
99
+ if ("drop".equals(policy)) {
100
+ logger.info("Mock location dropped (mockLocationPolicy=drop)");
101
+ return;
102
+ }
103
+ // "flag": leave it but caller can read isFromMockProvider() / mocked field.
104
+ // "allow": no-op.
105
+ }
106
+
93
107
  long locationId = mLocationDAO.persistLocation(location);
94
108
  location.setLocationId(locationId);
95
109
 
@@ -153,21 +167,44 @@ public class PostLocationTask {
153
167
 
154
168
  private boolean postLocation(BackgroundLocation location) {
155
169
  logger.debug("Executing PostLocationTask#postLocation");
156
- JSONArray jsonLocations = new JSONArray();
157
170
 
171
+ // LocationTemplate.locationToJson returns Object (JSONObject for HashMapLocationTemplate,
172
+ // JSONArray for ArrayListLocationTemplate). Resolve to the concrete type before calling
173
+ // the matching HttpPostService.postJSON overload.
174
+ Object jsonLocation;
158
175
  try {
159
- jsonLocations.put(mConfig.getTemplate().locationToJson(location));
176
+ jsonLocation = mConfig.getTemplate().locationToJson(location);
160
177
  } catch (JSONException e) {
161
178
  logger.warn("Location to json failed: {}", location.toString());
162
179
  return false;
163
180
  }
164
181
 
165
- String url = mConfig.getUrl();
166
- logger.debug("Posting json to url: {} headers: {}", url, mConfig.getHttpHeaders());
182
+ String urlTemplate = mConfig.getUrl();
183
+ // URL templating: substitute {lat}, {lon}, {timestamp_iso}, {device_id}, ... using the
184
+ // current location plus any static queryParams. For "single" mode this is per-location;
185
+ // for "batch" mode only static queryParams placeholders apply (location-derived ones
186
+ // would not make sense for an array).
187
+ String resolvedUrl = UrlTemplateResolver.resolve(urlTemplate, location, mConfig.getQueryParams());
188
+
189
+ String method = mConfig.getHttpMethod();
190
+ String mode = mConfig.getHttpMode();
191
+ logger.debug("Posting to url: {} method: {} mode: {} headers: {}",
192
+ resolvedUrl, method, mConfig.getHttpHeaders());
167
193
  int responseCode;
168
194
 
169
195
  try {
170
- responseCode = HttpPostService.postJSON(url, jsonLocations, mConfig.getHttpHeaders());
196
+ if ("single".equals(mode) || "GET".equals(method)) {
197
+ // GET cannot carry a JSON array body; force per-location request.
198
+ if (jsonLocation instanceof JSONArray) {
199
+ responseCode = HttpPostService.postJSON(resolvedUrl, (JSONArray) jsonLocation, mConfig.getHttpHeaders(), method);
200
+ } else {
201
+ responseCode = HttpPostService.postJSON(resolvedUrl, (JSONObject) jsonLocation, mConfig.getHttpHeaders(), method);
202
+ }
203
+ } else {
204
+ JSONArray jsonLocations = new JSONArray();
205
+ jsonLocations.put(jsonLocation);
206
+ responseCode = HttpPostService.postJSON(resolvedUrl, jsonLocations, mConfig.getHttpHeaders(), method);
207
+ }
171
208
  } catch (Exception e) {
172
209
  mHasConnectivity = mConnectivityListener.hasConnectivity();
173
210
  logger.warn("Error while posting locations: {}", e.getMessage());