@josuelmm/cordova-background-geolocation 4.2.3 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.npmignore +11 -0
  2. package/CHANGELOG.md +261 -0
  3. package/README.md +306 -115
  4. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +34 -0
  5. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +61 -1
  6. package/android/common/src/main/AndroidManifest.xml +1 -1
  7. package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +20 -3
  8. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +87 -1
  9. package/android/common/src/main/java/com/marianhello/bgloc/data/BackgroundLocation.java +94 -0
  10. package/android/common/src/main/java/com/marianhello/bgloc/data/ConfigJsonMapper.java +211 -0
  11. package/android/common/src/main/java/com/marianhello/bgloc/data/LocationTemplateFactory.java +6 -0
  12. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationContract.java +5 -1
  13. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteConfigurationDAO.java +32 -1
  14. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationContract.java +12 -2
  15. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteLocationDAO.java +33 -2
  16. package/android/common/src/main/java/com/marianhello/bgloc/data/sqlite/SQLiteOpenHelper.java +15 -1
  17. package/android/common/src/main/java/com/marianhello/bgloc/provider/AbstractLocationProvider.java +48 -1
  18. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +105 -6
  19. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +336 -250
  20. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +69 -19
  21. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +246 -21
  22. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +5 -2
  23. package/android/common/src/main/java/com/marianhello/bgloc/sync/BatchManager.java +46 -13
  24. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +23 -1
  25. package/ios/common/BackgroundGeolocation/MAURActivityLocationProvider.m +208 -70
  26. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +132 -5
  27. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +20 -0
  28. package/ios/common/BackgroundGeolocation/MAURConfig.h +7 -0
  29. package/ios/common/BackgroundGeolocation/MAURConfig.m +37 -2
  30. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.h +3 -0
  31. package/ios/common/BackgroundGeolocation/MAURConfigurationContract.m +3 -1
  32. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +10 -1
  33. package/ios/common/BackgroundGeolocation/MAURGeolocationOpenHelper.m +15 -1
  34. package/ios/common/BackgroundGeolocation/MAURLocation.h +12 -0
  35. package/ios/common/BackgroundGeolocation/MAURLocation.m +33 -4
  36. package/ios/common/BackgroundGeolocation/MAURLocationContract.h +4 -0
  37. package/ios/common/BackgroundGeolocation/MAURLocationContract.m +5 -1
  38. package/ios/common/BackgroundGeolocation/MAURLocationManager.m +19 -1
  39. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +9 -0
  40. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +59 -1
  41. package/ios/common/BackgroundGeolocation/MAURRawLocationProvider.m +10 -1
  42. package/ios/common/BackgroundGeolocation/MAURSQLiteConfigurationDAO.m +54 -4
  43. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.h +12 -0
  44. package/ios/common/BackgroundGeolocation/MAURSQLiteLocationDAO.m +125 -5
  45. package/package.json +31 -1
  46. package/plugin.xml +3 -10
  47. package/www/BackgroundGeolocation.d.ts +143 -3
  48. package/www/BackgroundGeolocation.js +11 -4
  49. package/CLAUDE.md +0 -56
  50. package/HISTORY.md +0 -871
  51. package/android/CDVBackgroundGeolocation/src/test/java/com/marianhello/ConfigMapperTest.java +0 -220
  52. package/android/common/src/androidTest/java/com/marianhello/bgloc/BackgroundGeolocationFacadeTest.java +0 -45
  53. package/android/common/src/androidTest/java/com/marianhello/bgloc/BatchManagerTest.java +0 -570
  54. package/android/common/src/androidTest/java/com/marianhello/bgloc/ConfigTest.java +0 -76
  55. package/android/common/src/androidTest/java/com/marianhello/bgloc/ContentProviderLocationDAOTest.java +0 -437
  56. package/android/common/src/androidTest/java/com/marianhello/bgloc/DBLogReaderTest.java +0 -95
  57. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationContentProviderTest.java +0 -159
  58. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceProxyTest.java +0 -161
  59. package/android/common/src/androidTest/java/com/marianhello/bgloc/LocationServiceTest.java +0 -247
  60. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteConfigurationDAOTest.java +0 -200
  61. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOTest.java +0 -457
  62. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteLocationDAOThreadTest.java +0 -96
  63. package/android/common/src/androidTest/java/com/marianhello/bgloc/SQLiteOpenHelperTest.java +0 -225
  64. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestPluginDelegate.java +0 -46
  65. package/android/common/src/androidTest/java/com/marianhello/bgloc/TestResourceResolver.java +0 -14
  66. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/MockLocationProvider.java +0 -50
  67. package/android/common/src/androidTest/java/com/marianhello/bgloc/provider/TestLocationProviderFactory.java +0 -17
  68. package/android/common/src/androidTest/java/com/marianhello/bgloc/sqlite/SQLiteOpenHelper10.java +0 -92
  69. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/LocationProviderTestCase.java +0 -107
  70. package/android/common/src/androidTest/java/com/marianhello/bgloc/test/TestConstants.java +0 -5
  71. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ArrayListLocationTemplateTest.java +0 -82
  72. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/BackgroundLocationTest.java +0 -128
  73. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/ConfigTest.java +0 -191
  74. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/DBLogReaderTest.java +0 -37
  75. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HashMapLocationTemplateTest.java +0 -216
  76. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/HttpPostServiceTest.java +0 -223
  77. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/LocationTemplateFactoryTest.java +0 -50
  78. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/PostLocationTaskTest.java +0 -180
  79. package/android/common/src/test/java/com/marianhello/backgroundgeolocation/TestHelper.java +0 -16
  80. package/ios/common/BackgroundGeolocation/SOMotionDetector/CHANGELOG.md +0 -2
  81. package/ios/common/BackgroundGeolocation/SOMotionDetector/LICENSE +0 -21
  82. package/ios/common/BackgroundGeolocation/SOMotionDetector/README.md +0 -135
  83. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOLocationManager.h +0 -80
  84. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOLocationManager.m +0 -147
  85. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionActivity.h +0 -30
  86. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionActivity.m +0 -42
  87. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionDetector.h +0 -99
  88. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOMotionDetector.m +0 -327
  89. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOStepDetector.h +0 -44
  90. package/ios/common/BackgroundGeolocation/SOMotionDetector/SOStepDetector.m +0 -94
  91. package/ios/common/BackgroundGeolocationTests/Info.plist +0 -24
  92. package/ios/common/BackgroundGeolocationTests/MAURBackgroundLocationTest.m +0 -185
  93. package/ios/common/BackgroundGeolocationTests/MAURConfigTest.m +0 -161
  94. package/ios/common/BackgroundGeolocationTests/MAURGeolocationOpenHelperTest.m +0 -102
  95. package/ios/common/BackgroundGeolocationTests/MAURLocationTest.m +0 -216
  96. package/ios/common/BackgroundGeolocationTests/MAURLocationUploaderTest.m +0 -55
  97. package/ios/common/BackgroundGeolocationTests/MAURLogReaderTest.m +0 -43
  98. package/ios/common/BackgroundGeolocationTests/MAURSQLiteConfigurationDAOTest.m +0 -102
  99. package/ios/common/BackgroundGeolocationTests/MAURSQLiteHelperTest.m +0 -41
  100. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOTests.m +0 -240
  101. package/ios/common/BackgroundGeolocationTests/MAURSQLiteLocationDAOThreadTest.m +0 -84
  102. package/ios/common/BackgroundGeolocationTests/MAURSQLiteOpenHelperTest.m +0 -144
  103. package/ios/common/scripts/xcode-refactor.js +0 -184
