@capgo/background-geolocation 7.0.9 → 7.0.11
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/README.md +161 -67
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocation.java +62 -0
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocationService.java +176 -0
- package/dist/docs.json +126 -30
- package/dist/esm/definitions.d.ts +74 -31
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -1
- package/dist/esm/web.js +101 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +101 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +101 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/Plugin.m +1 -0
- package/ios/Plugin/Plugin.swift +157 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,48 +9,39 @@
|
|
|
9
9
|
A Capacitor plugin that lets you receive geolocation updates even while the app is backgrounded.
|
|
10
10
|
It has a web API to facilitate for a similar usage, but background geolocation is not supported in a regular browser, only in an app environment.
|
|
11
11
|
|
|
12
|
+
## This plugin's history
|
|
13
|
+
|
|
14
|
+
Interestingly enough, this plugin has a lot of history. The initial solution from [Transistorsoft](https://github.com/transistorsoft) was a great piece of software, and I encourage using it if it fits your needs.
|
|
15
|
+
I tried it and understood that it prioritizes battery life over accuracy, which wasn't the right fit for my hiking app.
|
|
16
|
+
There was a very good fork maintained by **mauron85** specifically for that use case, and I was happy to help maintain it.
|
|
17
|
+
But at some point, **mauron85** stopped responding to messages on GitHub, and no one could continue maintaining it.
|
|
18
|
+
I hope mauron85 is safe and sound somewhere.
|
|
19
|
+
|
|
20
|
+
So I created a fork and started maintaining it [here](https://github.com/HaylLtd/cordova-background-geolocation-plugin).
|
|
21
|
+
It served me well for over half a decade, but I felt it was hard to maintain due to all its history, features, and bug fixes.
|
|
22
|
+
I also felt like there was a barrier to introducing new features because of its complexity.
|
|
23
|
+
|
|
24
|
+
So I started exploring what it would take to reduce that complexity—at the same time, I was envious of how small [`@capacitor-community/background-geolocation`](https://github.com/capacitor-community/background-geolocation) is.
|
|
25
|
+
I took the best of both worlds: tried to reduce the codebase in the original Cordova plugin and add some robustness to the Capacitor plugin.
|
|
26
|
+
|
|
27
|
+
That's how I ended up maintaining this one.
|
|
28
|
+
I hope you'll enjoy it!
|
|
29
|
+
|
|
30
|
+
|
|
12
31
|
## Usage
|
|
13
32
|
|
|
14
33
|
```javascript
|
|
15
34
|
import { BackgroundGeolocation } from "@capgo/background-geolocation";
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
// You do this by calling 'addWatcher' with an options object and a callback. A
|
|
19
|
-
// Promise is returned, which resolves to the callback ID used to remove the
|
|
20
|
-
// watcher in the future. The callback will be called every time a new location
|
|
21
|
-
// is available. Watchers can not be paused, only removed. Multiple watchers may
|
|
22
|
-
// exist simultaneously.
|
|
23
|
-
BackgroundGeolocation.addWatcher(
|
|
36
|
+
BackgroundGeolocation.start(
|
|
24
37
|
{
|
|
25
|
-
// If the "backgroundMessage" option is defined, the watcher will
|
|
26
|
-
// provide location updates whether the app is in the background or the
|
|
27
|
-
// foreground. If it is not defined, location updates are only
|
|
28
|
-
// guaranteed in the foreground. This is true on both platforms.
|
|
29
|
-
|
|
30
|
-
// On Android, a notification must be shown to continue receiving
|
|
31
|
-
// location updates in the background. This option specifies the text of
|
|
32
|
-
// that notification.
|
|
33
38
|
backgroundMessage: "Cancel to prevent battery drain.",
|
|
34
|
-
|
|
35
|
-
// The title of the notification mentioned above. Defaults to "Using
|
|
36
|
-
// your location".
|
|
37
39
|
backgroundTitle: "Tracking You.",
|
|
38
|
-
|
|
39
|
-
// Whether permissions should be requested from the user automatically,
|
|
40
|
-
// if they are not already granted. Defaults to "true".
|
|
41
40
|
requestPermissions: true,
|
|
42
|
-
|
|
43
|
-
// If "true", stale locations may be delivered while the device
|
|
44
|
-
// obtains a GPS fix. You are responsible for checking the "time"
|
|
45
|
-
// property. If "false", locations are guaranteed to be up to date.
|
|
46
|
-
// Defaults to "false".
|
|
47
41
|
stale: false,
|
|
48
|
-
|
|
49
|
-
// The minimum number of metres between subsequent locations. Defaults
|
|
50
|
-
// to 0.
|
|
51
42
|
distanceFilter: 50
|
|
52
43
|
},
|
|
53
|
-
|
|
44
|
+
(location, error) => {
|
|
54
45
|
if (error) {
|
|
55
46
|
if (error.code === "NOT_AUTHORIZED") {
|
|
56
47
|
if (window.confirm(
|
|
@@ -67,56 +58,33 @@ BackgroundGeolocation.addWatcher(
|
|
|
67
58
|
}
|
|
68
59
|
return console.error(error);
|
|
69
60
|
}
|
|
70
|
-
|
|
71
61
|
return console.log(location);
|
|
72
62
|
}
|
|
73
|
-
).then(
|
|
74
|
-
// When
|
|
75
|
-
|
|
76
|
-
BackgroundGeolocation.removeWatcher({
|
|
77
|
-
id: watcher_id
|
|
78
|
-
});
|
|
63
|
+
).then(() => {
|
|
64
|
+
// When location updates are no longer needed, the plugin should be stopped by calling
|
|
65
|
+
BackgroundGeolocation.stop();
|
|
79
66
|
});
|
|
80
67
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
longitude: 131.723423719132,
|
|
85
|
-
// Latitude in degrees.
|
|
86
|
-
latitude: -22.40106297456,
|
|
87
|
-
// Radius of horizontal uncertainty in metres, with 68% confidence.
|
|
88
|
-
accuracy: 11,
|
|
89
|
-
// Metres above sea level (or null).
|
|
90
|
-
altitude: 65,
|
|
91
|
-
// Vertical uncertainty in metres, with 68% confidence (or null).
|
|
92
|
-
altitudeAccuracy: 4,
|
|
93
|
-
// Deviation from true north in degrees (or null).
|
|
94
|
-
bearing: 159.60000610351562,
|
|
95
|
-
// True if the location was simulated by software, rather than GPS.
|
|
96
|
-
simulated: false,
|
|
97
|
-
// Speed in metres per second (or null).
|
|
98
|
-
speed: 23.51068878173828,
|
|
99
|
-
// Time the location was produced, in milliseconds since the unix epoch.
|
|
100
|
-
time: 1562731602000
|
|
101
|
-
}
|
|
68
|
+
// Set a planned route to get a notification sound when a new location arrives and it's not on the route:
|
|
69
|
+
|
|
70
|
+
BackgroundGeolocation.setPlannedRoute({soundFile: "assets/myFile.mp3", route: [[1,2], [3,4]], distance: 30 });
|
|
102
71
|
|
|
103
72
|
// If you just want the current location, try something like this. The longer
|
|
104
|
-
// the timeout, the more accurate the guess will be. I wouldn't go below about
|
|
105
|
-
|
|
106
|
-
function guess_location(callback, timeout) {
|
|
73
|
+
// the timeout, the more accurate the guess will be. I wouldn't go below about 100ms.
|
|
74
|
+
function guessLocation(callback, timeout) {
|
|
107
75
|
let last_location;
|
|
108
|
-
BackgroundGeolocation.
|
|
76
|
+
BackgroundGeolocation.start(
|
|
109
77
|
{
|
|
110
78
|
requestPermissions: false,
|
|
111
79
|
stale: true
|
|
112
80
|
},
|
|
113
|
-
|
|
81
|
+
(location) => {
|
|
114
82
|
last_location = location || undefined;
|
|
115
83
|
}
|
|
116
|
-
).then(
|
|
117
|
-
setTimeout(
|
|
84
|
+
).then(() => {
|
|
85
|
+
setTimeout(() => {
|
|
118
86
|
callback(last_location);
|
|
119
|
-
BackgroundGeolocation.
|
|
87
|
+
BackgroundGeolocation.stop();
|
|
120
88
|
}, timeout);
|
|
121
89
|
});
|
|
122
90
|
}
|
|
@@ -128,7 +96,7 @@ This plugin supports Capacitor v7:
|
|
|
128
96
|
|
|
129
97
|
| Capacitor | Plugin |
|
|
130
98
|
|------------|--------|
|
|
131
|
-
| v7 |
|
|
99
|
+
| v7 | v7 |
|
|
132
100
|
|
|
133
101
|
```sh
|
|
134
102
|
npm install @capgo/background-geolocation
|
|
@@ -211,6 +179,132 @@ Configration specific to Android can be made in `strings.xml`:
|
|
|
211
179
|
* [`start(...)`](#start)
|
|
212
180
|
* [`stop()`](#stop)
|
|
213
181
|
* [`openSettings()`](#opensettings)
|
|
182
|
+
* [`setPlannedRoute(...)`](#setplannedroute)
|
|
214
183
|
* [Interfaces](#interfaces)
|
|
215
184
|
|
|
216
185
|
</docgen-index>
|
|
186
|
+
|
|
187
|
+
<docgen-api>
|
|
188
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
189
|
+
|
|
190
|
+
Main plugin interface for background geolocation functionality.
|
|
191
|
+
Provides methods to manage location updates and access device settings.
|
|
192
|
+
|
|
193
|
+
### start(...)
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
start(options: StartOptions, callback: (position?: Location | undefined, error?: CallbackError | undefined) => void) => Promise<void>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
To start listening for changes in the device's location, call this method.
|
|
200
|
+
A Promise is returned to indicate that it finished the call. The callback will be called every time a new location
|
|
201
|
+
is available, or if there was an error when calling this method. Don't rely on promise rejection for this.
|
|
202
|
+
|
|
203
|
+
| Param | Type | Description |
|
|
204
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
205
|
+
| **`options`** | <code><a href="#startoptions">StartOptions</a></code> | The configuration options |
|
|
206
|
+
| **`callback`** | <code>(position?: <a href="#location">Location</a>, error?: <a href="#callbackerror">CallbackError</a>) => void</code> | The callback function invoked when a new location is available or an error occurs |
|
|
207
|
+
|
|
208
|
+
**Since:** 7.0.9
|
|
209
|
+
|
|
210
|
+
--------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
### stop()
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
stop() => Promise<void>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Stops location updates.
|
|
220
|
+
|
|
221
|
+
**Since:** 7.0.9
|
|
222
|
+
|
|
223
|
+
--------------------
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
### openSettings()
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
openSettings() => Promise<void>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Opens the device's location settings page.
|
|
233
|
+
Useful for directing users to enable location services or adjust permissions.
|
|
234
|
+
|
|
235
|
+
**Since:** 7.0.0
|
|
236
|
+
|
|
237
|
+
--------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
### setPlannedRoute(...)
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
setPlannedRoute(options: SetPlannedRouteOptions) => Promise<void>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Plays a sound file when the user deviates from the planned route.
|
|
247
|
+
This should be used to play a sound (in the background too, only for native).
|
|
248
|
+
|
|
249
|
+
| Param | Type | Description |
|
|
250
|
+
| ------------- | ------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
251
|
+
| **`options`** | <code><a href="#setplannedrouteoptions">SetPlannedRouteOptions</a></code> | The options for setting the planned route and sound file |
|
|
252
|
+
|
|
253
|
+
**Since:** 7.0.11
|
|
254
|
+
|
|
255
|
+
--------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
### Interfaces
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
#### StartOptions
|
|
262
|
+
|
|
263
|
+
The options for configuring for location updates.
|
|
264
|
+
|
|
265
|
+
| Prop | Type | Description | Default | Since |
|
|
266
|
+
| ------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----- |
|
|
267
|
+
| **`backgroundMessage`** | <code>string</code> | If the "backgroundMessage" option is defined, the plugin will provide location updates whether the app is in the background or the foreground. If it is not defined, location updates are only guaranteed in the foreground. This is true on both platforms. On Android, a notification must be shown to continue receiving location updates in the background. This option specifies the text of that notification. | | 7.0.9 |
|
|
268
|
+
| **`backgroundTitle`** | <code>string</code> | The title of the notification mentioned above. | <code>"Using your location"</code> | 7.0.9 |
|
|
269
|
+
| **`requestPermissions`** | <code>boolean</code> | Whether permissions should be requested from the user automatically, if they are not already granted. | <code>true</code> | 7.0.9 |
|
|
270
|
+
| **`stale`** | <code>boolean</code> | If "true", stale locations may be delivered while the device obtains a GPS fix. You are responsible for checking the "time" property. If "false", locations are guaranteed to be up to date. | <code>false</code> | 7.0.9 |
|
|
271
|
+
| **`distanceFilter`** | <code>number</code> | The distance in meters that the device must move before a new location update is triggered. This is used to filter out small movements and reduce the number of updates. | <code>0</code> | 7.0.9 |
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
#### Location
|
|
275
|
+
|
|
276
|
+
Represents a geographical location with various attributes.
|
|
277
|
+
Contains all the standard location properties returned by GPS/network providers.
|
|
278
|
+
|
|
279
|
+
| Prop | Type | Description | Since |
|
|
280
|
+
| ---------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
281
|
+
| **`latitude`** | <code>number</code> | Latitude in degrees. Range: -90.0 to +90.0 | 7.0.0 |
|
|
282
|
+
| **`longitude`** | <code>number</code> | Longitude in degrees. Range: -180.0 to +180.0 | 7.0.0 |
|
|
283
|
+
| **`accuracy`** | <code>number</code> | Radius of horizontal uncertainty in metres, with 68% confidence. Lower values indicate more accurate location. | 7.0.0 |
|
|
284
|
+
| **`altitude`** | <code>number \| null</code> | Metres above sea level (or null if not available). | 7.0.0 |
|
|
285
|
+
| **`altitudeAccuracy`** | <code>number \| null</code> | Vertical uncertainty in metres, with 68% confidence (or null if not available). | 7.0.0 |
|
|
286
|
+
| **`simulated`** | <code>boolean</code> | `true` if the location was simulated by software, rather than GPS. Useful for detecting mock locations in development or testing. | 7.0.0 |
|
|
287
|
+
| **`bearing`** | <code>number \| null</code> | Deviation from true north in degrees (or null if not available). Range: 0.0 to 360.0 | 7.0.0 |
|
|
288
|
+
| **`speed`** | <code>number \| null</code> | Speed in metres per second (or null if not available). | 7.0.0 |
|
|
289
|
+
| **`time`** | <code>number \| null</code> | Time the location was produced, in milliseconds since the unix epoch. Use this to check if a location is stale when using stale: true. | 7.0.0 |
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
#### CallbackError
|
|
293
|
+
|
|
294
|
+
Error object that may be passed to the location start callback.
|
|
295
|
+
Extends the standard Error with optional error codes.
|
|
296
|
+
|
|
297
|
+
| Prop | Type | Description | Since |
|
|
298
|
+
| ---------- | ------------------- | ----------------------------------------------------- | ----- |
|
|
299
|
+
| **`code`** | <code>string</code> | Optional error code for more specific error handling. | 7.0.0 |
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
#### SetPlannedRouteOptions
|
|
303
|
+
|
|
304
|
+
| Prop | Type | Description | Default | Since |
|
|
305
|
+
| --------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------ |
|
|
306
|
+
| **`soundFile`** | <code>string</code> | The name of the sound file to play. Must be a valid sound relative path in the app's public folder to work for both web and native platforms. There's no need to include the public folder in the path. | | 7.0.10 |
|
|
307
|
+
| **`route`** | <code>[number, number][]</code> | The planned route as an array of longitude and latitude pairs. Each pair represents a point on the route. This is used to define a route that the user can follow. The route is used to play a sound when the user deviates from it. | | 7.0.11 |
|
|
308
|
+
| **`distance`** | <code>number</code> | The distance in meters that the user must deviate from the planned route to trigger the sound. This is used to determine how far off the route the user can be before the sound is played. If not specified, a default value of 50 meters is used. | <code>50</code> | 7.0.11 |
|
|
309
|
+
|
|
310
|
+
</docgen-api>
|
package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocation.java
CHANGED
|
@@ -15,7 +15,9 @@ import android.net.Uri;
|
|
|
15
15
|
import android.os.Build;
|
|
16
16
|
import android.os.IBinder;
|
|
17
17
|
import android.provider.Settings;
|
|
18
|
+
import androidx.annotation.Nullable;
|
|
18
19
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
20
|
+
import com.getcapacitor.JSArray;
|
|
19
21
|
import com.getcapacitor.JSObject;
|
|
20
22
|
import com.getcapacitor.Logger;
|
|
21
23
|
import com.getcapacitor.PermissionState;
|
|
@@ -27,6 +29,8 @@ import com.getcapacitor.annotation.Permission;
|
|
|
27
29
|
import com.getcapacitor.annotation.PermissionCallback;
|
|
28
30
|
import com.google.android.gms.location.LocationServices;
|
|
29
31
|
import java.util.concurrent.CompletableFuture;
|
|
32
|
+
import org.json.JSONArray;
|
|
33
|
+
import org.json.JSONException;
|
|
30
34
|
import org.json.JSONObject;
|
|
31
35
|
|
|
32
36
|
@CapacitorPlugin(
|
|
@@ -188,6 +192,64 @@ public class BackgroundGeolocation extends Plugin {
|
|
|
188
192
|
call.resolve();
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
@PluginMethod
|
|
196
|
+
public void setPlannedRoute(PluginCall call) {
|
|
197
|
+
String soundFile = call.getString("soundFile");
|
|
198
|
+
if (soundFile == null || soundFile.isEmpty()) {
|
|
199
|
+
call.reject("Sound file is required");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (serviceConnectionFuture == null) {
|
|
203
|
+
call.reject(
|
|
204
|
+
"Service not started, make sure to call start() first",
|
|
205
|
+
"NOT_STARTED"
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
double[][] javaDoubleArray = getJavaDoubleArray(call.getArray("route"));
|
|
211
|
+
serviceConnectionFuture
|
|
212
|
+
.thenAccept(service -> {
|
|
213
|
+
service.setPlannedRoute(
|
|
214
|
+
soundFile,
|
|
215
|
+
javaDoubleArray,
|
|
216
|
+
call.getFloat("distance", 50f)
|
|
217
|
+
);
|
|
218
|
+
call.resolve();
|
|
219
|
+
})
|
|
220
|
+
.exceptionally(throwable -> {
|
|
221
|
+
call.reject("Failed to set route: " + throwable.getMessage());
|
|
222
|
+
return null;
|
|
223
|
+
});
|
|
224
|
+
} catch (Exception ex) {
|
|
225
|
+
call.reject("Unable to parse route parameters");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private static double[][] getJavaDoubleArray(JSArray jsArray)
|
|
230
|
+
throws JSONException {
|
|
231
|
+
int rows = jsArray.length();
|
|
232
|
+
if (rows == 0) {
|
|
233
|
+
return new double[0][2];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
JSONArray firstRow = jsArray.getJSONArray(0);
|
|
237
|
+
int cols = firstRow.length();
|
|
238
|
+
|
|
239
|
+
var javaDoubleArray = new double[rows][cols];
|
|
240
|
+
|
|
241
|
+
for (int i = 0; i < rows; i++) {
|
|
242
|
+
JSONArray rowArray = jsArray.getJSONArray(i);
|
|
243
|
+
if (rowArray.length() != cols) {
|
|
244
|
+
throw new JSONException("Input array is not a consistent 2D array.");
|
|
245
|
+
}
|
|
246
|
+
for (int j = 0; j < cols; j++) {
|
|
247
|
+
javaDoubleArray[i][j] = rowArray.getDouble(j);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return javaDoubleArray;
|
|
251
|
+
}
|
|
252
|
+
|
|
191
253
|
// Checks if device-wide location services are disabled
|
|
192
254
|
private static Boolean isLocationEnabled(Context context) {
|
|
193
255
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
@@ -5,9 +5,12 @@ import android.app.PendingIntent;
|
|
|
5
5
|
import android.app.Service;
|
|
6
6
|
import android.content.Context;
|
|
7
7
|
import android.content.Intent;
|
|
8
|
+
import android.content.res.AssetFileDescriptor;
|
|
9
|
+
import android.content.res.AssetManager;
|
|
8
10
|
import android.graphics.Color;
|
|
9
11
|
import android.location.LocationListener;
|
|
10
12
|
import android.location.LocationManager;
|
|
13
|
+
import android.media.MediaPlayer;
|
|
11
14
|
import android.os.Binder;
|
|
12
15
|
import android.os.Build;
|
|
13
16
|
import android.os.IBinder;
|
|
@@ -23,6 +26,8 @@ public class BackgroundGeolocationService extends Service {
|
|
|
23
26
|
(BackgroundGeolocationService.class.getPackage().getName() + ".broadcast");
|
|
24
27
|
private final IBinder binder = new LocalBinder();
|
|
25
28
|
|
|
29
|
+
private static final double EARTH_RADIUS_M = 6371000;
|
|
30
|
+
|
|
26
31
|
// Must be unique for this application.
|
|
27
32
|
private static final int NOTIFICATION_ID = 28351;
|
|
28
33
|
|
|
@@ -30,6 +35,10 @@ public class BackgroundGeolocationService extends Service {
|
|
|
30
35
|
|
|
31
36
|
private LocationManager client;
|
|
32
37
|
private LocationListener locationCallback;
|
|
38
|
+
private MediaPlayer mediaPlayer;
|
|
39
|
+
private double[][] route;
|
|
40
|
+
private double distanceThreshold;
|
|
41
|
+
private boolean isOffRoute;
|
|
33
42
|
|
|
34
43
|
@Override
|
|
35
44
|
public IBinder onBind(Intent intent) {
|
|
@@ -43,10 +52,33 @@ public class BackgroundGeolocationService extends Service {
|
|
|
43
52
|
@Override
|
|
44
53
|
public boolean onUnbind(Intent intent) {
|
|
45
54
|
client.removeUpdates(locationCallback);
|
|
55
|
+
releaseMediaPlayer();
|
|
46
56
|
stopSelf();
|
|
47
57
|
return false;
|
|
48
58
|
}
|
|
49
59
|
|
|
60
|
+
@Override
|
|
61
|
+
public void onDestroy() {
|
|
62
|
+
client.removeUpdates(locationCallback);
|
|
63
|
+
super.onDestroy();
|
|
64
|
+
releaseMediaPlayer();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private void releaseMediaPlayer() {
|
|
68
|
+
if (mediaPlayer == null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (mediaPlayer.isPlaying()) {
|
|
73
|
+
mediaPlayer.stop();
|
|
74
|
+
}
|
|
75
|
+
mediaPlayer.release();
|
|
76
|
+
} catch (Exception e) {
|
|
77
|
+
Logger.error("Error releasing MediaPlayer", e);
|
|
78
|
+
}
|
|
79
|
+
mediaPlayer = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
// Handles requests from the activity.
|
|
51
83
|
public class LocalBinder extends Binder {
|
|
52
84
|
|
|
@@ -56,10 +88,19 @@ public class BackgroundGeolocationService extends Service {
|
|
|
56
88
|
final String notificationMessage,
|
|
57
89
|
float distanceFilter
|
|
58
90
|
) {
|
|
91
|
+
releaseMediaPlayer();
|
|
59
92
|
client = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
|
60
93
|
callbackId = id;
|
|
61
94
|
|
|
62
95
|
locationCallback = location -> {
|
|
96
|
+
if (mediaPlayer != null) {
|
|
97
|
+
double[] point = { location.getLongitude(), location.getLatitude() };
|
|
98
|
+
var offRoute = distancePointToRoute(point) > distanceThreshold;
|
|
99
|
+
if (offRoute == true && isOffRoute == false) {
|
|
100
|
+
mediaPlayer.start();
|
|
101
|
+
}
|
|
102
|
+
isOffRoute = offRoute;
|
|
103
|
+
}
|
|
63
104
|
Intent intent = new Intent(ACTION_BROADCAST);
|
|
64
105
|
intent.putExtra("location", location);
|
|
65
106
|
intent.putExtra("id", callbackId);
|
|
@@ -104,8 +145,47 @@ public class BackgroundGeolocationService extends Service {
|
|
|
104
145
|
client.removeUpdates(locationCallback);
|
|
105
146
|
stopForeground(true);
|
|
106
147
|
stopSelf();
|
|
148
|
+
releaseMediaPlayer();
|
|
107
149
|
return callbackId;
|
|
108
150
|
}
|
|
151
|
+
|
|
152
|
+
void setPlannedRoute(
|
|
153
|
+
String filePath,
|
|
154
|
+
double[][] routeCoordinates,
|
|
155
|
+
float distance
|
|
156
|
+
) {
|
|
157
|
+
route = routeCoordinates;
|
|
158
|
+
distanceThreshold = distance;
|
|
159
|
+
isOffRoute = true;
|
|
160
|
+
try {
|
|
161
|
+
if (mediaPlayer != null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
mediaPlayer = new MediaPlayer();
|
|
165
|
+
AssetManager am = getApplicationContext().getResources().getAssets();
|
|
166
|
+
AssetFileDescriptor assetFileDescriptor = am.openFd(
|
|
167
|
+
"public/" + filePath
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
mediaPlayer.setDataSource(
|
|
171
|
+
assetFileDescriptor.getFileDescriptor(),
|
|
172
|
+
assetFileDescriptor.getStartOffset(),
|
|
173
|
+
assetFileDescriptor.getLength()
|
|
174
|
+
);
|
|
175
|
+
mediaPlayer.setLooping(false);
|
|
176
|
+
|
|
177
|
+
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
|
|
178
|
+
Logger.error("MediaPlayer error: what=" + what + ", extra=" + extra);
|
|
179
|
+
releaseMediaPlayer();
|
|
180
|
+
return true; // Indicate we handled the error
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
mediaPlayer.prepareAsync();
|
|
184
|
+
} catch (Exception e) {
|
|
185
|
+
Logger.error("PlaySound: Unexpected error", e);
|
|
186
|
+
releaseMediaPlayer();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
private Notification createBackgroundNotification(
|
|
@@ -196,4 +276,100 @@ public class BackgroundGeolocationService extends Service {
|
|
|
196
276
|
int id = getAppResourceIdentifier(name, "string", context);
|
|
197
277
|
return id == 0 ? fallback : context.getString(id);
|
|
198
278
|
}
|
|
279
|
+
|
|
280
|
+
private static double haversine(double[] point1, double[] point2) {
|
|
281
|
+
double lon1 = point1[0];
|
|
282
|
+
double lat1 = point1[1];
|
|
283
|
+
double lon2 = point2[0];
|
|
284
|
+
double lat2 = point2[1];
|
|
285
|
+
|
|
286
|
+
double dLat = Math.toRadians(lat2 - lat1);
|
|
287
|
+
double dLon = Math.toRadians(lon2 - lon1);
|
|
288
|
+
|
|
289
|
+
double a =
|
|
290
|
+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
291
|
+
Math.cos(Math.toRadians(lat1)) *
|
|
292
|
+
Math.cos(Math.toRadians(lat2)) *
|
|
293
|
+
Math.sin(dLon / 2) *
|
|
294
|
+
Math.sin(dLon / 2);
|
|
295
|
+
|
|
296
|
+
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
297
|
+
|
|
298
|
+
return EARTH_RADIUS_M * c;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private static double distancePointToLineSegment(
|
|
302
|
+
double[] point,
|
|
303
|
+
double[] lineStart,
|
|
304
|
+
double[] lineEnd
|
|
305
|
+
) {
|
|
306
|
+
// Calculate the distances between the three points using Haversine
|
|
307
|
+
double dist_A_B = haversine(point, lineStart);
|
|
308
|
+
double dist_A_C = haversine(point, lineEnd);
|
|
309
|
+
double dist_B_C = haversine(lineStart, lineEnd);
|
|
310
|
+
|
|
311
|
+
// Handle the edge case where the line segment is a single point
|
|
312
|
+
if (dist_B_C == 0) {
|
|
313
|
+
return dist_A_B;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check if the angles at the line segment's endpoints are obtuse.
|
|
317
|
+
// We use the Law of Cosines (c^2 = a^2 + b^2 - 2ab*cos(C))
|
|
318
|
+
// If cos(C) < 0, the angle is obtuse.
|
|
319
|
+
|
|
320
|
+
// Angle at B (lineStart)
|
|
321
|
+
// Use a small epsilon to handle floating point inaccuracies in division by zero
|
|
322
|
+
double cos_B =
|
|
323
|
+
(Math.pow(dist_A_B, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_C, 2)) /
|
|
324
|
+
(2 * dist_A_B * dist_B_C);
|
|
325
|
+
if (cos_B < 0) {
|
|
326
|
+
return dist_A_B;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Angle at C (lineEnd)
|
|
330
|
+
double cos_C =
|
|
331
|
+
(Math.pow(dist_A_C, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_B, 2)) /
|
|
332
|
+
(2 * dist_A_C * dist_B_C);
|
|
333
|
+
if (cos_C < 0) {
|
|
334
|
+
return dist_A_C;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// If both angles are acute, the closest point is on the line segment itself.
|
|
338
|
+
// We can calculate the distance (height of the triangle) using its area.
|
|
339
|
+
|
|
340
|
+
// 1. Calculate the semi-perimeter of the triangle ABC
|
|
341
|
+
double s = (dist_A_B + dist_A_C + dist_B_C) / 2;
|
|
342
|
+
|
|
343
|
+
// 2. Calculate the area using Heron's formula
|
|
344
|
+
double area = Math.sqrt(
|
|
345
|
+
Math.max(0, s * (s - dist_A_B) * (s - dist_A_C) * (s - dist_B_C))
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// 3. The distance is the height of the triangle from point A to the base BC
|
|
349
|
+
// Area = 0.5 * base * height => height = 2 * Area / base
|
|
350
|
+
return (2 * area) / dist_B_C;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
public double distancePointToRoute(double[] point) {
|
|
354
|
+
// If the polyline has less than 2 points, we can't form a segment.
|
|
355
|
+
if (this.route.length < 2) {
|
|
356
|
+
if (this.route.length == 1) {
|
|
357
|
+
return haversine(point, this.route[0]);
|
|
358
|
+
}
|
|
359
|
+
return Double.POSITIVE_INFINITY; // No line segments to measure against
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
double minDistance = Double.POSITIVE_INFINITY;
|
|
363
|
+
|
|
364
|
+
for (int i = 0; i < this.route.length - 1; i++) {
|
|
365
|
+
double[] lineStart = this.route[i];
|
|
366
|
+
double[] lineEnd = this.route[i + 1];
|
|
367
|
+
double distance = distancePointToLineSegment(point, lineStart, lineEnd);
|
|
368
|
+
if (distance < minDistance) {
|
|
369
|
+
minDistance = distance;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return minDistance;
|
|
374
|
+
}
|
|
199
375
|
}
|