@capgo/background-geolocation 8.0.32 → 8.0.34

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.
@@ -9,13 +9,14 @@ import android.content.Context;
9
9
  import android.content.Intent;
10
10
  import android.content.IntentFilter;
11
11
  import android.content.ServiceConnection;
12
+ import android.content.pm.PackageManager;
12
13
  import android.location.Location;
13
14
  import android.location.LocationManager;
14
15
  import android.net.Uri;
15
16
  import android.os.Build;
16
17
  import android.os.IBinder;
17
18
  import android.provider.Settings;
18
- import androidx.annotation.Nullable;
19
+ import androidx.core.content.ContextCompat;
19
20
  import androidx.localbroadcastmanager.content.LocalBroadcastManager;
20
21
  import com.getcapacitor.JSArray;
21
22
  import com.getcapacitor.JSObject;
@@ -27,7 +28,13 @@ import com.getcapacitor.PluginMethod;
27
28
  import com.getcapacitor.annotation.CapacitorPlugin;
28
29
  import com.getcapacitor.annotation.Permission;
29
30
  import com.getcapacitor.annotation.PermissionCallback;
31
+ import com.google.android.gms.location.Geofence;
32
+ import com.google.android.gms.location.GeofencingClient;
33
+ import com.google.android.gms.location.GeofencingRequest;
30
34
  import com.google.android.gms.location.LocationServices;
35
+ import java.net.URL;
36
+ import java.util.Collections;
37
+ import java.util.Set;
31
38
  import java.util.concurrent.CompletableFuture;
32
39
  import org.json.JSONArray;
33
40
  import org.json.JSONException;
@@ -37,6 +44,7 @@ import org.json.JSONObject;
37
44
  name = "BackgroundGeolocation",
38
45
  permissions = {
39
46
  @Permission(strings = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }, alias = "location"),
47
+ @Permission(strings = { Manifest.permission.ACCESS_BACKGROUND_LOCATION }, alias = "backgroundLocation"),
40
48
  @Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = "notification")
41
49
  }
42
50
  )