@@ -11,9 +11,18 @@ import android.location.LocationListener;
11
11
  import android.location.LocationManager;
12
12
  import android.os.Build;
13
13
  import android.os.Bundle;
14
+ import android.os.Looper;
15
+
16
+ import com.google.android.gms.common.ConnectionResult;
17
+ import com.google.android.gms.common.GoogleApiAvailability;
18
+ import com.google.android.gms.location.FusedLocationProviderClient;
19
+ import com.google.android.gms.location.LocationCallback;
20
+ import com.google.android.gms.location.LocationRequest;
21
+ import com.google.android.gms.location.LocationResult;
22
+ import com.google.android.gms.location.LocationServices;
23
+ import com.google.android.gms.location.Priority;
14
24
 
15
25
  import com.marianhello.bgloc.Config;
16
- import com.marianhello.bgloc.provider.AbstractLocationProvider;
17
26
  import com.marianhello.utils.ToneGenerator.Tone;
18
27
 
19
28
  import java.util.List;
@@ -23,22 +32,38 @@ import static java.lang.Math.pow;
23
32
  import static java.lang.Math.round;
24
33
 
25
34
 
35
+ /**
36
+ * v4.5.2 — Distance-filter provider with a runtime-chosen backend:
37
+ * <ul>
38
+ * <li><b>Fused path</b> (Play Services available): {@link FusedLocationProviderClient}
39
+ * + {@link LocationCallback}. Better fused GPS+Network blending and battery.</li>
40
+ * <li><b>Legacy path</b> (Play Services missing — Huawei/HMS, AOSP, China ROMs):
41
+ * {@link android.location.LocationManager} + {@link LocationListener}. Preserves the
42
+ * original DISTANCE_FILTER behavior so the plugin works on every Android device.</li>
43
+ * </ul>
44
+ *
45
+ * Stationary detection in both paths relies on the existing alarm-driven polling
46
+ * (no geofencing / proximity alerts), per product decision.
47
+ */
26
48
  public class DistanceFilterLocationProvider extends AbstractLocationProvider implements LocationListener {
27
49
 
28
50
  private static final String TAG = DistanceFilterLocationProvider.class.getSimpleName();
29
51
  private static final String P_NAME = "com.marianhello.bgloc";
30
52
 
31
- private static final String STATIONARY_REGION_ACTION = P_NAME + ".STATIONARY_REGION_ACTION";
32
- private static final String STATIONARY_ALARM_ACTION = P_NAME + ".STATIONARY_ALARM_ACTION";
33
- private static final String SINGLE_LOCATION_UPDATE_ACTION = P_NAME + ".SINGLE_LOCATION_UPDATE_ACTION";
34
- private static final String STATIONARY_LOCATION_MONITOR_ACTION = P_NAME + ".STATIONARY_LOCATION_MONITOR_ACTION";
53
+ private static final String STATIONARY_ALARM_ACTION = P_NAME + ".STATIONARY_ALARM_ACTION";
54
+ private static final String SINGLE_LOCATION_UPDATE_ACTION = P_NAME + ".SINGLE_LOCATION_UPDATE_ACTION";
55
+ private static final String STATIONARY_LOCATION_MONITOR_ACTION = P_NAME + ".STATIONARY_LOCATION_MONITOR_ACTION";
35
56
 
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.
57
+ // v4.5.1: defaults overridable per-config via config.stationaryTimeout / stationaryPollInterval / stationaryPollFast
58
+ private static final long DEFAULT_STATIONARY_TIMEOUT = 5 * 1000 * 60;
59
+ private static final long DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_LAZY = 3 * 1000 * 60;
60
+ private static final long DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE = 1 * 1000 * 60;
39
61
  private static final int MAX_STATIONARY_ACQUISITION_ATTEMPTS = 5;
40
62
  private static final int MAX_SPEED_ACQUISITION_ATTEMPTS = 3;
41
63
 
64
+ // v4.5.2 — Aggressive interval used while acquiring stationary location or speed (FLP path).
65
+ private static final long ACQUISITION_INTERVAL_MS = 1000L;
66
+
42
67
  private Boolean isMoving = false;
43
68
  private Boolean isAcquiringStationaryLocation = false;
44
69
  private Boolean isAcquiringSpeed = false;
@@ -49,21 +74,69 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
49
74
  private float stationaryRadius;
50
75
  private PendingIntent stationaryAlarmPI;
51
76
  private PendingIntent stationaryLocationPollingPI;
77
+ private PendingIntent singleUpdatePI; // legacy path only
52
78
  private long stationaryLocationPollingInterval;
53
- private PendingIntent stationaryRegionPI;
54
- private PendingIntent singleUpdatePI;
79
+
55
80
  private Integer scaledDistanceFilter;
56
81
 
57
- private LocationManager locationManager;
82
+ private FusedLocationProviderClient fusedClient; // null on the legacy path
83
+ private LocationManager locationManager; // always non-null after onCreate
58
84
  private AlarmManager alarmManager;
85
+ private boolean usingFused = false;
59
86
 
60
87
  private boolean isStarted = false;
61
88
 
89
+ /** v4.5.1: read overrides from {@link com.marianhello.bgloc.Config}; fall back to defaults. */
90
+ private long getStationaryTimeout() {
91
+ Integer v = mConfig != null ? mConfig.getStationaryTimeout() : null;
92
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_TIMEOUT;
93
+ }
94
+ private long getStationaryPollLazy() {
95
+ Integer v = mConfig != null ? mConfig.getStationaryPollInterval() : null;
96
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_LAZY;
97
+ }
98
+ private long getStationaryPollFast() {
99
+ Integer v = mConfig != null ? mConfig.getStationaryPollFast() : null;
100
+ return v != null ? v.longValue() : DEFAULT_STATIONARY_LOCATION_POLLING_INTERVAL_AGGRESSIVE;
101
+ }
102
+
62
103
  public DistanceFilterLocationProvider(Context context) {
63
104
  super(context);
64
105
  PROVIDER_ID = Config.DISTANCE_FILTER_PROVIDER;
65
106
  }
66
107
 
108
+ // ====== FLP path callbacks ======
109
+
110
+ private final LocationCallback fusedCallback = new LocationCallback() {
111
+ @Override
112
+ public void onLocationResult(LocationResult result) {
113
+ if (result == null) return;
114
+ List<Location> locations = result.getLocations();
115
+ if (locations == null) return;
116
+ for (Location location : locations) {
117
+ handleNewLocation(location);
118
+ }
119
+ }
120
+ };
121
+
122
+ /** FLP one-shot callback used by the stationary polling alarm. */
123
+ private final LocationCallback fusedPollCallback = new LocationCallback() {
124
+ @Override
125
+ public void onLocationResult(LocationResult result) {
126
+ if (result == null || fusedClient == null) return;
127
+ try {
128
+ fusedClient.removeLocationUpdates(this);
129
+ } catch (Exception ignored) { /* fire-and-forget */ }
130
+ Location loc = result.getLastLocation();
131
+ if (loc != null) {
132
+ logger.debug("Stationary monitor single update: {}", loc);
133
+ onPollStationaryLocation(loc);
134
+ }
135
+ }
136
+ };
137
+
138
+ // ====== Lifecycle ======
139
+
67
140
  @Override
68
141
  public void onCreate() {
69
142
  super.onCreate();
@@ -71,56 +144,46 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
71
144
  locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
72
145
  alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
73
146
 
147
+ // v4.5.2 — pick the location backend at runtime. Play Services missing
148
+ // (Huawei/HMS, AOSP, China ROMs) → use the OS LocationManager so the
149
+ // provider still works.
150
+ int gps = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mContext);
151
+ usingFused = (gps == ConnectionResult.SUCCESS);
152
+ if (usingFused) {
153
+ fusedClient = LocationServices.getFusedLocationProviderClient(mContext);
154
+ logger.info("DISTANCE_FILTER_PROVIDER using FusedLocationProviderClient (Play Services available).");
155
+ } else {
156
+ logger.info("DISTANCE_FILTER_PROVIDER falling back to LocationManager (Play Services unavailable, code={}).", gps);
157
+ }
158
+
74
159
  int updateCurrentFlag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
75
160
  ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
76
161
  : PendingIntent.FLAG_UPDATE_CURRENT;
77
-
78
- int cancelCurrentFlag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
79
- ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
162
+ // v4.5.2: singleUpdatePI must be MUTABLE on API 31+ because
163
+ // LocationManager.requestSingleUpdate() fills the resulting Location
164
+ // into the intent's extras at delivery time. FLAG_IMMUTABLE blocks that
165
+ // population, so the receiver would never see the fix.
166
+ int singleUpdateFlag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
167
+ ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE
80
168
  : PendingIntent.FLAG_CANCEL_CURRENT;
81
169
 
82
170
  Intent stationaryAlarmIntent = new Intent(mContext, StationaryAlarmReceiver.class);
83
171
  stationaryAlarmIntent.setAction(STATIONARY_ALARM_ACTION);
84
-
85
- // Stop-detection PI (requestCode 9000)
86
172
  stationaryAlarmPI = PendingIntent.getBroadcast(mContext, 9000, stationaryAlarmIntent, updateCurrentFlag);
87
173
  registerReceiver(stationaryAlarmReceiver, new IntentFilter(STATIONARY_ALARM_ACTION));
88
174
 
89
- Intent stationaryRegionIntent = new Intent(mContext, StationaryRegionReceiver.class);
90
- stationaryRegionIntent.setAction(STATIONARY_REGION_ACTION);
91
-
92
- // Stationary region PI (requestCode 9001)
93
- stationaryRegionPI = PendingIntent.getBroadcast(mContext, 9001, stationaryRegionIntent, cancelCurrentFlag);
94
- registerReceiver(stationaryRegionReceiver, new IntentFilter(STATIONARY_REGION_ACTION));
95
-
96
175
  Intent stationaryLocationMonitorIntent = new Intent(mContext, StationaryLocationMonitorReceiver.class);
97
176
  stationaryLocationMonitorIntent.setAction(STATIONARY_LOCATION_MONITOR_ACTION);
98
-
99
- // Stationary location monitor PI (requestCode 9002)
100
177
  stationaryLocationPollingPI = PendingIntent.getBroadcast(mContext, 9002, stationaryLocationMonitorIntent, updateCurrentFlag);
101
178
  registerReceiver(stationaryLocationMonitorReceiver, new IntentFilter(STATIONARY_LOCATION_MONITOR_ACTION));
102
179
 
103
- Intent singleLocationUpdateIntent = new Intent(mContext, SingleUpdateReceiver.class);
104
- singleLocationUpdateIntent.setAction(SINGLE_LOCATION_UPDATE_ACTION);
105
-
106
- // One-shot PI (requestCode 9003, currently unused)
107
- singleUpdatePI = PendingIntent.getBroadcast(mContext, 9003, singleLocationUpdateIntent, cancelCurrentFlag);
108
- registerReceiver(singleUpdateReceiver, new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION));
109
-
110
- // v3.4 Phase 3: Criteria API removed (deprecated since Android 12 / API 31).
111
- // Provider selection is now explicit (GPS-first / Network-fallback) via pickProvider().
112
- }
113
-
114
- /** v3.4 Phase 3: replaces getBestProvider(criteria, true). */
115
- private String pickProvider() {
116
- if (locationManager == null) return null;
117
- if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
118
- return LocationManager.GPS_PROVIDER;
119
- }
120
- if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
121
- return LocationManager.NETWORK_PROVIDER;
180
+ // Legacy single-update PI + receiver (only used when usingFused == false).
181
+ if (!usingFused) {
182
+ Intent singleLocationUpdateIntent = new Intent(mContext, SingleUpdateReceiver.class);
183
+ singleLocationUpdateIntent.setAction(SINGLE_LOCATION_UPDATE_ACTION);
184
+ singleUpdatePI = PendingIntent.getBroadcast(mContext, 9003, singleLocationUpdateIntent, singleUpdateFlag);
185
+ registerReceiver(singleUpdateReceiver, new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION));
122
186
  }
