@josuelmm/cordova-background-geolocation 3.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.npmignore +4 -0
  2. package/CHANGELOG.md +290 -0
  3. package/CLAUDE.md +56 -0
  4. package/HISTORY.md +125 -0
  5. package/README.md +189 -4
  6. package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +90 -0
  7. package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +310 -1
  8. package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +127 -0
  9. package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +27 -11
  10. package/android/common/src/main/java/com/marianhello/bgloc/Config.java +268 -0
  11. package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +86 -26
  12. package/android/common/src/main/java/com/marianhello/bgloc/PluginDelegate.java +26 -0
  13. package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +42 -5
  14. package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
  15. package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
  16. package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
  17. package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
  18. package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
  19. package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
  20. package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
  21. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +305 -6
  22. package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
  23. package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
  24. package/android/dependencies.gradle +0 -3
  25. package/angular/background-geolocation-events.ts +21 -0
  26. package/angular/background-geolocation.service.ts +63 -0
  27. package/angular/dist/background-geolocation-events.d.ts +18 -1
  28. package/angular/dist/background-geolocation.service.d.ts +36 -0
  29. package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
  30. package/angular/dist/esm2022/background-geolocation.service.mjs +35 -1
  31. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +55 -0
  32. package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
  33. package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +312 -1
  34. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +22 -0
  35. package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +400 -15
  36. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
  37. package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
  38. package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
  39. package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
  40. package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
  41. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
  42. package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +97 -44
  43. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
  44. package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
  45. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
  46. package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
  47. package/package.json +41 -1
  48. package/plugin.xml +19 -8
  49. package/www/BackgroundGeolocation.d.ts +517 -3
  50. package/www/BackgroundGeolocation.js +54 -1
  51. package/RELEASE.MD +0 -16
