@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
@@ -300,7 +300,10 @@ public class SQLiteLocationDAO implements LocationDAO {
300
300
  .append(LocationEntry.COLUMN_NAME_LOCATION_PROVIDER).append("= ?,")
301
301
  .append(LocationEntry.COLUMN_NAME_BATCH_START_MILLIS).append("= ?,")
302
302
  .append(LocationEntry.COLUMN_NAME_STATUS).append("= ?,")
303
- .append(LocationEntry.COLUMN_NAME_MOCK_FLAGS).append("= ?")
303
+ .append(LocationEntry.COLUMN_NAME_MOCK_FLAGS).append("= ?,")
304
+ .append(LocationEntry.COLUMN_NAME_EVENTS_JSON).append("= ?,")
305
+ .append(LocationEntry.COLUMN_NAME_BATTERY_LEVEL).append("= ?,")
306
+ .append(LocationEntry.COLUMN_NAME_IS_CHARGING).append("= ?")
304
307
  .append(" WHERE ").append(LocationEntry._ID)
305
308
  .append("= ?")
306
309
  .toString();
@@ -325,6 +328,9 @@ public class SQLiteLocationDAO implements LocationDAO {
325
328
  location.getBatchStartMillis(),
326
329
  location.getStatus(),
327
330
  location.getMockFlags(),
331
+ location.hasDrivingEvents() ? location.getDrivingEvents().toString() : null,
332
+ location.getBatteryLevel(),
333
+ location.isCharging() != null ? (location.isCharging() ? 1 : 0) : null,
328
334
  locationId
329
335
  });
330
336
 
@@ -459,6 +465,18 @@ public class SQLiteLocationDAO implements LocationDAO {
459
465
  l.setStatus(c.getInt(c.getColumnIndex(LocationEntry.COLUMN_NAME_STATUS)));
460
466
  l.setLocationId(c.getLong(c.getColumnIndex(LocationEntry._ID)));
461
467
  l.setMockFlags(c.getInt((c.getColumnIndex(LocationEntry.COLUMN_NAME_MOCK_FLAGS))));
468
+ // v4.5: events / battery / charging
469
+ int idxEv = c.getColumnIndex(LocationEntry.COLUMN_NAME_EVENTS_JSON);
470
+ if (idxEv >= 0 && !c.isNull(idxEv)) {
471
+ String s = c.getString(idxEv);
472
+ if (s != null && !s.isEmpty()) {
473
+ try { l.setDrivingEvents(new org.json.JSONArray(s)); } catch (org.json.JSONException ignored) {}
474
+ }
475
+ }
476
+ int idxBat = c.getColumnIndex(LocationEntry.COLUMN_NAME_BATTERY_LEVEL);
477
+ if (idxBat >= 0 && !c.isNull(idxBat)) l.setBatteryLevel(c.getInt(idxBat));
478
+ int idxChg = c.getColumnIndex(LocationEntry.COLUMN_NAME_IS_CHARGING);
479
+ if (idxChg >= 0 && !c.isNull(idxChg)) l.setCharging(c.getInt(idxChg) == 1);
462
480
 
463
481
  return l;
464
482
  }
@@ -485,6 +503,16 @@ public class SQLiteLocationDAO implements LocationDAO {
485
503
  values.put(LocationEntry.COLUMN_NAME_STATUS, l.getStatus());
486
504
  values.put(LocationEntry.COLUMN_NAME_BATCH_START_MILLIS, l.getBatchStartMillis());
487
505
  values.put(LocationEntry.COLUMN_NAME_MOCK_FLAGS, l.getMockFlags());
506
+ // v4.5.1: always write — NULL when absent — to clear stale values on maxRows recycle.
507
+ if (l.hasDrivingEvents()) {
508
+ values.put(LocationEntry.COLUMN_NAME_EVENTS_JSON, l.getDrivingEvents().toString());
509
+ } else {
510
+ values.putNull(LocationEntry.COLUMN_NAME_EVENTS_JSON);
511
+ }
512
+ if (l.getBatteryLevel() != null) values.put(LocationEntry.COLUMN_NAME_BATTERY_LEVEL, l.getBatteryLevel());
513
+ else values.putNull(LocationEntry.COLUMN_NAME_BATTERY_LEVEL);
514
+ if (l.isCharging() != null) values.put(LocationEntry.COLUMN_NAME_IS_CHARGING, l.isCharging() ? 1 : 0);
515
+ else values.putNull(LocationEntry.COLUMN_NAME_IS_CHARGING);
488
516
 
489
517
  return values;
490
518
  }
