Skip to content

Commit

Permalink
Add support for Google’s Location Services API (#185)
Browse files Browse the repository at this point in the history
* feat(android-backend): refactor module

* feat(android): play services manager

* feat(android): fix permission crash
  • Loading branch information
michalchudziak authored Aug 25, 2022
1 parent 6abb2a8 commit bdf892e
Show file tree
Hide file tree
Showing 8 changed files with 559 additions and 380 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Supported options:

* `skipPermissionRequests` (boolean, iOS-only) - 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`.
* `locationProvider` (string, Android-only) - Either `"playServices"`, `"android"`. Determines wether to use `Google’s Location Services API` or `Android’s Location API`. Defaults to `playServices`. Falls back to Android's Location API if play services aren't available.

---

Expand Down Expand Up @@ -244,6 +245,8 @@ Invokes the success callback whenever the location changes. Returns a `watchId`

Supported options:

* `interval` -- The rate in milliseconds at which your app prefers to receive location updates. Note that the location updates may be somewhat faster or slower than this rate to optimize for battery usage, or there may be no updates at all (if the device has no connectivity, for example).
* `fastestInterval` -- The fastest rate in milliseconds at which your app can handle location updates. Unless your app benefits from receiving updates more quickly than the rate specified in `interval`, you don't need to set it.
* `timeout` (ms) - Is a positive value representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. Defaults to INFINITY.
* `maximumAge` (ms) - Is a positive value indicating the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to 0, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to Infinity the device will always return a cached position regardless of its age. Defaults to INFINITY.
* `enableHighAccuracy` (bool) - Is a boolean representing if to use GPS or not. If set to true, a GPS position will be requested. If set to false, a WIFI location will be requested.
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ repositories {
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+'
implementation 'com.google.android.gms:play-services-location:20.0.0'
}

if (isNewArchitectureEnabled()) {
Expand Down
4 changes: 3 additions & 1 deletion android/src/legacy/RNCGeolocationModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public String getName() {
}

@ReactMethod
public void setConfiguration(ReadableMap config) { }
public void setConfiguration(ReadableMap config) {
mImpl.setConfiguration(config);
}

@ReactMethod
public void requestAuthorization() { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package com.reactnativecommunity.geolocation;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Handler;

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 final LocationListener mLocationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("geolocationDidChange", locationToMap(location));
}

@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (status == LocationProvider.OUT_OF_SERVICE) {
emitError(PositionError.POSITION_UNAVAILABLE, "Provider " + provider + " is out of service.");
} else if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
emitError(PositionError.TIMEOUT, "Provider " + provider + " is temporarily unavailable.");
}
}

@Override
public void onProviderEnabled(String provider) { }

@Override
public void onProviderDisabled(String provider) { }
};

protected AndroidLocationManager(ReactApplicationContext reactContext) {
super(reactContext);
}

public void getCurrentLocationData(
ReadableMap options,
final Callback success,
Callback error) {
AndroidLocationManager.LocationOptions locationOptions = AndroidLocationManager.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;
}

new AndroidLocationManager.SingleUpdateRequest(locationManager, provider, locationOptions.timeout, success, error)
.invoke(location);
} catch (SecurityException e) {
throw e;
}
}

public void startObserving(ReadableMap options) {
if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) {
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;
}
}

public void stopObserving() {
LocationManager locationManager =
(LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(mLocationListener);
mWatchedProvider = null;
}

@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;
}
}
// 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);
if (provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) {
return 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;
}
}
}
};
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;
}
}

@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;
}

public void invoke(Location location) {
mOldLocation = location;
mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener);
mHandler.postDelayed(mTimeoutRunnable, mTimeout);
}

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;
}

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);
}
}
}
Loading

0 comments on commit bdf892e

Please sign in to comment.