@josuelmm/cordova-background-geolocation 4.2.2 → 4.5.1

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 (86) hide show
  1. package/.npmignore +11 -0
  2. package/CHANGELOG.md +213 -0
  3. package/HISTORY.md +73 -0
  4. package/README.md +45 -74
  5. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +24 -0
  6. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +61 -1
  7. package/android/common/src/main/AndroidManifest.xml +1 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +6 -3
  9. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +65 -1
  10. package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +1 -1
  11. package/android/common/src/main/java/com/marianhello/bgloc/data/BackgroundLocation.java +94 -0
  12. package/android/common/src/main/java/com/marianhello/bgloc/data/ConfigJsonMapper.java +205 -0
  13. package/android/common/src/main/java/com/marianhello/bgloc/data/LocationTemplateFactory.java +6 -0
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +5 -1
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +32 -1
  16. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationContract.java +12 -2
  17. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +33 -2
  18. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +15 -1
  19. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +23 -8
  20. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +246 -21
  21. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +5 -2
  22. package/android/common/src/main/java/com/marianhello/bgloc/sync/BatchManager.java +46 -13
  23. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +23 -1
  24. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +111 -5
  25. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +20 -0
  26. package/ios/common/BackgroundGeolocation/MAURConfig.h +2 -0
  27. package/ios/common/BackgroundGeolocation/MAURConfig.m +16 -2
  28. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +3 -0
  29. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +3 -1
  30. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +15 -1
  31. package/ios/common/BackgroundGeolocation/MAURLocation.h +12 -0
  32. package/ios/common/BackgroundGeolocation/MAURLocation.m +33 -4
  33. package/ios/common/BackgroundGeolocation/MAURLocationContract.h +4 -0
  34. package/ios/common/BackgroundGeolocation/MAURLocationContract.m +5 -1
  35. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +9 -0
  36. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +59 -1
  37. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +54 -4
  38. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +12 -0
  39. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +125 -5
  40. package/package.json +36 -1
  41. package/plugin.xml +3 -2
  42. package/www/BackgroundGeolocation.d.ts +114 -3
  43. package/www/BackgroundGeolocation.js +11 -4
  44. package/CLAUDE.md +0 -56
  45. package/android/CDVBackgroundGeolocation/src/test/java/com/marianhello/ConfigMapperTest.java +0 -220
  46. package/android/common/src/androidTest/java/com/marianhello/bgloc/BackgroundGeolocationFacadeTest.java +0 -45
  47. package/android/common/src/androidTest/java/com/marianhello/bgloc/BatchManagerTest.java +0 -570
  48. package/android/common/src/androidTest/java/com/marianhello/bgloc/ConfigTest.java +0 -76
  49. package/android/common/src/androidTest/java/com/marianhello/bgloc/ContentProviderLocationDAOTest.java +0 -437
  50. package/android/common/src/androidTest/java/com/marianhello/bgloc/DBLogReaderTest.java +0 -95
  51. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationContentProviderTest.java +0 -159
  52. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceProxyTest.java +0 -161
  53. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceTest.java +0 -247
  54. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteConfigurationDAOTest.java +0 -200
  55. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOTest.java +0 -457
  56. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOThreadTest.java +0 -96
  57. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteOpenHelperTest.java +0 -225
  58. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestPluginDelegate.java +0 -46
  59. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestResourceResolver.java +0 -14
  60. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/MockLocationProvider.java +0 -50
  61. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/TestLocationProviderFactory.java +0 -17
  62. package/android/common/src/androidTest/java/com/marianhello/bgloc/sqlite/SQLiteOpenHelper10.java +0 -92
  63. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/LocationProviderTestCase.java +0 -107
  64. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/TestConstants.java +0 -5
  65. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ArrayListLocationTemplateTest.java +0 -82
  66. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/BackgroundLocationTest.java +0 -128
  67. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ConfigTest.java +0 -191
  68. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/DBLogReaderTest.java +0 -37
  69. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HashMapLocationTemplateTest.java +0 -216
  70. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HttpPostServiceTest.java +0 -223
  71. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/LocationTemplateFactoryTest.java +0 -50
  72. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/PostLocationTaskTest.java +0 -180
  73. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/TestHelper.java +0 -16
  74. package/ios/common/BackgroundGeolocationTests/Info.plist +0 -24
  75. package/ios/common/BackgroundGeolocationTests/MAURBackgroundLocationTest.m +0 -185
  76. package/ios/common/BackgroundGeolocationTests/MAURConfigTest.m +0 -161
  77. package/ios/common/BackgroundGeolocationTests/MAURGeolocationOpenHelperTest.m +0 -102
  78. package/ios/common/BackgroundGeolocationTests/MAURLocationTest.m +0 -216
  79. package/ios/common/BackgroundGeolocationTests/MAURLocationUploaderTest.m +0 -55
  80. package/ios/common/BackgroundGeolocationTests/MAURLogReaderTest.m +0 -43
  81. package/ios/common/BackgroundGeolocationTests/MAURSQLiteConfigurationDAOTest.m +0 -102
  82. package/ios/common/BackgroundGeolocationTests/MAURSQLiteHelperTest.m +0 -41
  83. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOTests.m +0 -240
  84. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOThreadTest.m +0 -84
  85. package/ios/common/BackgroundGeolocationTests/MAURSQLiteOpenHelperTest.m +0 -144
  86. package/ios/common/scripts/xcode-refactor.js +0 -184
@@ -188,12 +188,22 @@ public class BatchManager {
188
188
  }