123
- return null;
124
187
  }
125
188
 
126
189
  @Override
@@ -141,7 +204,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
141
204
  return;
142
205
  }
143
206
 
144
- logger.info("Start recording");
207
+ logger.info("Start recording (path={})", usingFused ? "fused" : "legacy");
145
208
  scaledDistanceFilter = mConfig.getDistanceFilter();
146
209
  isStarted = true;
147
210
  setPace(false);
@@ -153,12 +216,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
153
216
  return;
154
217
  }
155
218
  try {
156
- if (locationManager != null) {
157
- locationManager.removeUpdates(this);
158
- if (stationaryRegionPI != null) {
159
- locationManager.removeProximityAlert(stationaryRegionPI);
160
- }
161
- }
219
+ unsubscribeLocationUpdates();
162
220
  if (alarmManager != null) {
163
221
  if (stationaryAlarmPI != null) alarmManager.cancel(stationaryAlarmPI);
164
222
  if (stationaryLocationPollingPI != null) alarmManager.cancel(stationaryLocationPollingPI);
@@ -192,31 +250,82 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
192
250
  return isStarted;
193
251
  }
194
252
 
253
+ /** GPS first, fall back to Network — used only by the legacy LocationManager path. */
254
+ private String pickProvider() {
255
+ if (locationManager == null) return null;
256
+ if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
257
+ return LocationManager.GPS_PROVIDER;
258
+ }
259
+ if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
260
+ return LocationManager.NETWORK_PROVIDER;
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /** Cheap check used to decide whether to emit SERVICE_ERROR when subscribing. */
266
+ private boolean anyProviderEnabled() {
267
+ if (locationManager == null) return true;
268
+ boolean gpsOn = false, netOn = false;
269
+ try { gpsOn = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); }
270
+ catch (Exception ignored) { /* may throw on devices with no GPS hardware */ }
271
+ try { netOn = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); }
272
+ catch (Exception ignored) { }
273
+ return gpsOn || netOn;
274
+ }
275
+
276
+ /** Translate the plugin's desiredAccuracy buckets into FLP priorities (FLP path only). */
277
+ private int translatePriority(Integer accuracy) {
278
+ if (accuracy == null) {
279
+ return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
280
+ }
281
+ if (accuracy >= 10000) {
282
+ return Priority.PRIORITY_PASSIVE;
283
+ }
284
+ if (accuracy >= 1000) {
285
+ return Priority.PRIORITY_LOW_POWER;
286
+ }
287
+ if (accuracy >= 100) {
288
+ return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
289
+ }
290
+ return Priority.PRIORITY_HIGH_ACCURACY;
291
+ }
292
+
293
+ private void unsubscribeLocationUpdates() {
294
+ try {
295
+ if (usingFused && fusedClient != null) {
296
+ fusedClient.removeLocationUpdates(fusedCallback);
297
+ fusedClient.removeLocationUpdates(fusedPollCallback);
298
+ }
299
+ if (locationManager != null) {
300
+ locationManager.removeUpdates(this);
301
+ }
302
+ } catch (SecurityException ignored) {
303
+ }
304
+ }
305
+
195
306
  /**
196
- *
197
- * @param value set true to engage "aggressive", battery-consuming tracking, false for stationary-region tracking
307
+ * @param value true → aggressive moving tracking, false → stationary monitoring
198
308
  */