@@ -46,6 +54,9 @@ public class BackgroundGeolocation extends Plugin {
46
54
 
47
55
  private CompletableFuture<BackgroundGeolocationService.LocalBinder> serviceConnectionFuture;
48
56
  private CompletableFuture<Void> locationPermissionFuture;
57
+ private CompletableFuture<Void> geofencePermissionFuture;
58
+ private BroadcastReceiver serviceReceiver;
59
+ private BroadcastReceiver geofenceEventReceiver;
49
60
 
50
61
  private void fetchLastLocation(PluginCall call) {
51
62
  try {
@@ -199,6 +210,236 @@ public class BackgroundGeolocation extends Plugin {
199
210
  }
200
211
  }
201
212
 
213
+ @PluginMethod
214
+ public void setupGeofencing(PluginCall call) {
215
+ String url = call.getString("url");
216
+ if (url != null && !url.isEmpty()) {
217
+ try {
218
+ URL urlObject = new URL(url);
219
+ String protocol = urlObject.getProtocol();
220
+ if (!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) {
221
+ call.reject("Given url is not valid");
222
+ return;
223
+ }
224
+ } catch (Exception exception) {
225
+ call.reject("Given url is not valid");
226
+ return;
227
+ }
228
+ }
229
+
230
+ JSObject payload = call.getObject("payload", new JSObject());
231
+ GeofenceStore.saveSetup(getContext(), url, call.getBoolean("notifyOnEntry", true), call.getBoolean("notifyOnExit", true), payload);
232
+
233
+ if (!call.getBoolean("requestPermissions", true)) {
234
+ call.resolve();
235
+ return;
236
+ }
237
+
238
+ requestGeofencePermissions(call)
239
+ .thenRun(call::resolve)
240
+ .exceptionally((throwable) -> {
241
+ call.reject("Background location permission is required for geofencing", "NOT_AUTHORIZED");
242
+ return null;
243
+ });
244
+ }
245
+
246
+ @PluginMethod
247
+ public void addGeofence(PluginCall call) {
248
+ if (!hasGeofencePermissions()) {
249
+ call.reject("Background location permission is required for geofencing", "NOT_AUTHORIZED");
250
+ return;
251
+ }
252
+ if (!isLocationEnabled(getContext())) {
253
+ call.reject("Location services disabled.", "NOT_AUTHORIZED");
254
+ return;
255
+ }
256
+
257
+ Double latitude = call.getDouble("latitude");
258
+ Double longitude = call.getDouble("longitude");
259
+ String identifier = call.getString("identifier");
260
+ double radius = call.getDouble("radius", 50.0);
261
+ if (identifier == null || identifier.isEmpty()) {
262
+ call.reject("Identifier is required");
263
+ return;
264
+ }
265
+ if (latitude == null || latitude < -90 || latitude > 90) {
266
+ call.reject("Latitude must be between -90 and 90");
267
+ return;
268
+ }
269
+ if (longitude == null || longitude < -180 || longitude > 180) {
270
+ call.reject("Longitude must be between -180 and 180");
271
+ return;
272
+ }
273
+ if (radius <= 0) {
274
+ call.reject("Radius must be greater than 0");
275
+ return;
276
+ }
277
+
278
+ boolean notifyOnEntry = call.getBoolean("notifyOnEntry", GeofenceStore.getNotifyOnEntry(getContext()));
279
+ boolean notifyOnExit = call.getBoolean("notifyOnExit", GeofenceStore.getNotifyOnExit(getContext()));
280
+ int transitionTypes = 0;
281
+ int initialTrigger = 0;
282
+ if (notifyOnEntry) {
283
+ transitionTypes |= Geofence.GEOFENCE_TRANSITION_ENTER;
284
+ initialTrigger |= GeofencingRequest.INITIAL_TRIGGER_ENTER;
285
+ }
286
+ if (notifyOnExit) {
287
+ transitionTypes |= Geofence.GEOFENCE_TRANSITION_EXIT;
288
+ }
289
+ if (transitionTypes == 0) {
290
+ call.reject("At least one transition must be enabled");
291
+ return;
292
+ }
293
+
294
+ JSObject payload = call.getObject("payload", new JSObject());
295
+ Geofence geofence = new Geofence.Builder()
296
+ .setRequestId(identifier)
297
+ .setCircularRegion(latitude, longitude, (float) radius)
298
+ .setTransitionTypes(transitionTypes)
299
+ .setExpirationDuration(Geofence.NEVER_EXPIRE)
300
+ .build();
301
+ GeofencingRequest request = new GeofencingRequest.Builder().setInitialTrigger(initialTrigger).addGeofence(geofence).build();
302
+
303
+ try {
304
+ getGeofencingClient()
305
+ .addGeofences(request, getGeofencePendingIntent())
306
+ .addOnSuccessListener((unused) -> {
307
+ try {
308
+ GeofenceStore.saveRegion(
309
+ getContext(),
310
+ identifier,
311
+ latitude,
312
+ longitude,
313
+ (float) radius,
314
+ notifyOnEntry,
315
+ notifyOnExit,
316
+ payload
317
+ );
318
+ call.resolve();
319
+ } catch (JSONException exception) {
320
+ call.reject("Could not persist geofence", exception);
321
+ }
322
+ })
323
+ .addOnFailureListener((exception) -> call.reject("Could not start monitoring the geofence", exception));
324
+ } catch (SecurityException exception) {
325
+ call.reject("Background location permission is required for geofencing", "NOT_AUTHORIZED", exception);
326
+ }
327
+ }
328
+
329
+ @PluginMethod
330
+ public void removeGeofence(PluginCall call) {
331
+ String identifier = call.getString("identifier");
332
+ if (identifier == null || identifier.isEmpty()) {
333
+ call.reject("Identifier is required");
334
+ return;
335
+ }
336
+ getGeofencingClient()
337
+ .removeGeofences(Collections.singletonList(identifier))
338
+ .addOnSuccessListener((unused) -> {
339
+ GeofenceStore.removeRegion(getContext(), identifier);
340
+ call.resolve();
341
+ })
342
+ .addOnFailureListener((exception) -> call.reject("Could not stop monitoring the geofence", exception));
343
+ }
344
+
345
+ @PluginMethod
346
+ public void removeAllGeofences(PluginCall call) {
347
+ getGeofencingClient()
348
+ .removeGeofences(getGeofencePendingIntent())
349
+ .addOnSuccessListener((unused) -> {
350
+ GeofenceStore.clearRegions(getContext());
351
+ call.resolve();
352
+ })
353
+ .addOnFailureListener((exception) -> call.reject("Could not stop monitoring geofences", exception));
354
+ }
355
+
356
+ @PluginMethod
357
+ public void getMonitoredGeofences(PluginCall call) {
358
+ JSObject result = new JSObject();
359
+ Set<String> regionIds = GeofenceStore.getRegionIds(getContext());
360
+ JSArray regions = new JSArray();
361
+ for (String regionId : regionIds) {
362
+ regions.put(regionId);
363
+ }
364
+ result.put("regions", regions);
365
+ call.resolve(result);
366
+ }
367
+
368
+ private CompletableFuture<Void> requestGeofencePermissions(PluginCall call) {
369
+ if (hasGeofencePermissions()) {
370
+ return CompletableFuture.completedFuture(null);
371
+ }
372
+ if (geofencePermissionFuture != null) {
373
+ return geofencePermissionFuture;
374
+ }
375
+ CompletableFuture<Void> future = new CompletableFuture<>();
376
+ geofencePermissionFuture = future;
377
+ if (getPermissionState("location") != PermissionState.GRANTED) {
378
+ requestPermissionForAlias("location", call, "geofenceLocationPermissionsCallback");
379
+ return future;
380
+ }
381
+ requestBackgroundLocationPermissionIfNeeded(call);
382
+ return future;
383
+ }
384
+
385
+ @PermissionCallback
386
+ private void geofenceLocationPermissionsCallback(PluginCall call) {
387
+ if (geofencePermissionFuture == null) {
388
+ return;
389
+ }
390
+ if (getPermissionState("location") != PermissionState.GRANTED) {
391
+ geofencePermissionFuture.completeExceptionally(new SecurityException("User denied location permission"));
392
+ geofencePermissionFuture = null;
393
+ return;
394
+ }
395
+ requestBackgroundLocationPermissionIfNeeded(call);
396
+ }
397
+
398
+ @PermissionCallback
399
+ private void geofenceBackgroundPermissionsCallback(PluginCall call) {
400
+ if (geofencePermissionFuture == null) {
401
+ return;
402
+ }
403
+ if (!hasBackgroundLocationPermission()) {
404
+ geofencePermissionFuture.completeExceptionally(new SecurityException("User denied background location permission"));
405
+ geofencePermissionFuture = null;
406
+ return;
407
+ }
408
+ geofencePermissionFuture.complete(null);
409
+ geofencePermissionFuture = null;
410
+ }
411
+
412
+ private void requestBackgroundLocationPermissionIfNeeded(PluginCall call) {
413
+ if (hasBackgroundLocationPermission()) {
414
+ geofencePermissionFuture.complete(null);
415
+ geofencePermissionFuture = null;
416
+ return;
417
+ }
418
+ requestPermissionForAlias("backgroundLocation", call, "geofenceBackgroundPermissionsCallback");
419
+ }
420
+
421
+ private boolean hasGeofencePermissions() {
422
+ return getPermissionState("location") == PermissionState.GRANTED && hasBackgroundLocationPermission();
423
+ }
424
+
425
+ private boolean hasBackgroundLocationPermission() {
426
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
427
+ return true;
428
+ }
429
+ return (
430
+ ContextCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
431
+ PackageManager.PERMISSION_GRANTED
432
+ );
433
+ }
434
+
435
+ private GeofencingClient getGeofencingClient() {
436
+ return LocationServices.getGeofencingClient(getContext());
437
+ }
438
+
439
+ private android.app.PendingIntent getGeofencePendingIntent() {
440
+ return GeofenceBroadcastReceiver.createPendingIntent(getContext());
441
+ }
442
+
202
443
  private static double[][] getJavaDoubleArray(JSArray jsArray) throws JSONException {
203
444
  int rows = jsArray.length();
204
445
  if (rows == 0) {
@@ -277,6 +518,27 @@ public class BackgroundGeolocation extends Plugin {
277
518
  }
278
519
  }
279
520
 
521
+ private class GeofenceEventReceiver extends BroadcastReceiver {
522
+
523
+ @Override
524
+ public void onReceive(Context context, Intent intent) {
525
+ boolean errorEvent = GeofenceStore.ACTION_GEOFENCE_ERROR.equals(intent.getAction());
526
+ String payload = intent.getStringExtra(errorEvent ? GeofenceStore.EXTRA_GEOFENCE_ERROR : GeofenceStore.EXTRA_GEOFENCE_PAYLOAD);
527
+ if (payload == null || payload.isEmpty()) {
528
+ return;
529
+ }
530
+ try {
531
+ notifyListeners(
532
+ errorEvent ? "geofenceError" : "geofenceTransition",
533
+ GeofenceStore.toJSObject(new JSONObject(payload)),
534
+ true
535
+ );
536
+ } catch (JSONException exception) {
537
+ Logger.error("Could not parse geofence payload", exception);
538
+ }
539
+ }
540
+ }
541
+
280
542
  @Override
281
543
  public void load() {
282
544
  super.load();
@@ -299,10 +561,16 @@ public class BackgroundGeolocation extends Plugin {
299
561
  manager.createNotificationChannel(channel);
300
562
  }
301
563
 
564
+ serviceReceiver = new ServiceReceiver();
302
565
  LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(
303
- new ServiceReceiver(),
566
+ serviceReceiver,
304
567
  new IntentFilter(BackgroundGeolocationService.ACTION_BROADCAST)
305
568
  );
569
+
570
+ geofenceEventReceiver = new GeofenceEventReceiver();
571
+ IntentFilter geofenceFilter = new IntentFilter(GeofenceStore.ACTION_GEOFENCE_EVENT);
572
+ geofenceFilter.addAction(GeofenceStore.ACTION_GEOFENCE_ERROR);
573
+ LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(geofenceEventReceiver, geofenceFilter);
306
574
  }
307
575
 
308
576
  private CompletableFuture<BackgroundGeolocationService.LocalBinder> getServiceConnection() {
@@ -347,6 +615,17 @@ public class BackgroundGeolocation extends Plugin {
347
615
  if (locationPermissionFuture != null && !locationPermissionFuture.isDone()) {
348
616
  locationPermissionFuture.cancel(true);
349
617
  }
618
+ if (geofencePermissionFuture != null && !geofencePermissionFuture.isDone()) {
619
+ geofencePermissionFuture.cancel(true);
620
+ }
621
+ if (serviceReceiver != null) {
622
+ LocalBroadcastManager.getInstance(this.getContext()).unregisterReceiver(serviceReceiver);
623
+ serviceReceiver = null;
624
+ }
625
+ if (geofenceEventReceiver != null) {
626
+ LocalBroadcastManager.getInstance(this.getContext()).unregisterReceiver(geofenceEventReceiver);
627
+ geofenceEventReceiver = null;
628
+ }
350
629
  super.handleOnDestroy();
351
630
  }
352
631
 
@@ -0,0 +1,58 @@
1
+ package com.capgo.capacitor_background_geolocation;
2
+
3
+ import android.content.BroadcastReceiver;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+ import com.getcapacitor.Logger;
7
+ import com.google.android.gms.location.LocationServices;
8
+ import com.google.android.gms.tasks.Task;
9
+ import com.google.android.gms.tasks.Tasks;
10
+ import java.util.ArrayList;
11
+ import java.util.List;
12
+ import org.json.JSONObject;
13
+
14
+ public class GeofenceBootReceiver extends BroadcastReceiver {
15
+
16
+ @Override
17
+ public void onReceive(Context context, Intent intent) {
18
+ if (intent == null || !shouldRestoreAction(intent.getAction())) {
19
+ return;
20
+ }
21
+
22
+ PendingResult pendingResult = goAsync();
23
+ List<Task<Void>> tasks = restorePersistedGeofences(context);
24
+ if (tasks.isEmpty()) {
25
+ pendingResult.finish();
26
+ return;
27
+ }
28
+ Tasks.whenAllComplete(tasks).addOnCompleteListener((unused) -> pendingResult.finish());
29
+ }
30
+
31
+ static boolean shouldRestoreAction(String action) {
32
+ return Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_MY_PACKAGE_REPLACED.equals(action);
33
+ }
34
+
35
+ private static List<Task<Void>> restorePersistedGeofences(Context context) {
36
+ List<Task<Void>> tasks = new ArrayList<>();
37
+ var client = LocationServices.getGeofencingClient(context);
38
+ for (String identifier : GeofenceStore.getRegionIds(context)) {
39
+ JSONObject region = GeofenceStore.getRegion(context, identifier);
40
+ try {
41
+ tasks.add(
42
+ client
43
+ .addGeofences(GeofenceStore.buildGeofencingRequest(region), GeofenceBroadcastReceiver.createPendingIntent(context))
44
+ .addOnSuccessListener((unused) -> Logger.debug("Restored geofence after boot: " + identifier))
45
+ .addOnFailureListener((exception) ->
46
+ Logger.error("Could not restore geofence after boot: " + identifier, exception)
47
+ )
48
+ );
49
+ } catch (SecurityException exception) {
50
+ Logger.error("Missing permission to restore geofence after boot: " + identifier, exception);
51
+ } catch (Exception exception) {
52
+ GeofenceStore.removeRegion(context, identifier);
53
+ Logger.error("Invalid persisted geofence removed after boot: " + identifier, exception);
54
+ }
55
+ }
56
+ return tasks;
57
+ }
58
+ }
@@ -0,0 +1,83 @@
1
+ package com.capgo.capacitor_background_geolocation;
2
+
3
+ import android.app.PendingIntent;
4
+ import android.content.BroadcastReceiver;
5
+ import android.content.Context;
6
+ import android.content.Intent;
7
+ import android.os.Build;
8
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
9
+ import com.getcapacitor.Logger;
10
+ import com.google.android.gms.location.Geofence;
11
+ import com.google.android.gms.location.GeofenceStatusCodes;
12
+ import com.google.android.gms.location.GeofencingEvent;
13
+ import java.util.List;
14
+ import org.json.JSONObject;
15
+
16
+ public class GeofenceBroadcastReceiver extends BroadcastReceiver {
17
+
18
+ private static final int GEOFENCE_PENDING_INTENT_REQUEST_CODE = 83620;
19
+
20
+ @Override
21
+ public void onReceive(Context context, Intent intent) {
22
+ GeofencingEvent event = GeofencingEvent.fromIntent(intent);
23
+ if (event == null) {
24
+ return;
25
+ }
26
+ if (event.hasError()) {
27
+ int errorCode = event.getErrorCode();
28
+ String message = GeofenceStatusCodes.getStatusCodeString(errorCode);
29
+ Logger.error("Geofence event failed with code: " + errorCode);
30
+ if (shouldClearStoredRegions(errorCode)) {
31
+ GeofenceStore.clearRegions(context);
32
+ }
33
+ try {
34
+ JSONObject data = new JSONObject();
35
+ data.put("code", errorCode);
36
+ data.put("message", message);
37
+ Intent localIntent = new Intent(GeofenceStore.ACTION_GEOFENCE_ERROR);
38
+ localIntent.putExtra(GeofenceStore.EXTRA_GEOFENCE_ERROR, data.toString());
39
+ LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
40
+ } catch (Exception exception) {
41
+ Logger.error("Failed to emit geofence error", exception);
42
+ }
43
+ return;
44
+ }
45
+
46
+ int transition = event.getGeofenceTransition();
47
+ if (transition != Geofence.GEOFENCE_TRANSITION_ENTER && transition != Geofence.GEOFENCE_TRANSITION_EXIT) {
48
+ return;
49
+ }
50
+
51
+ List<Geofence> triggeringGeofences = event.getTriggeringGeofences();
52
+ if (triggeringGeofences == null || triggeringGeofences.isEmpty()) {
53
+ return;
54
+ }
55
+
56
+ boolean enter = transition == Geofence.GEOFENCE_TRANSITION_ENTER;
57
+ try {
58
+ for (Geofence geofence : triggeringGeofences) {
59
+ JSONObject data = GeofenceStore.buildTransitionData(context, geofence.getRequestId(), enter);
60
+ Intent localIntent = new Intent(GeofenceStore.ACTION_GEOFENCE_EVENT);
61
+ localIntent.putExtra(GeofenceStore.EXTRA_GEOFENCE_PAYLOAD, data.toString());
62
+ LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
63
+ GeofenceStore.enqueueTransition(context, data);
64
+ }
65
+ } catch (Exception exception) {
66
+ Logger.error("Failed to handle geofence transition", exception);
67
+ }
68
+ }
69
+
70
+ static boolean shouldClearStoredRegions(int errorCode) {
71
+ return errorCode == GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE;
72
+ }
73
+
74
+ static PendingIntent createPendingIntent(Context context) {
75
+ Intent intent = new Intent(context, GeofenceBroadcastReceiver.class);
76
+ intent.setPackage(context.getPackageName());
77
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
78
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
79
+ flags |= PendingIntent.FLAG_MUTABLE;
80
+ }
81
+ return PendingIntent.getBroadcast(context, GEOFENCE_PENDING_INTENT_REQUEST_CODE, intent, flags);
82
+ }
83
+ }