189
189
 
190
190
  private void writeValue(Object value) throws IOException {
191
- if (value instanceof String ) {
191
+ if (value == null || value == JSONObject.NULL) {
192
+ writer.nullValue();
193
+ } else if (value instanceof String ) {
192
194
  writer.value((String) value);
193
195
  } else if (value instanceof Map) {
194
196
  writeMap((Map) value);
195
197
  } else if (value instanceof List) {
196
198
  writeList((List) value);
199
+ // v4.5.1 — handle JSONArray / JSONObject so that placeholders that resolve to
200
+ // structured data (@events → JSONArray of events) are written as real JSON arrays /
201
+ // objects, not as escaped strings. Without this, a fix coming out of the sync queue
202
+ // serialised "events" as "[{\"type\":\"hardBrake\"}]" literal.
203
+ } else if (value instanceof org.json.JSONArray) {
204
+ writeJsonArray((org.json.JSONArray) value);
205
+ } else if (value instanceof org.json.JSONObject) {
206
+ writeJsonObject((org.json.JSONObject) value);
197
207
  } else if (Integer.class.isInstance(value)) {
198
208
  writer.value((Integer) value);
199
209
  } else if (Double.class.isInstance(value)) {
@@ -204,13 +214,44 @@ public class BatchManager {
204
214
  writer.value((Long) value);
205
215
  } else if (Boolean.class.isInstance(value)) {
206
216
  writer.value((Boolean) value);
207
- } else if (value == JSONObject.NULL) {
208
- writer.nullValue();
209
217
  } else {
210
218
  writer.value(String.valueOf(value));
211
219
  }
212
220
  }
213
221
 
222
+ private void writeJsonArray(org.json.JSONArray arr) throws IOException {
223
+ writer.beginArray();
224
+ for (int i = 0; i < arr.length(); i++) {
225
+ Object v = arr.opt(i);
226
+ writeValue(v == null ? JSONObject.NULL : v);
227
+ }
228
+ writer.endArray();
229
+ }
230
+
231
+ private void writeJsonObject(org.json.JSONObject obj) throws IOException {
232
+ writer.beginObject();
233
+ Iterator<String> keys = obj.keys();
234
+ while (keys.hasNext()) {
235
+ String k = keys.next();
236
+ writer.name(k);
237
+ writeValue(obj.opt(k));
238
+ }
239
+ writer.endObject();
240
+ }
241
+
242
+ /** v4.5.1 — resolve a template string. If it is a placeholder ("@foo") and the location
243
+ * has no value, return JSONObject.NULL so the writer emits `null` instead of the literal
244
+ * "@foo" string. */
245
+ private Object resolveTemplateValue(Object value) {
246
+ if (value instanceof String) {
247
+ String s = (String) value;
248
+ Object resolved = location.getValueForKey(s);
249
+ if (resolved != null) return resolved;
250
+ if (s.startsWith("@")) return JSONObject.NULL;
251
+ }
252
+ return value;
253
+ }
254
+
214
255
  public void writeMap(Map values) throws IOException {
215
256
  writer.beginObject();
216
257
  Iterator<?> it = values.entrySet().iterator();
@@ -218,12 +259,8 @@ public class BatchManager {
218
259
  Map.Entry<String, Object> pair = (Map.Entry) it.next();
219
260
  String key = pair.getKey();
220
261
  Object value = pair.getValue();
221
- Object locationValue = null;
222
- if (value instanceof String) {
223
- locationValue = location.getValueForKey((String)value);
224
- }
225
262
  writer.name(key);
226
- writeValue(locationValue != null ? locationValue : value);
263
+ writeValue(resolveTemplateValue(value));
227
264
  }
228
265
  writer.endObject();
229
266
  }
@@ -233,11 +270,7 @@ public class BatchManager {
233
270
  Iterator<?> it = values.iterator();
234
271
  while (it.hasNext()) {
235
272
  Object value = it.next();
236
- Object locationValue = null;
237
- if (value instanceof String) {
238
- locationValue = location.getValueForKey((String) value);
239
- }
240
- writeValue(locationValue != null ? locationValue : value);
273
+ writeValue(resolveTemplateValue(value));
241
274
  }
242
275
  writer.endArray();
243
276
  }
@@ -135,6 +135,28 @@ static NSString * const TAG = @"CDVBackgroundGeolocation";
135
135
  callbackId:command.callbackId];
136
136
  }
137
137
 
138
+ // v4.5: runtime permission helpers — paridad de API con Android. iOS no expone gates
139
+ // separados para background location / activity recognition / notifications, así que
140
+ // resolvemos siempre con notRequired:YES.
141
+ - (void) requestBackgroundLocationPermission:(CDVInvokedUrlCommand *)command
142
+ {
143
+ [self sendNotRequiredPermissionResult:command];
144
+ }
145
+ - (void) requestActivityRecognitionPermission:(CDVInvokedUrlCommand *)command
146
+ {
147
+ [self sendNotRequiredPermissionResult:command];
148
+ }
149
+ - (void) requestNotificationPermission:(CDVInvokedUrlCommand *)command
150
+ {
151
+ [self sendNotRequiredPermissionResult:command];
152
+ }
153
+ - (void) sendNotRequiredPermissionResult:(CDVInvokedUrlCommand *)command
154
+ {
155
+ NSDictionary *r = @{ @"granted": @YES, @"notRequired": @YES };
156
+ [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:r]
157
+ callbackId:command.callbackId];
158
+ }
159
+
138
160
  // v4.1 GPS-derived sensor-like events