@@ -511,7 +539,10 @@ public class SQLiteLocationDAO implements LocationDAO {
511
539
  LocationEntry.COLUMN_NAME_LOCATION_PROVIDER,
512
540
  LocationEntry.COLUMN_NAME_STATUS,
513
541
  LocationEntry.COLUMN_NAME_BATCH_START_MILLIS,
514
- LocationEntry.COLUMN_NAME_MOCK_FLAGS
542
+ LocationEntry.COLUMN_NAME_MOCK_FLAGS,
543
+ LocationEntry.COLUMN_NAME_EVENTS_JSON,
544
+ LocationEntry.COLUMN_NAME_BATTERY_LEVEL,
545
+ LocationEntry.COLUMN_NAME_IS_CHARGING
515
546
  };
516
547
 
517
548
  return columns;
@@ -22,7 +22,7 @@ import static com.marianhello.bgloc.data.sqlite.SQLiteLocationContract.LocationE
22
22
  public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
23
23
  private static final String TAG = SQLiteOpenHelper.class.getName();
24
24
  public static final String SQLITE_DATABASE_NAME = "cordova_bg_geolocation.db";
25
- public static final int DATABASE_VERSION = 20;
25
+ public static final int DATABASE_VERSION = 22;
26
26
 
27
27
  public static final String TEXT_TYPE = " TEXT";
28
28
  public static final String INTEGER_TYPE = " INTEGER";
@@ -143,6 +143,20 @@ public class SQLiteOpenHelper extends android.database.sqlite.SQLiteOpenHelper {
143
143
  case 19:
144
144
  alterSql.add(SessionEntry.SQL_CREATE_SESSION_TABLE);
145
145
  alterSql.add(SessionEntry.SQL_CREATE_SESSION_TABLE_TIME_IDX);
146
+ case 20:
147
+ // v4.4.1: store the full Config as a single JSON blob so future-added fields
148
+ // do not require a per-field schema bump.
149
+ alterSql.add("ALTER TABLE " + ConfigurationEntry.TABLE_NAME +
150
+ " ADD COLUMN " + ConfigurationEntry.COLUMN_NAME_CONFIG_JSON + TEXT_TYPE);
151
+ case 21:
152
+ // v4.5.0: persist driving events / battery / isCharging on each location so
153
+ // they survive the sync queue (POST failure → SQLite → background sync).
154
+ alterSql.add("ALTER TABLE " + LocationEntry.TABLE_NAME +
155
+ " ADD COLUMN " + LocationEntry.COLUMN_NAME_EVENTS_JSON + TEXT_TYPE);
156
+ alterSql.add("ALTER TABLE " + LocationEntry.TABLE_NAME +
157
+ " ADD COLUMN " + LocationEntry.COLUMN_NAME_BATTERY_LEVEL + INTEGER_TYPE);
158
+ alterSql.add("ALTER TABLE " + LocationEntry.TABLE_NAME +
159
+ " ADD COLUMN " + LocationEntry.COLUMN_NAME_IS_CHARGING + INTEGER_TYPE);
146
160
 
147
161
  break; // DO NOT FORGET TO MOVE DOWN BREAK ON DB UPGRADE!!!
148
162
  default:
@@ -33,9 +33,10 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
33
33
  private static final String SINGLE_LOCATION_UPDATE_ACTION = P_NAME + ".SINGLE_LOCATION_UPDATE_ACTION";
34
34
  private static final String STATIONARY_LOCATION_MONITOR_ACTION = P_NAME + ".STATIONARY_LOCATION_MONITOR_ACTION";
35
35
 
36
- private static final long STATIONARY_TIMEOUT = 5 * 1000 * 60; // 5 minutes.
37
- private static final long STATIONARY_LOCATION_POLLING_INTERVAL_LAZY = 3 * 1000 * 60; // 3 minutes.
38
- private static final long STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE = 1 * 1000 * 60; // 1 minute.
36
+ // v4.5.1: defaults overridable per-config via config.stationaryTimeout / stationaryPollInterval / stationaryPollFast
37
+ private static final long DEFAULT_STATIONARY_TIMEOUT = 5 * 1000 * 60;
38
+ private static final long DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_LAZY = 3 * 1000 * 60;
39
+ private static final long DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE = 1 * 1000 * 60;
39
40
  private static final int MAX_STATIONARY_ACQUISITION_ATTEMPTS = 5;
40
41
  private static final int MAX_SPEED_ACQUISITION_ATTEMPTS = 3;
41
42
 
@@ -50,6 +51,20 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
50
51
  private PendingIntent stationaryAlarmPI;
51
52
  private PendingIntent stationaryLocationPollingPI;
52
53
  private long stationaryLocationPollingInterval;
54
+
55
+ /** v4.5.1: read overrides from {@link com.marianhello.bgloc.Config}; fall back to defaults. */
56
+ private long getStationaryTimeout() {
57
+ Integer v = mConfig != null ? mConfig.getStationaryTimeout() : null;
58
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_TIMEOUT;
59
+ }
60
+ private long getStationaryPollLazy() {
61
+ Integer v = mConfig != null ? mConfig.getStationaryPollInterval() : null;
62
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_LAZY;
63
+ }
64
+ private long getStationaryPollFast() {
65
+ Integer v = mConfig != null ? mConfig.getStationaryPollFast() : null;
66
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE;
67
+ }
53
68
  private PendingIntent stationaryRegionPI;
54
69
  private PendingIntent singleUpdatePI;
55
70
  private Integer scaledDistanceFilter;
@@ -367,7 +382,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
367
382
  public void resetStationaryAlarm() {
368
383
  if (alarmManager == null || stationaryAlarmPI == null) return;
369
384
  alarmManager.cancel(stationaryAlarmPI);
370
- alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + STATIONARY_TIMEOUT, stationaryAlarmPI);
385
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + getStationaryTimeout(), stationaryAlarmPI);
371
386
  }
