diff --git a/README.md b/README.md
index 6a37550..8ce82cc 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
The Geolocation API 📍 module for React Native that extends the [Geolocation web spec](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation).
+**This fork adds support for background location updates, allowing your app to receive location updates even when it's not in the foreground.**
+
Supports TurboModules ⚡️ and legacy React Native architecture.
Fully compatible with TypeScript.
@@ -32,11 +34,15 @@ Supports modern [Play Services Location API](https://developers.google.com/andro
## Getting started
-`yarn add @react-native-community/geolocation`
+This is a fork of the original `@react-native-community/geolocation` package.
+
+To install this forked version:
+
+`yarn add https://github.com/quan118/react-native-geolocation`
or
-`npm install @react-native-community/geolocation --save`
+`npm install https://github.com/quan118/react-native-geolocationn --save`
## Configuration and Permissions
@@ -70,6 +76,15 @@ or
``
+
+To enable background location updates, add the following permissions:
+
+```xml
+
+
+
+```
+
Android API >= 18 Positions will also contain a `mocked` boolean to indicate if position was created from a mock provider.
@@ -181,7 +196,7 @@ Supported options:
* `skipPermissionRequests` (boolean) - Defaults to `false`. If `true`, you must request permissions before using Geolocation APIs.
* `authorizationLevel` (string, iOS-only) - Either `"whenInUse"`, `"always"`, or `"auto"`. Changes whether the user will be asked to give "always" or "when in use" location services permission. Any other value or `auto` will use the default behaviour, where the permission level is based on the contents of your `Info.plist`.
-* `enableBackgroundLocationUpdates` (boolean, iOS-only) - When using `skipPermissionRequests`, toggle wether to automatically enableBackgroundLocationUpdates. Defaults to true.
+* `enableBackgroundLocationUpdates` (boolean) - When using `skipPermissionRequests`, toggle wether to automatically enableBackgroundLocationUpdates. Defaults to true.
* `locationProvider` (string, Android-only) - Either `"playServices"`, `"android"`, or `"auto"`. Determines wether to use `Google’s Location Services API` or `Android’s Location API`. The `"auto"` mode defaults to `android`, and falls back to Android's Location API if play services aren't available.
---
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 868bb8b..6a1e39b 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,6 +1,12 @@
-
+
+
+
\ No newline at end of file
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java
index 55d2619..eea43c3 100644
--- a/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java
@@ -1,6 +1,5 @@
package com.reactnativecommunity.geolocation;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -9,25 +8,29 @@
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
+import android.os.Build;
import android.os.Handler;
-
+import android.os.IBinder;
+import android.content.ServiceConnection;
+import android.content.Intent;
+import android.content.ComponentName;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
-import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SystemClock;
import com.facebook.react.modules.core.DeviceEventManagerModule;
-import javax.annotation.Nullable;
-
@SuppressLint("MissingPermission")
public class AndroidLocationManager extends BaseLocationManager {
- private @Nullable
- String mWatchedProvider;
+ private boolean mIsServiceRunning = false;
+ private @Nullable LocationHandler mLocationHandler;
private final LocationListener mLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
@@ -53,226 +56,339 @@ public void onProviderDisabled(String provider) {
}
};
- protected AndroidLocationManager(ReactApplicationContext reactContext) {
- super(reactContext);
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ LocationService.LocalBinder binder = (LocationService.LocalBinder) service;
+ mLocationHandler = binder.getService();
+ final AndroidLocationManager.LocationHandlerImpl locationHandlerImpl =
+ new AndroidLocationManager.LocationHandlerImpl(
+ (LocationService)mLocationHandler,
+ mLocationListener,
+ AndroidLocationManager.this);
+ ((LocationService) mLocationHandler).setLocationHandler(locationHandlerImpl);
+ mIsServiceRunning = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ mLocationHandler = null;
+ mIsServiceRunning = false;
+ }
+ };
+
+ protected AndroidLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) {
+ super(reactContext, enableBackgroundLocationUpdates);
+ Log.d("AndroidLocationManager", "enableBackgroundLocationUpdates:" + enableBackgroundLocationUpdates);
+ if (enableBackgroundLocationUpdates) {
+ startService();
+ } else {
+ mLocationHandler = new LocationHandlerImpl(reactContext, mLocationListener, this);
+ }
}
public void getCurrentLocationData(
ReadableMap options,
final Callback success,
Callback error) {
- AndroidLocationManager.LocationOptions locationOptions = AndroidLocationManager.LocationOptions.fromReactMap(options);
+ LocationOptions locationOptions = LocationOptions.fromReactMap(options);
try {
- LocationManager locationManager =
- (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
- String provider = getValidProvider(locationManager, locationOptions.highAccuracy);
- if (provider == null) {
- error.invoke(
- PositionError.buildError(
- PositionError.POSITION_UNAVAILABLE, "No location provider available."));
- return;
- }
- Location location = locationManager.getLastKnownLocation(provider);
- if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) {
- success.invoke(locationToMap(location));
- return;
+ if (mIsServiceRunning) {
+ if (!mEnableBackgroundLocationUpdates) {
+ stopService();
+ mLocationHandler = new LocationHandlerImpl(mReactContext, mLocationListener, this);
+ } else if (mLocationHandler == null) {
+ mLocationHandler = new LocationHandlerImpl(mReactContext, mLocationListener, this);
+ }
+ } else {
+ if (mEnableBackgroundLocationUpdates) {
+ startService();
+ }
}
- new AndroidLocationManager.SingleUpdateRequest(locationManager, provider, locationOptions.timeout, success, error)
- .invoke(location);
+ mLocationHandler.getCurrentLocation(locationOptions, success, error);
} catch (SecurityException e) {
throw e;
}
}
+ @Override
public void startObserving(ReadableMap options) {
- if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) {
+ LocationOptions locationOptions = LocationOptions.fromReactMap(options);
+ mLocationHandler.startLocationUpdates(locationOptions);
+ }
+
+ @Override
+ public void stopObserving() {
+// LocationManager locationManager =
+// (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
+// locationManager.removeUpdates(mLocationListener);
+// mWatchedProvider = null;
+ mLocationHandler.stopLocationUpdates();
+ }
+
+ private void startService() {
+ if (mIsServiceRunning) {
return;
}
- LocationOptions locationOptions = LocationOptions.fromReactMap(options);
- try {
- LocationManager locationManager =
- (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
- String provider = getValidProvider(locationManager, locationOptions.highAccuracy);
- if (provider == null) {
- emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available.");
- return;
- }
- if (!provider.equals(mWatchedProvider)) {
- locationManager.removeUpdates(mLocationListener);
- locationManager.requestLocationUpdates(
- provider,
- 1000,
- locationOptions.distanceFilter,
- mLocationListener);
- }
- mWatchedProvider = provider;
- } catch (SecurityException e) {
- throw e;
+ Intent intent = new Intent(mReactContext, LocationService.class);
+
+ // Start the LocationService as a foreground service
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mReactContext.startForegroundService(intent);
+ } else {
+ mReactContext.startService(intent);
}
+
+ mReactContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
- public void stopObserving() {
- LocationManager locationManager =
- (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
- locationManager.removeUpdates(mLocationListener);
- mWatchedProvider = null;
+ @Override
+ public void stopService() {
+ if (!mIsServiceRunning) {
+ return;
+ }
+
+ mReactContext.unbindService(mConnection);
+ Intent intent = new Intent(mReactContext, LocationService.class);
+ mReactContext.stopService(intent);
}
- @Nullable
- private String getValidProvider(LocationManager locationManager, boolean highAccuracy) {
- String provider =
- highAccuracy ? LocationManager.GPS_PROVIDER : LocationManager.NETWORK_PROVIDER;
- if (!locationManager.isProviderEnabled(provider)) {
- provider = provider.equals(LocationManager.GPS_PROVIDER)
- ? LocationManager.NETWORK_PROVIDER
- : LocationManager.GPS_PROVIDER;
- if (!locationManager.isProviderEnabled(provider)) {
- return null;
+ private static class LocationHandlerImpl implements LocationHandler {
+ private static final String TAG = "AndroidLocationHandlerImpl";
+ private final LocationListener mLocationListener;
+ private final Context mContext;
+ private @Nullable String mWatchedProvider;
+ private final EventEmitter mEventEmitter;
+
+ public LocationHandlerImpl(Context context, LocationListener locationListener, EventEmitter errorEmitter) {
+ mContext = context;
+ mLocationListener = locationListener;
+ mEventEmitter = errorEmitter;
+ }
+
+ public void startLocationUpdates(LocationOptions options) {
+ Log.i(TAG, "startLocationUpdates");
+ if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) {
+ return;
+ }
+
+ try {
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ String provider = getValidProvider(locationManager, options.highAccuracy);
+ if (provider == null) {
+ // handle error
+ // emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available.");
+ mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available.");
+ return;
+ }
+ if (!provider.equals(mWatchedProvider)) {
+ locationManager.removeUpdates(mLocationListener);
+ locationManager.requestLocationUpdates(
+ provider,
+ 1000,
+ options.distanceFilter,
+ mLocationListener);
+ }
+ mWatchedProvider = provider;
+ } catch (SecurityException e) {
+ throw e;
}
}
- // If it's an enabled provider, but we don't have permissions, ignore it
- int finePermission = ContextCompat.checkSelfPermission(mReactContext, android.Manifest.permission.ACCESS_FINE_LOCATION);
- int coarsePermission = ContextCompat.checkSelfPermission(mReactContext, android.Manifest.permission.ACCESS_COARSE_LOCATION);
- if ((provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) ||
- (provider.equals(LocationManager.NETWORK_PROVIDER) && coarsePermission != PackageManager.PERMISSION_GRANTED)) {
- return null;
+
+ public void stopLocationUpdates() {
+ Log.i(TAG, "stopLocationUpdates");
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ locationManager.removeUpdates(mLocationListener);
+ mWatchedProvider = null;
}
- return provider;
- }
- private static class SingleUpdateRequest {
-
- private final Callback mSuccess;
- private final Callback mError;
- private final LocationManager mLocationManager;
- private final String mProvider;
- private final long mTimeout;
- private Location mOldLocation;
- private final Handler mHandler = new Handler();
- private final Runnable mTimeoutRunnable = new Runnable() {
- @Override
- public void run() {
- synchronized (SingleUpdateRequest.this) {
- if (!mTriggered) {
- mError.invoke(PositionError.buildError(PositionError.TIMEOUT, "Location request timed out"));
- mLocationManager.removeUpdates(mLocationListener);
- FLog.i(ReactConstants.TAG, "LocationModule: Location request timed out");
- mTriggered = true;
- }
+ public void getCurrentLocation(LocationOptions options,
+ final Callback success,
+ Callback error) {
+ Log.i(TAG, "getCurrentLocation");
+
+ try {
+ LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+
+ String provider = getValidProvider(locationManager, options.highAccuracy);
+ if (provider == null) {
+ error.invoke(
+ PositionError.buildError(
+ PositionError.POSITION_UNAVAILABLE, "No location provider available."));
+ return;
}
- }
- };
- private final LocationListener mLocationListener = new LocationListener() {
- @Override
- public void onLocationChanged(Location location) {
- synchronized (SingleUpdateRequest.this) {
- if (!mTriggered && isBetterLocation(location, mOldLocation)) {
- mSuccess.invoke(locationToMap(location));
- mHandler.removeCallbacks(mTimeoutRunnable);
- mTriggered = true;
- mLocationManager.removeUpdates(mLocationListener);
- }
- mOldLocation = location;
+ Location location = locationManager.getLastKnownLocation(provider);
+ if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < options.maximumAge) {
+ success.invoke(locationToMap(location));
+ return;
}
- }
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) {
+ new LocationHandlerImpl.SingleUpdateRequest(locationManager, provider, options.timeout, success, error).invoke(location);
+ } catch (SecurityException e) {
+ throw e;
}
+ }
+
+ @Nullable
+ private String getValidProvider(LocationManager locationManager, boolean highAccuracy) {
+ String provider = highAccuracy ? LocationManager.GPS_PROVIDER : LocationManager.NETWORK_PROVIDER;
+ if (!locationManager.isProviderEnabled(provider)) {
+ provider = provider.equals(LocationManager.GPS_PROVIDER)
+ ? LocationManager.NETWORK_PROVIDER
+ : LocationManager.GPS_PROVIDER;
- @Override
- public void onProviderEnabled(String provider) {
+ if (!locationManager.isProviderEnabled(provider)) {
+ return null;
+ }
}
- @Override
- public void onProviderDisabled(String provider) {
+ // If it's an enabled provider, but we don't have permissions, ignore it
+ int finePermission = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.ACCESS_FINE_LOCATION);
+ int coarsePermission = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.ACCESS_COARSE_LOCATION);
+
+ if ((provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) ||
+ (provider.equals(LocationManager.NETWORK_PROVIDER) && coarsePermission != PackageManager.PERMISSION_GRANTED)) {
+ return null;
}
- };
- private boolean mTriggered;
-
- private SingleUpdateRequest(
- LocationManager locationManager,
- String provider,
- long timeout,
- Callback success,
- Callback error) {
- mLocationManager = locationManager;
- mProvider = provider;
- mTimeout = timeout;
- mSuccess = success;
- mError = error;
- }
- public void invoke(Location location) {
- mOldLocation = location;
- mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener);
- mHandler.postDelayed(mTimeoutRunnable, mTimeout);
+ return provider;
}
- private static final int TWO_MINUTES = 1000 * 60 * 2;
-
- /**
- * Determines whether one Location reading is better than the current Location fix
- * taken from Android Examples https://developer.android.com/guide/topics/location/strategies.html
- *
- * @param location The new Location that you want to evaluate
- * @param currentBestLocation The current Location fix, to which you want to compare the new one
- */
- private boolean isBetterLocation(Location location, Location currentBestLocation) {
- if (currentBestLocation == null) {
- // A new location is always better than no location
- return true;
- }
+ private static class SingleUpdateRequest {
+ private final Callback mSuccess;
+ private final Callback mError;
+ private final LocationManager mLocationManager;
+ private final String mProvider;
+ private final long mTimeout;
+ private Location mOldLocation;
+ private final Handler mHandler = new Handler();
+ private final Runnable mTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (SingleUpdateRequest.this) {
+ if (!mTriggered) {
+ mError.invoke(PositionError.TIMEOUT, "Location request timed out");
+ mLocationManager.removeUpdates(mLocationListener);
+ mTriggered = true;
+ }
+ }
+ }
+ };
+ private final LocationListener mLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(@NonNull Location location) {
+ synchronized (SingleUpdateRequest.this) {
+ if (!mTriggered && isBetterLocation(location, mOldLocation)) {
+ mSuccess.invoke(location);
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ mTriggered = true;
+ mLocationManager.removeUpdates(mLocationListener);
+ }
+
+ mOldLocation = location;
+ }
+ }
- // Check whether the new location fix is newer or older
- long timeDelta = location.getTime() - currentBestLocation.getTime();
- boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
- boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
- boolean isNewer = timeDelta > 0;
-
- // If it's been more than two minutes since the current location, use the new location
- // because the user has likely moved
- if (isSignificantlyNewer) {
- return true;
- // If the new location is more than two minutes older, it must be worse
- } else if (isSignificantlyOlder) {
- return false;
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
+ };
+ private boolean mTriggered;
+
+ private SingleUpdateRequest(
+ LocationManager locationManager,
+ String provider,
+ long timeout,
+ Callback success,
+ Callback error) {
+ mLocationManager = locationManager;
+ mProvider = provider;
+ mTimeout = timeout;
+ mSuccess = success;
+ mError = error;
}
- // Check whether the new location fix is more or less accurate
- int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
- boolean isLessAccurate = accuracyDelta > 0;
- boolean isMoreAccurate = accuracyDelta < 0;
- boolean isSignificantlyLessAccurate = accuracyDelta > 200;
-
- // Check if the old and new location are from the same provider
- boolean isFromSameProvider = isSameProvider(location.getProvider(),
- currentBestLocation.getProvider());
-
- // Determine location quality using a combination of timeliness and accuracy
- if (isMoreAccurate) {
- return true;
- } else if (isNewer && !isLessAccurate) {
- return true;
- } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
- return true;
+ public void invoke(Location location) {
+ mOldLocation = location;
+ mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener);
+ mHandler.postDelayed(mTimeoutRunnable, mTimeout);
}
- return false;
- }
+ private static final int TWO_MINUTES = 1000 * 60 * 2;
+
+ /**
+ * Determines whether one Location reading is better than the current Location fix
+ * taken from Android Examples https://developer.android.com/guide/topics/location/strategies.html
+ *
+ * @param location The new Location that you want to evaluate
+ * @param currentBestLocation The current Location fix, to which you want to compare the new one
+ */
+ private boolean isBetterLocation(Location location, Location currentBestLocation) {
+ if (currentBestLocation == null) {
+ // A new location is always better than no location
+ return true;
+ }
+
+ // Check whether the new location fix is newer or older
+ long timeDelta = location.getTime() - currentBestLocation.getTime();
+ boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
+ boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
+ boolean isNewer = timeDelta > 0;
+
+ // If it's been more than two minutes since the current location, use the new location
+ // because the user has likely moved
+ if (isSignificantlyNewer) {
+ return true;
+ // If the new location is more than two minutes older, it must be worse
+ } else if (isSignificantlyOlder) {
+ return false;
+ }
+
+ // Check whether the new location fix is more or less accurate
+ int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
+ boolean isLessAccurate = accuracyDelta > 0;
+ boolean isMoreAccurate = accuracyDelta < 0;
+ boolean isSignificantlyLessAccurate = accuracyDelta > 200;
+
+ // Check if the old and new location are from the same provider
+ boolean isFromSameProvider = isSameProvider(location.getProvider(),
+ currentBestLocation.getProvider());
+
+ // Determine location quality using a combination of timeliness and accuracy
+ if (isMoreAccurate) {
+ return true;
+ } else if (isNewer && !isLessAccurate) {
+ return true;
+ } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
+ return true;
+ }
- /**
- * Checks whether two providers are the same
- */
- private boolean isSameProvider(String provider1, String provider2) {
- if (provider1 == null) {
- return provider2 == null;
+ return false;
+ }
+
+ /**
+ * Checks whether two providers are the same
+ */
+ private boolean isSameProvider(String provider1, String provider2) {
+ if (provider1 == null) {
+ return provider2 == null;
+ }
+ return provider1.equals(provider2);
}
- return provider1.equals(provider2);
}
}
}
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java
index 02d2e85..8947e99 100644
--- a/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java
@@ -11,12 +11,13 @@
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
-public abstract class BaseLocationManager {
- protected static final float RCT_DEFAULT_LOCATION_ACCURACY = 100;
+public abstract class BaseLocationManager implements EventEmitter {
public ReactApplicationContext mReactContext;
+ public boolean mEnableBackgroundLocationUpdates;
- protected BaseLocationManager(ReactApplicationContext reactContext) {
+ protected BaseLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) {
mReactContext = reactContext;
+ mEnableBackgroundLocationUpdates = enableBackgroundLocationUpdates;
}
protected static WritableMap locationToMap(Location location) {
@@ -70,55 +71,18 @@ protected static void putIntoMap(WritableMap map, String key, Object value) {
}
}
- protected void emitError(int code, String message) {
+ public void emitError(int code, String message) {
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("geolocationError", PositionError.buildError(code, message));
}
+ public void emit(String message, Object o) {
+ mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit(message, o);
+ }
+
abstract public void getCurrentLocationData(ReadableMap options, final Callback success, Callback error);
abstract public void startObserving(ReadableMap options);
abstract public void stopObserving();
-
- protected static class LocationOptions {
- protected final int interval;
- protected final int fastestInterval;
- protected final long timeout;
- protected final double maximumAge;
- protected final boolean highAccuracy;
- protected final float distanceFilter;
-
- private LocationOptions(
- int interval,
- int fastestInterval,
- long timeout,
- double maximumAge,
- boolean highAccuracy,
- float distanceFilter) {
- this.interval = interval;
- this.fastestInterval = fastestInterval;
- this.timeout = timeout;
- this.maximumAge = maximumAge;
- this.highAccuracy = highAccuracy;
- this.distanceFilter = distanceFilter;
- }
-
- protected static LocationOptions fromReactMap(ReadableMap map) {
- // precision might be dropped on timeout (double -> int conversion), but that's OK
- int interval =
- map.hasKey("interval") ? map.getInt("interval") : 10000;
- int fastestInterval =
- map.hasKey("fastestInterval") ? map.getInt("fastestInterval") : -1;
- long timeout =
- map.hasKey("timeout") ? (long) map.getDouble("timeout") : 1000 * 60 * 10;
- double maximumAge =
- map.hasKey("maximumAge") ? map.getDouble("maximumAge") : Double.POSITIVE_INFINITY;
- boolean highAccuracy =
- map.hasKey("enableHighAccuracy") && map.getBoolean("enableHighAccuracy");
- float distanceFilter = map.hasKey("distanceFilter") ?
- (float) map.getDouble("distanceFilter") :
- RCT_DEFAULT_LOCATION_ACCURACY;
-
- return new LocationOptions(interval, fastestInterval, timeout, maximumAge, highAccuracy, distanceFilter);
- }
- }
+ abstract public void stopService();
}
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java b/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java
new file mode 100644
index 0000000..dfca465
--- /dev/null
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java
@@ -0,0 +1,6 @@
+package com.reactnativecommunity.geolocation;
+
+public interface EventEmitter {
+ void emitError(int code, String message);
+ void emit(String message, Object obj);
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java b/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java
index 5b42ba4..3e87a5e 100644
--- a/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java
@@ -35,7 +35,7 @@ public class GeolocationModule extends ReactContextBaseJavaModule {
public GeolocationModule(ReactApplicationContext reactContext) {
super(reactContext);
mConfiguration = Configuration.getDefault();
- mLocationManager = new AndroidLocationManager(reactContext);
+ mLocationManager = new AndroidLocationManager(reactContext, mConfiguration.enableBackgroundLocationUpdates);
}
@Override
@@ -44,20 +44,25 @@ public String getName() {
}
public void setConfiguration(ReadableMap config) {
- mConfiguration = Configuration.fromReactMap(config);
- onConfigurationChange(mConfiguration);
+ Configuration configuration = Configuration.fromReactMap(config);
+ onConfigurationChange(configuration);
}
private void onConfigurationChange(Configuration config) {
ReactApplicationContext reactContext = mLocationManager.mReactContext;
- if (Objects.equals(config.locationProvider, "android") && mLocationManager instanceof PlayServicesLocationManager) {
- mLocationManager = new AndroidLocationManager(reactContext);
- } else if (Objects.equals(config.locationProvider, "playServices") && mLocationManager instanceof AndroidLocationManager) {
+ if (Objects.equals(config.locationProvider, "android") && mLocationManager instanceof PlayServicesLocationManager
+ || config.enableBackgroundLocationUpdates != mConfiguration.enableBackgroundLocationUpdates) {
+ mLocationManager.stopService();
+ mLocationManager = new AndroidLocationManager(reactContext, config.enableBackgroundLocationUpdates);
+ } else if (Objects.equals(config.locationProvider, "playServices") && mLocationManager instanceof AndroidLocationManager
+ || config.enableBackgroundLocationUpdates != mConfiguration.enableBackgroundLocationUpdates) {
GoogleApiAvailability availability = new GoogleApiAvailability();
if (availability.isGooglePlayServicesAvailable(reactContext.getApplicationContext()) == ConnectionResult.SUCCESS) {
- mLocationManager = new PlayServicesLocationManager(reactContext);
+ mLocationManager.stopService();
+ mLocationManager = new PlayServicesLocationManager(reactContext, config.enableBackgroundLocationUpdates);
}
}
+ mConfiguration = config;
}
/**
@@ -172,14 +177,16 @@ private void emitLocationPermissionMissing(SecurityException e) {
private static class Configuration {
String locationProvider;
Boolean skipPermissionRequests;
+ Boolean enableBackgroundLocationUpdates;
- private Configuration(String locationProvider, boolean skipPermissionRequests) {
+ private Configuration(String locationProvider, boolean skipPermissionRequests, boolean enableBackgroundLocationUpdates) {
this.locationProvider = locationProvider;
this.skipPermissionRequests = skipPermissionRequests;
+ this.enableBackgroundLocationUpdates = enableBackgroundLocationUpdates;
}
protected static Configuration getDefault() {
- return new Configuration("auto", false);
+ return new Configuration("auto", false, false);
}
protected static Configuration fromReactMap(ReadableMap map) {
@@ -187,7 +194,8 @@ protected static Configuration fromReactMap(ReadableMap map) {
map.hasKey("locationProvider") ? map.getString("locationProvider") : "auto";
boolean skipPermissionRequests =
map.hasKey("skipPermissionRequests") ? map.getBoolean("skipPermissionRequests") : false;
- return new Configuration(locationProvider, skipPermissionRequests);
+ boolean enableBackgroundLocationUpdates = map.hasKey("enableBackgroundLocationUpdates") ? map.getBoolean("enableBackgroundLocationUpdates") : false;
+ return new Configuration(locationProvider, skipPermissionRequests, enableBackgroundLocationUpdates);
}
}
}
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java
new file mode 100644
index 0000000..f7f3931
--- /dev/null
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java
@@ -0,0 +1,14 @@
+package com.reactnativecommunity.geolocation;
+
+import com.facebook.react.bridge.Callback;
+
+public interface LocationHandler {
+ void getCurrentLocation(LocationOptions options, Callback success, Callback error);
+
+ void startLocationUpdates(LocationOptions options);
+
+ void stopLocationUpdates();
+}
+
+
+
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java
new file mode 100644
index 0000000..3e821b5
--- /dev/null
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java
@@ -0,0 +1,47 @@
+package com.reactnativecommunity.geolocation;
+
+import com.facebook.react.bridge.ReadableMap;
+
+public class LocationOptions {
+ protected static final float RCT_DEFAULT_LOCATION_ACCURACY = 100;
+ protected final int interval;
+ protected final int fastestInterval;
+ protected final long timeout;
+ protected final double maximumAge;
+ protected final boolean highAccuracy;
+ protected final float distanceFilter;
+
+ private LocationOptions(
+ int interval,
+ int fastestInterval,
+ long timeout,
+ double maximumAge,
+ boolean highAccuracy,
+ float distanceFilter) {
+ this.interval = interval;
+ this.fastestInterval = fastestInterval;
+ this.timeout = timeout;
+ this.maximumAge = maximumAge;
+ this.highAccuracy = highAccuracy;
+ this.distanceFilter = distanceFilter;
+ }
+
+ protected static LocationOptions fromReactMap(ReadableMap map) {
+ // precision might be dropped on timeout (double -> int conversion), but that's OK
+ int interval =
+ map.hasKey("interval") ? map.getInt("interval") : 10000;
+ int fastestInterval =
+ map.hasKey("fastestInterval") ? map.getInt("fastestInterval") : -1;
+ long timeout =
+ map.hasKey("timeout") ? (long) map.getDouble("timeout") : 1000 * 60 * 10;
+ double maximumAge =
+ map.hasKey("maximumAge") ? map.getDouble("maximumAge") : Double.POSITIVE_INFINITY;
+ boolean highAccuracy =
+ map.hasKey("enableHighAccuracy") && map.getBoolean("enableHighAccuracy");
+ float distanceFilter = map.hasKey("distanceFilter") ?
+ (float) map.getDouble("distanceFilter") :
+ RCT_DEFAULT_LOCATION_ACCURACY;
+
+ return new LocationOptions(interval, fastestInterval, timeout, maximumAge, highAccuracy, distanceFilter);
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java
new file mode 100644
index 0000000..60c89f2
--- /dev/null
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java
@@ -0,0 +1,98 @@
+package com.reactnativecommunity.geolocation;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import androidx.core.app.NotificationCompat;
+import android.util.Log;
+
+import com.facebook.react.bridge.Callback;
+
+public class LocationService extends Service implements LocationHandler {
+ private static final String TAG = "LocationService";
+ private final IBinder binder = new LocalBinder();
+ private static final String CHANNEL_ID = "com.reactnativecommunity.geolocation.location_service";
+ private static final int NOTIFICATION_ID = 1;
+ private LocationHandler mLocationHandler;
+
+ public class LocalBinder extends Binder {
+ LocationService getService() {
+ return LocationService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(TAG, "onCreate");
+ createNotificationChannel();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.i(TAG, "onDestroy");
+ stopLocationUpdates(); // Clean up resources
+ }
+
+ // will be called when `startService(Intent)` being called
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "onStartCommand");
+ Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Location Service")
+ .setContentText("Running")
+ .setSmallIcon(android.R.drawable.ic_menu_mylocation)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .build();
+
+ startForeground(NOTIFICATION_ID, notification);
+
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ public void setLocationHandler(LocationHandler locationHandler) {
+ this.mLocationHandler = locationHandler;
+ }
+
+ public void startLocationUpdates(LocationOptions options) {
+ Log.i(TAG, "startLocationUpdates");
+ mLocationHandler.startLocationUpdates(options);
+ }
+
+ public void stopLocationUpdates() {
+ Log.i(TAG, "stopLocationUpdates");
+ mLocationHandler.stopLocationUpdates();
+ }
+
+ public void getCurrentLocation(LocationOptions options, final Callback success,
+ Callback error) {
+ Log.i(TAG, "getCurrentLocation");
+ this.mLocationHandler.getCurrentLocation(options, success, error);
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // 1. create a channel
+ NotificationChannel serviceChannel = new NotificationChannel(
+ CHANNEL_ID,
+ "GDL Location Service Channel",
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+
+ // 2. register the notification channel
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ manager.createNotificationChannel(serviceChannel);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java
index 423f7a5..a65758c 100644
--- a/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java
+++ b/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java
@@ -2,7 +2,12 @@
import android.annotation.SuppressLint;
import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.location.Location;
+import android.os.Build;
+import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.content.Context;
@@ -26,163 +31,112 @@
import com.google.android.gms.location.Priority;
import com.google.android.gms.location.SettingsClient;
+import javax.annotation.Nullable;
+
@SuppressLint("MissingPermission")
public class PlayServicesLocationManager extends BaseLocationManager {
- private FusedLocationProviderClient mFusedLocationClient;
- private LocationCallback mLocationCallback;
- private LocationCallback mSingleLocationCallback;
- private SettingsClient mLocationServicesSettingsClient;
-
- protected PlayServicesLocationManager(ReactApplicationContext reactContext) {
- super(reactContext);
- mFusedLocationClient = LocationServices.getFusedLocationProviderClient(reactContext);
- mLocationServicesSettingsClient = LocationServices.getSettingsClient(reactContext);
- }
+ private boolean mIsServiceRunning = false;
+ private @Nullable LocationHandler mLocationHandler;
- @Override
- public void getCurrentLocationData(ReadableMap options, Callback success, Callback error) {
- AndroidLocationManager.LocationOptions locationOptions = AndroidLocationManager.LocationOptions.fromReactMap(options);
-
- Activity currentActivity = mReactContext.getCurrentActivity();
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ LocationService.LocalBinder binder = (LocationService.LocalBinder) service;
+ mLocationHandler = binder.getService();
+ final PlayServicesLocationManager.LocationHandlerImpl locationHandlerImpl =
+ new PlayServicesLocationManager.LocationHandlerImpl(
+ (LocationService) mLocationHandler,
+ mReactContext.getCurrentActivity(),
+ PlayServicesLocationManager.this);
+ ((LocationService) mLocationHandler).setLocationHandler(locationHandlerImpl);
+ mIsServiceRunning = true;
+ }
- if (currentActivity == null) {
- mSingleLocationCallback = createSingleLocationCallback(success, error);
- checkLocationSettings(options, mSingleLocationCallback, error);
- return;
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ mLocationHandler = null;
+ mIsServiceRunning = false;
}
+ };
- try {
- mFusedLocationClient.getLastLocation()
- .addOnSuccessListener(currentActivity, location -> {
- if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) {
- success.invoke(locationToMap(location));
- } else {
- mSingleLocationCallback = createSingleLocationCallback(success, error);
- checkLocationSettings(options, mSingleLocationCallback, error);
- }
- });
- } catch (SecurityException e) {
- throw e;
+ protected PlayServicesLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) {
+ super(reactContext, enableBackgroundLocationUpdates);
+ Log.d("PlayServicesLocationManager", "enableBackgroundLocationUpdates:" + enableBackgroundLocationUpdates);
+
+ if (enableBackgroundLocationUpdates) {
+ startService();
+ } else {
+ mLocationHandler = new LocationHandlerImpl(reactContext, mReactContext.getCurrentActivity(), this);
}
}
@Override
- public void startObserving(ReadableMap options) {
- mLocationCallback = new LocationCallback() {
- @Override
- public void onLocationResult(LocationResult locationResult) {
- if (locationResult == null) {
- emitError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/observer).");
- return;
- }
-
- mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("geolocationDidChange", locationToMap(locationResult.getLastLocation()));
- }
+ public void getCurrentLocationData(ReadableMap options, Callback success, Callback error) {
+ LocationOptions locationOptions = LocationOptions.fromReactMap(options);
- @Override
- public void onLocationAvailability(LocationAvailability locationAvailability) {
- if (!locationAvailability.isLocationAvailable()) {
- emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider).");
+ try {
+ if (mIsServiceRunning) {
+ if (!mEnableBackgroundLocationUpdates) {
+ stopService();
+ mLocationHandler = new LocationHandlerImpl(mReactContext, mReactContext.getCurrentActivity(), this);
+ } else if (mLocationHandler == null) {
+ mLocationHandler = new LocationHandlerImpl(mReactContext, mReactContext.getCurrentActivity(), this);
+ }
+ } else {
+ if (mEnableBackgroundLocationUpdates) {
+ startService();
}
}
- };
- checkLocationSettings(options, mLocationCallback, null);
- }
-
- @Override
- public void stopObserving() {
- if(mLocationCallback == null) {
- return;
+ mLocationHandler.getCurrentLocation(locationOptions, success, error);
+ } catch (SecurityException e) {
+ throw e;
}
- mFusedLocationClient.removeLocationUpdates(mLocationCallback);
}
- private void checkLocationSettings(ReadableMap options, LocationCallback locationCallback, Callback error) {
+ @Override
+ public void startObserving(ReadableMap options) {
LocationOptions locationOptions = LocationOptions.fromReactMap(options);
- LocationRequest.Builder requestBuilder = new LocationRequest.Builder(locationOptions.interval);
- requestBuilder.setPriority(locationOptions.highAccuracy ? Priority.PRIORITY_HIGH_ACCURACY : Priority.PRIORITY_LOW_POWER);
- requestBuilder.setMaxUpdateAgeMillis((long) locationOptions.maximumAge);
-
- if (locationOptions.fastestInterval >= 0) {
- requestBuilder.setMinUpdateIntervalMillis(locationOptions.fastestInterval);
- }
-
- if (locationOptions.distanceFilter >= 0) {
- requestBuilder.setMinUpdateDistanceMeters(locationOptions.distanceFilter);
- }
- LocationRequest locationRequest = requestBuilder.build();
-
- LocationSettingsRequest.Builder settingsBuilder = new LocationSettingsRequest.Builder();
- settingsBuilder.addLocationRequest(locationRequest);
- LocationSettingsRequest locationSettingsRequest = settingsBuilder.build();
- mLocationServicesSettingsClient.checkLocationSettings(locationSettingsRequest)
- .addOnSuccessListener(locationSettingsResponse -> requestLocationUpdates(locationRequest, locationCallback))
- .addOnFailureListener(err -> {
- if(isAnyProviderAvailable()){
- requestLocationUpdates(locationRequest, locationCallback);
- return;
- }
-
- if (error != null) {
- error.invoke(
- PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).")
- );
- return;
- }
- emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).");
- });
+ mLocationHandler.startLocationUpdates(locationOptions);
}
- private void requestLocationUpdates(LocationRequest locationRequest, LocationCallback locationCallback) {
- try {
- mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());
- } catch (SecurityException e) {
- throw e;
- }
+ @Override
+ public void stopObserving() {
+ mLocationHandler.stopLocationUpdates();
}
- private boolean isAnyProviderAvailable() {
- if (mReactContext == null) {
- return false;
+ private void startService() {
+ if (mIsServiceRunning) {
+ return;
}
- LocationManager locationManager =
- (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
- return locationManager != null && (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER));
- }
- private LocationCallback createSingleLocationCallback(Callback success, Callback error) {
- final CallbackHolder callbackHolder = new CallbackHolder(success, error);
+ Intent intent = new Intent(mReactContext, LocationService.class);
- return new LocationCallback() {
- @Override
- public void onLocationResult(@NonNull LocationResult locationResult) {
- Location location = locationResult.getLastLocation();
-
- if (location == null) {
- callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/lastLocation)."));
- return;
- }
+ // Start the LocationService as a foreground service
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ mReactContext.startForegroundService(intent);
+ } else {
+ mReactContext.startService(intent);
+ }
- callbackHolder.success(location);
+ mReactContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
- mFusedLocationClient.removeLocationUpdates(mSingleLocationCallback);
- mSingleLocationCallback = null;
- }
+ @Override
+ public void stopService() {
+ if (!mIsServiceRunning) {
+ return;
+ }
- @Override
- public void onLocationAvailability(@NonNull LocationAvailability locationAvailability) {
- if (!locationAvailability.isLocationAvailable()) {
- callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/lastLocation)."));
- }
- }
- };
+ mReactContext.unbindService(mConnection);
+ Intent intent = new Intent(mReactContext, LocationService.class);
+ mReactContext.stopService(intent);
}
private static class CallbackHolder {
Callback success;
Callback error;
+
public CallbackHolder(Callback success, Callback error) {
this.success = success;
this.error = error;
@@ -206,4 +160,156 @@ public void success(Location location) {
this.success = null;
}
}
+
+ private static class LocationHandlerImpl implements LocationHandler {
+ private static final String TAG = "PlayServicesLocationHandlerImpl";
+ private final Context mContext;
+ private final Activity mActivity;
+ private FusedLocationProviderClient mFusedLocationClient;
+ private SettingsClient mLocationServicesSettingsClient;
+ private LocationCallback mSingleLocationCallback;
+ private LocationCallback mLocationCallback;
+ private final EventEmitter mEventEmitter;
+
+ public LocationHandlerImpl(Context context, Activity activity, EventEmitter errorEmitter) {
+ mContext = context;
+ mActivity = activity;
+ mEventEmitter = errorEmitter;
+ mFusedLocationClient = LocationServices.getFusedLocationProviderClient(context);
+ mLocationServicesSettingsClient = LocationServices.getSettingsClient(context);
+ }
+
+ public void startLocationUpdates(LocationOptions options) {
+ mLocationCallback = new LocationCallback() {
+ @Override
+ public void onLocationResult(LocationResult locationResult) {
+ if (locationResult == null) {
+ mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/observer).");
+ return;
+ }
+
+ mEventEmitter.emit("geolocationDidChange", locationToMap(locationResult.getLastLocation()));
+ }
+
+ @Override
+ public void onLocationAvailability(LocationAvailability locationAvailability) {
+ if (!locationAvailability.isLocationAvailable()) {
+ mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider).");
+ }
+ }
+ };
+
+ checkLocationSettings(options, mLocationCallback, null);
+ }
+
+ public void stopLocationUpdates() {
+ if (mLocationCallback == null) {
+ return;
+ }
+ mFusedLocationClient.removeLocationUpdates(mLocationCallback);
+ }
+
+ public void getCurrentLocation(LocationOptions locationOptions, final Callback success,
+ Callback error) {
+ if (mActivity == null) {
+ mSingleLocationCallback = createSingleLocationCallback(success, error);
+ checkLocationSettings(locationOptions, mSingleLocationCallback, error);
+ return;
+ }
+
+ try {
+ mFusedLocationClient.getLastLocation()
+ .addOnSuccessListener(mActivity, location -> {
+ if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) {
+ success.invoke(locationToMap(location));
+ } else {
+ mSingleLocationCallback = createSingleLocationCallback(success, error);
+ checkLocationSettings(locationOptions, mSingleLocationCallback, error);
+ }
+ });
+ } catch (SecurityException e) {
+ throw e;
+ }
+ }
+
+ private LocationCallback createSingleLocationCallback(Callback success, Callback error) {
+ final CallbackHolder callbackHolder = new CallbackHolder(success, error);
+
+ return new LocationCallback() {
+ @Override
+ public void onLocationResult(@NonNull LocationResult locationResult) {
+ Location location = locationResult.getLastLocation();
+
+ if (location == null) {
+ callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/lastLocation)."));
+ return;
+ }
+
+ callbackHolder.success(location);
+
+ mFusedLocationClient.removeLocationUpdates(mSingleLocationCallback);
+ mSingleLocationCallback = null;
+ }
+
+ @Override
+ public void onLocationAvailability(@NonNull LocationAvailability locationAvailability) {
+ if (!locationAvailability.isLocationAvailable()) {
+ callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/lastLocation)."));
+ }
+ }
+ };
+ }
+
+ private void requestLocationUpdates(LocationRequest locationRequest, LocationCallback locationCallback) {
+ try {
+ mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper());
+ } catch (SecurityException e) {
+ throw e;
+ }
+ }
+
+ private void checkLocationSettings(LocationOptions locationOptions, LocationCallback locationCallback, Callback error) {
+ LocationRequest.Builder requestBuilder = new LocationRequest.Builder(locationOptions.interval);
+ requestBuilder.setPriority(locationOptions.highAccuracy ? Priority.PRIORITY_HIGH_ACCURACY : Priority.PRIORITY_LOW_POWER);
+ requestBuilder.setMaxUpdateAgeMillis((long) locationOptions.maximumAge);
+
+ if (locationOptions.fastestInterval >= 0) {
+ requestBuilder.setMinUpdateIntervalMillis(locationOptions.fastestInterval);
+ }
+
+ if (locationOptions.distanceFilter >= 0) {
+ requestBuilder.setMinUpdateDistanceMeters(locationOptions.distanceFilter);
+ }
+ LocationRequest locationRequest = requestBuilder.build();
+
+ LocationSettingsRequest.Builder settingsBuilder = new LocationSettingsRequest.Builder();
+ settingsBuilder.addLocationRequest(locationRequest);
+ LocationSettingsRequest locationSettingsRequest = settingsBuilder.build();
+ mLocationServicesSettingsClient.checkLocationSettings(locationSettingsRequest)
+ .addOnSuccessListener(locationSettingsResponse -> requestLocationUpdates(locationRequest, locationCallback))
+ .addOnFailureListener(err -> {
+ if (isAnyProviderAvailable()) {
+ requestLocationUpdates(locationRequest, locationCallback);
+ return;
+ }
+
+ if (error != null) {
+ error.invoke(
+ PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).")
+ );
+ return;
+ }
+ mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).");
+ });
+ }
+
+ private boolean isAnyProviderAvailable() {
+ if (mContext == null) {
+ return false;
+ }
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ return locationManager != null && (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER));
+ }
+ }
}
diff --git a/ios/RNCGeolocation.mm b/ios/RNCGeolocation.mm
index 03df64d..e50d6b4 100644
--- a/ios/RNCGeolocation.mm
+++ b/ios/RNCGeolocation.mm
@@ -64,7 +64,8 @@ + (RNCGeolocationConfiguration)RNCGeolocationConfiguration:(id)json
return (RNCGeolocationConfiguration) {
.skipPermissionRequests = [RCTConvert BOOL:options[@"skipPermissionRequests"]],
- .authorizationLevel = [RCTConvert RNCGeolocationAuthorizationLevel:options[@"authorizationLevel"]]
+ .authorizationLevel = [RCTConvert RNCGeolocationAuthorizationLevel:options[@"authorizationLevel"]],
+ .enableBackgroundLocationUpdates = [RCTConvert BOOL:options[@"enableBackgroundLocationUpdates"]]
};
}
@@ -173,17 +174,17 @@ - (dispatch_queue_t)methodQueue
- (void)beginLocationUpdatesWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy distanceFilter:(CLLocationDistance)distanceFilter useSignificantChanges:(BOOL)useSignificantChanges
{
+ if (!_locationManager) {
+ _locationManager = [CLLocationManager new];
+ _locationManager.delegate = self;
+ }
+
if (!_locationConfiguration.skipPermissionRequests) {
[self requestAuthorization:nil error:nil];
} else if (_locationConfiguration.enableBackgroundLocationUpdates) {
[self enableBackgroundLocationUpdates];
}
- if (!_locationManager) {
- _locationManager = [CLLocationManager new];
- _locationManager.delegate = self;
- }
-
_locationManager.distanceFilter = distanceFilter;
_locationManager.desiredAccuracy = desiredAccuracy;
_usingSignificantChanges = useSignificantChanges;
@@ -298,9 +299,7 @@ - (void)enableBackgroundLocationUpdates
// iOS 9+ requires explicitly enabling background updates
NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
if (backgroundModes && [backgroundModes containsObject:@"location"]) {
- if ([_locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
- [_locationManager setAllowsBackgroundLocationUpdates:YES];
- }
+ _locationManager.allowsBackgroundLocationUpdates = YES;
}
#endif
}