139
161
  - (void) sendDrivingEventN:(NSString *)name note:(NSNotification *)note
140
162
  {
@@ -513,7 +535,7 @@ static NSString * const TAG = @"CDVBackgroundGeolocation";
513
535
  - (void) getPluginVersion:(CDVInvokedUrlCommand*)command
514
536
  {
515
537
  NSLog(@"%@ #%@", TAG, @"getPluginVersion");
516
- NSString *version = @"4.2.2"; // keep in sync with plugin.xml and Android PLUGIN_VERSION
538
+ NSString *version = @"4.5.1"; // keep in sync with plugin.xml and Android PLUGIN_VERSION
517
539
  CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:version];
518
540
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
519
541
  }
@@ -104,6 +104,8 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
104
104
  NSTimeInterval drLastHardBrakeAt, drLastRapidAccelAt, drLastSharpTurnAt, drLastCrashAt;
105
105
  // v4.2 sensor fusion
106
106
  MAURSensorFusionDetector *sensorFusion;
107
+ // v4.3 — events buffered when no simultaneous fix is available; drained onto next location.
108
+ NSMutableArray *pendingDrivingEvents;
107
109
  }
108
110
 
109
111
 
@@ -132,12 +134,27 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
132
134
 
133
135
  postLocationTask = [[MAURPostLocationTask alloc] init];
134
136
  postLocationTask.delegate = self;
135
-
137
+
136
138
  localNotification = [[UILocalNotification alloc] init];
137
139
  localNotification.timeZone = [NSTimeZone defaultTimeZone];
138
-
140
+
139
141
  isStarted = NO;
140
-
142
+ pendingDrivingEvents = [[NSMutableArray alloc] init];
143
+
144
+ // v4.5.1 — wire pending events + battery snapshot into the post task so they run AFTER
145
+ // any locationTransform that may produce a new instance / return nil. The previous flow
146
+ // (flush BEFORE add:) lost buffered events whenever the transform returned nil.
147
+ postLocationTask.pendingDrivingEventsBuffer = pendingDrivingEvents;
148
+ __weak typeof(self) weakSelf = self;
149
+ postLocationTask.attachBatterySnapshot = ^(MAURLocation * _Nonnull loc) {
150
+ __strong typeof(self) strongSelf = weakSelf;
151
+ if (strongSelf == nil) return;
152
+ BOOL includeBat = (strongSelf->_config == nil
153
+ || strongSelf->_config.includeBattery == nil
154
+ || [strongSelf->_config.includeBattery boolValue]);
155
+ if (includeBat) [strongSelf attachBatterySnapshotTo:loc];
156
+ };
157
+
141
158
  return self;
142
159
  }
143
160
 
@@ -401,6 +418,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
401
418
  // MAURSensorFusionListener
402
419
  - (void) onSensorCrashWithImpactG:(double)impactG location:(MAURLocation *)location
403
420
  {
421
+ [self bufferPendingEvent:@"possibleCrash" extra:@{@"value": @(impactG), @"source": @"sensor"}];
404
422
  NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
405
423
  if (location != nil) userInfo[@"location"] = location;
406
424
  userInfo[@"value"] = @(impactG);
@@ -411,6 +429,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
411
429
  }
412
430
  - (void) onPhoneUsageWhileDriving:(MAURLocation *)location
413
431
  {
432
+ [self bufferPendingEvent:@"phoneUsageWhileDriving" extra:nil];
414
433
  NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
415
434
  if (location != nil) userInfo[@"location"] = location;
416
435
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURPhoneUsageWhileDrivingNotification
@@ -418,6 +437,77 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
418
437
  userInfo:userInfo];
419
438
  }
420
439
 