372
387
 
373
388
  private Integer calculateDistanceFilter(Float speed) {
@@ -401,7 +416,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
401
416
 
402
417
  this.stationaryRadius = proximityRadius;
403
418
 
404
- startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_LAZY);
419
+ startPollingStationaryLocation(getStationaryPollLazy());
405
420
  } catch (SecurityException e) {
406
421
  logger.error("Security exception: {}", e.getMessage());
407
422
  this.handleSecurityException(e);
@@ -464,9 +479,9 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
464
479
  if (distance > stationaryRadius) {
465
480
  onExitStationaryRegion(location);
466
481
  } else if (distance > 0) {
467
- startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE);
468
- } else if (stationaryLocationPollingInterval != STATIONARY_LOCATION_POLLING_INTERVAL_LAZY) {
469
- startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_LAZY);
482
+ startPollingStationaryLocation(getStationaryPollFast());
483
+ } else if (stationaryLocationPollingInterval != getStationaryPollLazy()) {
484
+ startPollingStationaryLocation(getStationaryPollLazy());
470
485
  }
471
486
  }
472
487
 
@@ -36,6 +36,7 @@ import android.os.Message;
36
36
  import android.os.PowerManager;
37
37
  import android.os.Process;
38
38
  import androidx.annotation.Nullable;
39
+ import androidx.core.content.ContextCompat;
39
40
  import androidx.localbroadcastmanager.content.LocalBroadcastManager;
40
41
 
41
42
  import com.marianhello.bgloc.Config;
@@ -179,6 +180,13 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
179
180
  private com.marianhello.bgloc.sensor.SensorFusionDetector mSensorFusion;
180
181
  /** v4.2 Phase 8: cached tripActive state so hot-reload can re-inject it. */
181
182
  private volatile boolean mDrivingTripActive = false;
183
+ /** v4.3: events fired without a simultaneous fix (providerChange, sensor crash, phone usage,
184
+ * manual SOS) buffered here and flushed onto the next location's `events` array.
185
+ * v4.4.1: capped at PENDING_DRIVING_EVENTS_MAX entries (oldest evicted) and entries older
186
+ * than PENDING_DRIVING_EVENTS_TTL_MS are dropped at flush time. */
187
+ private final org.json.JSONArray mPendingDrivingEvents = new org.json.JSONArray();
188
+ private static final int PENDING_DRIVING_EVENTS_MAX = 20;
189
+ private static final long PENDING_DRIVING_EVENTS_TTL_MS = 60_000L;
182
190
  private static final long WATCHDOG_INTERVAL_MS = 60_000L;
183
191
  private final Handler mMainHandler = new Handler(Looper.getMainLooper());
184
192
  private final Runnable mWatchdogRunnable = new Runnable() {
@@ -188,12 +196,23 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
188
196
  if (!Boolean.TRUE.equals(mConfig.getEnableWatchdog())) return;
189
197
  long now = System.currentTimeMillis();
190
198
  if (mLastLocationTime > 0 && (now - mLastLocationTime) > WATCHDOG_INTERVAL_MS) {
191
- logger.info("Location watchdog: no update in {}s, restarting provider", WATCHDOG_INTERVAL_MS / 1000);
192
- try {
193
- mProvider.onStop();
194
- mProvider.onStart();
195
- } catch (Exception e) {
196
- logger.warn("Watchdog restart failed", e);
199
+ // v4.5.1: when drivingEvents is enabled, treat "no fixes" while NOT tripActive as
200
+ // intentional stationary → don't restart (saves battery). When drivingEvents is
201
+ // disabled (the plugin has no notion of "trip"), keep the legacy behaviour of
202
+ // restarting on every stale window.
203
+ Config.DrivingEventsOptions de = mConfig.getDrivingEvents();
204
+ boolean drivingEnabled = de != null && de.enabled;
205
+ boolean shouldRestart = !drivingEnabled || mDrivingTripActive;
206
+ if (shouldRestart) {
207
+ logger.info("Location watchdog: no update in {}s, restarting provider", WATCHDOG_INTERVAL_MS / 1000);
208
+ try {
209
+ mProvider.onStop();
210
+ mProvider.onStart();
211
+ } catch (Exception e) {
212
+ logger.warn("Watchdog restart failed", e);
213
+ }
214
+ } else {
215
+ logger.debug("Location watchdog: stationary (no active trip); skipping restart");
197
216
  }
198
217
  }
199
218
  mMainHandler.postDelayed(this, WATCHDOG_INTERVAL_MS);
@@ -476,9 +495,12 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
476
495
  mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
477
496
  }
478
497
  }