199
309
  private void setPace(Boolean value) {
200
310
  if (!isStarted) {
201
311
  return;
202
312
  }
203
- if (mConfig == null || locationManager == null) {
313
+ if (mConfig == null) {
204
314
  return;
205
315
  }
206
316
 
207
317
  logger.info("Setting pace: {}", value);
208
318
 
209
- Boolean wasMoving = isMoving;
210
- isMoving = value;
211
- isAcquiringStationaryLocation = false;
212
- isAcquiringSpeed = false;
213
- stationaryLocation = null;
319
+ Boolean wasMoving = isMoving;
320
+ isMoving = value;
321
+ isAcquiringStationaryLocation = false;
322
+ isAcquiringSpeed = false;
323
+ stationaryLocation = null;
214
324
 
215
325
  try {
216
- locationManager.removeUpdates(this);
326
+ unsubscribeLocationUpdates();
217
327
 
218
328
  if (isMoving) {
219
- // setPace can be called while moving, after distanceFilter has been recalculated. We don't want to re-acquire velocity in this case.
220
329
  if (!wasMoving) {
221
330
  isAcquiringSpeed = true;
222
331
  }
@@ -224,95 +333,134 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
224
333
  isAcquiringStationaryLocation = true;
225
334
  }
226
335
 
227
- // Temporarily turn on super-aggressive geolocation on all providers when acquiring velocity or stationary location.
336
+ if (!anyProviderEnabled()) {
337
+ handleServiceError("No location provider available (GPS and Network disabled).");
338
+ }
339
+
340
+ if (usingFused) {
341
+ subscribeFused();
342
+ } else {
343
+ subscribeLegacy();
344
+ }
345
+ } catch (SecurityException e) {
346
+ logger.error("Security exception: {}", e.getMessage());
347
+ this.handleSecurityException(e);
348
+ }
349
+ }
350
+
351
+ private void subscribeFused() {
352
+ if (fusedClient == null) return;
353
+ LocationRequest request;
354
+ if (isAcquiringSpeed || isAcquiringStationaryLocation) {
355
+ locationAcquisitionAttempts = 0;
356
+ request = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, ACQUISITION_INTERVAL_MS)
357
+ .setMinUpdateIntervalMillis(ACQUISITION_INTERVAL_MS)
358
+ .setWaitForAccurateLocation(false)
359
+ .build();
360
+ } else {
361
+ int priority = translatePriority(mConfig.getDesiredAccuracy());
362
+ long interval = mConfig.getInterval();
363
+ LocationRequest.Builder b = new LocationRequest.Builder(priority, interval)
364
+ .setMinUpdateIntervalMillis(Math.min(interval, 1000L))
365
+ .setWaitForAccurateLocation(false);
366
+ if (scaledDistanceFilter != null && scaledDistanceFilter > 0) {
367
+ b.setMinUpdateDistanceMeters(scaledDistanceFilter.floatValue());
368
+ }
369
+ request = b.build();
370
+ }
371
+ try {
372
+ fusedClient.requestLocationUpdates(request, fusedCallback, Looper.getMainLooper());
373
+ } catch (SecurityException e) {
374
+ this.handleSecurityException(e);
375
+ }
376
+ }
377
+
378
+ private void subscribeLegacy() {
379
+ if (locationManager == null) return;
380
+ try {
228
381
  if (isAcquiringSpeed || isAcquiringStationaryLocation) {
229
382
  locationAcquisitionAttempts = 0;
230
- // Turn on each provider aggressively for a short period of time
383
+ // Burst: subscribe to every non-passive provider for fastest lock.
231
384
  List<String> matchingProviders = locationManager.getAllProviders();
232
- for (String provider: matchingProviders) {
385
+ for (String provider : matchingProviders) {
233
386
  if (!LocationManager.PASSIVE_PROVIDER.equals(provider)) {
234
387
  logger.info("Requesting location updates from provider {}", provider);
235
388
  locationManager.requestLocationUpdates(provider, 0, 0, this);
236
389
  }
237
390
  }
238
391
  } else {
239
- String provider = pickProvider();
240
- if (provider == null) {
392
+ // v4.5.2 subscribe to GPS AND Network simultaneously when both
393
+ // are available. The previous version only used GPS-or-Network
394
+ // (excluyente), which on cheap/vehicular Androids could leave the
395
+ // app waiting for a GPS fix while a quick Network fix was available.
396
+ boolean gpsOn = false, netOn = false;
397
+ try { gpsOn = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); }
398
+ catch (Exception ignored) { }
399
+ try { netOn = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); }
400
+ catch (Exception ignored) { }
401
+ if (!gpsOn && !netOn) {
241
402
  logger.warn("No location provider available (GPS and Network disabled)");
242
403
  return;
243
404
  }
244
- logger.info("Requesting location updates from provider {}", provider);
245
- locationManager.requestLocationUpdates(provider, mConfig.getInterval(), scaledDistanceFilter, this);
405
+ long interval = mConfig.getInterval();
406
+ int distance = scaledDistanceFilter != null ? scaledDistanceFilter : 0;
407
+ if (gpsOn) {
408
+ logger.info("Requesting location updates from GPS");
409
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, distance, this);
410
+ }
411
+ if (netOn) {
412
+ logger.info("Requesting location updates from Network");
413
+ locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, interval, distance, this);
414
+ }
246
415
  }