440
+ // v4.3 — driving event helpers
441
+ - (void) attachDrivingEvent:(NSString *)type to:(MAURLocation *)loc extra:(NSDictionary *)extra
442
+ {
443
+ if (loc == nil || type == nil) return;
444
+ NSMutableDictionary *ev = [NSMutableDictionary dictionary];
445
+ ev[@"type"] = type;
446
+ ev[@"time"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000.0));
447
+ if (extra != nil) [ev addEntriesFromDictionary:extra];
448
+ if (loc.drivingEvents == nil) loc.drivingEvents = [NSMutableArray array];
449
+ [loc.drivingEvents addObject:ev];
450
+ }
451
+
452
+ // v4.4.1 — pending driving events: cap + TTL.
453
+ static NSInteger const kPendingDrivingEventsMax = 20;
454
+ static NSTimeInterval const kPendingDrivingEventsTTLMs = 60000.0;
455
+
456
+ - (void) bufferPendingEvent:(NSString *)type extra:(NSDictionary *)extra
457
+ {
458
+ if (type == nil) return;
459
+ NSMutableDictionary *ev = [NSMutableDictionary dictionary];
460
+ ev[@"type"] = type;
461
+ ev[@"time"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000.0));
462
+ if (extra != nil) [ev addEntriesFromDictionary:extra];
463
+ @synchronized (pendingDrivingEvents) {
464
+ while ((NSInteger)[pendingDrivingEvents count] >= kPendingDrivingEventsMax) {
465
+ [pendingDrivingEvents removeObjectAtIndex:0];
466
+ }
467
+ [pendingDrivingEvents addObject:ev];
468
+ }
469
+ }
470
+
471
+ - (void) flushPendingDrivingEventsTo:(MAURLocation *)loc
472
+ {
473
+ if (loc == nil) return;
474
+ NSTimeInterval nowMs = [[NSDate date] timeIntervalSince1970] * 1000.0;
475
+ @synchronized (pendingDrivingEvents) {
476
+ if ([pendingDrivingEvents count] == 0) return;
477
+ if (loc.drivingEvents == nil) loc.drivingEvents = [NSMutableArray array];
478
+ for (NSDictionary *ev in pendingDrivingEvents) {
479
+ NSNumber *t = ev[@"time"];
480
+ NSTimeInterval evMs = t != nil ? [t doubleValue] : nowMs;
481
+ if (nowMs - evMs <= kPendingDrivingEventsTTLMs) {
482
+ [loc.drivingEvents addObject:ev];
483
+ }
484
+ }
485
+ [pendingDrivingEvents removeAllObjects];
486
+ }
487
+ }
488
+
489
+ // v4.4 — read device battery via UIDevice. Calling main thread; safe from any thread.
490
+ - (void) attachBatterySnapshotTo:(MAURLocation *)loc
491
+ {
492
+ if (loc == nil) return;
493
+ void (^read)(void) = ^{
494
+ UIDevice *device = [UIDevice currentDevice];
495
+ if (!device.batteryMonitoringEnabled) device.batteryMonitoringEnabled = YES;
496
+ float lvl = device.batteryLevel; // 0.0 - 1.0, or -1 if unknown
497
+ if (lvl >= 0) {
498
+ loc.batteryLevel = @((int) round(lvl * 100.0));
499
+ }
500
+ UIDeviceBatteryState state = device.batteryState;
501
+ BOOL charging = (state == UIDeviceBatteryStateCharging || state == UIDeviceBatteryStateFull);
502
+ loc.isCharging = @(charging);
503
+ };
504
+ if ([NSThread isMainThread]) {
505
+ read();
506
+ } else {
507
+ dispatch_sync(dispatch_get_main_queue(), read);
508
+ }
509
+ }
510
+
421
511
  - (void) drivingDetectorFeed:(MAURLocation *)loc
422
512
  {
423
513
  if (loc == nil || _config == nil) return;
@@ -446,6 +536,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
446
536
  NSString *provider = loc.provider;
447
537
  if (provider != nil && ![provider isEqualToString:drLastProvider]) {
448
538
  drLastProvider = provider;
539
+ [self attachDrivingEvent:@"providerChange" to:loc extra:@{@"provider": provider}];
449
540
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURProviderChangeNotification
450
541
  object:self
451
542
  userInfo:@{@"provider": provider}];
@@ -465,6 +556,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
465
556
  drBelowMovingSince = 0;
466
557
  if (!drIsMoving) {
467
558
  drIsMoving = YES;
559
+ [self attachDrivingEvent:@"moving" to:loc extra:nil];
468
560
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURMovingNotification
469
561
  object:self
470
562
  userInfo:@{@"location": loc}];
@@ -476,6 +568,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
476
568
  drTripActive = YES;
477
569
  drTripStartedAt = now;
478
570
  drTripDistanceMeters = 0;
571
+ [self attachDrivingEvent:@"tripStart" to:loc extra:nil];
479
572
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURTripStartNotification
480
573
  object:self
481
574
  userInfo:@{@"location": loc}];
@@ -490,6 +583,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
490
583
  if (drBelowMovingSince == 0) drBelowMovingSince = now;
491
584
  if (drIsMoving && (now - drBelowMovingSince) >= stoppedDuration) {
492
585
  drIsMoving = NO;
586
+ [self attachDrivingEvent:@"stopped" to:loc extra:nil];
493
587
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURStoppedNotification
494
588
  object:self
495
589
  userInfo:@{@"location": loc}];
@@ -497,6 +591,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
497
591
  NSTimeInterval durMs = (now - drTripStartedAt) * 1000.0;
498
592
  double dist = drTripDistanceMeters;
499
593
  drTripActive = NO;
594
+ [self attachDrivingEvent:@"tripEnd" to:loc extra:@{@"distance": @(dist), @"durationMs": @((long long)durMs)}];
500
595
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURTripEndNotification
501
596
  object:self
