mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-03-06 22:37:14 +08:00
Move Android code for Geolocation out
Reviewed By: cpojer Differential Revision: D14692916 fbshipit-source-id: 6bcfda8aa8c7d22dce36828dae9f57068815ce20
This commit is contained in:
committed by
Facebook Github Bot
parent
ff66600224
commit
0b62ff0b94
@@ -1,20 +0,0 @@
|
||||
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library")
|
||||
|
||||
rn_android_library(
|
||||
name = "location",
|
||||
srcs = glob(["**/*.java"]),
|
||||
is_androidx = True,
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
deps = [
|
||||
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
|
||||
react_native_dep("third-party/android/support/v4:lib-support-v4"),
|
||||
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
|
||||
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||
react_native_target("java/com/facebook/react/common:common"),
|
||||
react_native_target("java/com/facebook/react/module/annotations:annotations"),
|
||||
react_native_target("java/com/facebook/react/modules/core:core"),
|
||||
],
|
||||
)
|
||||
@@ -1,377 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.location;
|
||||
|
||||
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.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.ReactConstants;
|
||||
import com.facebook.react.common.SystemClock;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Native module that exposes Geolocation to JS.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@ReactModule(name = LocationModule.NAME)
|
||||
public class LocationModule extends ReactContextBaseJavaModule {
|
||||
|
||||
public static final String NAME = "LocationObserver";
|
||||
private @Nullable String mWatchedProvider;
|
||||
private static final float RCT_DEFAULT_LOCATION_ACCURACY = 100;
|
||||
|
||||
private final LocationListener mLocationListener = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
getReactApplicationContext().getJSModule(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) { }
|
||||
};
|
||||
|
||||
public LocationModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
private static class LocationOptions {
|
||||
private final long timeout;
|
||||
private final double maximumAge;
|
||||
private final boolean highAccuracy;
|
||||
private final float distanceFilter;
|
||||
|
||||
private LocationOptions(
|
||||
long timeout,
|
||||
double maximumAge,
|
||||
boolean highAccuracy,
|
||||
float distanceFilter) {
|
||||
this.timeout = timeout;
|
||||
this.maximumAge = maximumAge;
|
||||
this.highAccuracy = highAccuracy;
|
||||
this.distanceFilter = distanceFilter;
|
||||
}
|
||||
|
||||
private static LocationOptions fromReactMap(ReadableMap map) {
|
||||
// precision might be dropped on timeout (double -> int conversion), but that's OK
|
||||
long timeout =
|
||||
map.hasKey("timeout") ? (long) map.getDouble("timeout") : Long.MAX_VALUE;
|
||||
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(timeout, maximumAge, highAccuracy, distanceFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current position. This can return almost immediately if the location is cached or
|
||||
* request an update, which might take a while.
|
||||
*
|
||||
* @param options map containing optional arguments: timeout (millis), maximumAge (millis) and
|
||||
* highAccuracy (boolean)
|
||||
*/
|
||||
@ReactMethod
|
||||
public void getCurrentPosition(
|
||||
ReadableMap options,
|
||||
final Callback success,
|
||||
Callback error) {
|
||||
LocationOptions locationOptions = LocationOptions.fromReactMap(options);
|
||||
|
||||
try {
|
||||
LocationManager locationManager =
|
||||
(LocationManager) getReactApplicationContext().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 SingleUpdateRequest(locationManager, provider, locationOptions.timeout, success, error)
|
||||
.invoke(location);
|
||||
} catch (SecurityException e) {
|
||||
throwLocationPermissionMissing(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for location updates. These will be emitted via the
|
||||
* {@link RCTDeviceEventEmitter} as {@code geolocationDidChange} events.
|
||||
*
|
||||
* @param options map containing optional arguments: highAccuracy (boolean)
|
||||
*/
|
||||
@ReactMethod
|
||||
public void startObserving(ReadableMap options) {
|
||||
if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) {
|
||||
return;
|
||||
}
|
||||
LocationOptions locationOptions = LocationOptions.fromReactMap(options);
|
||||
|
||||
try {
|
||||
LocationManager locationManager =
|
||||
(LocationManager) getReactApplicationContext().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) {
|
||||
throwLocationPermissionMissing(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening for location updates.
|
||||
*
|
||||
* NB: this is not balanced with {@link #startObserving}: any number of calls to that method will
|
||||
* be canceled by just one call to this one.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void stopObserving() {
|
||||
LocationManager locationManager =
|
||||
(LocationManager) getReactApplicationContext().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(getReactApplicationContext(), android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
if (provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) {
|
||||
return null;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static WritableMap locationToMap(Location location) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
WritableMap coords = Arguments.createMap();
|
||||
coords.putDouble("latitude", location.getLatitude());
|
||||
coords.putDouble("longitude", location.getLongitude());
|
||||
coords.putDouble("altitude", location.getAltitude());
|
||||
coords.putDouble("accuracy", location.getAccuracy());
|
||||
coords.putDouble("heading", location.getBearing());
|
||||
coords.putDouble("speed", location.getSpeed());
|
||||
map.putMap("coords", coords);
|
||||
map.putDouble("timestamp", location.getTime());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
map.putBoolean("mocked", location.isFromMockProvider());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private void emitError(int code, String message) {
|
||||
getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class)
|
||||
.emit("geolocationError", PositionError.buildError(code, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a clearer exception message than the default one.
|
||||
*/
|
||||
private static void throwLocationPermissionMissing(SecurityException e) {
|
||||
throw new SecurityException(
|
||||
"Looks like the app doesn't have the permission to access location.\n" +
|
||||
"Add the following line to your app's AndroidManifest.xml:\n" +
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />", e);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.location;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
/**
|
||||
* @see {https://developer.mozilla.org/en-US/docs/Web/API/PositionError}
|
||||
*/
|
||||
public class PositionError {
|
||||
/**
|
||||
* The acquisition of the geolocation information failed because
|
||||
* the page didn't have the permission to do it.
|
||||
*/
|
||||
public static int PERMISSION_DENIED = 1;
|
||||
|
||||
/**
|
||||
* The acquisition of the geolocation failed because at least one
|
||||
* internal source of position returned an internal error.
|
||||
*/
|
||||
public static int POSITION_UNAVAILABLE = 2;
|
||||
|
||||
/**
|
||||
* The time allowed to acquire the geolocation, defined by
|
||||
* PositionOptions.timeout information was reached before the information was obtained.
|
||||
*/
|
||||
public static int TIMEOUT = 3;
|
||||
|
||||
public static WritableMap buildError(int code, String message) {
|
||||
WritableMap error = Arguments.createMap();
|
||||
error.putInt("code", code);
|
||||
|
||||
if (message != null) {
|
||||
error.putString("message", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide error types in error message. Feature parity with iOS
|
||||
*/
|
||||
error.putInt("PERMISSION_DENIED", PERMISSION_DENIED);
|
||||
error.putInt("POSITION_UNAVAILABLE", POSITION_UNAVAILABLE);
|
||||
error.putInt("TIMEOUT", TIMEOUT);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ rn_android_library(
|
||||
react_native_target("java/com/facebook/react/modules/i18nmanager:i18nmanager"),
|
||||
react_native_target("java/com/facebook/react/modules/image:image"),
|
||||
react_native_target("java/com/facebook/react/modules/intent:intent"),
|
||||
react_native_target("java/com/facebook/react/modules/location:location"),
|
||||
react_native_target("java/com/facebook/react/modules/netinfo:netinfo"),
|
||||
react_native_target("java/com/facebook/react/modules/network:network"),
|
||||
react_native_target("java/com/facebook/react/modules/permissions:permissions"),
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.facebook.react.modules.fresco.FrescoModule;
|
||||
import com.facebook.react.modules.i18nmanager.I18nManagerModule;
|
||||
import com.facebook.react.modules.image.ImageLoaderModule;
|
||||
import com.facebook.react.modules.intent.IntentModule;
|
||||
import com.facebook.react.modules.location.LocationModule;
|
||||
import com.facebook.react.modules.netinfo.NetInfoModule;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import com.facebook.react.modules.permissions.PermissionsModule;
|
||||
@@ -88,7 +87,6 @@ import javax.inject.Provider;
|
||||
ImageLoaderModule.class,
|
||||
ImageStoreManager.class,
|
||||
IntentModule.class,
|
||||
LocationModule.class,
|
||||
NativeAnimatedModule.class,
|
||||
NetworkingModule.class,
|
||||
NetInfoModule.class,
|
||||
@@ -239,14 +237,6 @@ public class MainReactPackage extends LazyReactPackage {
|
||||
return new IntentModule(context);
|
||||
}
|
||||
}),
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
LocationModule.class,
|
||||
new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return new LocationModule(context);
|
||||
}
|
||||
}),
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
NativeAnimatedModule.class,
|
||||
new Provider<NativeModule>() {
|
||||
|
||||
Reference in New Issue
Block a user