@@ -0,0 +1,265 @@
1
+ package com.marianhello.bgloc.driving;
2
+
3
+ import com.marianhello.bgloc.data.BackgroundLocation;
4
+
5
+ /**
6
+ * v4.0 Phase 6 — Driver insights state machine (GPS-only).
7
+ *
8
+ * Pure-Java helper, no Android imports. Hosted by {@code LocationServiceImpl}, which
9
+ * feeds it every received location and surfaces emitted events via the plugin
10
+ * {@code MSG_ON_*} broadcast pipeline.
11
+ *
12
+ * Heuristics:
13
+ * moving = speed > minMovingSpeed
14
+ * stopped = !moving for stoppedDuration ms
15
+ * tripStart = stopped → moving with speed >= minTripSpeed sustained for minTripDuration ms
16
+ * tripEnd = moving → stopped (after a tripStart)
17
+ * speeding = first crossing above speedLimit (km/h); rearms on drop below
18
+ *
19
+ * Sensor-fusion events (hardBrake / sharpTurn / possibleCrash) are intentionally NOT
20
+ * implemented in this class. They require linear acceleration + gyroscope sampling and
21
+ * are planned for v4.1 in a separate {@code SensorFusionDetector}.
22
+ */
23
+ public class DrivingEventsDetector {
24
+
25
+ public interface Listener {
26
+ void onMoving(BackgroundLocation location);
27
+ void onStopped(BackgroundLocation location);
28
+ void onTripStart(BackgroundLocation location);
29
+ /** distance in meters, durationMs in milliseconds. */
30
+ void onTripEnd(BackgroundLocation location, double distance, long durationMs);
31
+ void onSpeeding(BackgroundLocation location, double speedKmh, double limitKmh);
32
+ void onProviderChange(String provider);
33
+ // v4.1 GPS-derived driving events
34
+ /** GPS-derived deceleration (m/s²). Negative number, more negative = harder brake. */
35
+ void onHardBrake(BackgroundLocation location, double decelMps2);
36
+ /** GPS-derived acceleration (m/s²). */
37
+ void onRapidAcceleration(BackgroundLocation location, double accelMps2);
38
+ /** Bearing change rate (deg/s). */
39
+ void onSharpTurn(BackgroundLocation location, double degPerSec);
40
+ /** Velocity drop in km/h within {@code crashWindowMs} while tripActive. */
41
+ void onPossibleCrash(BackgroundLocation location, double velocityDropKmh);
42
+ }
43
+
44
+ public static class Config {
45
+ public boolean enabled = false;
46
+ public double speedLimitKmh = 0; // 0 disables speeding
47
+ public double minMovingSpeedMps = 1.0; // ~3.6 km/h
48
+ public long stoppedDurationMs = 60_000;
49
+ public double minTripSpeedMps = 3.0; // ~10.8 km/h
50
+ public long minTripDurationMs = 30_000;
51
+ // v4.1 GPS-derived driving events. 0 disables each one.
52
+ public double hardBrakeMps2 = 3.5; // -m/s² threshold (positive value)
53
+ public double rapidAccelMps2 = 3.5; // m/s² threshold
54
+ public double sharpTurnDegPerSec = 30; // deg/sec, requires speed > 5 m/s
55
+ public double crashImpactKmh = 25; // velocity drop within crashWindow
56
+ public long crashWindowMs = 2_000; // window to evaluate the velocity drop
57
+ }
58
+
59
+ private final Listener listener;
60
+ private Config cfg = new Config();
61
+
62
+ // State
63
+ private boolean isMoving = false;
64
+ private boolean tripActive = false;
65
+ private long tripStartedAt = 0;
66
+ private double tripDistanceMeters = 0;
67
+ private double tripStartLat, tripStartLon;
68
+ private boolean hasTripStartCoord = false;
69
+
70
+ private long aboveTripSpeedSince = 0; // first sample with speed >= minTripSpeed
71
+ private long belowMovingSinceMs = 0; // first sample with speed < minMovingSpeed
72
+
73
+ private boolean wasSpeeding = false;
74
+ private String lastProvider;
75
+
76
+ private double prevLat, prevLon;
77
+ private boolean hasPrev = false;
78
+
79
+ // v4.1 GPS-derived deltas
80
+ private double prevSpeedMps = 0.0;
81
+ private long prevSpeedAt = 0L;
82
+ private double prevBearingDeg = 0.0;
83
+ private boolean hasPrevBearing = false;
84
+ private long prevBearingAt = 0L;
85
+ /** Cooldown so we don't refire the same event on every fix in a sustained brake. */
86
+ private long lastHardBrakeAt = 0L, lastRapidAccelAt = 0L, lastSharpTurnAt = 0L, lastCrashAt = 0L;
87
+ private static final long DRIVING_EVENT_COOLDOWN_MS = 4_000L;
88
+
89
+ public DrivingEventsDetector(Listener listener) {
90
+ this.listener = listener;
91
+ }
92
+
93
+ public synchronized void setConfig(Config c) {
94
+ if (c != null) this.cfg = c;
95
+ }
96
+
97
+ /** Reset internal state. Called when service stops. */
98
+ public synchronized void reset() {
99
+ prevSpeedMps = 0.0;
100
+ prevSpeedAt = 0L;
101
+ prevBearingDeg = 0.0;
102
+ hasPrevBearing = false;
103
+ prevBearingAt = 0L;
104
+ lastHardBrakeAt = lastRapidAccelAt = lastSharpTurnAt = lastCrashAt = 0L;
105
+ isMoving = false;
106
+ tripActive = false;
107
+ tripStartedAt = 0;
108
+ tripDistanceMeters = 0;
109
+ hasTripStartCoord = false;
110
+ aboveTripSpeedSince = 0;
111
+ belowMovingSinceMs = 0;
112
+ wasSpeeding = false;
113
+ lastProvider = null;
114
+ hasPrev = false;
115
+ }
116
+
117
+ public synchronized void onLocation(BackgroundLocation loc) {
118
+ if (!cfg.enabled || loc == null) return;
119
+ long now = System.currentTimeMillis();
120
+ double speed = loc.hasSpeed() ? loc.getSpeed() : 0.0;
121
+
122
+ // Provider change
123
+ String provider = loc.getProvider();
124
+ if (provider != null && !provider.equals(lastProvider)) {
125
+ lastProvider = provider;
126
+ if (listener != null) listener.onProviderChange(provider);
127
+ }
128
+
129
+ // Distance accumulator (very simple: planar haversine approximation via
130
+ // Location.distanceBetween is not used here to keep this class platform-free;
131
+ // consumer can swap to BackgroundLocation.distanceTo in onTripEnd if desired).
132
+ double curLat = loc.getLatitude();
133
+ double curLon = loc.getLongitude();
134
+ if (hasPrev && tripActive) {
135
+ tripDistanceMeters += haversineMeters(prevLat, prevLon, curLat, curLon);
136
+ }
137
+ prevLat = curLat;
138
+ prevLon = curLon;
139
+ hasPrev = true;
140
+
141
+ // Moving / stopped state
142
+ boolean nowMoving = speed >= cfg.minMovingSpeedMps;
143
+ if (nowMoving) {
144
+ belowMovingSinceMs = 0;
145
+ if (!isMoving) {
146
+ isMoving = true;
147
+ if (listener != null) listener.onMoving(loc);
148
+ }
149
+ // Trip start arming
150
+ if (!tripActive) {
151
+ if (speed >= cfg.minTripSpeedMps) {
152
+ if (aboveTripSpeedSince == 0) aboveTripSpeedSince = now;
153
+ if (now - aboveTripSpeedSince >= cfg.minTripDurationMs) {
154
+ tripActive = true;
155
+ tripStartedAt = now;
156
+ tripDistanceMeters = 0;
157
+ tripStartLat = curLat;
158
+ tripStartLon = curLon;
159
+ hasTripStartCoord = true;
160
+ if (listener != null) listener.onTripStart(loc);
161
+ }
162
+ } else {
163
+ aboveTripSpeedSince = 0;
164
+ }
165
+ }
166
+ } else {
167
+ aboveTripSpeedSince = 0;
168
+ if (belowMovingSinceMs == 0) belowMovingSinceMs = now;
169
+ if (isMoving && (now - belowMovingSinceMs) >= cfg.stoppedDurationMs) {
170
+ isMoving = false;
171
+ if (listener != null) listener.onStopped(loc);
172
+ if (tripActive) {
173
+ long durMs = now - tripStartedAt;
174
+ double dist = tripDistanceMeters;
175
+ tripActive = false;
176
+ if (listener != null) listener.onTripEnd(loc, dist, durMs);
177
+ }
178
+ }
179
+ }
180
+
181
+ // Speeding (km/h)
182
+ if (cfg.speedLimitKmh > 0) {
183
+ double kmh = speed * 3.6;
184
+ if (kmh > cfg.speedLimitKmh) {
185
+ if (!wasSpeeding) {
186
+ wasSpeeding = true;
187
+ if (listener != null) listener.onSpeeding(loc, kmh, cfg.speedLimitKmh);
188
+ }
189
+ } else {
190
+ // Rearm: emit again on next crossing.
191
+ wasSpeeding = false;
192
+ }
193
+ }
194
+
195
+ // v4.1 GPS-derived driving events (only meaningful during an active trip)
196
+ if (tripActive && prevSpeedAt > 0) {
197
+ long dtMs = now - prevSpeedAt;
198
+ if (dtMs > 0 && dtMs <= 5_000) {
199
+ double dt = dtMs / 1000.0;
200
+ double dv = speed - prevSpeedMps; // m/s
201
+ double accel = dv / dt; // m/s²
202
+
203
+ if (cfg.hardBrakeMps2 > 0
204
+ && accel <= -cfg.hardBrakeMps2
205
+ && (now - lastHardBrakeAt) >= DRIVING_EVENT_COOLDOWN_MS) {
206
+ lastHardBrakeAt = now;
207
+ if (listener != null) listener.onHardBrake(loc, accel);
208
+ }
209
+ if (cfg.rapidAccelMps2 > 0
210
+ && accel >= cfg.rapidAccelMps2
211
+ && (now - lastRapidAccelAt) >= DRIVING_EVENT_COOLDOWN_MS) {
212
+ lastRapidAccelAt = now;
213
+ if (listener != null) listener.onRapidAcceleration(loc, accel);
214
+ }
215
+
216
+ // Possible crash: sustained velocity drop greater than crashImpactKmh in <= crashWindow.
217
+ if (cfg.crashImpactKmh > 0 && dtMs <= cfg.crashWindowMs) {
218
+ double dropKmh = (prevSpeedMps - speed) * 3.6; // positive when slowing down
219
+ if (dropKmh >= cfg.crashImpactKmh
220
+ && speed < 1.5 // ended near stop
221
+ && prevSpeedMps * 3.6 >= cfg.crashImpactKmh
222
+ && (now - lastCrashAt) >= DRIVING_EVENT_COOLDOWN_MS) {
223
+ lastCrashAt = now;
224
+ if (listener != null) listener.onPossibleCrash(loc, dropKmh);
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ // Sharp turn (bearing change rate) — requires meaningful speed to avoid GPS jitter.
231
+ if (cfg.sharpTurnDegPerSec > 0 && loc.hasBearing() && speed >= 5.0 && hasPrevBearing) {
232
+ long dtMs = now - prevBearingAt;
233
+ if (dtMs > 0 && dtMs <= 5_000) {
234
+ double bearing = loc.getBearing();
235
+ double diff = Math.abs(bearing - prevBearingDeg);
236
+ if (diff > 180) diff = 360 - diff;
237
+ double rate = diff * 1000.0 / dtMs;
238
+ if (rate >= cfg.sharpTurnDegPerSec
239
+ && (now - lastSharpTurnAt) >= DRIVING_EVENT_COOLDOWN_MS) {
240
+ lastSharpTurnAt = now;
241
+ if (listener != null) listener.onSharpTurn(loc, rate);
242
+ }
243
+ }
244
+ prevBearingDeg = loc.getBearing();
245
+ prevBearingAt = now;
246
+ } else if (loc.hasBearing()) {
247
+ prevBearingDeg = loc.getBearing();
248
+ prevBearingAt = now;
249
+ hasPrevBearing = true;
250
+ }
251
+
252
+ prevSpeedMps = speed;
253
+ prevSpeedAt = now;
254
+ }
255
+
256
+ private static double haversineMeters(double lat1, double lon1, double lat2, double lon2) {
257
+ double R = 6371000.0;
258
+ double dLat = Math.toRadians(lat2 - lat1);
259
+ double dLon = Math.toRadians(lon2 - lon1);
260
+ double a = Math.sin(dLat/2) * Math.sin(dLat/2)
261
+ + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
262
+ * Math.sin(dLon/2) * Math.sin(dLon/2);
263
+ return 2 * R * Math.asin(Math.sqrt(a));
264
+ }
265
+ }
@@ -0,0 +1,115 @@
1
+ package com.marianhello.bgloc.http;
2
+
3
+ import com.marianhello.bgloc.data.BackgroundLocation;
4
+
5
+ import java.text.SimpleDateFormat;
6
+ import java.util.Date;
7
+ import java.util.HashMap;
8
+ import java.util.Locale;
9
+ import java.util.Map;
10
+ import java.util.TimeZone;
11
+ import java.util.regex.Matcher;
12
+ import java.util.regex.Pattern;
13
+
14
+ /**
15
+ * Resolves placeholders like {lat}, {lon}, {timestamp_iso}, {device_id}, ...
16
+ * in a URL template using a single BackgroundLocation and an optional queryParams map.
17
+ *
18
+ * Placeholders not found in the location/queryParams are left as-is so that
19
+ * partial templates (e.g. only static keys for batch mode) keep working.
20
+ *
21
+ * Usage:
22
+ * String url = UrlTemplateResolver.resolve(template, location, queryParams);
23
+ * // location may be null for batch mode (only queryParams keys are resolved).
24
+ */
25
+ public final class UrlTemplateResolver {
26
+
27
+ private static final Pattern PLACEHOLDER = Pattern.compile("\\{([a-zA-Z0-9_]+)\\}");
28
+
29
+ private UrlTemplateResolver() { /* no instances */ }
30
+
31
+ public static String resolve(String template, BackgroundLocation location, Map<String, ?> queryParams) {
32
+ if (template == null || template.isEmpty()) return template;
33
+ Map<String, String> ctx = buildContext(location, queryParams);
34
+ Matcher m = PLACEHOLDER.matcher(template);
35
+ StringBuffer sb = new StringBuffer(template.length());
36
+ while (m.find()) {
37
+ String key = m.group(1);
38
+ String value = ctx.get(key);
39
+ if (value != null) {
40
+ m.appendReplacement(sb, Matcher.quoteReplacement(urlEncode(value)));
41
+ } else {
42
+ // Leave placeholder as-is if no value available.
43
+ m.appendReplacement(sb, Matcher.quoteReplacement(m.group(0)));
44
+ }
45
+ }
46
+ m.appendTail(sb);
47
+ return sb.toString();
48
+ }
49
+
50
+ public static boolean hasPlaceholders(String template) {
51
+ if (template == null) return false;
52
+ return PLACEHOLDER.matcher(template).find();
53
+ }
54
+
55
+ private static Map<String, String> buildContext(BackgroundLocation loc, Map<String, ?> queryParams) {
56
+ Map<String, String> ctx = new HashMap<String, String>();
57
+
58
+ // queryParams first so location-derived values can override if user wants
59
+ if (queryParams != null) {
60
+ for (Map.Entry<String, ?> e : queryParams.entrySet()) {
61
+ if (e.getValue() != null) {
62
+ ctx.put(e.getKey(), String.valueOf(e.getValue()));
63
+ }
64
+ }
65
+ }
66
+
67
+ if (loc != null) {
68
+ ctx.put("latitude", String.valueOf(loc.getLatitude()));
69
+ ctx.put("longitude", String.valueOf(loc.getLongitude()));
70
+ ctx.put("lat", String.valueOf(loc.getLatitude()));
71
+ ctx.put("lon", String.valueOf(loc.getLongitude()));
72
+
73
+ long timeMs = loc.getTime();
74
+ ctx.put("time", String.valueOf(timeMs));
75
+ ctx.put("timestamp", String.valueOf(timeMs));
76
+ ctx.put("timestamp_iso", isoUtc(timeMs));
77
+
78
+ if (loc.hasSpeed()) ctx.put("speed", String.valueOf(loc.getSpeed()));
79
+ if (loc.hasAltitude()) ctx.put("altitude", String.valueOf(loc.getAltitude()));
80
+ if (loc.hasBearing()) ctx.put("bearing", String.valueOf(loc.getBearing()));
81
+ if (loc.hasAccuracy()) ctx.put("accuracy", String.valueOf(loc.getAccuracy()));
82
+
83
+ if (loc.getProvider() != null) ctx.put("provider", loc.getProvider());
84
+ // is_moving derived from speed when available (>0.5 m/s ~ walking pace).
85
+ if (loc.hasSpeed()) {
86
+ ctx.put("is_moving", loc.getSpeed() > 0.5f ? "true" : "false");
87
+ }
88
+ // {activity} is not produced by BackgroundLocation by default; the user can supply
89
+ // a value via queryParams (already populated above).
90
+ }
91
+
92
+ return ctx;
93
+ }
94
+
95
+ /**
96
+ * URL-encode a placeholder value for use in URL paths and query strings.
97
+ * Spaces become %20, not + (which is form-encoding).
98
+ */
99
+ private static String urlEncode(String s) {
100
+ if (s == null) return "";
101
+ try {
102
+ String enc = java.net.URLEncoder.encode(s, "UTF-8");
103
+ // URLEncoder uses application/x-www-form-urlencoded; convert "+" back to "%20" for URL safety.
104
+ return enc.replace("+", "%20");
105
+ } catch (Exception e) {
106
+ return s;
107
+ }
108
+ }
109
+
110
+ private static String isoUtc(long ms) {
111
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
112
+ sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
113
+ return sdf.format(new Date(ms));
114
+ }
115
+ }
@@ -0,0 +1,214 @@
1
+ package com.marianhello.bgloc.oem;
2
+
3
+ import android.app.Activity;
4
+ import android.content.ComponentName;
5
+ import android.content.Context;
6
+ import android.content.Intent;
7
+ import android.net.Uri;
8
+ import android.os.Build;
9
+ import android.os.PowerManager;
10
+ import android.provider.Settings;
11
+
12
+ import org.json.JSONArray;
13
+ import org.json.JSONException;
14
+ import org.json.JSONObject;
15
+
16
+ /**
17
+ * v3.6 Phase 5: Battery / OEM helpers.
18
+ *
19
+ * Standard Android Doze whitelist:
20
+ * - {@link #isIgnoringBatteryOptimizations(Context)}
21
+ * - {@link #requestIgnoreBatteryOptimizations(Activity)}
22
+ * - {@link #openBatterySettings(Activity)}
23
+ *
24
+ * OEM-specific "auto-start" / "background activity" screens (Xiaomi MIUI, Huawei
25
+ * EMUI, Oppo ColorOS, Vivo FunTouch, Samsung One UI). These cannot be granted
26
+ * programmatically — the user must toggle them in Settings.
27
+ */
28
+ public final class BatteryOemHelper {
29
+
30
+ private BatteryOemHelper() { /* no instances */ }
31
+
32
+ public static boolean isIgnoringBatteryOptimizations(Context ctx) {
33
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true;
34
+ try {
35
+ PowerManager pm = (PowerManager) ctx.getSystemService(Context.POWER_SERVICE);
36
+ return pm != null && pm.isIgnoringBatteryOptimizations(ctx.getPackageName());
37
+ } catch (Exception e) {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Opens the system battery-optimisation prompt for the app.
44
+ * Does not throw; logs and silently returns if the system dialog is missing.
45
+ */
46
+ public static void requestIgnoreBatteryOptimizations(Activity activity) {
47
+ if (activity == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
48
+ try {
49
+ Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
50
+ intent.setData(Uri.parse("package:" + activity.getPackageName()));
51
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
52
+ activity.startActivity(intent);
53
+ } catch (Exception ignored) {
54
+ openBatterySettings(activity);
55
+ }
56
+ }
57
+
58
+ public static void openBatterySettings(Activity activity) {
59
+ if (activity == null) return;
60
+ // Try the per-app battery usage screen first; fall back to app-info.
61
+ try {
62
+ Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
63
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
64
+ activity.startActivity(intent);
65
+ } catch (Exception e) {
66
+ try {
67
+ Intent fallback = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
68
+ fallback.setData(Uri.parse("package:" + activity.getPackageName()));
69
+ fallback.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
70
+ activity.startActivity(fallback);
71
+ } catch (Exception ignored) {}
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Open the OEM-specific "auto-start" / "background activity" screen.
77
+ * Returns a JSON object describing what was opened so the JS layer can show
78
+ * appropriate copy: { opened, manufacturer, screen }.
79
+ */
80
+ public static JSONObject openAutoStartSettings(Activity activity) throws JSONException {
81
+ JSONObject out = new JSONObject();
82
+ String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase() : "";
83
+ out.put("manufacturer", manufacturer);
84
+ out.put("opened", false);
85
+ out.put("screen", "");
86
+
87
+ if (activity == null) return out;
88
+
89
+ ComponentName component = autoStartComponent(manufacturer);
90
+ if (component != null) {
91
+ try {
92
+ Intent intent = new Intent();
93
+ intent.setComponent(component);
94
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
95
+ activity.startActivity(intent);
96
+ out.put("opened", true);
97
+ out.put("screen", component.flattenToShortString());
98
+ return out;
99
+ } catch (Exception ignored) { /* fall through to app-info */ }
100
+ }
101
+
102
+ // Fallback: standard application details page so the user can toggle background restrictions.
103
+ try {
104
+ Intent fallback = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
105
+ fallback.setData(Uri.parse("package:" + activity.getPackageName()));
106
+ fallback.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
107
+ activity.startActivity(fallback);
108
+ out.put("opened", true);
109
+ out.put("screen", "android.settings.APPLICATION_DETAILS_SETTINGS");
110
+ } catch (Exception ignored) {}
111
+ return out;
112
+ }
113
+
114
+ /** Known OEM auto-start screen components. Verified against AOSP / OEM forks; may vary by ROM. */
115
+ private static ComponentName autoStartComponent(String manufacturer) {
116
+ if (manufacturer == null) return null;
117
+ if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi") || manufacturer.contains("poco")) {
118
+ return new ComponentName("com.miui.securitycenter",
119
+ "com.miui.permcenter.autostart.AutoStartManagementActivity");
120
+ }
121
+ if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
122
+ return new ComponentName("com.huawei.systemmanager",
123
+ "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity");
124
+ }
125
+ if (manufacturer.contains("oppo")) {
126
+ return new ComponentName("com.coloros.safecenter",
127
+ "com.coloros.safecenter.permission.startup.StartupAppListActivity");
128
+ }
129
+ if (manufacturer.contains("vivo")) {
130
+ return new ComponentName("com.vivo.permissionmanager",
131
+ "com.vivo.permissionmanager.activity.BgStartUpManagerActivity");
132
+ }
133
+ if (manufacturer.contains("samsung")) {
134
+ // Samsung does not expose a stable component for "Sleeping apps"; return null and
135
+ // let the caller fall back to app-info, where the user can disable battery optimisation.
136
+ return null;
137
+ }
138
+ if (manufacturer.contains("oneplus")) {
139
+ return new ComponentName("com.oneplus.security",
140
+ "com.oneplus.security.chainlaunch.view.ChainLaunchAppListActivity");
141
+ }
142
+ if (manufacturer.contains("asus")) {
143
+ return new ComponentName("com.asus.mobilemanager",
144
+ "com.asus.mobilemanager.entry.FunctionActivity");
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Returns OEM-specific guidance steps. The caller renders them as a help screen.
151
+ */
152
+ public static JSONObject getManufacturerHelp() throws JSONException {
153
+ JSONObject out = new JSONObject();
154
+ String m = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase() : "";
155
+ out.put("manufacturer", m);
156
+ out.put("steps", new JSONArray(stepsFor(m)));
157
+ return out;
158
+ }
159
+
160
+ private static String[] stepsFor(String manufacturer) {
161
+ if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi") || manufacturer.contains("poco")) {
162
+ return new String[] {
163
+ "Settings → Apps → Manage apps → [your app] → Autostart → enable.",
164
+ "Settings → Apps → Manage apps → [your app] → Battery saver → No restrictions.",
165
+ "Settings → Apps → Manage apps → [your app] → Other permissions → Display pop-up windows while running in the background → Allow.",
166
+ "Lock the app in Recents (drag it down to keep it in memory)."
167
+ };
168
+ }
169
+ if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
170
+ return new String[] {
171
+ "Settings → Apps → [your app] → Battery → App launch → switch off Manage automatically and enable Auto-launch + Run in background.",
172
+ "Settings → Battery → App launch → [your app] → manage manually."
173
+ };
174
+ }
175
+ if (manufacturer.contains("oppo")) {
176
+ return new String[] {
177
+ "Settings → Battery → Power Consumption Protection → [your app] → Allow.",
178
+ "Settings → Apps → App management → [your app] → Permissions → Auto-start → Allow.",
179
+ "Settings → Privacy permissions → Startup manager → [your app] → enable."
180
+ };
181
+ }
182
+ if (manufacturer.contains("vivo")) {
183
+ return new String[] {
184
+ "Settings → Battery → High background power consumption → [your app] → Allow.",
185
+ "Settings → More settings → Permission management → Auto-start → [your app] → enable."
186
+ };
187
+ }
188
+ if (manufacturer.contains("samsung")) {
189
+ return new String[] {
190
+ "Settings → Apps → [your app] → Battery → Unrestricted.",
191
+ "Settings → Battery and device care → Battery → Background usage limits → Sleeping apps → make sure [your app] is NOT listed.",
192
+ "Settings → Battery and device care → Battery → Background usage limits → Never sleeping apps → add [your app]."
193
+ };
194
+ }
195
+ if (manufacturer.contains("oneplus")) {
196
+ return new String[] {
197
+ "Settings → Battery → Battery optimisation → [your app] → Don't optimise.",
198
+ "Settings → Apps → [your app] → Battery → Background activity → Allow."
199
+ };
200
+ }
201
+ if (manufacturer.contains("asus")) {
202
+ return new String[] {
203
+ "Settings → Apps → [your app] → Battery → Battery saver → Off.",
204
+ "Mobile Manager → Auto-start manager → [your app] → enable."
205
+ };
206
+ }
207
+ // Generic Android.
208
+ return new String[] {
209
+ "Settings → Apps → [your app] → Battery → Unrestricted.",
210
+ "Settings → Apps → [your app] → Permissions → Location → Allow all the time.",
211
+ "Disable battery optimisation for [your app] in Settings → Battery."
212
+ };
213
+ }
214
+ }
@@ -21,6 +21,7 @@ import com.google.android.gms.location.LocationCallback;
21
21
  import com.google.android.gms.location.LocationRequest;
22
22
  import com.google.android.gms.location.LocationResult;
23
23
  import com.google.android.gms.location.LocationServices;
24
+ import com.google.android.gms.location.Priority;
24
25
  import com.marianhello.bgloc.Config;
25
26
  import com.marianhello.bgloc.data.BackgroundActivity;
26
27
 
@@ -136,10 +137,12 @@ public class ActivityRecognitionLocationProvider extends AbstractLocationProvide
136
137
  if (fusedLocationClient == null || mConfig == null) { return; }
137
138
 
138
139
  int priority = translateDesiredAccuracy(mConfig.getDesiredAccuracy());
139
- LocationRequest locationRequest = LocationRequest.create()
140
- .setPriority(priority)
141
- .setFastestInterval(mConfig.getFastestInterval())
142
- .setInterval(mConfig.getInterval());
140
+ // v3.4: LocationRequest.Builder (play-services-location 21.0.0+) replaces deprecated
141
+ // LocationRequest.create() + setPriority/setInterval/setFastestInterval.
142
+ LocationRequest locationRequest = new LocationRequest.Builder(priority, mConfig.getInterval())
143
+ .setMinUpdateIntervalMillis(mConfig.getFastestInterval())
144
+ .setWaitForAccurateLocation(false)
145
+ .build();
143
146
 
144
147
  try {
145
148
  fusedLocationClient.requestLocationUpdates(
@@ -213,19 +216,20 @@ public class ActivityRecognitionLocationProvider extends AbstractLocationProvide
213
216
  * 1000: least aggressive, least accurate, best for battery.
214
217
  */
215
218
  private int translateDesiredAccuracy(Integer accuracy) {
219
+ // v3.4: Priority.* (play-services-location 21.0.0+) replaces deprecated LocationRequest.PRIORITY_*.
216
220
  if (accuracy == null) {
217
- return LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
221
+ return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
218
222
  }
219
223
  if (accuracy >= 10000) {
220
- return LocationRequest.PRIORITY_NO_POWER;
224
+ return Priority.PRIORITY_PASSIVE;
221
225
  }
222
226
  if (accuracy >= 1000) {
223
- return LocationRequest.PRIORITY_LOW_POWER;
227
+ return Priority.PRIORITY_LOW_POWER;
224
228
  }
225
229
  if (accuracy >= 100) {
226
- return LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;
230
+ return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
227
231
  }
228
- return LocationRequest.PRIORITY_HIGH_ACCURACY;
232
+ return Priority.PRIORITY_HIGH_ACCURACY;
229
233
  }
230
234
 
231
235
  public static DetectedActivity getProbableActivity(List<DetectedActivity> detectedActivities) {