@josuelmm/cordova-background-geolocation 3.0.2 → 3.1.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 (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +114 -8
  3. package/RELEASE.MD +1 -1
  4. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +20 -0
  5. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +32 -2
  6. package/android/CDVBackgroundGeolocation/src/test/java/com/marianhello/ConfigMapperTest.java +12 -0
  7. package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +24 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +87 -0
  9. package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +34 -1
  10. package/android/common/src/main/java/com/marianhello/bgloc/data/LocationDAO.java +5 -0
  11. package/android/common/src/main/java/com/marianhello/bgloc/data/provider/ContentProviderLocationDAO.java +11 -0
  12. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +10 -0
  13. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +24 -0
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +13 -0
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +13 -1
  16. package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +14 -9
  17. package/angular/background-geolocation.service.ts +14 -0
  18. package/angular/dist/background-geolocation.service.d.ts +2 -0
  19. package/angular/dist/esm2022/background-geolocation.service.mjs +7 -1
  20. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +6 -0
  21. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
  22. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.h +2 -0
  23. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +16 -0
  24. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +2 -0
  25. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +22 -0
  26. package/ios/common/BackgroundGeolocation/MAURConfig.h +3 -0
  27. package/ios/common/BackgroundGeolocation/MAURConfig.m +20 -1
  28. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +1 -0
  29. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +1 -0
  30. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +5 -1
  31. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +4 -3
  32. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +16 -9
  33. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +2 -0
  34. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +20 -0
  35. package/package.json +3 -2
  36. package/plugin.xml +1 -1
  37. package/www/BackgroundGeolocation.d.ts +65 -0
  38. package/www/BackgroundGeolocation.js +12 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.1.0](https://github.com/josuelmm/cordova-background-geolocation/tree/3.1.0) (2026-02-21)
4
+
5
+ ### Added
6
+
7
+ - **`getPendingSyncCount(success?, fail?)`** — Returns the number of locations pending to be synced (not yet sent to `syncUrl`). Use with `forceSync()` to show "X locations pending" and let the user trigger sync on demand. (Android, iOS)
8
+ - **`clearSync(success?, fail?)`** — Clears the pending sync queue: discards all locations waiting to be sent to `syncUrl` (they will not be synced). Use when the user wants to discard pending locations (e.g. "Clear queue" button). (Android, iOS)
9
+ - **Config option `sync`** (default `true`) — When `false`, automatic sync and `forceSync()` do not run; locations are still stored and can be synced later by setting `sync: true`. (Android, iOS)
10
+ - **Config options for sync notification texts (Android):** `notificationSyncTitle`, `notificationSyncText`, `notificationSyncCompletedText`, `notificationSyncFailedText` — Customize or localize the notification shown while locations are syncing to the server.
11
+
12
+ ### Fixed
13
+
14
+ - **Android:** `forceSync()` now correctly calls `callbackContext.success()` so the JS Promise resolves.
15
+ - **Android SyncAdapter:** Use `currentSyncConfig` (not out-of-scope `config`) when building sync notifications.
16
+ - **Android & iOS:** When `sync` was never set in config, DB column `sync_enabled` could be NULL; the plugin now treats NULL as “not set” (sync enabled by default) instead of “sync disabled”, so `forceSync()` and automatic sync work when the user did not pass `sync: false`.
17
+ - **Android sync with `Content-Type: application/x-www-form-urlencoded`:** Batch sync was sending one POST with `locations=<url-encoded-json-array>`, which many servers reject (400). Sync now sends **one POST per location** with the same flat `key=value&key=value` format as real-time posting, so the same endpoint accepts both.
18
+
19
+ ### Changed
20
+
21
+ - Version bump to 3.1.0.
22
+
23
+ [Full Changelog](https://github.com/josuelmm/cordova-background-geolocation/compare/3.0.2...3.1.0)
24
+
25
+ ---
26
+
3
27
  ## [3.0.2](https://github.com/josuelmm/cordova-background-geolocation/tree/3.0.2) (2026-02-21)
4
28
 
5
29
  ### Fixed
package/README.md CHANGED
@@ -161,7 +161,31 @@ BackgroundGeolocation.FOREGROUND_MODE
161
161
 
162
162
  ### 1. Configure
163
163
 
164
- Set your preferred provider, accuracy, intervals, and optional server URL:
164
+ Set your preferred provider, accuracy, intervals, and optional server URLs. All options are optional; you can reconfigure later with a subset (options are merged).
165
+
166
+ **Main options:**
167
+
168
+ | Option | Description |
169
+ |--------|-------------|
170
+ | `locationProvider` | `ACTIVITY_PROVIDER`, `DISTANCE_FILTER_PROVIDER`, or `RAW_PROVIDER` |
171
+ | `desiredAccuracy` | `HIGH_ACCURACY`, `MEDIUM_ACCURACY`, `LOW_ACCURACY`, `PASSIVE_ACCURACY` |
172
+ | `distanceFilter` | Minimum metres the device must move before an update (e.g. 50) |
173
+ | `stationaryRadius` | Metres from “stationary” point before aggressive tracking (e.g. 50) |
174
+ | `interval` / `fastestInterval` / `activitiesInterval` | Android timing (ms) |
175
+ | `notificationTitle` / `notificationText` | Android foreground notification text |
176
+ | `url` | Server URL where **each** location is posted immediately (if post fails, it goes to sync queue) |
177
+ | `syncUrl` | Server URL where **pending** locations are sent in batch (when count reaches `syncThreshold` or on `forceSync()`) |
178
+ | `syncThreshold` | Number of pending locations before automatic batch sync (default 100) |
179
+ | `sync` | When `true` (default), automatic sync and `forceSync()` run. When `false`, sync is disabled (locations are still stored). |
180
+ | `httpHeaders` | Headers for every POST (e.g. `{ 'Content-Type': 'application/json', 'Authorization': 'Bearer TOKEN' }`) |
181
+ | `postTemplate` | Object or array of properties to send (use `@latitude`, `@longitude`, etc.). See [Custom post template](docs/api.md#custom-post-template). |
182
+ | `maxLocations` | Max locations kept in DB (default 10000). Should be &gt; `syncThreshold`. |
183
+
184
+ **Android-only:** `notificationSyncTitle`, `notificationSyncText`, `notificationSyncCompletedText`, `notificationSyncFailedText` — texts shown in the notification while syncing (defaults in English; set for localization). `startForeground`, `notificationsEnabled`, `startOnBoot`, `stopOnTerminate`, `enableWatchdog`.
185
+
186
+ **iOS-only:** `activityType`, `pauseLocationUpdates`, `saveBatteryOnBackground`.
187
+
188
+ Example:
165
189
 
166
190
  ```js
167
191
  BackgroundGeolocation.configure({
@@ -176,15 +200,17 @@ BackgroundGeolocation.configure({
176
200
  fastestInterval: 5000,
177
201
  activitiesInterval: 10000,
178
202
  url: 'https://yourserver.com/location',
179
- // HTTP headers can be sent in two ways:
180
- // 1) Static: set httpHeaders here — same headers on every POST/sync request (e.g. API key, Content-Type).
203
+ syncUrl: 'https://yourserver.com/location',
204
+ syncThreshold: 5,
205
+ sync: true,
181
206
  httpHeaders: {
182
- 'X-FOO': 'bar',
207
+ 'Content-Type': 'application/json',
183
208
  'Authorization': 'Bearer YOUR_TOKEN'
184
209
  },
185
210
  postTemplate: {
186
211
  lat: '@latitude',
187
- lon: '@longitude'
212
+ lon: '@longitude',
213
+ timestamp: '@time'
188
214
  }
189
215
  });
190
216
  ```
@@ -224,7 +250,82 @@ BackgroundGeolocation.on('http_authorization', function () {
224
250
  BackgroundGeolocation.stop();
225
251
  ```
226
252
 
227
- More options (stationary, activity, start/stop events, headless task) are in the [documentation](https://josuelmm.github.io/cordova-background-geolocation/). For **Angular** (service, methods, events, example), see [Angular (Ionic Angular)](https://josuelmm.github.io/cordova-background-geolocation/angular).
253
+ ### 5. Sync queue (syncUrl): pending count, force sync, clear queue
254
+
255
+ When you use `syncUrl`, locations that fail to post to `url` (or that are only queued for sync) are sent in batch to `syncUrl`. You can:
256
+
257
+ - **Get pending count** — `getPendingSyncCount()` returns how many locations are waiting to be synced.
258
+ - **Force sync now** — `forceSync()` sends all pending locations immediately (ignores `syncThreshold`). No-op if `sync: false`.
259
+ - **Clear queue** — `clearSync()` discards all pending locations (they will not be sent). Use for a “Clear queue” or “Discard” button.
260
+
261
+ ```js
262
+ // Show "X locations pending" and let user sync or clear
263
+ BackgroundGeolocation.getPendingSyncCount()
264
+ .then(function (count) {
265
+ console.log('Pending to sync:', count);
266
+ // e.g. show UI: "Sync (5)" or "Clear queue"
267
+ });
268
+
269
+ // User taps "Sync now"
270
+ BackgroundGeolocation.forceSync().then(function () {
271
+ console.log('Sync completed');
272
+ });
273
+
274
+ // User taps "Clear queue"
275
+ BackgroundGeolocation.clearSync().then(function () {
276
+ console.log('Queue cleared');
277
+ });
278
+ ```
279
+
280
+ ### 6. Other methods (summary)
281
+
282
+ | Method | Description |
283
+ |--------|-------------|
284
+ | `getConfig(success, fail)` | Get current configuration (merged options). |
285
+ | `getLocations(success, fail)` | Get all stored locations. |
286
+ | `getValidLocations(success, fail)` | Get locations not yet posted (valid only). |
287
+ | `deleteLocation(id, success, fail)` | Delete one location by id. |
288
+ | `deleteAllLocations(success, fail)` | Delete all stored locations. |
289
+ | `getCurrentLocation(success, fail, options)` | One-shot location (e.g. timeout, maximumAge). |
290
+ | `getPluginVersion(success, fail)` | Plugin version string (e.g. "3.1.0"). |
291
+ | `checkStatus(success, fail)` | Service status (isRunning, authorization, etc.). |
292
+ | `showAppSettings()` / `openSettings()` | Open app settings. |
293
+ | `showLocationSettings()` | Open system location settings. |
294
+ | `getLogEntries(limit, fromId, minLevel, success, fail)` | Debug log entries. |
295
+
296
+ All methods return a **Promise** if you omit the `success` / `fail` callbacks.
297
+
298
+ ### 7. Events (summary)
299
+
300
+ Subscribe with `BackgroundGeolocation.on(eventName, callback)`. Unsubscribe with the returned object’s `remove()` or by calling `removeAllListeners(eventName)`.
301
+
302
+ | Event | Payload | When |
303
+ |-------|---------|------|
304
+ | `location` | Location object | New location (foreground/background). |
305
+ | `stationary` | Location | Device stopped (activity provider). |
306
+ | `activity` | Activity type | Activity changed (walking, driving, still). |
307
+ | `error` | `{ code, message }` | Error (e.g. permission, timeout). |
308
+ | `start` | — | Tracking started. |
309
+ | `stop` | — | Tracking stopped. |
310
+ | `authorization` | status | Permission status changed. |
311
+ | `background` / `foreground` | — | App entered background/foreground. |
312
+ | `http_authorization` | — | Server returned 401; refresh token and reconfigure headers. |
313
+ | `abort_requested` | — | Server returned 285 (updates not required). |
314
+
315
+ Full event payloads and options: [Events](docs/events.md). Full API (all options, all methods): [API](docs/api.md).
316
+
317
+ ### New in 3.1.0
318
+
319
+ - **`getPendingSyncCount()`** — Number of locations pending to be synced. Use with `forceSync()` for “X pending” UI.
320
+ - **`forceSync()`** — Sends all pending locations to `syncUrl` immediately. Promise now resolves correctly on Android.
321
+ - **`clearSync()`** — Clears the pending sync queue (discard without sending).
322
+ - **Config `sync`** (default `true`) — Set `sync: false` to disable automatic sync and `forceSync()`; locations are still stored.
323
+ - **Config `notificationSyncTitle`, `notificationSyncText`, `notificationSyncCompletedText`, `notificationSyncFailedText`** (Android) — Customize or localize the notification shown while syncing.
324
+ - **Sync with `Content-Type: application/x-www-form-urlencoded`** — Batch sync now sends **one POST per location** (same flat format as real-time), so the same server endpoint works for both.
325
+
326
+ ---
327
+
328
+ More (stationary, activity, headless task, Angular) is in the [documentation](https://josuelmm.github.io/cordova-background-geolocation/). For **Angular** (service, methods, events), see [docs/angular.md](docs/angular.md).
228
329
 
229
330
  ---
230
331
 
@@ -293,8 +394,13 @@ No extra wrapper (e.g. Awesome Cordova Plugins) is required.
293
394
 
294
395
  ## Documentation and changelog
295
396
 
296
- - [Documentation](https://josuelmm.github.io/cordova-background-geolocation/) (API, options, examples)
297
- - [CHANGELOG](CHANGELOG.md) for version history
397
+ - **[Documentation](https://josuelmm.github.io/cordova-background-geolocation/)** — Full docs (API, options, examples).
398
+ - **[API reference](docs/api.md)** All `configure` options, every method (`configure`, `start`, `stop`, `getPendingSyncCount`, `forceSync`, `clearSync`, `getConfig`, `getLocations`, etc.), TypeScript types.
399
+ - **[HTTP posting](docs/http_posting.md)** — `url` vs `syncUrl`, headers, Content-Type (JSON vs form-urlencoded), sync batch behaviour, `getPendingSyncCount` / `forceSync` / `clearSync`.
400
+ - **[Events](docs/events.md)** — All events (`location`, `error`, `stationary`, `activity`, `http_authorization`, etc.) and payloads.
401
+ - **[Angular / Ionic](docs/angular.md)** — Injectable service, module, same API.
402
+ - **[Example](docs/example.md)** — Full example with events and sync.
403
+ - **[CHANGELOG](CHANGELOG.md)** — Version history.
298
404
 
299
405
  This project is based on [@mauron85/cordova-plugin-background-geolocation](https://github.com/mauron85/cordova-plugin-background-geolocation) and the original by [christocracy](https://github.com/christocracy). Maintained at [josuelmm/cordova-background-geolocation](https://github.com/josuelmm/cordova-background-geolocation). Issues and PRs welcome.
300
406
 
package/RELEASE.MD CHANGED
@@ -1,5 +1,5 @@
1
1
  In order to release a version the following actions are needed:
2
2
  1. Update the version in the package.json, lock and plugin.xml files
3
3
  2. Go to [releases and create a new release](https://github.com/josuelmm/cordova-background-geolocation/releases/new) in GitHub
4
- 3. Make sure to create a tag too with the version name (e.g. v3.0.2)
4
+ 3. Make sure to create a tag too with the version name (e.g. v3.1.0)
5
5
  4. Click create. Github actions will publish the package to npm
@@ -42,6 +42,18 @@ public class ConfigMapper {
42
42
  if (jObject.has("notificationText")) {
43
43
  config.setNotificationText(!jObject.isNull("notificationText") ? jObject.getString("notificationText") : Config.NullString);
44
44
  }
45
+ if (jObject.has("notificationSyncTitle")) {
46
+ config.setNotificationSyncTitle(jObject.isNull("notificationSyncTitle") ? null : jObject.getString("notificationSyncTitle"));
47
+ }
48
+ if (jObject.has("notificationSyncText")) {
49
+ config.setNotificationSyncText(jObject.isNull("notificationSyncText") ? null : jObject.getString("notificationSyncText"));
50
+ }
51
+ if (jObject.has("notificationSyncCompletedText")) {
52
+ config.setNotificationSyncCompletedText(jObject.isNull("notificationSyncCompletedText") ? null : jObject.getString("notificationSyncCompletedText"));
53
+ }
54
+ if (jObject.has("notificationSyncFailedText")) {
55
+ config.setNotificationSyncFailedText(jObject.isNull("notificationSyncFailedText") ? null : jObject.getString("notificationSyncFailedText"));
56
+ }
45
57
  if (jObject.has("stopOnTerminate")) {
46
58
  config.setStopOnTerminate(jObject.getBoolean("stopOnTerminate"));
47
59
  }
@@ -84,6 +96,9 @@ public class ConfigMapper {
84
96
  if (jObject.has("syncThreshold")) {
85
97
  config.setSyncThreshold(jObject.getInt("syncThreshold"));
86
98
  }
99
+ if (jObject.has("sync")) {
100
+ config.setSyncEnabled(jObject.getBoolean("sync"));
101
+ }
87
102
  if (jObject.has("httpHeaders")) {
88
103
  config.setHttpHeaders(jObject.getJSONObject("httpHeaders"));
89
104
  }
@@ -114,6 +129,10 @@ public class ConfigMapper {
114
129
  json.put("notificationsEnabled", config.getNotificationsEnabled());
115
130
  json.put("notificationTitle", config.getNotificationTitle() != Config.NullString ? config.getNotificationTitle() : JSONObject.NULL);
116
131
  json.put("notificationText", config.getNotificationText() != Config.NullString ? config.getNotificationText() : JSONObject.NULL);
132
+ json.put("notificationSyncTitle", config.getNotificationSyncTitle());
133
+ json.put("notificationSyncText", config.getNotificationSyncText());
134
+ json.put("notificationSyncCompletedText", config.getNotificationSyncCompletedText());
135
+ json.put("notificationSyncFailedText", config.getNotificationSyncFailedText());
117
136
  json.put("notificationIconLarge", config.getLargeNotificationIcon() != Config.NullString ? config.getLargeNotificationIcon() : JSONObject.NULL);
118
137
  json.put("notificationIconSmall", config.getSmallNotificationIcon() != Config.NullString ? config.getSmallNotificationIcon() : JSONObject.NULL);
119
138
  json.put("notificationIconColor", config.getNotificationIconColor() != Config.NullString ? config.getNotificationIconColor() : JSONObject.NULL);
@@ -128,6 +147,7 @@ public class ConfigMapper {
128
147
  json.put("url", config.getUrl() != Config.NullString ? config.getUrl() : JSONObject.NULL);
129
148
  json.put("syncUrl", config.getSyncUrl() != Config.NullString ? config.getSyncUrl() : JSONObject.NULL);
130
149
  json.put("syncThreshold", config.getSyncThreshold());
150
+ json.put("sync", config.getSyncEnabled());
131
151
  json.put("httpHeaders", new JSONObject(config.getHttpHeaders()));
132
152
  json.put("maxLocations", config.getMaxLocations());
133
153
  json.put("enableWatchdog", Boolean.TRUE.equals(config.getEnableWatchdog()));
@@ -71,10 +71,12 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
71
71
  public static final String ACTION_END_TASK = "endTask";
72
72
  public static final String ACTION_REGISTER_HEADLESS_TASK = "registerHeadlessTask";
73
73
  public static final String ACTION_FORCE_SYNC = "forceSync";
74
+ public static final String ACTION_CLEAR_SYNC = "clearSync";
75
+ public static final String ACTION_GET_PENDING_SYNC_COUNT = "getPendingSyncCount";
74
76
  public static final String ACTION_GET_PLUGIN_VERSION = "getPluginVersion";
75
77
 
76
78
  /** Plugin version; keep in sync with plugin.xml. */
77
- public static final String PLUGIN_VERSION = "3.0.0";
79
+ public static final String PLUGIN_VERSION = "3.1.0";
78
80
 
79
81
  private BackgroundGeolocationFacade facade;
80
82
 
@@ -367,7 +369,35 @@ public class BackgroundGeolocationPlugin extends CordovaPlugin implements Plugin
367
369
  return true;
368
370
  } else if (ACTION_FORCE_SYNC.equals(action)) {
369
371
  logger.debug("Forced location sync requested");
370
- facade.forceSync();
372
+ runOnWebViewThread(new Runnable() {
373
+ @Override
374
+ public void run() {
375
+ facade.forceSync();
376
+ callbackContext.success();
377
+ }
378
+ });
379
+ return true;
380
+ } else if (ACTION_CLEAR_SYNC.equals(action)) {
381
+ runOnWebViewThread(new Runnable() {
382
+ @Override
383
+ public void run() {
384
+ facade.clearSync();
385
+ callbackContext.success();
386
+ }
387
+ });
388
+ return true;
389
+ } else if (ACTION_GET_PENDING_SYNC_COUNT.equals(action)) {
390
+ runOnWebViewThread(new Runnable() {
391
+ @Override
392
+ public void run() {
393
+ try {
394
+ long count = facade.getPendingSyncCount();
395
+ callbackContext.success((int) Math.min(count, Integer.MAX_VALUE));
396
+ } catch (Exception e) {
397
+ callbackContext.sendPluginResult(ErrorPluginResult.from("getPendingSyncCount failed", e, PluginException.SERVICE_ERROR));
398
+ }
399
+ }
400
+ });
371
401
  return true;
372
402
  } else if (ACTION_GET_PLUGIN_VERSION.equals(action)) {
373
403
  callbackContext.success(PLUGIN_VERSION);
@@ -34,6 +34,10 @@ public class ConfigMapperTest {
34
34
  Assert.assertEquals(config.isDebugging().booleanValue(), jConfig.getBoolean("debug"));
35
35
  Assert.assertEquals(config.getNotificationTitle(), jConfig.getString("notificationTitle"));
36
36
  Assert.assertEquals(config.getNotificationText(), jConfig.getString("notificationText"));
37
+ Assert.assertEquals(config.getNotificationSyncTitle(), jConfig.getString("notificationSyncTitle"));
38
+ Assert.assertEquals(config.getNotificationSyncText(), jConfig.getString("notificationSyncText"));
39
+ Assert.assertEquals(config.getNotificationSyncCompletedText(), jConfig.getString("notificationSyncCompletedText"));
40
+ Assert.assertEquals(config.getNotificationSyncFailedText(), jConfig.getString("notificationSyncFailedText"));
37
41
  Assert.assertEquals(config.getStopOnTerminate().booleanValue(), jConfig.getBoolean("stopOnTerminate"));
38
42
  Assert.assertEquals(config.getStartOnBoot().booleanValue(), jConfig.getBoolean("startOnBoot"));
39
43
  Assert.assertEquals(config.getLocationProvider().intValue(), jConfig.getInt("locationProvider"));
@@ -48,6 +52,7 @@ public class ConfigMapperTest {
48
52
  Assert.assertEquals(config.getUrl(), jConfig.getString("url"));
49
53
  Assert.assertEquals(config.getSyncUrl(), jConfig.getString("syncUrl"));
50
54
  Assert.assertEquals(config.getSyncThreshold().intValue(), jConfig.getInt("syncThreshold"));
55
+ Assert.assertEquals(Boolean.TRUE.equals(config.getSyncEnabled()), jConfig.getBoolean("sync"));
51
56
  Assert.assertEquals(new JSONObject(config.getHttpHeaders()).toString(), jConfig.getJSONObject("httpHeaders").toString());
52
57
  Assert.assertEquals(config.getMaxLocations().intValue(), jConfig.getInt("maxLocations"));
53
58
  Assert.assertEquals(LocationTemplateFactory.getDefault().toString(), jConfig.get("postTemplate").toString());
@@ -90,6 +95,13 @@ public class ConfigMapperTest {
90
95
  Assert.assertFalse(config.hasSmallNotificationIcon());
91
96
  }
92
97
 
98
+ /** When "sync" is not in JSON, getSyncEnabled() must return true (default). */
99
+ @Test
100
+ public void testSyncDefaultWhenNotInJson() throws JSONException {
101
+ Config config = ConfigMapper.fromJSONObject(new JSONObject());
102
+ Assert.assertTrue("sync not in JSON should default to true", config.getSyncEnabled());
103
+ }
104
+
93
105
  @Test
94
106
  public void testNullablePropsToJSONObject() throws JSONException {
95
107
  Config config = new Config();
@@ -407,9 +407,14 @@ public class BackgroundGeolocationFacade {
407
407
  * Force location sync
408
408
  *
409
409
  * Method is ignoring syncThreshold and also user sync settings preference
410
- * and sync locations to defined syncUrl
410
+ * and sync locations to defined syncUrl. No-op if sync is disabled in config (sync: false).
411
411
  */
412
412
  public void forceSync() {
413
+ Config config = getConfig();
414
+ if (!Boolean.TRUE.equals(config.getSyncEnabled())) {
415
+ logger.debug("Sync disabled in config, skipping forceSync");
416
+ return;
417
+ }
413
418
  logger.debug("Sync locations forced");
414
419
  ResourceResolver resolver = ResourceResolver.newInstance(getContext());
415
420
  Account syncAccount = AccountHelper.CreateSyncAccount(getContext(), resolver.getAccountName(),
@@ -417,6 +422,24 @@ public class BackgroundGeolocationFacade {
417
422
  SyncService.sync(syncAccount, resolver.getAuthority(), true);
418
423
  }
419
424
 
425
+ /**
426
+ * Returns the number of locations pending to be synced (not yet sent to syncUrl).
427
+ */
428
+ public long getPendingSyncCount() {
429
+ LocationDAO dao = DAOFactory.createLocationDAO(getContext());
430
+ return dao.getLocationsForSyncCount(Long.MAX_VALUE);
431
+ }
432
+
433
+ /**
434
+ * Clear the pending sync queue: mark all locations waiting to be synced as deleted.
435
+ * They will not be sent to syncUrl. Use when the user wants to discard pending locations.
436
+ */
437
+ public void clearSync() {
438
+ LocationDAO dao = DAOFactory.createLocationDAO(getContext());
439
+ int count = dao.deletePendingSyncLocations();
440
+ logger.debug("Cleared {} pending sync locations", count);
441
+ }
442
+
420
443
  public int getAuthorizationStatus() {
421
444
  return hasPermissions() ? AUTHORIZATION_AUTHORIZED : AUTHORIZATION_DENIED;
422
445
  }
@@ -45,6 +45,10 @@ public class Config implements Parcelable
45
45
  private Boolean debug;
46
46
  private String notificationTitle;
47
47
  private String notificationText;
48
+ private String notificationSyncTitle;
49
+ private String notificationSyncText;
50
+ private String notificationSyncCompletedText;
51
+ private String notificationSyncFailedText;
48
52
  private String notificationIconLarge;
49
53
  private String notificationIconSmall;
50
54
  private String notificationIconColor;
@@ -60,6 +64,7 @@ public class Config implements Parcelable
60
64
  private String url;
61
65
  private String syncUrl;
62
66
  private Integer syncThreshold;
67
+ private Boolean syncEnabled;
63
68
  private HashMap httpHeaders;
64
69
  private Integer maxLocations;
65
70
  private LocationTemplate template;
@@ -76,6 +81,10 @@ public class Config implements Parcelable
76
81
  this.debug = config.debug;
77
82
  this.notificationTitle = config.notificationTitle;
78
83
  this.notificationText = config.notificationText;
84
+ this.notificationSyncTitle = config.notificationSyncTitle;
85
+ this.notificationSyncText = config.notificationSyncText;
86
+ this.notificationSyncCompletedText = config.notificationSyncCompletedText;
87
+ this.notificationSyncFailedText = config.notificationSyncFailedText;
79
88
  this.notificationIconLarge = config.notificationIconLarge;
80
89
  this.notificationIconSmall = config.notificationIconSmall;
81
90
  this.notificationIconColor = config.notificationIconColor;
@@ -91,6 +100,7 @@ public class Config implements Parcelable
91
100
  this.url = config.url;
92
101
  this.syncUrl = config.syncUrl;
93
102
  this.syncThreshold = config.syncThreshold;
103
+ this.syncEnabled = config.syncEnabled;
94
104
  this.httpHeaders = CloneHelper.deepCopy(config.httpHeaders);
95
105
  this.maxLocations = config.maxLocations;
96
106
  this.enableWatchdog = config.enableWatchdog;
@@ -106,6 +116,10 @@ public class Config implements Parcelable
106
116
  setDebugging((Boolean) in.readValue(null));
107
117
  setNotificationTitle(in.readString());
108
118
  setNotificationText(in.readString());
119
+ setNotificationSyncTitle(in.readString());
120
+ setNotificationSyncText(in.readString());
121
+ setNotificationSyncCompletedText(in.readString());
122
+ setNotificationSyncFailedText(in.readString());
109
123
  setLargeNotificationIcon(in.readString());
110
124
  setSmallNotificationIcon(in.readString());
111
125
  setNotificationIconColor(in.readString());
@@ -121,6 +135,7 @@ public class Config implements Parcelable
121
135
  setUrl(in.readString());
122
136
  setSyncUrl(in.readString());
123
137
  setSyncThreshold(in.readInt());
138
+ setSyncEnabled((Boolean) in.readValue(null));
124
139
  setMaxLocations(in.readInt());
125
140
  setEnableWatchdog((Boolean) in.readValue(null));
126
141
  Bundle bundle = in.readBundle();
@@ -136,6 +151,10 @@ public class Config implements Parcelable
136
151
  config.debug = false;
137
152
  config.notificationTitle = "Background tracking";
138
153
  config.notificationText = "ENABLED";
154
+ config.notificationSyncTitle = "Syncing locations";
155
+ config.notificationSyncText = "Sync in progress";
156
+ config.notificationSyncCompletedText = "Sync completed";
157
+ config.notificationSyncFailedText = "Sync failed";
139
158
  config.notificationIconLarge = "";
140
159
  config.notificationIconSmall = "";
141
160
  config.notificationIconColor = "";
@@ -151,6 +170,7 @@ public class Config implements Parcelable
151
170
  config.url = "";
152
171
  config.syncUrl = "";
153
172
  config.syncThreshold = 100;
173
+ config.syncEnabled = true;
154
174
  config.httpHeaders = null;
155
175
  config.maxLocations = 10000;
156
176
  config.template = null;
@@ -171,6 +191,10 @@ public class Config implements Parcelable
171
191
  out.writeValue(isDebugging());
172
192
  out.writeString(getNotificationTitle());
173
193
  out.writeString(getNotificationText());
194
+ out.writeString(getNotificationSyncTitle());
195
+ out.writeString(getNotificationSyncText());
196
+ out.writeString(getNotificationSyncCompletedText());
197
+ out.writeString(getNotificationSyncFailedText());
174
198
  out.writeString(getLargeNotificationIcon());
175
199
  out.writeString(getSmallNotificationIcon());
176
200
  out.writeString(getNotificationIconColor());
@@ -186,6 +210,7 @@ public class Config implements Parcelable
186
210
  out.writeString(getUrl());
187
211
  out.writeString(getSyncUrl());
188
212
  out.writeInt(getSyncThreshold());
213
+ out.writeValue(getSyncEnabled());
189
214
  out.writeInt(getMaxLocations());
190
215
  out.writeValue(getEnableWatchdog());
191
216
  Bundle bundle = new Bundle();
@@ -293,6 +318,38 @@ public class Config implements Parcelable
293
318
  this.notificationText = notificationText;
294
319
  }
295
320
 
321
+ public String getNotificationSyncTitle() {
322
+ return notificationSyncTitle != null ? notificationSyncTitle : "Syncing locations";
323
+ }
324
+
325
+ public void setNotificationSyncTitle(String notificationSyncTitle) {
326
+ this.notificationSyncTitle = notificationSyncTitle;
327
+ }
328
+
329
+ public String getNotificationSyncText() {
330
+ return notificationSyncText != null ? notificationSyncText : "Sync in progress";
331
+ }
332
+
333
+ public void setNotificationSyncText(String notificationSyncText) {
334
+ this.notificationSyncText = notificationSyncText;
335
+ }
336
+
337
+ public String getNotificationSyncCompletedText() {
338
+ return notificationSyncCompletedText != null ? notificationSyncCompletedText : "Sync completed";
339
+ }
340
+
341
+ public void setNotificationSyncCompletedText(String notificationSyncCompletedText) {
342
+ this.notificationSyncCompletedText = notificationSyncCompletedText;
343
+ }
344
+
345
+ public String getNotificationSyncFailedText() {
346
+ return notificationSyncFailedText != null ? notificationSyncFailedText : "Sync failed";
347
+ }
348
+
349
+ public void setNotificationSyncFailedText(String notificationSyncFailedText) {
350
+ this.notificationSyncFailedText = notificationSyncFailedText;
351
+ }
352
+
296
353
  public boolean hasLargeNotificationIcon() {
297
354
  return notificationIconLarge != null && !notificationIconLarge.isEmpty();
298
355
  }
@@ -468,6 +525,20 @@ public class Config implements Parcelable
468
525
  this.syncThreshold = syncThreshold;
469
526
  }
470
527
 
528
+ public boolean hasSyncEnabled() {
529
+ return syncEnabled != null;
530
+ }
531
+
532
+ /** Whether synchronization to syncUrl is enabled. Default true. */
533
+ @Nullable
534
+ public Boolean getSyncEnabled() {
535
+ return syncEnabled != null ? syncEnabled : true;
536
+ }
537
+
538
+ public void setSyncEnabled(@Nullable Boolean syncEnabled) {
539
+ this.syncEnabled = syncEnabled;
540
+ }
541
+
471
542
  public boolean hasHttpHeaders() {
472
543
  return httpHeaders != null;
473
544
  }
@@ -562,6 +633,7 @@ public class Config implements Parcelable
562
633
  .append(" url=").append(getUrl())
563
634
  .append(" syncUrl=").append(getSyncUrl())
564
635
  .append(" syncThreshold=").append(getSyncThreshold())
636
+ .append(" syncEnabled=").append(getSyncEnabled())
565
637
  .append(" httpHeaders=").append(getHttpHeaders().toString())
566
638
  .append(" maxLocations=").append(getMaxLocations())
567
639
  .append(" postTemplate=").append(hasTemplate() ? getTemplate().toString() : null)
@@ -603,6 +675,18 @@ public class Config implements Parcelable
603
675
  if (config2.hasNotificationText()) {
604
676
  merger.setNotificationText(config2.getNotificationText());
605
677
  }
678
+ if (config2.notificationSyncTitle != null) {
679
+ merger.setNotificationSyncTitle(config2.getNotificationSyncTitle());
680
+ }
681
+ if (config2.notificationSyncText != null) {
682
+ merger.setNotificationSyncText(config2.getNotificationSyncText());
683
+ }
684
+ if (config2.notificationSyncCompletedText != null) {
685
+ merger.setNotificationSyncCompletedText(config2.getNotificationSyncCompletedText());
686
+ }
687
+ if (config2.notificationSyncFailedText != null) {
688
+ merger.setNotificationSyncFailedText(config2.getNotificationSyncFailedText());
689
+ }
606
690
  if (config2.hasStopOnTerminate()) {
607
691
  merger.setStopOnTerminate(config2.getStopOnTerminate());
608
692
  }
@@ -648,6 +732,9 @@ public class Config implements Parcelable
648
732
  if (config2.hasSyncThreshold()) {
649
733
  merger.setSyncThreshold(config2.getSyncThreshold());
650
734
  }
735
+ if (config2.hasSyncEnabled()) {
736
+ merger.setSyncEnabled(config2.getSyncEnabled());
737
+ }
651
738
  if (config2.hasHttpHeaders()) {
652
739
  merger.setHttpHeaders(config2.getHttpHeaders());
653
740
  }
@@ -203,11 +203,44 @@ public class HttpPostService {
203
203
  }
204
204
  stream.close();
205
205
  byte[] bodyBytes = baos.toByteArray();
206
+ String jsonString = new String(bodyBytes, StandardCharsets.UTF_8);
207
+
208
+ // When form-urlencoded and body is a JSON array, send one POST per location (same flat
209
+ // format as real-time posting) so the same server endpoint accepts both.
210
+ if (isFormUrlEncoded) {
211
+ try {
212
+ Object parsed = new JSONTokener(jsonString).nextValue();
213
+ if (parsed instanceof JSONArray) {
214
+ JSONArray arr = (JSONArray) parsed;
215
+ int len = arr.length();
216
+ if (len == 0) {
217
+ if (listener != null) listener.onProgress(100);
218
+ return 200;
219
+ }
220
+ for (int i = 0; i < len; i++) {
221
+ JSONObject item = arr.getJSONObject(i);
222
+ HttpPostService perRequest = new HttpPostService(mUrl);
223
+ int code = perRequest.postJSON(item, headers);
224
+ if (listener != null && len > 0) {
225
+ listener.onProgress((i + 1) * 100 / len);
226
+ }
227
+ if (code < 200 || code >= 300) {
228
+ return code;
229
+ }
230
+ }
231
+ if (listener != null) {
232
+ listener.onProgress(100);
233
+ }
234
+ return 200;
235
+ }
236
+ } catch (Exception e) {
237
+ // Fall through to single-POST with jsonToUrlEncoded (e.g. array wrap)
238
+ }
239
+ }
206
240
 
207
241
  byte[] outputBytes;
208
242
  if (isFormUrlEncoded) {
209
243
  try {
210
- String jsonString = new String(bodyBytes, StandardCharsets.UTF_8);
211
244
  String formBody = jsonToUrlEncoded(jsonString);
212
245
  outputBytes = formBody.getBytes(StandardCharsets.UTF_8);
213
246
  } catch (Exception e) {
@@ -19,4 +19,9 @@ public interface LocationDAO {
19
19
  BackgroundLocation deleteFirstUnpostedLocation();
20
20
  int deleteAllLocations();
21
21
  int deleteUnpostedLocations();
22
+ /**
23
+ * Delete (mark as deleted) all locations that are pending sync to syncUrl.
24
+ * Same effect as discarding the pending sync queue without sending to server.
25
+ */
26
+ int deletePendingSyncLocations();
22
27
  }
@@ -392,4 +392,15 @@ public class ContentProviderLocationDAO implements LocationDAO {
392
392
 
393
393
  return mResolver.update(mContentUri, values, whereClause, whereArgs);
394
394
  }
395
+
396
+ @Override
397
+ public int deletePendingSyncLocations() {
398
+ ContentValues values = new ContentValues();
399
+ values.put(LocationEntry.COLUMN_NAME_STATUS, BackgroundLocation.DELETED);
400
+
401
+ String whereClause = LocationEntry.COLUMN_NAME_STATUS + " = ?";
402
+ String[] whereArgs = { String.valueOf(BackgroundLocation.SYNC_PENDING) };
403
+
404
+ return mResolver.update(mContentUri, values, whereClause, whereArgs);
405
+ }
395
406
  }