502
597
  userInfo:@{
@@ -514,6 +609,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
514
609
  if (kmh > speedLimit) {
515
610
  if (!drWasSpeeding) {
516
611
  drWasSpeeding = YES;
612
+ [self attachDrivingEvent:@"speeding" to:loc extra:@{@"speedKmh": @(kmh), @"limitKmh": @(speedLimit)}];
517
613
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURSpeedingNotification
518
614
  object:self
519
615
  userInfo:@{
@@ -546,12 +642,14 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
546
642
  double accel = dv / dt;
547
643
  if (hardBrakeMps2 > 0 && accel <= -hardBrakeMps2 && (now - drLastHardBrakeAt) >= kCooldown) {
548
644
  drLastHardBrakeAt = now;
645
+ [self attachDrivingEvent:@"hardBrake" to:loc extra:@{@"value": @(accel)}];
549
646
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURHardBrakeNotification
550
647
  object:self
551
648
  userInfo:@{@"location": loc, @"value": @(accel)}];
552
649
  }
553
650
  if (rapidAccelMps2 > 0 && accel >= rapidAccelMps2 && (now - drLastRapidAccelAt) >= kCooldown) {
554
651
  drLastRapidAccelAt = now;
652
+ [self attachDrivingEvent:@"rapidAcceleration" to:loc extra:@{@"value": @(accel)}];
555
653
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURRapidAccelerationNotification
556
654
  object:self
557
655
  userInfo:@{@"location": loc, @"value": @(accel)}];
@@ -563,6 +661,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
563
661
  && drPrevSpeed * 3.6 >= crashImpactKmh
564
662
  && (now - drLastCrashAt) >= kCooldown) {
565
663
  drLastCrashAt = now;
664
+ [self attachDrivingEvent:@"possibleCrash" to:loc extra:@{@"value": @(dropKmh), @"source": @"gps"}];
566
665
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURPossibleCrashNotification
567
666
  object:self
568
667
  userInfo:@{@"location": loc, @"value": @(dropKmh), @"source": @"gps"}];
@@ -581,6 +680,7 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
581
680
  double rate = diff / dt;
582
681
  if (rate >= sharpTurnDegPerSec && (now - drLastSharpTurnAt) >= kCooldown) {
583
682
  drLastSharpTurnAt = now;
683
+ [self attachDrivingEvent:@"sharpTurn" to:loc extra:@{@"value": @(rate)}];
584
684
  [[NSNotificationCenter defaultCenter] postNotificationName:MAURSharpTurnNotification
585
685
  object:self
586
686
  userInfo:@{@"location": loc, @"value": @(rate)}];
@@ -917,7 +1017,10 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
917
1017
  {
918
1018
  DDLogDebug(@"%@ #onStationaryChanged", TAG);
919
1019
  stationaryLocation = location;
920
-
1020
+
1021
+ // v4.5.1 — enrichment moved into MAURPostLocationTask.add: so pending events / battery
1022
+ // land on the post-transform instance (and survive transforms that return new instances).
1023
+
921
1024
  [postLocationTask add:location];
922
1025
 
923
1026
  MAURConfig *config = [self getConfig];
@@ -946,10 +1049,13 @@ NSString * const MAURPhoneUsageWhileDrivingNotification = @"MAURPhoneUsageWhileD
946
1049
  stationaryLocation = nil;
947
1050
  lastReceivedLocation = location; // v3.5 Phase 4: cached for heartbeat payload
948
1051
 
949
- // v4.0 Phase 6: feed driver-insights state machine.
1052
+ // v4.0 Phase 6: feed driver-insights state machine. Listener may attach events to `location`.
950
1053
  [self drivingDetectorFeed:location];
951
1054
  // v4.2 Phase 8: keep sensor pipeline aware of the latest fix.
952
1055
  sensorFusion.lastLocation = location;
1056
+ // v4.5.1 — pending events drain + battery snapshot moved into MAURPostLocationTask.add:
1057
+ // so they run AFTER any user-supplied locationTransform. The previous order lost them
1058
+ // whenever the transform returned nil.
953
1059
 
954
1060
  [postLocationTask add:location];
955
1061
 
@@ -117,6 +117,10 @@ NSString * const MAURBackgroundSyncDidProgressNotification = @"MAURBackgroundSyn
117
117
 
118
118
  // Stash count so didCompleteWithError can report it as syncSuccess payload.
119
119
  objc_setAssociatedObject(task, "locationsSent", @([jsonArray count]), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
120
+ // v4.5.1: capture cutoff timestamp NOW so on success we delete only the rows that existed
121
+ // before the upload started. Locations persisted DURING the upload are preserved.
122
+ objc_setAssociatedObject(task, "uploadCutoff",
123
+ @([[NSDate date] timeIntervalSince1970]), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
120
124
 
121
125
  [task resume];
122
126
 
@@ -216,6 +220,10 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
216
220
 
217
221
  dispatch_async(dispatch_get_main_queue(), ^{
218
222
  if (error != nil) {
223
+ // v4.5.1 — restore SyncPending → PostPending so the failed locations get retried
224
+ // on the next sync window. Without this, a single network drop loses everything.
225
+ NSError *restErr = nil;
226
+ [[MAURSQLiteLocationDAO sharedInstance] restoreFailedSyncLocations:&restErr];
219
227
  NSString *msg = error.localizedDescription ?: @"";
220
228
  if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncFailed:httpStatus:message:)]) {
221
229
  [_delegate backgroundSyncFailed:self httpStatus:0 message:msg];
@@ -225,6 +233,9 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
225
233
  object:self
226
234
  userInfo:@{@"httpStatus": @0, @"message": msg}];
227
235
  } else if (!isStatusOkay) {
236
+ // v4.5.1 — server-side failure (5xx, 4xx other than 285/401): also restore the rows.
237
+ NSError *restErr = nil;
238
+ [[MAURSQLiteLocationDAO sharedInstance] restoreFailedSyncLocations:&restErr];
228
239
  NSString *msg = [NSString stringWithFormat:@"HTTP %ld", (long)statusCode];
229
240
  if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncFailed:httpStatus:message:)]) {
230
241
  [_delegate backgroundSyncFailed:self httpStatus:statusCode message:msg];
@@ -234,6 +245,15 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
234
245
  object:self
235
246
  userInfo:@{@"httpStatus": @(statusCode), @"message": msg}];
236
247
  } else {
248
+ // v4.5.1: drop SYNC_PENDING locations whose recorded_at is <= the captured upload-start
249
+ // cutoff. This preserves any new locations persisted DURING the upload (race window).
250
+ NSNumber *cutoffNum = objc_getAssociatedObject(task, "uploadCutoff");
251
+ NSTimeInterval cutoff = cutoffNum != nil ? [cutoffNum doubleValue] : [[NSDate date] timeIntervalSince1970];
252
+ NSError *delErr = nil;
253
+ BOOL deleted = [[MAURSQLiteLocationDAO sharedInstance] deleteSyncedLocationsBefore:cutoff error:&delErr];
254
+ if (!deleted) {
255
+ NSLog(@"deleteSyncedLocationsBefore after success failed: %@", delErr.localizedDescription ?: @"unknown");
256
+ }
237
257
  if (_delegate && [_delegate respondsToSelector:@selector(backgroundSyncSucceeded:locationsSent:)]) {
238
258
  [_delegate backgroundSyncSucceeded:self locationsSent:locationsSent];
239
259
  }
@@ -44,6 +44,8 @@ enum {
44
44
  @property NSString *mockLocationPolicy; // allow | flag | drop (default allow)
45
45
  // v4.0 Phase 6: driver insights — passed through as a dictionary; the facade reads keys at runtime.
46
46
  @property NSDictionary *drivingEvents;
47
+ // v4.4: stamp battery percentage + charging state onto every location (default ON).
48
+ @property NSNumber *includeBattery;
47
49
  @property NSNumber *_saveBatteryOnBackground;
48
50
  @property NSNumber *maxLocations;
49
51
  @property NSNumber *_pauseLocationUpdates;
@@ -12,7 +12,7 @@
12
12
 
13
13
  @implementation MAURConfig
14
14
 
15
- @synthesize stationaryRadius, distanceFilter, desiredAccuracy, _debug, activityType, activitiesInterval, _stopOnTerminate, url, syncUrl, syncThreshold, syncEnabled, httpHeaders, httpMethod, syncHttpMethod, httpMode, syncMode, queryParams, _showsBackgroundLocationIndicator, heartbeatInterval, mockLocationPolicy, drivingEvents, _saveBatteryOnBackground, maxLocations, _pauseLocationUpdates, locationProvider, _template;
15
+ @synthesize stationaryRadius, distanceFilter, desiredAccuracy, _debug, activityType, activitiesInterval, _stopOnTerminate, url, syncUrl, syncThreshold, syncEnabled, httpHeaders, httpMethod, syncHttpMethod, httpMode, syncMode, queryParams, _showsBackgroundLocationIndicator, heartbeatInterval, mockLocationPolicy, drivingEvents, includeBattery, _saveBatteryOnBackground, maxLocations, _pauseLocationUpdates, locationProvider, _template;
16
16
 
17
17
  -(instancetype) initWithDefaults {
18
18
  self = [super init];
@@ -117,6 +117,9 @@
117
117
  if ([config[@"drivingEvents"] isKindOfClass:[NSDictionary class]]) {
118
118
  instance.drivingEvents = config[@"drivingEvents"];
119
119
  }
120
+ if (isNotNull(config[@"includeBattery"])) {
121
+ instance.includeBattery = config[@"includeBattery"];
122
+ }
120
123
  if (isNotNull(config[@"saveBatteryOnBackground"])) {
121
124
  instance._saveBatteryOnBackground = config[@"saveBatteryOnBackground"];
122
125
  }
@@ -215,6 +218,9 @@
215
218
  if (newConfig.drivingEvents != nil) {
216
219
  merger.drivingEvents = newConfig.drivingEvents;
217
220
  }
221
+ if (newConfig.includeBattery != nil) {
222
+ merger.includeBattery = newConfig.includeBattery;
223
+ }
218
224
  if ([newConfig hasSaveBatteryOnBackground]) {
219
225
  merger._saveBatteryOnBackground = newConfig._saveBatteryOnBackground;
220
226
  }
@@ -259,6 +265,7 @@
259
265
  copy.heartbeatInterval = heartbeatInterval;
260
266
  copy.mockLocationPolicy = mockLocationPolicy;
261
267
  copy.drivingEvents = drivingEvents;
268
+ copy.includeBattery = includeBattery;
262
269
  copy._saveBatteryOnBackground = _saveBatteryOnBackground;
263
270
  copy.maxLocations = maxLocations;
264
271
  copy._pauseLocationUpdates = _pauseLocationUpdates;
@@ -512,9 +519,15 @@
512
519
  @"altitude": @"@altitude",
513
520
  @"latitude": @"@latitude",
514
521
  @"longitude": @"@longitude",
515
- @"provider": @"provider",
522
+ @"provider": @"@provider", // v4.5.1 — was literal "provider" (bug)
516
523
  @"locationProvider": @"@locationProvider",
517
524
  @"radius": @"@radius",
525
+ // v4.5.1 — README promete events/battery/isCharging en payload default. Sin esto,
526
+ // el template default que se usa siempre que la app no configura postTemplate omitía
527
+ // estos campos al serializar via toResultFromTemplate.
528
+ @"events": @"@events",
529
+ @"battery": @"@battery",
530
+ @"isCharging": @"@isCharging",
518
531
  };
519
532
  }
520
533
 
@@ -576,6 +589,7 @@
576
589
  if (self.heartbeatInterval != nil) [dict setObject:self.heartbeatInterval forKey:@"heartbeatInterval"];
577
590
  if (self.mockLocationPolicy != nil) [dict setObject:self.mockLocationPolicy forKey:@"mockLocationPolicy"];
578
591
  if (self.drivingEvents != nil) [dict setObject:self.drivingEvents forKey:@"drivingEvents"];
592
+ if (self.includeBattery != nil) [dict setObject:self.includeBattery forKey:@"includeBattery"];
579
593
  if ([self hasStationaryRadius]) [dict setObject:self.stationaryRadius forKey:@"stationaryRadius"];
580
594
  if ([self hasDistanceFilter]) [dict setObject:self.distanceFilter forKey:@"distanceFilter"];
581
595
  if ([self hasDesiredAccuracy]) [dict setObject:self.desiredAccuracy forKey:@"desiredAccuracy"];
@@ -40,6 +40,9 @@
40
40
  #define CC_COLUMN_NAME_PAUSE_LOCATION_UPDATES "pause_updates"
41
41
  #define CC_COLUMN_NAME_TEMPLATE "template"
42
42
  #define CC_COLUMN_NAME_LAST_UPDATED_AT "updated_at"
43
+ // v4.5: full Config JSON blob — paridad con Android. Storage of post-3.2 keys without
44
+ // new per-field columns (httpMethod, queryParams, drivingEvents, includeBattery, ...).
45
+ #define CC_COLUMN_NAME_CONFIG_JSON "config_json"
43
46
 
44
47
  @interface MAURConfigurationContract : NSObject
45
48
 
@@ -43,7 +43,9 @@
43
43
  @{ @"name": @CC_COLUMN_NAME_MAX_LOCATIONS, @"type": [SQLColumnType sqlColumnWithType: kInteger]},
44
44
  @{ @"name": @CC_COLUMN_NAME_PAUSE_LOCATION_UPDATES, @"type": [SQLColumnType sqlColumnWithType: kInteger]},
45
45
  @{ @"name": @CC_COLUMN_NAME_TEMPLATE, @"type": [SQLColumnType sqlColumnWithType: kText]},
46
- @{ @"name": @CC_COLUMN_NAME_LAST_UPDATED_AT, @"type": [SQLColumnType sqlColumnWithType: kInteger]}
46
+ @{ @"name": @CC_COLUMN_NAME_LAST_UPDATED_AT, @"type": [SQLColumnType sqlColumnWithType: kInteger]},
47
+ // v4.5: full Config JSON blob
48
+ @{ @"name": @CC_COLUMN_NAME_CONFIG_JSON, @"type": [SQLColumnType sqlColumnWithType: kText]}
47
49
  ];
48
50
 
49
51
  return [MAURSQLiteHelper createTableSqlStatement:@CC_TABLE_NAME columns:columns];
@@ -15,7 +15,9 @@
15
15
  @implementation MAURGeolocationOpenHelper
16
16
 
17
17
  static NSString *const kDatabaseName = @"cordova_bg_geolocation.db";
18
- static NSInteger const kDatabaseVersion = 5;
18
+ // v4.5.0: bumped to 7 to add events_json + battery_level + is_charging on locations,
19
+ // and config_json on configuration (paridad con Android v22).
20
+ static NSInteger const kDatabaseVersion = 7;
19
21
 
20
22
  - (instancetype)init
21
23
  {
@@ -94,6 +96,18 @@ static NSInteger const kDatabaseVersion = 5;
94
96
  [MAURSessionLocationContract createTableSQL],
95
97
  [NSString stringWithFormat:@"CREATE INDEX session_recorded_at_idx ON %@ (%@)", @LSC_TABLE_NAME, @LSC_COLUMN_NAME_RECORDED_AT]
96
98
  ]];
99
+ case 5:
100
+ // v4.5.0: persist driving events / battery / charging on locations.
101
+ [sql addObjectsFromArray: @[
102
+ @"ALTER TABLE " @LC_TABLE_NAME @" ADD COLUMN " @LC_COLUMN_NAME_EVENTS_JSON @" TEXT",
103
+ @"ALTER TABLE " @LC_TABLE_NAME @" ADD COLUMN " @LC_COLUMN_NAME_BATTERY_LEVEL @" INTEGER",
104
+ @"ALTER TABLE " @LC_TABLE_NAME @" ADD COLUMN " @LC_COLUMN_NAME_IS_CHARGING @" INTEGER"
105
+ ]];
106
+ case 6:
107
+ // v4.5.0: full config persisted as JSON blob (paridad con Android config_json).
108
+ [sql addObjectsFromArray: @[
109
+ [NSString stringWithFormat:@"ALTER TABLE %s ADD COLUMN %s TEXT", CC_TABLE_NAME, CC_COLUMN_NAME_CONFIG_JSON]
110
+ ]];
97
111
  break; // break only for previous db version (cascade statements)
98
112
  default:
99
113
  return;
@@ -39,6 +39,18 @@ typedef NS_ENUM(NSInteger, MAURLocationStatus) {
39
39
  @property (nonatomic, retain) NSDate *recordedAt;
40
40
  /** True if location was simulated by software (e.g. Simulator). iOS 15+. */
41
41
  @property (nonatomic, retain) NSNumber *simulated;
42
+ /**
43
+ * v4.3 — Driving events anexados a este fix.
44
+ * v4.5: persiste en SQLite (events_json TEXT) — sobrevive a la cola de sync.
45
+ * Cada elemento es un NSDictionary con al menos { "type": NSString, "time": NSNumber }.
46
+ */
47
+ @property (nonatomic, retain) NSMutableArray *drivingEvents;
48
+ /** v4.4 — Battery percentage (0-100) at the time of this fix.
49
+ * v4.5: persisted in SQLite (battery_level INTEGER). */
50
+ @property (nonatomic, retain) NSNumber *batteryLevel;
51
+ /** v4.4 — Whether the device is charging at the time of this fix.
52
+ * v4.5: persisted in SQLite (is_charging INTEGER). */
53
+ @property (nonatomic, retain) NSNumber *isCharging;
42
54
 
43
55
  + (instancetype) fromCLLocation:(CLLocation*)location;
44
56
  + (NSTimeInterval) locationAge:(CLLocation*)location;
@@ -20,13 +20,25 @@ enum {
20
20
  + (instancetype) map:(MAURLocation*)location;
21
21
  @end
22
22
 
23
- @implementation MAURLocationMapper
24
- MAURLocation* _location;
23
+ // v4.5.1 — _location was previously a file-scope global. With real-time post (background queue)
24
+ // and background sync (NSURLSession queue) running concurrently, the second [+map:] invocation
25
+ // overwrote _location while the first mapper was mid-serialize, producing mixed location fields
26
+ // at the backend. Now per-instance ivar.
27
+ @implementation MAURLocationMapper {
28
+ MAURLocation *_location;
29
+ }
25
30
 
26
31
  - (id) mapValue:(id)value
27
32
  {
28
33
  if ([value isKindOfClass:[NSString class]]) {
29
34
  id locationValue = [_location getValueForKey:value];
35
+ // v4.5.1 — for placeholder keys ("@time", "@events", "@battery", ...), if the location
36
+ // has no value, return NSNull instead of leaking the literal "@events" string to the
37
+ // backend. For non-placeholder static strings (e.g. a `deviceId` literal in postTemplate)
38
+ // keep the previous behaviour and return the string as-is.
39
+ if ([value hasPrefix:@"@"]) {
40
+ return locationValue != nil ? locationValue : [NSNull null];
41
+ }
30
42
  return locationValue != nil ? locationValue : value;
31
43
  } else if ([value isKindOfClass:[NSDictionary class]]) {
32
44
  return [self withDictionary:value];
@@ -63,7 +75,7 @@ MAURLocation* _location;
63
75
  + (instancetype) map:(MAURLocation*)location
64
76
  {
65
77
  MAURLocationMapper *instance = [[MAURLocationMapper alloc] init];
66
- _location = location;
78
+ instance->_location = location;
67
79
  return instance;
68
80
  }
69
81
  @end
@@ -71,7 +83,7 @@ MAURLocation* _location;
71
83
 
72
84
  @implementation MAURLocation
73
85
 
74
- @synthesize locationId, time, accuracy, altitudeAccuracy, speed, heading, altitude, latitude, longitude, provider, locationProvider, radius, isValid, recordedAt, simulated;
86
+ @synthesize locationId, time, accuracy, altitudeAccuracy, speed, heading, altitude, latitude, longitude, provider, locationProvider, radius, isValid, recordedAt, simulated, drivingEvents, batteryLevel, isCharging;
75
87
 
76
88
  + (instancetype) fromCLLocation:(CLLocation*)location;
77
89
  {
@@ -172,6 +184,11 @@ MAURLocation* _location;
172
184
  if (radius != nil) [dict setObject:radius forKey:@"radius"];
173
185
  if (recordedAt != nil) [dict setObject:[NSNumber numberWithDouble:([recordedAt timeIntervalSince1970] * 1000)] forKey:@"recordedAt"];
174
186
  if (simulated != nil) [dict setObject:simulated forKey:@"simulated"];
187
+ // v4.3 — driving events anexados a este fix
188
+ if (drivingEvents != nil && [drivingEvents count] > 0) [dict setObject:drivingEvents forKey:@"events"];
189
+ // v4.4 — battery snapshot
190
+ if (batteryLevel != nil) [dict setObject:batteryLevel forKey:@"battery"];
191
+ if (isCharging != nil) [dict setObject:isCharging forKey:@"isCharging"];
175
192
 
176
193
  return dict;
177
194
  }
@@ -227,6 +244,13 @@ MAURLocation* _location;
227
244
  if ([key isEqualToString:@"@simulated"]) {
228
245
  return simulated;
229
246
  }
247
+ // v4.3 — driving events array (nil when no events on this fix; mapper drops nil keys).
248
+ if ([key isEqualToString:@"@events"]) {
249
+ return (drivingEvents != nil && [drivingEvents count] > 0) ? drivingEvents : nil;
250
+ }
251
+ // v4.4 — battery snapshot
252
+ if ([key isEqualToString:@"@battery"]) return batteryLevel;
253
+ if ([key isEqualToString:@"@isCharging"]) return isCharging;
230
254
 
231
255
  return nil;
232
256
  }
@@ -355,6 +379,11 @@ MAURLocation* _location;
355
379
  copy.radius = radius;
356
380
  copy.isValid = isValid;
357
381
  copy.simulated = simulated;
382
+ // v4.3: copy driving events array reference (transient; mutating one affects the other,
383
+ // but in practice the original is discarded right after the copy is posted).
384
+ copy.drivingEvents = drivingEvents != nil ? [drivingEvents mutableCopy] : nil;
385
+ copy.batteryLevel = batteryLevel;
386
+ copy.isCharging = isCharging;
358
387
  }
359
388
 
360
389
  return copy;
@@ -23,6 +23,10 @@
23
23
  #define LC_COLUMN_NAME_LOCATION_PROVIDER "service_provider"
24
24
  #define LC_COLUMN_NAME_STATUS "valid"
25
25
  #define LC_COLUMN_NAME_RECORDED_AT "recorded_at"
26
+ // v4.5 — survive sync queue
27
+ #define LC_COLUMN_NAME_EVENTS_JSON "events_json"
28
+ #define LC_COLUMN_NAME_BATTERY_LEVEL "battery_level"
29
+ #define LC_COLUMN_NAME_IS_CHARGING "is_charging"
26
30
 
27
31
  @interface MAURLocationContract : NSObject
28
32