@capgo/background-geolocation 7.0.12 → 7.0.18
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/CapgoBackgroundGeolocation.podspec +4 -3
- package/Package.swift +28 -0
- package/README.md +19 -2
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocation.java +270 -336
- package/android/src/main/java/com/capgo/capacitor_background_geolocation/BackgroundGeolocationService.java +268 -326
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +2 -2
- package/dist/esm/web.js +16 -24
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +16 -24
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +16 -24
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin/Plugin.swift → Sources/CapgoBackgroundGeolocationPlugin/CapgoCapacitorBackgroundGeolocationPlugin.swift} +9 -1
- package/ios/Tests/CapgoBackgroundGeolocationPluginTests/CapgoBackgroundGeolocationPluginTests.swift +15 -0
- package/package.json +13 -12
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -9
|
@@ -22,354 +22,296 @@ import com.getcapacitor.Logger;
|
|
|
22
22
|
// added, and demoted when the last background watcher is removed.
|
|
23
23
|
public class BackgroundGeolocationService extends Service {
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
private static final int NOTIFICATION_ID = 28351;
|
|
33
|
-
|
|
34
|
-
private String callbackId;
|
|
35
|
-
|
|
36
|
-
private LocationManager client;
|
|
37
|
-
private LocationListener locationCallback;
|
|
38
|
-
private MediaPlayer mediaPlayer;
|
|
39
|
-
private double[][] route;
|
|
40
|
-
private double distanceThreshold;
|
|
41
|
-
private boolean isOffRoute;
|
|
42
|
-
|
|
43
|
-
@Override
|
|
44
|
-
public IBinder onBind(Intent intent) {
|
|
45
|
-
return binder;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Some devices allow a foreground service to outlive the application's main
|
|
49
|
-
// activity, leading to nasty crashes as reported in issue #59. If we learn
|
|
50
|
-
// that the application has been killed, all watchers are stopped and the
|
|
51
|
-
// service is terminated immediately.
|
|
52
|
-
@Override
|
|
53
|
-
public boolean onUnbind(Intent intent) {
|
|
54
|
-
client.removeUpdates(locationCallback);
|
|
55
|
-
releaseMediaPlayer();
|
|
56
|
-
stopSelf();
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
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
|
-
|
|
82
|
-
// Handles requests from the activity.
|
|
83
|
-
public class LocalBinder extends Binder {
|
|
84
|
-
|
|
85
|
-
void start(
|
|
86
|
-
final String id,
|
|
87
|
-
final String notificationTitle,
|
|
88
|
-
final String notificationMessage,
|
|
89
|
-
float distanceFilter
|
|
90
|
-
) {
|
|
91
|
-
releaseMediaPlayer();
|
|
92
|
-
client = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
|
93
|
-
callbackId = id;
|
|
94
|
-
|
|
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
|
-
}
|
|
104
|
-
Intent intent = new Intent(ACTION_BROADCAST);
|
|
105
|
-
intent.putExtra("location", location);
|
|
106
|
-
intent.putExtra("id", callbackId);
|
|
107
|
-
LocalBroadcastManager.getInstance(
|
|
108
|
-
getApplicationContext()
|
|
109
|
-
).sendBroadcast(intent);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
client.requestLocationUpdates(
|
|
114
|
-
LocationManager.GPS_PROVIDER,
|
|
115
|
-
1000,
|
|
116
|
-
distanceFilter,
|
|
117
|
-
locationCallback
|
|
118
|
-
);
|
|
119
|
-
} catch (SecurityException ignore) {
|
|
120
|
-
// According to Android Studio, this method can throw a Security Exception if
|
|
121
|
-
// permissions are not yet granted. Rather than check the permissions, which is fiddly,
|
|
122
|
-
// we simply ignore the exception.
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Promote the service to the foreground if necessary.
|
|
126
|
-
// Ideally we would only call 'startForeground' if the service is not already
|
|
127
|
-
// foregrounded. Unfortunately, 'getForegroundServiceType' was only introduced
|
|
128
|
-
// in API level 29 and seems to behave weirdly, as reported in #120. However,
|
|
129
|
-
// it appears that 'startForeground' is idempotent, so we just call it repeatedly
|
|
130
|
-
// each time a background watcher is added.
|
|
131
|
-
try {
|
|
132
|
-
// This method has been known to fail due to weird
|
|
133
|
-
// permission bugs, so we prevent any exceptions from
|
|
134
|
-
// crashing the app. See issue #86.
|
|
135
|
-
startForeground(
|
|
136
|
-
NOTIFICATION_ID,
|
|
137
|
-
createBackgroundNotification(notificationTitle, notificationMessage)
|
|
138
|
-
);
|
|
139
|
-
} catch (Exception exception) {
|
|
140
|
-
Logger.error("Failed to foreground service", exception);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
25
|
+
static final String ACTION_BROADCAST = (BackgroundGeolocationService.class.getPackage().getName() + ".broadcast");
|
|
26
|
+
private final IBinder binder = new LocalBinder();
|
|
27
|
+
|
|
28
|
+
private static final double EARTH_RADIUS_M = 6371000;
|
|
29
|
+
|
|
30
|
+
// Must be unique for this application.
|
|
31
|
+
private static final int NOTIFICATION_ID = 28351;
|
|
143
32
|
|
|
144
|
-
String
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
33
|
+
private String callbackId;
|
|
34
|
+
|
|
35
|
+
private LocationManager client;
|
|
36
|
+
private LocationListener locationCallback;
|
|
37
|
+
private MediaPlayer mediaPlayer;
|
|
38
|
+
private double[][] route;
|
|
39
|
+
private double distanceThreshold;
|
|
40
|
+
private boolean isOffRoute;
|
|
41
|
+
|
|
42
|
+
@Override
|
|
43
|
+
public IBinder onBind(Intent intent) {
|
|
44
|
+
return binder;
|
|
150
45
|
}
|
|
151
46
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
if (mediaPlayer != null) {
|
|
162
|
-
return;
|
|
47
|
+
// Some devices allow a foreground service to outlive the application's main
|
|
48
|
+
// activity, leading to nasty crashes as reported in issue #59. If we learn
|
|
49
|
+
// that the application has been killed, all watchers are stopped and the
|
|
50
|
+
// service is terminated immediately.
|
|
51
|
+
@Override
|
|
52
|
+
public boolean onUnbind(Intent intent) {
|
|
53
|
+
if (client != null && locationCallback != null) {
|
|
54
|
+
client.removeUpdates(locationCallback);
|
|
163
55
|
}
|
|
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
56
|
releaseMediaPlayer();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private Notification createBackgroundNotification(
|
|
192
|
-
String backgroundTitle,
|
|
193
|
-
String backgroundMessage
|
|
194
|
-
) {
|
|
195
|
-
Notification.Builder builder = new Notification.Builder(
|
|
196
|
-
getApplicationContext()
|
|
197
|
-
)
|
|
198
|
-
.setContentTitle(backgroundTitle)
|
|
199
|
-
.setContentText(backgroundMessage)
|
|
200
|
-
.setOngoing(true)
|
|
201
|
-
.setPriority(Notification.PRIORITY_HIGH)
|
|
202
|
-
.setWhen(System.currentTimeMillis());
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
String name = getAppString(
|
|
206
|
-
"capacitor_background_geolocation_notification_icon",
|
|
207
|
-
"mipmap/ic_launcher",
|
|
208
|
-
getApplicationContext()
|
|
209
|
-
);
|
|
210
|
-
String[] parts = name.split("/");
|
|
211
|
-
// It is actually necessary to set a valid icon for the notification to behave
|
|
212
|
-
// correctly when tapped. If there is no icon specified, tapping it will open the
|
|
213
|
-
// app's settings, rather than bringing the application to the foreground.
|
|
214
|
-
builder.setSmallIcon(
|
|
215
|
-
getAppResourceIdentifier(parts[1], parts[0], getApplicationContext())
|
|
216
|
-
);
|
|
217
|
-
} catch (Exception e) {
|
|
218
|
-
Logger.error("Could not set notification icon", e);
|
|
57
|
+
stopSelf();
|
|
58
|
+
return false;
|
|
219
59
|
}
|
|
220
60
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
builder.setColor(Color.parseColor(color));
|
|
229
|
-
}
|
|
230
|
-
} catch (Exception e) {
|
|
231
|
-
Logger.error("Could not set notification color", e);
|
|
61
|
+
@Override
|
|
62
|
+
public void onDestroy() {
|
|
63
|
+
if (client != null && locationCallback != null) {
|
|
64
|
+
client.removeUpdates(locationCallback);
|
|
65
|
+
}
|
|
66
|
+
super.onDestroy();
|
|
67
|
+
releaseMediaPlayer();
|
|
232
68
|
}
|
|
233
69
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
70
|
+
private void releaseMediaPlayer() {
|
|
71
|
+
if (mediaPlayer == null) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
if (mediaPlayer.isPlaying()) {
|
|
76
|
+
mediaPlayer.stop();
|
|
77
|
+
}
|
|
78
|
+
mediaPlayer.release();
|
|
79
|
+
} catch (Exception e) {
|
|
80
|
+
Logger.error("Error releasing MediaPlayer", e);
|
|
81
|
+
}
|
|
82
|
+
mediaPlayer = null;
|
|
247
83
|
}
|
|
248
84
|
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
85
|
+
// Handles requests from the activity.
|
|
86
|
+
public class LocalBinder extends Binder {
|
|
87
|
+
|
|
88
|
+
void start(final String id, final String notificationTitle, final String notificationMessage, float distanceFilter) {
|
|
89
|
+
releaseMediaPlayer();
|
|
90
|
+
client = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
|
91
|
+
callbackId = id;
|
|
92
|
+
|
|
93
|
+
locationCallback = (location) -> {
|
|
94
|
+
if (mediaPlayer != null) {
|
|
95
|
+
double[] point = { location.getLongitude(), location.getLatitude() };
|
|
96
|
+
var offRoute = distancePointToRoute(point) > distanceThreshold;
|
|
97
|
+
if (offRoute == true && isOffRoute == false) {
|
|
98
|
+
mediaPlayer.start();
|
|
99
|
+
}
|
|
100
|
+
isOffRoute = offRoute;
|
|
101
|
+
}
|
|
102
|
+
Intent intent = new Intent(ACTION_BROADCAST);
|
|
103
|
+
intent.putExtra("location", location);
|
|
104
|
+
intent.putExtra("id", callbackId);
|
|
105
|
+
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
client.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, distanceFilter, locationCallback);
|
|
110
|
+
} catch (SecurityException ignore) {
|
|
111
|
+
// According to Android Studio, this method can throw a Security Exception if
|
|
112
|
+
// permissions are not yet granted. Rather than check the permissions, which is fiddly,
|
|
113
|
+
// we simply ignore the exception.
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Promote the service to the foreground if necessary.
|
|
117
|
+
// Ideally we would only call 'startForeground' if the service is not already
|
|
118
|
+
// foregrounded. Unfortunately, 'getForegroundServiceType' was only introduced
|
|
119
|
+
// in API level 29 and seems to behave weirdly, as reported in #120. However,
|
|
120
|
+
// it appears that 'startForeground' is idempotent, so we just call it repeatedly
|
|
121
|
+
// each time a background watcher is added.
|
|
122
|
+
try {
|
|
123
|
+
// This method has been known to fail due to weird
|
|
124
|
+
// permission bugs, so we prevent any exceptions from
|
|
125
|
+
// crashing the app. See issue #86.
|
|
126
|
+
startForeground(NOTIFICATION_ID, createBackgroundNotification(notificationTitle, notificationMessage));
|
|
127
|
+
} catch (Exception exception) {
|
|
128
|
+
Logger.error("Failed to foreground service", exception);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
String stop() {
|
|
133
|
+
client.removeUpdates(locationCallback);
|
|
134
|
+
stopForeground(true);
|
|
135
|
+
stopSelf();
|
|
136
|
+
releaseMediaPlayer();
|
|
137
|
+
return callbackId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void setPlannedRoute(String filePath, double[][] routeCoordinates, float distance) {
|
|
141
|
+
route = routeCoordinates;
|
|
142
|
+
distanceThreshold = distance;
|
|
143
|
+
isOffRoute = true;
|
|
144
|
+
try {
|
|
145
|
+
if (mediaPlayer != null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
mediaPlayer = new MediaPlayer();
|
|
149
|
+
AssetManager am = getApplicationContext().getResources().getAssets();
|
|
150
|
+
AssetFileDescriptor assetFileDescriptor = am.openFd("public/" + filePath);
|
|
151
|
+
|
|
152
|
+
mediaPlayer.setDataSource(
|
|
153
|
+
assetFileDescriptor.getFileDescriptor(),
|
|
154
|
+
assetFileDescriptor.getStartOffset(),
|
|
155
|
+
assetFileDescriptor.getLength()
|
|
156
|
+
);
|
|
157
|
+
mediaPlayer.setLooping(false);
|
|
158
|
+
|
|
159
|
+
mediaPlayer.setOnErrorListener((mp, what, extra) -> {
|
|
160
|
+
Logger.error("MediaPlayer error: what=" + what + ", extra=" + extra);
|
|
161
|
+
releaseMediaPlayer();
|
|
162
|
+
return true; // Indicate we handled the error
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
mediaPlayer.prepareAsync();
|
|
166
|
+
} catch (Exception e) {
|
|
167
|
+
Logger.error("PlaySound: Unexpected error", e);
|
|
168
|
+
releaseMediaPlayer();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
254
171
|
}
|
|
255
172
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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;
|
|
173
|
+
private Notification createBackgroundNotification(String backgroundTitle, String backgroundMessage) {
|
|
174
|
+
Notification.Builder builder = new Notification.Builder(getApplicationContext())
|
|
175
|
+
.setContentTitle(backgroundTitle)
|
|
176
|
+
.setContentText(backgroundMessage)
|
|
177
|
+
.setOngoing(true)
|
|
178
|
+
.setPriority(Notification.PRIORITY_HIGH)
|
|
179
|
+
.setWhen(System.currentTimeMillis());
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
String name = getAppString("capacitor_background_geolocation_notification_icon", "mipmap/ic_launcher", getApplicationContext());
|
|
183
|
+
String[] parts = name.split("/");
|
|
184
|
+
// It is actually necessary to set a valid icon for the notification to behave
|
|
185
|
+
// correctly when tapped. If there is no icon specified, tapping it will open the
|
|
186
|
+
// app's settings, rather than bringing the application to the foreground.
|
|
187
|
+
builder.setSmallIcon(getAppResourceIdentifier(parts[1], parts[0], getApplicationContext()));
|
|
188
|
+
} catch (Exception e) {
|
|
189
|
+
Logger.error("Could not set notification icon", e);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
String color = getAppString("capacitor_background_geolocation_notification_color", null, getApplicationContext());
|
|
194
|
+
if (color != null) {
|
|
195
|
+
builder.setColor(Color.parseColor(color));
|
|
196
|
+
}
|
|
197
|
+
} catch (Exception e) {
|
|
198
|
+
Logger.error("Could not set notification color", e);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Intent launchIntent = getApplicationContext()
|
|
202
|
+
.getPackageManager()
|
|
203
|
+
.getLaunchIntentForPackage(getApplicationContext().getPackageName());
|
|
204
|
+
if (launchIntent != null) {
|
|
205
|
+
launchIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
|
206
|
+
builder.setContentIntent(
|
|
207
|
+
PendingIntent.getActivity(
|
|
208
|
+
getApplicationContext(),
|
|
209
|
+
0,
|
|
210
|
+
launchIntent,
|
|
211
|
+
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Set the Channel ID for Android O.
|
|
217
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
218
|
+
builder.setChannelId(BackgroundGeolocationService.class.getPackage().getName());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return builder.build();
|
|
314
222
|
}
|
|
315
223
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
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;
|
|
224
|
+
// Gets the identifier of the app's resource by name, returning 0 if not found.
|
|
225
|
+
private static int getAppResourceIdentifier(String name, String defType, Context context) {
|
|
226
|
+
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
|
327
227
|
}
|
|
328
228
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (cos_C < 0) {
|
|
334
|
-
return dist_A_C;
|
|
229
|
+
// Gets a string from the app's strings.xml file, resorting to a fallback if it is not defined.
|
|
230
|
+
public static String getAppString(String name, String fallback, Context context) {
|
|
231
|
+
int id = getAppResourceIdentifier(name, "string", context);
|
|
232
|
+
return id == 0 ? fallback : context.getString(id);
|
|
335
233
|
}
|
|
336
234
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
235
|
+
private static double haversine(double[] point1, double[] point2) {
|
|
236
|
+
double lon1 = point1[0];
|
|
237
|
+
double lat1 = point1[1];
|
|
238
|
+
double lon2 = point2[0];
|
|
239
|
+
double lat2 = point2[1];
|
|
240
|
+
|
|
241
|
+
double dLat = Math.toRadians(lat2 - lat1);
|
|
242
|
+
double dLon = Math.toRadians(lon2 - lon1);
|
|
243
|
+
|
|
244
|
+
double a =
|
|
245
|
+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
246
|
+
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
247
|
+
|
|
248
|
+
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
249
|
+
|
|
250
|
+
return EARTH_RADIUS_M * c;
|
|
360
251
|
}
|
|
361
252
|
|
|
362
|
-
double
|
|
253
|
+
private static double distancePointToLineSegment(double[] point, double[] lineStart, double[] lineEnd) {
|
|
254
|
+
// Calculate the distances between the three points using Haversine
|
|
255
|
+
double dist_A_B = haversine(point, lineStart);
|
|
256
|
+
double dist_A_C = haversine(point, lineEnd);
|
|
257
|
+
double dist_B_C = haversine(lineStart, lineEnd);
|
|
258
|
+
|
|
259
|
+
// Handle the edge case where the line segment is a single point
|
|
260
|
+
if (dist_B_C == 0) {
|
|
261
|
+
return dist_A_B;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check if the angles at the line segment's endpoints are obtuse.
|
|
265
|
+
// We use the Law of Cosines (c^2 = a^2 + b^2 - 2ab*cos(C))
|
|
266
|
+
// If cos(C) < 0, the angle is obtuse.
|
|
267
|
+
|
|
268
|
+
// Angle at B (lineStart)
|
|
269
|
+
// Use a small epsilon to handle floating point inaccuracies in division by zero
|
|
270
|
+
double cos_B = (Math.pow(dist_A_B, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_C, 2)) / (2 * dist_A_B * dist_B_C);
|
|
271
|
+
if (cos_B < 0) {
|
|
272
|
+
return dist_A_B;
|
|
273
|
+
}
|
|
363
274
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
275
|
+
// Angle at C (lineEnd)
|
|
276
|
+
double cos_C = (Math.pow(dist_A_C, 2) + Math.pow(dist_B_C, 2) - Math.pow(dist_A_B, 2)) / (2 * dist_A_C * dist_B_C);
|
|
277
|
+
if (cos_C < 0) {
|
|
278
|
+
return dist_A_C;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// If both angles are acute, the closest point is on the line segment itself.
|
|
282
|
+
// We can calculate the distance (height of the triangle) using its area.
|
|
283
|
+
|
|
284
|
+
// 1. Calculate the semi-perimeter of the triangle ABC
|
|
285
|
+
double s = (dist_A_B + dist_A_C + dist_B_C) / 2;
|
|
286
|
+
|
|
287
|
+
// 2. Calculate the area using Heron's formula
|
|
288
|
+
double area = Math.sqrt(Math.max(0, s * (s - dist_A_B) * (s - dist_A_C) * (s - dist_B_C)));
|
|
289
|
+
|
|
290
|
+
// 3. The distance is the height of the triangle from point A to the base BC
|
|
291
|
+
// Area = 0.5 * base * height => height = 2 * Area / base
|
|
292
|
+
return (2 * area) / dist_B_C;
|
|
371
293
|
}
|
|
372
294
|
|
|
373
|
-
|
|
374
|
-
|
|
295
|
+
public double distancePointToRoute(double[] point) {
|
|
296
|
+
// If the polyline has less than 2 points, we can't form a segment.
|
|
297
|
+
if (this.route.length < 2) {
|
|
298
|
+
if (this.route.length == 1) {
|
|
299
|
+
return haversine(point, this.route[0]);
|
|
300
|
+
}
|
|
301
|
+
return Double.POSITIVE_INFINITY; // No line segments to measure against
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
double minDistance = Double.POSITIVE_INFINITY;
|
|
305
|
+
|
|
306
|
+
for (int i = 0; i < this.route.length - 1; i++) {
|
|
307
|
+
double[] lineStart = this.route[i];
|
|
308
|
+
double[] lineEnd = this.route[i + 1];
|
|
309
|
+
double distance = distancePointToLineSegment(point, lineStart, lineEnd);
|
|
310
|
+
if (distance < minDistance) {
|
|
311
|
+
minDistance = distance;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return minDistance;
|
|
316
|
+
}
|
|
375
317
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The options for configuring for location updates.\n *\n * @since 7.0.9\n */\nexport interface StartOptions {\n /**\n * If the \"backgroundMessage\" option is defined, the plugin will\n * provide location updates whether the app is in the background or the\n * foreground. If it is not defined, location updates are only\n * guaranteed in the foreground. This is true on both platforms.\n *\n * On Android, a notification must be shown to continue receiving\n * location updates in the background. This option specifies the text of\n * that notification.\n *\n * @since 7.0.9\n * @example \"Getting your location to provide better service\"\n */\n backgroundMessage?: string;\n /**\n * The title of the notification mentioned above.\n *\n * @since 7.0.9\n * @default \"Using your location\"\n * @example \"Location Service\"\n */\n backgroundTitle?: string;\n /**\n * Whether permissions should be requested from the user automatically,\n * if they are not already granted.\n *\n * @since 7.0.9\n * @default true\n * @example\n * // Auto-request permissions\n * requestPermissions: true\n *\n * // Don't auto-request, handle manually\n * requestPermissions: false\n */\n requestPermissions?: boolean;\n /**\n * If \"true\", stale locations may be delivered while the device\n * obtains a GPS fix. You are responsible for checking the \"time\"\n * property. If \"false\", locations are guaranteed to be up to date.\n *\n * @since 7.0.9\n * @default false\n * @example\n * // Allow stale locations for faster initial response\n * stale: true\n *\n * // Only fresh locations\n * stale: false\n */\n stale?: boolean;\n /**\n * The distance in meters that the device must move before a new location update is triggered.\n * This is used to filter out small movements and reduce the number of updates.\n *\n * @since 7.0.9\n * @default 0\n * @example\n * // Update every 10 meters\n * distanceFilter: 10\n *\n * // Update on any movement\n * distanceFilter: 0\n */\n distanceFilter?: number;\n}\n\n/**\n * Represents a geographical location with various attributes.\n * Contains all the standard location properties returned by GPS/network providers.\n *\n * @since 7.0.0\n */\nexport interface Location {\n /**\n * Latitude in degrees.\n * Range: -90.0 to +90.0\n *\n * @since 7.0.0\n * @example 40.7128\n */\n latitude: number;\n /**\n * Longitude in degrees.\n * Range: -180.0 to +180.0\n *\n * @since 7.0.0\n * @example -74.0060\n */\n longitude: number;\n /**\n * Radius of horizontal uncertainty in metres, with 68% confidence.\n * Lower values indicate more accurate location.\n *\n * @since 7.0.0\n * @example 5.0\n */\n accuracy: number;\n /**\n * Metres above sea level (or null if not available).\n *\n * @since 7.0.0\n * @example 10.5\n */\n altitude: number | null;\n /**\n * Vertical uncertainty in metres, with 68% confidence (or null if not available).\n *\n * @since 7.0.0\n * @example 3.0\n */\n altitudeAccuracy: number | null;\n /**\n * `true` if the location was simulated by software, rather than GPS.\n * Useful for detecting mock locations in development or testing.\n *\n * @since 7.0.0\n * @example false\n */\n simulated: boolean;\n /**\n * Deviation from true north in degrees (or null if not available).\n * Range: 0.0 to 360.0\n *\n * @since 7.0.0\n * @example 45.5\n */\n bearing: number | null;\n /**\n * Speed in metres per second (or null if not available).\n *\n * @since 7.0.0\n * @example 2.5\n */\n speed: number | null;\n /**\n * Time the location was produced, in milliseconds since the unix epoch.\n * Use this to check if a location is stale when using stale: true.\n *\n * @since 7.0.0\n * @example 1640995200000\n */\n time: number | null;\n}\n\n/**\n * Error object that may be passed to the location start callback.\n * Extends the standard Error with optional error codes.\n *\n * @since 7.0.0\n */\nexport interface CallbackError extends Error {\n /**\n * Optional error code for more specific error handling.\n *\n * @since 7.0.0\n * @example \"PERMISSION_DENIED\"\n */\n code?: string;\n}\n\nexport interface SetPlannedRouteOptions {\n /**\n * The name of the sound file to play.\n * Must be a valid sound relative path in the app's public folder to work for both web and native platforms.\n * There's no need to include the public folder in the path.\n * @since 7.0.10\n * @example \"notification.mp3\"\n * */\n soundFile: string;\n /**\n * The planned route as an array of longitude and latitude pairs.\n * Each pair represents a point on the route.\n * This is used to define a route that the user can follow.\n * The route is used to play a sound when the user deviates from it.\n * @since 7.0.11\n * @example [[-74.0060, 40.7128], [-118.2437, 34.0522]]\n */\n route: [number, number][];\n\n /**\n * The distance in meters that the user must deviate from the planned route to trigger the sound.\n * This is used to determine how far off the route the user can be before the sound is played.\n * If not specified, a default value of 50 meters is used.\n * @since 7.0.11\n * @default 50\n * @example 50\n */\n distance: number;\n}\n\n/**\n * Main plugin interface for background geolocation functionality.\n * Provides methods to manage location updates and access device settings.\n *\n * @since 7.0.0\n */\nexport interface BackgroundGeolocationPlugin {\n /**\n * To start listening for changes in the device's location, call this method.\n * A Promise is returned to indicate that it finished the call. The callback will be called every time a new location\n * is available, or if there was an error when calling this method. Don't rely on promise rejection for this.\n *\n * @param options The configuration options\n * @param callback The callback function invoked when a new location is available or an error occurs\n * @returns A promise that resolves when the method is successfully called\n *\n * @since 7.0.9\n * @example\n * await BackgroundGeolocation.start(\n * {\n * backgroundMessage: \"App is using your location in the background\",\n * backgroundTitle: \"Location Service\",\n * requestPermissions: true,\n * stale: false,\n * distanceFilter: 10\n * },\n * (location, error) => {\n * if (error) {\n * console.error('Location error:', error);\n * return;\n * }\n * if (location) {\n * console.log('New location:', location.latitude, location.longitude);\n * }\n * }\n * );\n */\n start(
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The options for configuring for location updates.\n *\n * @since 7.0.9\n */\nexport interface StartOptions {\n /**\n * If the \"backgroundMessage\" option is defined, the plugin will\n * provide location updates whether the app is in the background or the\n * foreground. If it is not defined, location updates are only\n * guaranteed in the foreground. This is true on both platforms.\n *\n * On Android, a notification must be shown to continue receiving\n * location updates in the background. This option specifies the text of\n * that notification.\n *\n * @since 7.0.9\n * @example \"Getting your location to provide better service\"\n */\n backgroundMessage?: string;\n /**\n * The title of the notification mentioned above.\n *\n * @since 7.0.9\n * @default \"Using your location\"\n * @example \"Location Service\"\n */\n backgroundTitle?: string;\n /**\n * Whether permissions should be requested from the user automatically,\n * if they are not already granted.\n *\n * @since 7.0.9\n * @default true\n * @example\n * // Auto-request permissions\n * requestPermissions: true\n *\n * // Don't auto-request, handle manually\n * requestPermissions: false\n */\n requestPermissions?: boolean;\n /**\n * If \"true\", stale locations may be delivered while the device\n * obtains a GPS fix. You are responsible for checking the \"time\"\n * property. If \"false\", locations are guaranteed to be up to date.\n *\n * @since 7.0.9\n * @default false\n * @example\n * // Allow stale locations for faster initial response\n * stale: true\n *\n * // Only fresh locations\n * stale: false\n */\n stale?: boolean;\n /**\n * The distance in meters that the device must move before a new location update is triggered.\n * This is used to filter out small movements and reduce the number of updates.\n *\n * @since 7.0.9\n * @default 0\n * @example\n * // Update every 10 meters\n * distanceFilter: 10\n *\n * // Update on any movement\n * distanceFilter: 0\n */\n distanceFilter?: number;\n}\n\n/**\n * Represents a geographical location with various attributes.\n * Contains all the standard location properties returned by GPS/network providers.\n *\n * @since 7.0.0\n */\nexport interface Location {\n /**\n * Latitude in degrees.\n * Range: -90.0 to +90.0\n *\n * @since 7.0.0\n * @example 40.7128\n */\n latitude: number;\n /**\n * Longitude in degrees.\n * Range: -180.0 to +180.0\n *\n * @since 7.0.0\n * @example -74.0060\n */\n longitude: number;\n /**\n * Radius of horizontal uncertainty in metres, with 68% confidence.\n * Lower values indicate more accurate location.\n *\n * @since 7.0.0\n * @example 5.0\n */\n accuracy: number;\n /**\n * Metres above sea level (or null if not available).\n *\n * @since 7.0.0\n * @example 10.5\n */\n altitude: number | null;\n /**\n * Vertical uncertainty in metres, with 68% confidence (or null if not available).\n *\n * @since 7.0.0\n * @example 3.0\n */\n altitudeAccuracy: number | null;\n /**\n * `true` if the location was simulated by software, rather than GPS.\n * Useful for detecting mock locations in development or testing.\n *\n * @since 7.0.0\n * @example false\n */\n simulated: boolean;\n /**\n * Deviation from true north in degrees (or null if not available).\n * Range: 0.0 to 360.0\n *\n * @since 7.0.0\n * @example 45.5\n */\n bearing: number | null;\n /**\n * Speed in metres per second (or null if not available).\n *\n * @since 7.0.0\n * @example 2.5\n */\n speed: number | null;\n /**\n * Time the location was produced, in milliseconds since the unix epoch.\n * Use this to check if a location is stale when using stale: true.\n *\n * @since 7.0.0\n * @example 1640995200000\n */\n time: number | null;\n}\n\n/**\n * Error object that may be passed to the location start callback.\n * Extends the standard Error with optional error codes.\n *\n * @since 7.0.0\n */\nexport interface CallbackError extends Error {\n /**\n * Optional error code for more specific error handling.\n *\n * @since 7.0.0\n * @example \"PERMISSION_DENIED\"\n */\n code?: string;\n}\n\nexport interface SetPlannedRouteOptions {\n /**\n * The name of the sound file to play.\n * Must be a valid sound relative path in the app's public folder to work for both web and native platforms.\n * There's no need to include the public folder in the path.\n * @since 7.0.10\n * @example \"notification.mp3\"\n * */\n soundFile: string;\n /**\n * The planned route as an array of longitude and latitude pairs.\n * Each pair represents a point on the route.\n * This is used to define a route that the user can follow.\n * The route is used to play a sound when the user deviates from it.\n * @since 7.0.11\n * @example [[-74.0060, 40.7128], [-118.2437, 34.0522]]\n */\n route: [number, number][];\n\n /**\n * The distance in meters that the user must deviate from the planned route to trigger the sound.\n * This is used to determine how far off the route the user can be before the sound is played.\n * If not specified, a default value of 50 meters is used.\n * @since 7.0.11\n * @default 50\n * @example 50\n */\n distance: number;\n}\n\n/**\n * Main plugin interface for background geolocation functionality.\n * Provides methods to manage location updates and access device settings.\n *\n * @since 7.0.0\n */\nexport interface BackgroundGeolocationPlugin {\n /**\n * To start listening for changes in the device's location, call this method.\n * A Promise is returned to indicate that it finished the call. The callback will be called every time a new location\n * is available, or if there was an error when calling this method. Don't rely on promise rejection for this.\n *\n * @param options The configuration options\n * @param callback The callback function invoked when a new location is available or an error occurs\n * @returns A promise that resolves when the method is successfully called\n *\n * @since 7.0.9\n * @example\n * await BackgroundGeolocation.start(\n * {\n * backgroundMessage: \"App is using your location in the background\",\n * backgroundTitle: \"Location Service\",\n * requestPermissions: true,\n * stale: false,\n * distanceFilter: 10\n * },\n * (location, error) => {\n * if (error) {\n * console.error('Location error:', error);\n * return;\n * }\n * if (location) {\n * console.log('New location:', location.latitude, location.longitude);\n * }\n * }\n * );\n */\n start(options: StartOptions, callback: (position?: Location, error?: CallbackError) => void): Promise<void>;\n\n /**\n * Stops location updates.\n *\n * @returns A promise that resolves when the plugin stops successfully removed\n *\n * @since 7.0.9\n * @example\n * await BackgroundGeolocation.stop();\n */\n stop(): Promise<void>;\n\n /**\n * Opens the device's location settings page.\n * Useful for directing users to enable location services or adjust permissions.\n *\n * @returns A promise that resolves when the settings page is opened\n *\n * @since 7.0.0\n * @example\n * // Direct user to location settings\n * await BackgroundGeolocation.openSettings();\n */\n openSettings(): Promise<void>;\n\n /**\n * Plays a sound file when the user deviates from the planned route.\n * This should be used to play a sound (in the background too, only for native).\n *\n * @param options The options for setting the planned route and sound file\n * @returns A promise that resolves when the route is set successfully\n *\n * @since 7.0.11\n * @example\n * await BackgroundGeolocation.setPlannedRoute({\n * soundFile: \"notification.mp3\",\n * route: [[-74.0060, 40.7128], [-118.2437, 34.0522]]\n * });\n */\n setPlannedRoute(options: SetPlannedRouteOptions): Promise<void>;\n}\n"]}
|