@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.
- package/.npmignore +4 -0
- package/CHANGELOG.md +290 -0
- package/CLAUDE.md +56 -0
- package/HISTORY.md +125 -0
- package/README.md +189 -4
- package/android/CDVBackgroundGeolocation/src/main/java/com/marianhello/bgloc/cordova/ConfigMapper.java +90 -0
- package/android/CDVBackgroundGeolocation/src/main/java/com/tenforwardconsulting/bgloc/cordova/BackgroundGeolocationPlugin.java +310 -1
- package/android/common/src/main/java/com/marianhello/bgloc/BackgroundGeolocationFacade.java +127 -0
- package/android/common/src/main/java/com/marianhello/bgloc/BootCompletedReceiver.java +27 -11
- package/android/common/src/main/java/com/marianhello/bgloc/Config.java +268 -0
- package/android/common/src/main/java/com/marianhello/bgloc/HttpPostService.java +86 -26
- package/android/common/src/main/java/com/marianhello/bgloc/PluginDelegate.java +26 -0
- package/android/common/src/main/java/com/marianhello/bgloc/PostLocationTask.java +42 -5
- package/android/common/src/main/java/com/marianhello/bgloc/driving/DrivingEventsDetector.java +265 -0
- package/android/common/src/main/java/com/marianhello/bgloc/http/UrlTemplateResolver.java +115 -0
- package/android/common/src/main/java/com/marianhello/bgloc/oem/BatteryOemHelper.java +214 -0
- package/android/common/src/main/java/com/marianhello/bgloc/provider/ActivityRecognitionLocationProvider.java +13 -9
- package/android/common/src/main/java/com/marianhello/bgloc/provider/DistanceFilterLocationProvider.java +29 -40
- package/android/common/src/main/java/com/marianhello/bgloc/provider/RawLocationProvider.java +14 -34
- package/android/common/src/main/java/com/marianhello/bgloc/sensor/SensorFusionDetector.java +199 -0
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceImpl.java +305 -6
- package/android/common/src/main/java/com/marianhello/bgloc/service/LocationServiceProxy.java +14 -2
- package/android/common/src/main/java/com/marianhello/bgloc/sync/SyncAdapter.java +50 -3
- package/android/dependencies.gradle +0 -3
- package/angular/background-geolocation-events.ts +21 -0
- package/angular/background-geolocation.service.ts +63 -0
- package/angular/dist/background-geolocation-events.d.ts +18 -1
- package/angular/dist/background-geolocation.service.d.ts +36 -0
- package/angular/dist/esm2022/background-geolocation-events.mjs +22 -1
- package/angular/dist/esm2022/background-geolocation.service.mjs +35 -1
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs +55 -0
- package/angular/dist/fesm2022/josuelmm-cordova-background-geolocation.mjs.map +1 -1
- package/ios/CDVBackgroundGeolocation/CDVBackgroundGeolocation.m +312 -1
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.h +22 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundGeolocationFacade.m +400 -15
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.h +12 -0
- package/ios/common/BackgroundGeolocation/MAURBackgroundSync.m +83 -5
- package/ios/common/BackgroundGeolocation/MAURConfig.h +15 -0
- package/ios/common/BackgroundGeolocation/MAURConfig.m +100 -3
- package/ios/common/BackgroundGeolocation/MAURDistanceFilterLocationProvider.m +29 -2
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.h +4 -0
- package/ios/common/BackgroundGeolocation/MAURPostLocationTask.m +97 -44
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.h +41 -0
- package/ios/common/BackgroundGeolocation/MAURSensorFusionDetector.m +137 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.h +31 -0
- package/ios/common/BackgroundGeolocation/MAURUrlTemplateResolver.m +107 -0
- package/package.json +41 -1
- package/plugin.xml +19 -8
- package/www/BackgroundGeolocation.d.ts +517 -3
- package/www/BackgroundGeolocation.js +54 -1
- 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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.
|
|
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
|
|
221
|
+
return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
|
|
218
222
|
}
|
|
219
223
|
if (accuracy >= 10000) {
|
|
220
|
-
return
|
|
224
|
+
return Priority.PRIORITY_PASSIVE;
|
|
221
225
|
}
|
|
222
226
|
if (accuracy >= 1000) {
|
|
223
|
-
return
|
|
227
|
+
return Priority.PRIORITY_LOW_POWER;
|
|
224
228
|
}
|
|
225
229
|
if (accuracy >= 100) {
|
|
226
|
-
return
|
|
230
|
+
return Priority.PRIORITY_BALANCED_POWER_ACCURACY;
|
|
227
231
|
}
|
|
228
|
-
return
|
|
232
|
+
return Priority.PRIORITY_HIGH_ACCURACY;
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
public static DetectedActivity getProbableActivity(List<DetectedActivity> detectedActivities) {
|