479
- if (mWakeLock != null && !mWakeLock.isHeld()) {
498
+ // v4.5.1: only hold a permanent CPU wake lock when wakeLockMode == 'always'.
499
+ // Default 'posting' acquires only briefly during onLocation/post; 'none' never.
500
+ String wlMode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
501
+ if ("always".equals(wlMode) && mWakeLock != null && !mWakeLock.isHeld()) {
480
502
  mWakeLock.acquire();
481
- logger.debug("Wake lock acquired");
503
+ logger.debug("Wake lock acquired (mode=always)");
482
504
  }
483
505
 
484
506
  if (Boolean.TRUE.equals(mConfig.getEnableWatchdog())) {
@@ -536,18 +558,21 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
536
558
  mDrivingDetector = new com.marianhello.bgloc.driving.DrivingEventsDetector(
537
559
  new com.marianhello.bgloc.driving.DrivingEventsDetector.Listener() {
538
560
  @Override public void onMoving(BackgroundLocation l) {
561
+ attachDrivingEvent(l, "moving", null);
539
562
  Bundle b = new Bundle();
540
563
  b.putInt("action", MSG_ON_MOVING);
541
564
  if (l != null) b.putParcelable("payload", l);
542
565
  broadcastMessage(b);
543
566
  }
544
567
  @Override public void onStopped(BackgroundLocation l) {
568
+ attachDrivingEvent(l, "stopped", null);
545
569
  Bundle b = new Bundle();
546
570
  b.putInt("action", MSG_ON_STOPPED);
547
571
  if (l != null) b.putParcelable("payload", l);
548
572
  broadcastMessage(b);
549
573
  }
550
574
  @Override public void onTripStart(BackgroundLocation l) {
575
+ attachDrivingEvent(l, "tripStart", null);
551
576
  Bundle b = new Bundle();
552
577
  b.putInt("action", MSG_ON_TRIP_START);
553
578
  if (l != null) b.putParcelable("payload", l);
@@ -556,6 +581,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
556
581
  if (mSensorFusion != null) mSensorFusion.setTripActive(true);
557
582
  }
558
583
  @Override public void onTripEnd(BackgroundLocation l, double distance, long durationMs) {
584
+ org.json.JSONObject extra = new org.json.JSONObject();
585
+ try { extra.put("distance", distance); extra.put("durationMs", durationMs); } catch (org.json.JSONException ignored) {}
586
+ attachDrivingEvent(l, "tripEnd", extra);
559
587
  Bundle b = new Bundle();
560
588
  b.putInt("action", MSG_ON_TRIP_END);
561
589
  if (l != null) b.putParcelable("payload", l);
@@ -566,6 +594,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
566
594
  if (mSensorFusion != null) mSensorFusion.setTripActive(false);
567
595
  }
568
596
  @Override public void onSpeeding(BackgroundLocation l, double speedKmh, double limitKmh) {
597
+ org.json.JSONObject extra = new org.json.JSONObject();
598
+ try { extra.put("speedKmh", speedKmh); extra.put("limitKmh", limitKmh); } catch (org.json.JSONException ignored) {}
599
+ attachDrivingEvent(l, "speeding", extra);
569
600
  Bundle b = new Bundle();
570
601
  b.putInt("action", MSG_ON_SPEEDING);
571
602
  if (l != null) b.putParcelable("payload", l);
@@ -574,12 +605,19 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
574
605
  broadcastMessage(b);
575
606
  }
576
607
  @Override public void onProviderChange(String provider) {
608
+ // No location associated; buffer for next fix.
609
+ org.json.JSONObject ev = new org.json.JSONObject();
610
+ try { ev.put("type", "providerChange"); ev.put("provider", provider != null ? provider : ""); ev.put("time", System.currentTimeMillis()); } catch (org.json.JSONException ignored) {}
611
+ enqueuePendingDrivingEvent(ev);
577
612
  Bundle b = new Bundle();
578
613
  b.putInt("action", MSG_ON_PROVIDER_CHANGE);
579
614
  b.putString("provider", provider != null ? provider : "");
580
615
  broadcastMessage(b);
581
616
  }
582
617
  @Override public void onHardBrake(BackgroundLocation l, double decelMps2) {
618
+ org.json.JSONObject extra = new org.json.JSONObject();
619
+ try { extra.put("value", decelMps2); } catch (org.json.JSONException ignored) {}
620
+ attachDrivingEvent(l, "hardBrake", extra);
583
621
  Bundle b = new Bundle();
584
622
  b.putInt("action", MSG_ON_HARD_BRAKE);
585
623
  if (l != null) b.putParcelable("payload", l);
@@ -587,6 +625,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
587
625
  broadcastMessage(b);
588
626
  }
589
627
  @Override public void onRapidAcceleration(BackgroundLocation l, double accelMps2) {
628
+ org.json.JSONObject extra = new org.json.JSONObject();
629
+ try { extra.put("value", accelMps2); } catch (org.json.JSONException ignored) {}
630
+ attachDrivingEvent(l, "rapidAcceleration", extra);
590
631
  Bundle b = new Bundle();
591
632
  b.putInt("action", MSG_ON_RAPID_ACCELERATION);
592
633
  if (l != null) b.putParcelable("payload", l);
@@ -594,6 +635,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
594
635
  broadcastMessage(b);
595
636
  }
596
637
  @Override public void onSharpTurn(BackgroundLocation l, double degPerSec) {
638
+ org.json.JSONObject extra = new org.json.JSONObject();
639
+ try { extra.put("value", degPerSec); } catch (org.json.JSONException ignored) {}
640
+ attachDrivingEvent(l, "sharpTurn", extra);
597
641
  Bundle b = new Bundle();
598
642
  b.putInt("action", MSG_ON_SHARP_TURN);
599
643
  if (l != null) b.putParcelable("payload", l);
@@ -601,6 +645,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
601
645
  broadcastMessage(b);
602
646
  }
603
647
  @Override public void onPossibleCrash(BackgroundLocation l, double velocityDropKmh) {
648
+ org.json.JSONObject extra = new org.json.JSONObject();
649
+ try { extra.put("value", velocityDropKmh); extra.put("source", "gps"); } catch (org.json.JSONException ignored) {}
650
+ attachDrivingEvent(l, "possibleCrash", extra);
604
651
  Bundle b = new Bundle();
605
652
  b.putInt("action", MSG_ON_POSSIBLE_CRASH);
606
653
  if (l != null) b.putParcelable("payload", l);
@@ -638,6 +685,15 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
638
685
  com.marianhello.bgloc.sensor.SensorFusionDetector.Listener l =
639
686
  new com.marianhello.bgloc.sensor.SensorFusionDetector.Listener() {
640
687
  @Override public void onSensorCrash(BackgroundLocation lastLocation, double impactG) {
688
+ // Buffer for next fix (sensor events fire async to GPS).
689
+ try {
690
+ org.json.JSONObject ev = new org.json.JSONObject();
691
+ ev.put("type", "possibleCrash");
692
+ ev.put("value", impactG);
693
+ ev.put("source", "sensor");
694
+ ev.put("time", System.currentTimeMillis());
695
+ enqueuePendingDrivingEvent(ev);
696
+ } catch (org.json.JSONException ignored) {}
641
697
  Bundle b = new Bundle();
642
698
  b.putInt("action", MSG_ON_POSSIBLE_CRASH);
643
699
  if (lastLocation != null) b.putParcelable("payload", lastLocation);
@@ -646,6 +702,12 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
646
702
  broadcastMessage(b);
647
703
  }
648
704
  @Override public void onPhoneUsageWhileDriving(BackgroundLocation lastLocation) {
705
+ try {
706
+ org.json.JSONObject ev = new org.json.JSONObject();
707
+ ev.put("type", "phoneUsageWhileDriving");
708
+ ev.put("time", System.currentTimeMillis());
709
+ enqueuePendingDrivingEvent(ev);
710
+ } catch (org.json.JSONException ignored) {}
649
711
  Bundle b = new Bundle();
650
712
  b.putInt("action", MSG_ON_PHONE_USAGE_WHILE_DRIVING);
651
713
  if (lastLocation != null) b.putParcelable("payload", lastLocation);
@@ -673,6 +735,89 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
673
735
  if (sIsRunning) mSensorFusion.start();
674
736
  }
675
737
 
738
+ /** v4.3 — append a {type, time, ...extra} entry to the location's events array. */
739
+ private void attachDrivingEvent(BackgroundLocation loc, String type, org.json.JSONObject extra) {
740
+ if (loc == null || type == null) return;
741
+ try {
742
+ org.json.JSONObject ev = new org.json.JSONObject();
743
+ ev.put("type", type);
744
+ ev.put("time", System.currentTimeMillis());
745
+ if (extra != null) {
746
+ java.util.Iterator<String> keys = extra.keys();
747
+ while (keys.hasNext()) {
748
+ String k = keys.next();
749
+ ev.put(k, extra.opt(k));
750
+ }
751
+ }
752
+ loc.addDrivingEvent(ev);
753
+ } catch (org.json.JSONException ignored) {}
754
+ }
755
+
756
+ /** v4.5.1 — acquire a short, time-bounded wake lock when wakeLockMode is 'posting'. */
757
+ private void acquireWakeLockForPosting() {
758
+ if (mWakeLock == null || mConfig == null) return;
759
+ String mode = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
760
+ if (!"posting".equals(mode)) return;
761
+ try {
762
+ // Bounded: SQLite write + HTTP POST should finish well within 30 s.
763
+ if (!mWakeLock.isHeld()) mWakeLock.acquire(30_000L);
764
+ } catch (Throwable ignored) { /* best-effort */ }
765
+ }
766
+
767
+ /** v4.4 — read current device battery via sticky broadcast and stamp it onto the location.
768
+ * No permission required. Sticky broadcast returns instantly without blocking.
769
+ * v4.4.1: route through the application context to bypass our own registerReceiver()
770
+ * override (which forces RECEIVER_NOT_EXPORTED + handler — incompatible with sticky-only reads). */
771
+ private void attachBatterySnapshot(BackgroundLocation loc) {
772
+ if (loc == null) return;
773
+ try {
774
+ android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED);
775
+ android.content.Intent batteryStatus = getApplicationContext().registerReceiver(null, filter);
776
+ if (batteryStatus == null) return;
777
+ int level = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
778
+ int scale = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
779
+ if (level >= 0 && scale > 0) {
780
+ loc.setBatteryLevel((int) Math.round(level * 100.0 / scale));
781
+ }
782
+ int status = batteryStatus.getIntExtra(android.os.BatteryManager.EXTRA_STATUS, -1);
783
+ boolean charging = (status == android.os.BatteryManager.BATTERY_STATUS_CHARGING
784
+ || status == android.os.BatteryManager.BATTERY_STATUS_FULL);
785
+ loc.setCharging(charging);
786
+ } catch (Throwable ignored) { /* best-effort; never fail the fix */ }
787
+ }
788
+
789
+ /** v4.3 — drain pending events (those fired without a simultaneous fix) onto this location.
790
+ * v4.4.1: drop entries older than PENDING_DRIVING_EVENTS_TTL_MS so we don't anexar an event
791
+ * whose context (location, speed, etc.) is no longer relevant. */
792
+ private void flushPendingDrivingEvents(BackgroundLocation loc) {
793
+ if (loc == null) return;
794
+ long now = System.currentTimeMillis();
795
+ synchronized (mPendingDrivingEvents) {
796
+ int n = mPendingDrivingEvents.length();
797
+ if (n == 0) return;
798
+ for (int i = 0; i < n; i++) {
799
+ org.json.JSONObject ev = mPendingDrivingEvents.optJSONObject(i);
800
+ if (ev == null) continue;
801
+ long t = ev.optLong("time", now);
802
+ if (now - t <= PENDING_DRIVING_EVENTS_TTL_MS) {
803
+ loc.addDrivingEvent(ev);
804
+ }
805
+ }
806
+ for (int i = n - 1; i >= 0; i--) mPendingDrivingEvents.remove(i);
807
+ }
808
+ }
809
+
810
+ /** v4.4.1 — append to pending events with cap (oldest evicted). */
811
+ private void enqueuePendingDrivingEvent(org.json.JSONObject ev) {
812
+ if (ev == null) return;
813
+ synchronized (mPendingDrivingEvents) {
814
+ while (mPendingDrivingEvents.length() >= PENDING_DRIVING_EVENTS_MAX) {
815
+ mPendingDrivingEvents.remove(0);
816
+ }
817
+ mPendingDrivingEvents.put(ev);
818
+ }
819
+ }
820
+
676
821
  @Override
677
822
  public synchronized void startForegroundService() {
678
823
  start();
@@ -725,8 +870,9 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
725
870
  * Required before starting a location foreground service on API 34+.
726
871
  */
727
872
  private boolean hasLocationPermission() {
728
- return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
729
- || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
873
+ // v4.5.1 ContextCompat handles API < 23 (permissions granted at install time).
874
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
875
+ || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
730
876
  }
731
877
 
732
878
  /**
@@ -791,16 +937,32 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
791
937
  mProvider.onCommand(LocationProvider.CMD_SWITCH_MODE,
792
938
  LocationProvider.FOREGROUND_MODE);
793
939
  }
794
- // Android 14+ (API 34): type must match the merged manifest. Read it dynamically; if unknown, do not start.
795
- if (Build.VERSION.SDK_INT >= 34) {
796
- int type = getManifestForegroundServiceType();
797
- if (type == 0) {
798
- logger.error("Cannot start foreground: manifest foregroundServiceType missing or unreadable");
940
+ // Android 14+ (API 34): type is required. Android 12-13 (API 31-33): type accepted (preferred).
941
+ // FOREGROUND_SERVICE_TYPE_LOCATION = 0x00000008. Resolve from merged manifest first;
942
+ // if reflection fails or manifest merge missed the attribute, fall back to LOCATION
943
+ // hardcoded so the FGS still promotes (otherwise: no notification, no background tracking).
944
+ try {
945
+ if (Build.VERSION.SDK_INT >= 30) {
946
+ int type = getManifestForegroundServiceType();
947
+ if (type == 0) {
948
+ // Defensive fallback: every consumer of this plugin requires location FGS.
949
+ // Logging at warn so the failure is visible without breaking the service.
950
+ logger.warn("Manifest foregroundServiceType unreadable; defaulting to LOCATION (0x8). "
951
+ + "Verify merged AndroidManifest has foregroundServiceType=\"location\"." );
952
+ type = 0x00000008; // ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
953
+ }
954
+ super.startForeground(NOTIFICATION_ID, notification, type);
955
+ } else {
956
+ super.startForeground(NOTIFICATION_ID, notification);
957
+ }
958
+ } catch (Throwable t) {
959
+ logger.error("startForeground threw {}; retrying without type", t.getMessage());
960
+ try {
961
+ super.startForeground(NOTIFICATION_ID, notification);
962
+ } catch (Throwable t2) {
963
+ logger.error("startForeground retry failed: {}", t2.getMessage());
799
964
  return;
800
965
  }
801
- super.startForeground(NOTIFICATION_ID, notification, type);
802
- } else {
803
- super.startForeground(NOTIFICATION_ID, notification);
804
966
  }
805
967
  mIsInForeground = true;
806
968
  scheduleNotificationUpdater();
@@ -955,6 +1117,26 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
955
1117
  if (!equalsDrivingEvents(prevDe, newDe)) {
956
1118
  configureDrivingDetector();
957
1119
  }
1120
+ // v4.5.1 — hot-reload wakeLockMode: when transitioning between always /
1121
+ // posting / none, the existing permanent lock (if any) must be released,
1122
+ // or a new permanent lock acquired. Without this, switching mode at runtime
1123
+ // either leaked CPU or left the service running without the requested lock.
1124
+ String prevWl = currentConfig.getWakeLockMode() != null ? currentConfig.getWakeLockMode() : "posting";
1125
+ String newWl = mConfig.getWakeLockMode() != null ? mConfig.getWakeLockMode() : "posting";
1126
+ if (!prevWl.equals(newWl) && mWakeLock != null) {
1127
+ if ("always".equals(newWl)) {
1128
+ if (!mWakeLock.isHeld()) {
1129
+ try { mWakeLock.acquire(); logger.debug("Wake lock acquired (hot-reload → always)"); }
1130
+ catch (Throwable t) { logger.warn("Wake lock acquire failed", t); }
1131
+ }
1132
+ } else {
1133
+ // 'posting' or 'none' — release any permanent lock; per-fix lock continues to work via acquireWakeLockForPosting().
1134
+ if (mWakeLock.isHeld()) {
1135
+ try { mWakeLock.release(); logger.debug("Wake lock released (hot-reload → {})", newWl); }
1136
+ catch (Throwable t) { logger.warn("Wake lock release failed", t); }
1137
+ }
1138
+ }
1139
+ }
958
1140
  }
959
1141
  }
960
1142
  });
@@ -1022,15 +1204,19 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1022
1204
 
1023
1205
  @Override
1024
1206
  public void onLocation(BackgroundLocation location) {
1207
+ // v4.5.1: in 'posting' wake-lock mode, hold the CPU briefly so SQLite writes + HTTP
1208
+ // POST finish before the system returns to deep sleep. 30s ceiling — plenty for a fix.
1209
+ acquireWakeLockForPosting();
1025
1210
  mLastLocationTime = System.currentTimeMillis();
1026
1211
  mLastReceivedLocation = location;
1027
1212
  sLastReceivedLocation = location;
1028
1213
 
1029
- // v4.0 Phase 6: feed the driver-insights state machine.
1214
+ // v4.0 Phase 6: feed the driver-insights state machine on the *raw* location so speed/bearing
1215
+ // come straight from the sensors. Listener attaches events to this same instance.
1030
1216
  if (mDrivingDetector != null) {
1031
1217
  mDrivingDetector.onLocation(location);
1032
1218
  }
1033
- // v4.2 Phase 8: keep sensor pipeline aware of the latest fix.
1219
+ // v4.2 Phase 8: keep sensor pipeline aware of the latest raw fix.
1034
1220
  if (mSensorFusion != null) {
1035
1221
  mSensorFusion.setLastLocation(location);
1036
1222
  }
@@ -1056,12 +1242,38 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1056
1242
  }
1057
1243
  logger.debug("New location {}", location.toString());
1058
1244
 
1245
+ // v4.5.1 — events were attached to the RAW location above (so detector heuristics see real
1246
+ // speed/bearing). If transformLocation() returns a NEW instance, we'd lose those events
1247
+ // and the battery snapshot. Solution: copy them across to the transformed instance below.
1248
+ org.json.JSONArray rawEvents = location.getDrivingEvents();
1249
+ Integer rawBatteryLevel = null;
1250
+ Boolean rawIsCharging = null;
1251
+
1059
1252
  location = transformLocation(location);
1060
1253
  if (location == null) {
1061
1254
  logger.debug("Skipping location as requested by the locationTransform");
1062
1255
  return;
1063
1256
  }
1064
1257
 
1258
+ // Re-attach events to the transformed location if the transform produced a new instance.
1259
+ if (rawEvents != null && rawEvents.length() > 0 && location.getDrivingEvents() != rawEvents) {
1260
+ try {
1261
+ org.json.JSONArray copy = new org.json.JSONArray(rawEvents.toString());
1262
+ for (int i = 0; i < copy.length(); i++) {
1263
+ org.json.JSONObject ev = copy.optJSONObject(i);
1264
+ if (ev != null) location.addDrivingEvent(ev);
1265
+ }
1266
+ } catch (org.json.JSONException ignored) {}
1267
+ }
1268
+ // v4.3: drain pending events (providerChange/sensor crash/phone usage) onto the post-transform
1269
+ // instance so they always reach the backend.
1270
+ flushPendingDrivingEvents(location);
1271
+ // v4.4: stamp device battery snapshot onto the *transformed* location so it survives any
1272
+ // user-supplied locationTransform that creates a new instance.
1273
+ if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
1274
+ attachBatterySnapshot(location);
1275
+ }
1276
+
1065
1277
  Bundle bundle = new Bundle();
1066
1278
  bundle.putInt("action", MSG_ON_LOCATION);
1067
1279
  bundle.putParcelable("payload", location);
@@ -1091,6 +1303,11 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1091
1303
  logger.debug("Skipping location as requested by the locationTransform");
1092
1304
  return;
1093
1305
  }
1306
+ // v4.5.1 — same enrichment as regular fixes: drain pending events and stamp battery.
1307
+ flushPendingDrivingEvents(location);
1308
+ if (mConfig == null || !Boolean.FALSE.equals(mConfig.getIncludeBattery())) {
1309
+ attachBatterySnapshot(location);
1310
+ }
1094
1311
 
1095
1312
  Bundle bundle = new Bundle();
1096
1313
  bundle.putInt("action", MSG_ON_STATIONARY);
@@ -1190,7 +1407,15 @@ public class LocationServiceImpl extends Service implements ProviderDelegate, Lo
1190
1407
 
1191
1408
  @Override
1192
1409
  public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
1193
- return super.registerReceiver(receiver, filter, null, mServiceHandler, RECEIVER_NOT_EXPORTED);
1410
+ // v4.5.1 RECEIVER_NOT_EXPORTED flag is required on Android 13+ (API 33) for non-system
1411
+ // broadcasts and the 5-arg overload exists only from API 26. Guard for older OSs.
1412
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1413
+ return super.registerReceiver(receiver, filter, null, mServiceHandler, Context.RECEIVER_NOT_EXPORTED);
1414
+ }
1415
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1416
+ return super.registerReceiver(receiver, filter, null, mServiceHandler);
1417
+ }
1418
+ return super.registerReceiver(receiver, filter);
1194
1419
  }
1195
1420
 
1196
1421
  @Override
@@ -7,6 +7,8 @@ import android.content.pm.PackageManager;
7
7
  import android.os.Build;
8
8
  import android.util.Log;
9
9
 
10
+ import androidx.core.content.ContextCompat;
11
+
10
12
  import com.marianhello.bgloc.Config;
11
13
 
12
14
  public class LocationServiceProxy implements LocationService, LocationServiceInfo {
@@ -100,8 +102,9 @@ public class LocationServiceProxy implements LocationService, LocationServiceInf
100
102
  }
101
103
 
102
104
  private boolean hasLocationPermission() {
103
- return mContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
104
- || mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
105
+ // v4.5.1 ContextCompat handles API < 23 safely.
106
+ return ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
107
+ || ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
105
108
  }
106
109
 
107
110
  @Override