247
416
  } catch (SecurityException e) {
248
- logger.error("Security exception: {}", e.getMessage());
249
417
  this.handleSecurityException(e);
250
418
  }
251
419
  }
252
420
 
253
- // v3.4 Phase 3: translateDesiredAccuracy(...) returning Criteria.ACCURACY_* removed.
254
- // Provider selection no longer depends on Criteria; pickProvider() chooses GPS-first.
421
+ // ====== LocationListener (legacy path) ======
255
422
 
256
- /**
257
- * Returns the most accurate and timely previously detected location.
258
- * Where the last result is beyond the specified maximum distance or
259
- * latency a one-off location update is returned via the {@link LocationListener}
260
- * specified in {@link setChangedLocationListener}.
261
- * @param minTime Minimum time required between location updates.
262
- * @return The most accurate and / or timely previously detected location.
263
- */
264
- public Location getLastBestLocation() {
265
- if (mConfig == null || locationManager == null) {
266
- return null;
267
- }
268
- Location bestResult = null;
269
- String bestProvider = null;
270
- float bestAccuracy = Float.MAX_VALUE;
271
- long bestTime = Long.MIN_VALUE;
272
- long minTime = System.currentTimeMillis() - mConfig.getInterval();
423
+ @Override
424
+ public void onLocationChanged(Location location) {
425
+ handleNewLocation(location);
426
+ }
273
427
 
274
- logger.info("Fetching last best location: radius={} minTime={}", mConfig.getStationaryRadius(), minTime);
428
+ @Override
429
+ public void onStatusChanged(String provider, int status, Bundle extras) {
430
+ logger.debug("Provider {} status changed: {}", provider, status);
431
+ }
275
432
 
276
- try {
277
- // Iterate through all the providers on the system, keeping
278
- // note of the most accurate result within the acceptable time limit.
279
- // If no result is found within maxTime, return the newest Location.
280
- List<String> matchingProviders = locationManager.getAllProviders();
281
- for (String provider: matchingProviders) {
282
- Location location = locationManager.getLastKnownLocation(provider);
283
- if (location != null) {
284
- logger.debug("Test provider={} lat={} lon={} acy={} v={}m/s time={}", provider, location.getLatitude(), location.getLongitude(), location.getAccuracy(), location.getSpeed(), location.getTime());
285
- float accuracy = location.getAccuracy();
286
- long time = location.getTime();
287
- if ((time > minTime && accuracy < bestAccuracy)) {
288
- bestProvider = provider;
289
- bestResult = location;
290
- bestAccuracy = accuracy;
291
- bestTime = time;
292
- }
293
- }
294
- }
433
+ @Override
434
+ public void onProviderEnabled(String provider) {
435
+ logger.debug("Provider {} was enabled", provider);
436
+ }
295
437
 
296
- if (bestResult != null) {
297
- logger.debug("Best result found provider={} lat={} lon={} acy={} v={}m/s time={}", bestProvider, bestResult.getLatitude(), bestResult.getLongitude(), bestResult.getAccuracy(), bestResult.getSpeed(), bestResult.getTime());
298
- }
299
- } catch (SecurityException e) {
300
- logger.error("Security exception: {}", e.getMessage());
301
- this.handleSecurityException(e);
438
+ @Override
439
+ public void onProviderDisabled(String provider) {
440
+ logger.warn("Provider {} was disabled", provider);
441
+ // v4.5.2: surface as an error so JS layer can prompt the user to enable
442
+ // location services. Only when no fallback provider is left.
443
+ if (locationManager != null && pickProvider() == null) {
444
+ handleServiceError("Location provider '" + provider + "' disabled and no fallback available.");
302
445
  }
303
-
304
- return bestResult;
305
446
  }
306
447
 
307
- public void onLocationChanged(Location location) {
448
+ // ====== State machine (path-agnostic) ======
449
+
450
+ /**
451
+ * Same logic as the legacy {@code onLocationChanged}: handles the moving / stationary /
452
+ * acquisition state machine. Called from both the FLP callback and the LocationListener.
453
+ */
454
+ private void handleNewLocation(Location location) {
455
+ if (location == null) return;
308
456
  logger.debug("Location change: {} isMoving={}", location.toString(), isMoving);
309
457
 
310
458
  if (!isMoving && !isAcquiringStationaryLocation && stationaryLocation==null) {
311
- // Perhaps our GPS signal was interupted, re-acquire a stationaryLocation now.
459
+ // Perhaps our GPS signal was interrupted, re-acquire a stationary location now.
312
460
  setPace(false);
313
461
  }
314
462
 
315
- showDebugToast( "mv:" + isMoving + ",acy:" + location.getAccuracy() + ",v:" + location.getSpeed() + ",df:" + scaledDistanceFilter);
463
+ showDebugToast("mv:" + isMoving + ",acy:" + location.getAccuracy() + ",v:" + location.getSpeed() + ",df:" + scaledDistanceFilter);
316
464
 
317
465
  if (isAcquiringStationaryLocation) {
318
466
  if (stationaryLocation == null || stationaryLocation.getAccuracy() > location.getAccuracy()) {
@@ -320,17 +468,15 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
320
468
  }
321
469
  if (++locationAcquisitionAttempts == MAX_STATIONARY_ACQUISITION_ATTEMPTS) {
322
470
  isAcquiringStationaryLocation = false;
323
- startMonitoringStationaryRegion(stationaryLocation);
471
+ enterStationary(stationaryLocation);
324
472
  handleStationary(stationaryLocation, stationaryRadius);
325
473
  return;
326
474
  } else {
327
- // Unacceptable stationary-location: bail-out and wait for another.
328
475
  playDebugTone(Tone.BEEP);
329
476
  return;
330
477
  }
331
478
  } else if (isAcquiringSpeed) {
332
479
  if (++locationAcquisitionAttempts == MAX_SPEED_ACQUISITION_ATTEMPTS) {
333
- // Got enough samples, assume we're confident in reported speed now. Play "woohoo" sound.
334
480
  playDebugTone(Tone.DOODLY_DOO);
335
481
  isAcquiringSpeed = false;
336
482
  scaledDistanceFilter = calculateDistanceFilter(location.getSpeed());
@@ -342,11 +488,9 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
342
488
  } else if (isMoving) {
343
489
  playDebugTone(Tone.BEEP);
344
490
 
345
- // Only reset stationaryAlarm when accurate speed is detected, prevents spurious locations from resetting when stopped.
346
- if ( (location.getSpeed() >= 1) && (location.getAccuracy() <= mConfig.getStationaryRadius()) ) {
491
+ if ((location.getSpeed() >= 1) && (location.getAccuracy() <= mConfig.getStationaryRadius())) {
347
492
  resetStationaryAlarm();
348
493
  }
349
- // Calculate latest distanceFilter, if it changed by 5 m/s, we'll reconfigure our pace.
350
494
  Integer newDistanceFilter = calculateDistanceFilter(location.getSpeed());
351
495
  if (newDistanceFilter != scaledDistanceFilter.intValue()) {
352
496
  logger.info("Updating distanceFilter: new={} old={}", newDistanceFilter, scaledDistanceFilter);
@@ -359,7 +503,6 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
359
503
  } else if (stationaryLocation != null) {
360
504
  return;
361
505
  }
362
- // Go ahead and cache, push to server
363
506
  lastLocation = location;
364
507
  handleLocation(location);
365
508
  }
@@ -367,7 +510,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
367
510
  public void resetStationaryAlarm() {
368
511
  if (alarmManager == null || stationaryAlarmPI == null) return;
369
512
  alarmManager.cancel(stationaryAlarmPI);
370
- alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + STATIONARY_TIMEOUT, stationaryAlarmPI);
513
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + getStationaryTimeout(), stationaryAlarmPI);
371
514
  }
372
515
 
373
516
  private Integer calculateDistanceFilter(Float speed) {
@@ -379,52 +522,40 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
379
522
  return (newDistanceFilter.intValue() < 1000) ? newDistanceFilter.intValue() : 1000;
380
523
  }
381
524
 
382
- private void startMonitoringStationaryRegion(Location location) {
383
- if (location == null || locationManager == null || mConfig == null || stationaryRegionPI == null) return;
525
+ /**
526
+ * v4.5.2 Stop active updates and start the polling-based stationary monitor.
527
+ * The previous version also called {@code addProximityAlert} (geofence); that
528
+ * path has been removed per product decision (no geofencing).
529
+ */
530
+ private void enterStationary(Location location) {
531
+ if (location == null || mConfig == null) return;
384
532
  try {
385
- locationManager.removeUpdates(this);
533
+ unsubscribeLocationUpdates();
386
534
 
387
- float stationaryRadius = mConfig.getStationaryRadius();
388
- float proximityRadius = (location.getAccuracy() < stationaryRadius) ? stationaryRadius : location.getAccuracy();
535
+ float radius = mConfig.getStationaryRadius();
536
+ float proximityRadius = (location.getAccuracy() < radius) ? radius : location.getAccuracy();
389
537
  stationaryLocation = location;
390
-
391
- logger.info("startMonitoringStationaryRegion: lat={} lon={} acy={}", location.getLatitude(), location.getLongitude(), proximityRadius);
392
-
393
- // Here be the execution of the stationary region monitor
394
- locationManager.addProximityAlert(
395
- location.getLatitude(),
396
- location.getLongitude(),
397
- proximityRadius,
398
- (long)-1,
399
- stationaryRegionPI
400
- );
401
-
402
538
  this.stationaryRadius = proximityRadius;
403
539
 
404
- startPollingStationaryLocation(STATIONARY_LOCATION_POLLING_INTERVAL_LAZY);
540
+ logger.info("enterStationary: lat={} lon={} acy={} radius={}", location.getLatitude(), location.getLongitude(), location.getAccuracy(), proximityRadius);
541
+
542
+ startPollingStationaryLocation(getStationaryPollLazy());
405
543
  } catch (SecurityException e) {
406
544
  logger.error("Security exception: {}", e.getMessage());
407
545
  this.handleSecurityException(e);
408
546
  }
409
547
  }
410
548
 
411
- /**
412
- * User has exit his stationary region! Initiate aggressive geolocation!
413
- */
549
+ /** Engage aggressive geolocation after stationary exit. */
414
550
  public void onExitStationaryRegion(Location location) {
415
- if (location == null || alarmManager == null || locationManager == null) return;
551
+ if (location == null || alarmManager == null) return;
416
552
 
417
553
  playDebugTone(Tone.BEEP_BEEP_BEEP);
418
554
 
419
- logger.info("Exited stationary: lat={} long={} acy={}}'",
420
- location.getLatitude(), location.getLongitude(), location.getAccuracy());
555
+ logger.info("Exited stationary: lat={} long={} acy={}", location.getLatitude(), location.getLongitude(), location.getAccuracy());
421
556
 
422
557
  try {
423
- // Cancel the periodic stationary location monitor alarm.
424
558
  alarmManager.cancel(stationaryLocationPollingPI);
425
- // Kill the current region-monitor we just walked out of.
426
- locationManager.removeProximityAlert(stationaryRegionPI);
427
- // Engage aggressive tracking.
428
559
  this.setPace(true);
429
560
  } catch (SecurityException e) {
430
561
  logger.error("Security exception: {}", e.getMessage());
@@ -434,8 +565,6 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
434
565
 
435
566
  public void startPollingStationaryLocation(long interval) {
436
567
  if (alarmManager == null || stationaryLocationPollingPI == null) return;
437
- // proximity-alerts don't seem to work while suspended in latest Android 4.42 (works in 4.03). Have to use AlarmManager to sample
438
- // location at regular intervals with a one-shot.
439
568
  stationaryLocationPollingInterval = interval;
440
569
  alarmManager.cancel(stationaryLocationPollingPI);
441
570
  long start = System.currentTimeMillis() + 60_000;
@@ -445,7 +574,7 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
445
574
  public void onPollStationaryLocation(Location location) {
446
575
  if (location == null || mConfig == null) return;
447
576
 
448
- float stationaryRadius = mConfig.getStationaryRadius();
577
+ float radius = mConfig.getStationaryRadius();
449
578
  if (isMoving) {
450
579
  return;
451
580
  }
@@ -456,128 +585,84 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
456
585
  distance = abs(location.distanceTo(stationaryLocation) - stationaryLocation.getAccuracy() - location.getAccuracy());
457
586
  }
458
587
 
459
- showDebugToast("Stationary exit in " + (stationaryRadius-distance) + "m");
588
+ showDebugToast("Stationary exit in " + (radius - distance) + "m");
460
589
 
461
- // TODO http://www.cse.buffalo.edu/~demirbas/publications/proximity.pdf
462
- // determine if we're almost out of stationary-distance and increase monitoring-rate.
463
590
  logger.info("Distance from stationary location: {}", distance);
464
- if (distance > stationaryRadius) {
591
+ if (distance > radius) {
465
592
  onExitStationaryRegion(location);
466
593
  } 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);
594
+ startPollingStationaryLocation(getStationaryPollFast());
595
+ } else if (stationaryLocationPollingInterval != getStationaryPollLazy()) {
596
+ startPollingStationaryLocation(getStationaryPollLazy());
470
597
  }
471
598
  }
472
599
 
473
- /**
474
- * Broadcast receiver for receiving a single-update from LocationManager.
475
- */
476
- private class SingleUpdateReceiver extends BroadcastReceiver {
477
- @Override
478
- public void onReceive(Context context, Intent intent) {
479
- Bundle extras = intent.getExtras();
480
- if (extras == null) return;
481
- String key = LocationManager.KEY_LOCATION_CHANGED;
482
- Location location = extras.getParcelable(key);
483
- if (location != null) {
484
- logger.debug("Single location update: " + location.toString());
485
- onPollStationaryLocation(location);
486
- }
487
- }
488
- }
600
+ // ====== Receivers ======
489
601
 
490
- private BroadcastReceiver singleUpdateReceiver = new SingleUpdateReceiver();
491
-
492
- /**
493
- * Broadcast receiver which detects a user has stopped for a long enough time to be determined as STOPPED
494
- */
495
602
  private class StationaryAlarmReceiver extends BroadcastReceiver {
496
603
  @Override
497
- public void onReceive(Context context, Intent intent)
498
- {
604
+ public void onReceive(Context context, Intent intent) {
499
605
  logger.info("stationaryAlarm fired");
500
606
  setPace(false);
501
607
  }
502
608
  }
503
-
504
609
  private BroadcastReceiver stationaryAlarmReceiver = new StationaryAlarmReceiver();
505
610
 
506
611
  /**
507
- * Broadcast receiver to handle stationaryMonitor alarm, fired at low frequency while monitoring stationary-region.
508
- * This is required because latest Android proximity-alerts don't seem to operate while suspended. Regularly polling
509
- * the location seems to trigger the proximity-alerts while suspended.
612
+ * Triggered by the inexact repeating alarm to poll a single fresh location while
613
+ * inside the stationary region. Uses FLP one-shot when available, otherwise falls
614
+ * back to {@link LocationManager#requestSingleUpdate(String, PendingIntent)}.
510
615
  */
511
616
  private class StationaryLocationMonitorReceiver extends BroadcastReceiver {
512
617
  @Override
513
- public void onReceive(Context context, Intent intent)
514
- {
515
- if (locationManager == null || singleUpdatePI == null) return;
516
-
618
+ public void onReceive(Context context, Intent intent) {
517
619
  logger.info("Stationary location monitor fired");
518
620
  playDebugTone(Tone.DIALTONE);
519
621
 
520
- // v3.4 Phase 3: Criteria-based requestSingleUpdate removed (deprecated since API 31).
521
- // Use the provider-string overload, which is still supported and stable.
522
- String provider = pickProvider();
523
- if (provider == null) {
524
- logger.warn("Stationary monitor: no provider available");
525
- return;
526
- }
527
- try {
528
- locationManager.requestSingleUpdate(provider, singleUpdatePI);
529
- } catch (SecurityException e) {
530
- logger.error("Security exception: {}", e.getMessage());
531
- } catch (IllegalArgumentException e) {
532
- logger.warn("requestSingleUpdate failed: {}", e.getMessage());
622
+ if (usingFused && fusedClient != null) {
623
+ LocationRequest oneShot = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 0L)
624
+ .setMaxUpdates(1)
625
+ .setWaitForAccurateLocation(false)
626
+ .build();
627
+ try {
628
+ fusedClient.requestLocationUpdates(oneShot, fusedPollCallback, Looper.getMainLooper());
629
+ } catch (SecurityException e) {
630
+ logger.error("Security exception (FLP one-shot): {}", e.getMessage());
631
+ } catch (IllegalArgumentException e) {
632
+ logger.warn("FLP one-shot failed: {}", e.getMessage());
633
+ }
634
+ } else if (locationManager != null && singleUpdatePI != null) {
635
+ String provider = pickProvider();
636
+ if (provider == null) {
637
+ logger.warn("Stationary monitor: no provider available");
638
+ return;
639
+ }
640
+ try {
641
+ locationManager.requestSingleUpdate(provider, singleUpdatePI);
642
+ } catch (SecurityException e) {
643
+ logger.error("Security exception (single update): {}", e.getMessage());
644
+ } catch (IllegalArgumentException e) {
645
+ logger.warn("requestSingleUpdate failed: {}", e.getMessage());
646
+ }
533
647
  }
534
648
  }
535
649
  }
536
-
537
650
  private BroadcastReceiver stationaryLocationMonitorReceiver = new StationaryLocationMonitorReceiver();
538
651
 
539
- /**
540
- * Broadcast receiver which detects a user has exit his circular stationary-region determined by the greater of stationaryLocation.getAccuracy() OR stationaryRadius
541
- */
542
- private class StationaryRegionReceiver extends BroadcastReceiver {
652
+ /** Legacy single-update receiver — feeds {@link #onPollStationaryLocation(Location)}. */
653
+ private class SingleUpdateReceiver extends BroadcastReceiver {
543
654
  @Override
544
655
  public void onReceive(Context context, Intent intent) {
545
- String key = LocationManager.KEY_PROXIMITY_ENTERING;
546
- Boolean entering = intent.getBooleanExtra(key, false);
547
-
548
- if (entering) {
549
- logger.debug("Entering stationary region");
550
- if (isMoving) {
551
- setPace(false);
552
- }
553
- }
554
- else {
555
- logger.debug("Exiting stationary region");
556
- // There MUST be a valid, recent location if this event-handler was called.
557
- Location location = getLastBestLocation();
558
- if (location != null) {
559
- onExitStationaryRegion(location);
560
- }
656
+ Bundle extras = intent.getExtras();
657
+ if (extras == null) return;
658
+ Location location = extras.getParcelable(LocationManager.KEY_LOCATION_CHANGED);
659
+ if (location != null) {
660
+ logger.debug("Single location update: {}", location);
661
+ onPollStationaryLocation(location);
561
662
  }
562
663
  }
563
664
  }
564
-
565
- private BroadcastReceiver stationaryRegionReceiver = new StationaryRegionReceiver();
566
-
567
- public void onProviderDisabled(String provider) {
568
- // TODO Auto-generated method stub
569
- logger.debug("Provider {} was disabled", provider);
570
- }
571
-
572
- public void onProviderEnabled(String provider) {
573
- // TODO Auto-generated method stub
574
- logger.debug("Provider {} was enabled", provider);
575
- }
576
-
577
- public void onStatusChanged(String provider, int status, Bundle extras) {
578
- // TODO Auto-generated method stub
579
- logger.debug("Provider {} status changed: {}", provider, status);
580
- }
665
+ private BroadcastReceiver singleUpdateReceiver = new SingleUpdateReceiver();
581
666
 
582
667
  @Override
583
668
  public void onDestroy() {
@@ -590,9 +675,10 @@ public class DistanceFilterLocationProvider extends AbstractLocationProvider imp
590
675
  }
591
676
 
592
677
  try { unregisterReceiver(stationaryAlarmReceiver); } catch (Exception ignored) { }
593
- try { unregisterReceiver(singleUpdateReceiver); } catch (Exception ignored) { }
594
- try { unregisterReceiver(stationaryRegionReceiver); } catch (Exception ignored) { }
595
678
  try { unregisterReceiver(stationaryLocationMonitorReceiver); } catch (Exception ignored) { }
679
+ if (!usingFused) {
680
+ try { unregisterReceiver(singleUpdateReceiver); } catch (Exception ignored) { }
681
+ }
596
682
 
597
683
  super.onDestroy();
598
684
  }