@azatek/background-geolocation 1.0.0

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.
@@ -0,0 +1,348 @@
1
+ package com.equimaps.capacitor_background_geolocation;
2
+
3
+ import android.Manifest;
4
+ import android.app.Notification;
5
+ import android.app.NotificationChannel;
6
+ import android.app.NotificationManager;
7
+ import android.graphics.Color;
8
+ import android.app.PendingIntent;
9
+ import android.content.BroadcastReceiver;
10
+ import android.content.ComponentName;
11
+ import android.content.Context;
12
+ import android.content.Intent;
13
+ import android.content.IntentFilter;
14
+ import android.content.ServiceConnection;
15
+ import android.content.pm.PackageManager;
16
+ import android.location.Location;
17
+ import android.location.LocationManager;
18
+ import android.net.Uri;
19
+ import android.os.Build;
20
+ import android.os.IBinder;
21
+ import android.provider.Settings;
22
+
23
+ import com.getcapacitor.JSObject;
24
+ import com.getcapacitor.Logger;
25
+ import com.getcapacitor.PermissionState;
26
+ import com.getcapacitor.Plugin;
27
+ import com.getcapacitor.PluginCall;
28
+ import com.getcapacitor.PluginMethod;
29
+ import com.getcapacitor.annotation.CapacitorPlugin;
30
+ import com.getcapacitor.annotation.Permission;
31
+ import com.getcapacitor.annotation.PermissionCallback;
32
+ import com.google.android.gms.location.LocationServices;
33
+ import com.google.android.gms.tasks.OnSuccessListener;
34
+
35
+ import org.json.JSONObject;
36
+
37
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
38
+
39
+ @CapacitorPlugin(
40
+ name = "BackgroundGeolocation",
41
+ permissions = {
42
+ @Permission(
43
+ strings = {
44
+ Manifest.permission.ACCESS_COARSE_LOCATION,
45
+ Manifest.permission.ACCESS_FINE_LOCATION
46
+ },
47
+ alias = "location"
48
+ )
49
+ }
50
+ )
51
+ public class BackgroundGeolocation extends Plugin {
52
+ private BackgroundGeolocationService.LocalBinder service = null;
53
+ private Boolean stoppedWithoutPermissions = false;
54
+
55
+ private void fetchLastLocation(PluginCall call) {
56
+ try {
57
+ LocationServices.getFusedLocationProviderClient(
58
+ getContext()
59
+ ).getLastLocation().addOnSuccessListener(
60
+ getActivity(),
61
+ new OnSuccessListener<Location>() {
62
+ @Override
63
+ public void onSuccess(Location location) {
64
+ if (location != null) {
65
+ call.resolve(formatLocation(location));
66
+ }
67
+ }
68
+ }
69
+ );
70
+ } catch (SecurityException ignore) {}
71
+ }
72
+
73
+ @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK)
74
+ public void addWatcher(final PluginCall call) {
75
+ if (service == null) {
76
+ call.reject("Service not running.");
77
+ return;
78
+ }
79
+ call.setKeepAlive(true);
80
+
81
+ if (getPermissionState("location") != PermissionState.GRANTED) {
82
+ if (call.getBoolean("requestPermissions", true)) {
83
+ requestPermissionForAlias("location", call, "locationPermissionsCallback");
84
+ } else {
85
+ call.reject("Permission denied.", "NOT_AUTHORIZED");
86
+ }
87
+ } else if (!isLocationEnabled(getContext())) {
88
+ call.reject("Location services disabled.", "NOT_AUTHORIZED");
89
+ }
90
+ if (call.getBoolean("stale", false)) {
91
+ fetchLastLocation(call);
92
+ }
93
+ Notification backgroundNotification = null;
94
+ String backgroundMessage = call.getString("backgroundMessage");
95
+
96
+ if (backgroundMessage != null) {
97
+ Notification.Builder builder = new Notification.Builder(getContext())
98
+ .setContentTitle(
99
+ call.getString(
100
+ "backgroundTitle",
101
+ "Using your location"
102
+ )
103
+ )
104
+ .setContentText(backgroundMessage)
105
+ .setOngoing(true)
106
+ .setPriority(Notification.PRIORITY_HIGH)
107
+ .setWhen(System.currentTimeMillis());
108
+
109
+ try {
110
+ String name = getAppString(
111
+ "capacitor_background_geolocation_notification_icon",
112
+ "mipmap/ic_launcher"
113
+ );
114
+ String[] parts = name.split("/");
115
+ // It is actually necessary to set a valid icon for the notification to behave
116
+ // correctly when tapped. If there is no icon specified, tapping it will open the
117
+ // app's settings, rather than bringing the application to the foreground.
118
+ builder.setSmallIcon(
119
+ getAppResourceIdentifier(parts[1], parts[0])
120
+ );
121
+ } catch (Exception e) {
122
+ Logger.error("Could not set notification icon", e);
123
+ }
124
+
125
+ try {
126
+ String color = getAppString(
127
+ "capacitor_background_geolocation_notification_color",
128
+ null
129
+ );
130
+ if (color != null) {
131
+ builder.setColor(Color.parseColor(color));
132
+ }
133
+ } catch (Exception e) {
134
+ Logger.error("Could not set notification color", e);
135
+ }
136
+
137
+ Intent launchIntent = getContext().getPackageManager().getLaunchIntentForPackage(
138
+ getContext().getPackageName()
139
+ );
140
+ if (launchIntent != null) {
141
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
142
+ builder.setContentIntent(
143
+ PendingIntent.getActivity(
144
+ getContext(),
145
+ 0,
146
+ launchIntent,
147
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
148
+ )
149
+ );
150
+ }
151
+
152
+ // Set the Channel ID for Android O.
153
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
154
+ builder.setChannelId(BackgroundGeolocationService.class.getPackage().getName());
155
+ }
156
+
157
+ backgroundNotification = builder.build();
158
+ }
159
+ service.addWatcher(
160
+ call.getCallbackId(),
161
+ backgroundNotification,
162
+ call.getFloat("distanceFilter", 0f),
163
+ call.getInt("accuracy", 102)
164
+ );
165
+ }
166
+
167
+ @PermissionCallback
168
+ private void locationPermissionsCallback(PluginCall call) {
169
+
170
+ if (getPermissionState("location") != PermissionState.GRANTED) {
171
+ call.reject("User denied location permission", "NOT_AUTHORIZED");
172
+ return;
173
+ }
174
+ if (call.getBoolean("stale", false)) {
175
+ fetchLastLocation(call);
176
+ }
177
+ if (service != null) {
178
+ service.onPermissionsGranted();
179
+ // The handleOnResume method will now be called, and we don't need it to call
180
+ // service.onPermissionsGranted again so we reset this flag.
181
+ stoppedWithoutPermissions = false;
182
+ }
183
+ }
184
+
185
+ @PluginMethod()
186
+ public void removeWatcher(PluginCall call) {
187
+ String callbackId = call.getString("id");
188
+ if (callbackId == null) {
189
+ call.reject("Missing id.");
190
+ return;
191
+ }
192
+ service.removeWatcher(callbackId);
193
+ PluginCall savedCall = getBridge().getSavedCall(callbackId);
194
+ if (savedCall != null) {
195
+ savedCall.release(getBridge());
196
+ }
197
+ call.resolve();
198
+ }
199
+
200
+ @PluginMethod()
201
+ public void openSettings(PluginCall call) {
202
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
203
+ Uri uri = Uri.fromParts("package", getContext().getPackageName(), null);
204
+ intent.setData(uri);
205
+ getContext().startActivity(intent);
206
+ call.resolve();
207
+ }
208
+
209
+ // Checks if device-wide location services are disabled
210
+ private static Boolean isLocationEnabled(Context context) {
211
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
212
+ LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
213
+ return lm != null && lm.isLocationEnabled();
214
+ } else {
215
+ return (
216
+ Settings.Secure.getInt(
217
+ context.getContentResolver(),
218
+ Settings.Secure.LOCATION_MODE,
219
+ Settings.Secure.LOCATION_MODE_OFF
220
+ ) != Settings.Secure.LOCATION_MODE_OFF
221
+ );
222
+ }
223
+ }
224
+
225
+ private static JSObject formatLocation(Location location) {
226
+ JSObject obj = new JSObject();
227
+ obj.put("latitude", location.getLatitude());
228
+ obj.put("longitude", location.getLongitude());
229
+ // The docs state that all Location objects have an accuracy, but then why is there a
230
+ // hasAccuracy method? Better safe than sorry.
231
+ obj.put("accuracy", location.hasAccuracy() ? location.getAccuracy() : JSONObject.NULL);
232
+ obj.put("altitude", location.hasAltitude() ? location.getAltitude() : JSONObject.NULL);
233
+ if (Build.VERSION.SDK_INT >= 26 && location.hasVerticalAccuracy()) {
234
+ obj.put("altitudeAccuracy", location.getVerticalAccuracyMeters());
235
+ } else {
236
+ obj.put("altitudeAccuracy", JSONObject.NULL);
237
+ }
238
+ // In addition to mocking locations in development, Android allows the
239
+ // installation of apps which have the power to simulate location
240
+ // readings in other apps.
241
+ obj.put("simulated", location.isFromMockProvider());
242
+ obj.put("speed", location.hasSpeed() ? location.getSpeed() : JSONObject.NULL);
243
+ obj.put("bearing", location.hasBearing() ? location.getBearing() : JSONObject.NULL);
244
+ obj.put("time", location.getTime());
245
+ return obj;
246
+ }
247
+
248
+ // Receives messages from the service.
249
+ private class ServiceReceiver extends BroadcastReceiver {
250
+ @Override
251
+ public void onReceive(Context context, Intent intent) {
252
+ String id = intent.getStringExtra("id");
253
+ PluginCall call = getBridge().getSavedCall(id);
254
+ if (call == null) {
255
+ return;
256
+ }
257
+ Location location = intent.getParcelableExtra("location");
258
+ if (location != null) {
259
+ call.resolve(formatLocation(location));
260
+ } else {
261
+ Logger.debug("No locations received");
262
+ }
263
+ }
264
+ }
265
+
266
+ // Gets the identifier of the app's resource by name, returning 0 if not found.
267
+ private int getAppResourceIdentifier(String name, String defType) {
268
+ return getContext().getResources().getIdentifier(
269
+ name,
270
+ defType,
271
+ getContext().getPackageName()
272
+ );
273
+ }
274
+
275
+ // Gets a string from the app's strings.xml file, resorting to a fallback if it is not defined.
276
+ private String getAppString(String name, String fallback) {
277
+ int id = getAppResourceIdentifier(name, "string");
278
+ return id == 0 ? fallback : getContext().getString(id);
279
+ }
280
+
281
+ @Override
282
+ public void load() {
283
+ super.load();
284
+
285
+ // Android O requires a Notification Channel.
286
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
287
+ NotificationManager manager = (NotificationManager) getContext().getSystemService(
288
+ Context.NOTIFICATION_SERVICE
289
+ );
290
+ NotificationChannel channel = new NotificationChannel(
291
+ BackgroundGeolocationService.class.getPackage().getName(),
292
+ getAppString(
293
+ "capacitor_background_geolocation_notification_channel_name",
294
+ "Background Tracking"
295
+ ),
296
+ NotificationManager.IMPORTANCE_DEFAULT
297
+ );
298
+ channel.enableLights(false);
299
+ channel.enableVibration(false);
300
+ channel.setSound(null, null);
301
+ manager.createNotificationChannel(channel);
302
+ }
303
+
304
+ this.getContext().bindService(
305
+ new Intent(this.getContext(), BackgroundGeolocationService.class),
306
+ new ServiceConnection() {
307
+ @Override
308
+ public void onServiceConnected(ComponentName name, IBinder binder) {
309
+ BackgroundGeolocation.this.service = (BackgroundGeolocationService.LocalBinder) binder;
310
+ }
311
+
312
+ @Override
313
+ public void onServiceDisconnected(ComponentName name) {
314
+ }
315
+ },
316
+ Context.BIND_AUTO_CREATE
317
+ );
318
+
319
+ LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(
320
+ new ServiceReceiver(),
321
+ new IntentFilter(BackgroundGeolocationService.ACTION_BROADCAST)
322
+ );
323
+ }
324
+
325
+ @Override
326
+ protected void handleOnResume() {
327
+ if (service != null) {
328
+ if (stoppedWithoutPermissions && getPermissionState("location") == PermissionState.GRANTED) {
329
+ service.onPermissionsGranted();
330
+ }
331
+ }
332
+ super.handleOnResume();
333
+ }
334
+
335
+ @Override
336
+ protected void handleOnPause() {
337
+ stoppedWithoutPermissions = getPermissionState("location") != PermissionState.GRANTED;
338
+ super.handleOnPause();
339
+ }
340
+
341
+ @Override
342
+ protected void handleOnDestroy() {
343
+ if (service != null) {
344
+ service.stopService();
345
+ }
346
+ super.handleOnDestroy();
347
+ }
348
+ }
@@ -0,0 +1,204 @@
1
+ package com.equimaps.capacitor_background_geolocation;
2
+
3
+ import android.app.Notification;
4
+ import android.app.Service;
5
+ import android.content.Intent;
6
+ import android.content.pm.ServiceInfo;
7
+ import android.location.Location;
8
+ import android.os.Binder;
9
+ import android.os.Build;
10
+ import android.os.IBinder;
11
+
12
+ import com.getcapacitor.Logger;
13
+ import com.google.android.gms.location.FusedLocationProviderClient;
14
+ import com.google.android.gms.location.LocationAvailability;
15
+ import com.google.android.gms.location.LocationCallback;
16
+ import com.google.android.gms.location.LocationRequest;
17
+ import com.google.android.gms.location.LocationResult;
18
+ import com.google.android.gms.location.LocationServices;
19
+
20
+ import java.util.HashSet;
21
+
22
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
23
+
24
+ // A bound and started service that is promoted to a foreground service
25
+ // (showing a persistent notification) when the first background watcher is
26
+ // added, and demoted when the last background watcher is removed.
27
+ public class BackgroundGeolocationService extends Service {
28
+ static final String ACTION_BROADCAST = (
29
+ BackgroundGeolocationService.class.getPackage().getName() + ".broadcast"
30
+ );
31
+ private final IBinder binder = new LocalBinder();
32
+
33
+ // Must be unique for this application.
34
+ private static final int NOTIFICATION_ID = 28351;
35
+
36
+ private class Watcher {
37
+ public String id;
38
+ public FusedLocationProviderClient client;
39
+ public LocationRequest locationRequest;
40
+ public LocationCallback locationCallback;
41
+ public Notification backgroundNotification;
42
+ }
43
+ private HashSet<Watcher> watchers = new HashSet<Watcher>();
44
+
45
+ @Override
46
+ public IBinder onBind(Intent intent) {
47
+ return binder;
48
+ }
49
+
50
+ // Some devices allow a foreground service to outlive the application's main
51
+ // activity, leading to nasty crashes as reported in issue #59. If we learn
52
+ // that the application has been killed, all watchers are stopped and the
53
+ // service is terminated immediately.
54
+ @Override
55
+ public boolean onUnbind(Intent intent) {
56
+ for (Watcher watcher : watchers) {
57
+ watcher.client.removeLocationUpdates(watcher.locationCallback);
58
+ }
59
+ watchers = new HashSet<Watcher>();
60
+ stopSelf();
61
+ return false;
62
+ }
63
+
64
+ Notification getNotification() {
65
+ for (Watcher watcher : watchers) {
66
+ if (watcher.backgroundNotification != null) {
67
+ return watcher.backgroundNotification;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // Handles requests from the activity.
74
+ public class LocalBinder extends Binder {
75
+ void addWatcher(
76
+ final String id,
77
+ Notification backgroundNotification,
78
+ float distanceFilter,
79
+ int accuracy
80
+ ) {
81
+ FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(
82
+ BackgroundGeolocationService.this
83
+ );
84
+
85
+ // Map accuracy enum to Android Priority
86
+ int priority;
87
+ long interval;
88
+
89
+ switch (accuracy) {
90
+ case 100: // HIGH
91
+ priority = Priority.PRIORITY_HIGH_ACCURACY;
92
+ interval = 5000; // 5 seconds
93
+ break;
94
+ case 102: // BALANCED (Default)
95
+ priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY;
96
+ interval = 10000; // 10 seconds
97
+ break;
98
+ case 104: // LOW
99
+ priority = Priority.PRIORITY_LOW_POWER;
100
+ interval = 30000; // 30 seconds
101
+ break;
102
+ case 105: // PASSIVE
103
+ priority = Priority.PRIORITY_PASSIVE;
104
+ interval = 60000; // 60 seconds
105
+ break;
106
+ default:
107
+ priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY;
108
+ interval = 10000;
109
+ }
110
+
111
+ // Use modern LocationRequest.Builder API
112
+ LocationRequest locationRequest = new LocationRequest.Builder(priority, interval)
113
+ .setMinUpdateDistanceMeters(distanceFilter)
114
+ .setWaitForAccurateLocation(false)
115
+ .setMaxUpdateDelayMillis(interval)
116
+ .build();
117
+
118
+ LocationCallback callback = new LocationCallback(){
119
+ @Override
120
+ public void onLocationResult(LocationResult locationResult) {
121
+ Location location = locationResult.getLastLocation();
122
+ Intent intent = new Intent(ACTION_BROADCAST);
123
+ intent.putExtra("location", location);
124
+ intent.putExtra("id", id);
125
+ LocalBroadcastManager.getInstance(
126
+ getApplicationContext()
127
+ ).sendBroadcast(intent);
128
+ }
129
+ @Override
130
+ public void onLocationAvailability(LocationAvailability availability) {
131
+ if (!availability.isLocationAvailable()) {
132
+ Logger.debug("Location not available");
133
+ }
134
+ }
135
+ };
136
+
137
+ Watcher watcher = new Watcher();
138
+ watcher.id = id;
139
+ watcher.client = client;
140
+ watcher.locationRequest = locationRequest;
141
+ watcher.locationCallback = callback;
142
+ watcher.backgroundNotification = backgroundNotification;
143
+ watchers.add(watcher);
144
+
145
+ // According to Android Studio, this method can throw a Security Exception if
146
+ // permissions are not yet granted. Rather than check the permissions, which is fiddly,
147
+ // we simply ignore the exception.
148
+ try {
149
+ watcher.client.requestLocationUpdates(
150
+ watcher.locationRequest,
151
+ watcher.locationCallback,
152
+ null
153
+ );
154
+ } catch (SecurityException ignore) {}
155
+
156
+ // Promote the service to the foreground if necessary.
157
+ // Ideally we would only call 'startForeground' if the service is not already
158
+ // foregrounded. Unfortunately, 'getForegroundServiceType' was only introduced
159
+ // in API level 29 and seems to behave weirdly, as reported in #120. However,
160
+ // it appears that 'startForeground' is idempotent, so we just call it repeatedly
161
+ // each time a background watcher is added.
162
+ if (backgroundNotification != null) {
163
+ try {
164
+ // This method has been known to fail due to weird
165
+ // permission bugs, so we prevent any exceptions from
166
+ // crashing the app. See issue #86.
167
+ startForeground(NOTIFICATION_ID, backgroundNotification);
168
+ } catch (Exception exception) {
169
+ Logger.error("Failed to foreground service", exception);
170
+ }
171
+ }
172
+ }
173
+
174
+ void removeWatcher(String id) {
175
+ for (Watcher watcher : watchers) {
176
+ if (watcher.id.equals(id)) {
177
+ watcher.client.removeLocationUpdates(watcher.locationCallback);
178
+ watchers.remove(watcher);
179
+ if (getNotification() == null) {
180
+ stopForeground(true);
181
+ }
182
+ return;
183
+ }
184
+ }
185
+ }
186
+
187
+ void onPermissionsGranted() {
188
+ // If permissions were granted while the app was in the background, for example in
189
+ // the Settings app, the watchers need restarting.
190
+ for (Watcher watcher : watchers) {
191
+ watcher.client.removeLocationUpdates(watcher.locationCallback);
192
+ watcher.client.requestLocationUpdates(
193
+ watcher.locationRequest,
194
+ watcher.locationCallback,
195
+ null
196
+ );
197
+ }
198
+ }
199
+
200
+ void stopService() {
201
+ BackgroundGeolocationService.this.stopSelf();
202
+ }
203
+ }
204
+ }