Open source the Android Location module
Reviewed By: foghina Differential Revision: D2658581 fb-gh-sync-id: e95b21c5c7c06f3332d2a7c9fab8be9a2e6441cb
This commit is contained in:
parent
99b4901f6b
commit
494930afb2
|
@ -38,6 +38,7 @@ var COMPONENTS = [
|
||||||
var APIS = [
|
var APIS = [
|
||||||
require('./AccessibilityAndroidExample.android'),
|
require('./AccessibilityAndroidExample.android'),
|
||||||
require('./BorderExample'),
|
require('./BorderExample'),
|
||||||
|
require('./GeolocationExample'),
|
||||||
require('./IntentAndroidExample.android'),
|
require('./IntentAndroidExample.android'),
|
||||||
require('./LayoutEventsExample'),
|
require('./LayoutEventsExample'),
|
||||||
require('./LayoutExample'),
|
require('./LayoutExample'),
|
||||||
|
|
|
@ -29,12 +29,19 @@ type GeoOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* The Geolocation API follows the web spec:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/Geolocation
|
||||||
|
*
|
||||||
|
* ### iOS
|
||||||
* You need to include the `NSLocationWhenInUseUsageDescription` key
|
* You need to include the `NSLocationWhenInUseUsageDescription` key
|
||||||
* in Info.plist to enable geolocation. Geolocation is enabled by default
|
* in Info.plist to enable geolocation. Geolocation is enabled by default
|
||||||
* when you create a project with `react-native init`.
|
* when you create a project with `react-native init`.
|
||||||
*
|
*
|
||||||
* Geolocation follows the MDN specification:
|
* ### Android
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/Geolocation
|
* To request access to location, you need to add the following line to your
|
||||||
|
* app's `AndroidManifest.xml`:
|
||||||
|
*
|
||||||
|
* `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
|
||||||
*/
|
*/
|
||||||
var Geolocation = {
|
var Geolocation = {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,285 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.modules.location;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
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 com.facebook.react.bridge.Callback;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
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.SystemClock;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native module that exposes Geolocation to JS.
|
||||||
|
*/
|
||||||
|
public class LocationModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
private @Nullable String mWatchedProvider;
|
||||||
|
|
||||||
|
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("Provider " + provider + " is out of service.");
|
||||||
|
} else if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) {
|
||||||
|
emitError("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 "LocationObserver";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LocationOptions {
|
||||||
|
private final long timeout;
|
||||||
|
private final double maximumAge;
|
||||||
|
private final boolean highAccuracy;
|
||||||
|
|
||||||
|
private LocationOptions(long timeout, double maximumAge, boolean highAccuracy) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.maximumAge = maximumAge;
|
||||||
|
this.highAccuracy = highAccuracy;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
return new LocationOptions(timeout, maximumAge, highAccuracy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
LocationManager locationManager =
|
||||||
|
(LocationManager) getReactApplicationContext().getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
String provider = getValidProvider(locationManager, locationOptions.highAccuracy);
|
||||||
|
if (provider == null) {
|
||||||
|
error.invoke("No available location provider.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location location = null;
|
||||||
|
try {
|
||||||
|
location = locationManager.getLastKnownLocation(provider);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
throwLocationPermissionMissing(e);
|
||||||
|
}
|
||||||
|
if (location != null &&
|
||||||
|
SystemClock.currentTimeMillis() - location.getTime() < locationOptions.maximumAge) {
|
||||||
|
success.invoke(locationToMap(location));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new SingleUpdateRequest(locationManager, provider, locationOptions.timeout, success, error)
|
||||||
|
.invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
LocationManager locationManager =
|
||||||
|
(LocationManager) getReactApplicationContext().getSystemService(Context.LOCATION_SERVICE);
|
||||||
|
String provider = getValidProvider(locationManager, locationOptions.highAccuracy);
|
||||||
|
if (provider == null) {
|
||||||
|
emitError("No location provider available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!provider.equals(mWatchedProvider)) {
|
||||||
|
locationManager.removeUpdates(mLocationListener);
|
||||||
|
locationManager.requestLocationUpdates(provider, 1000, 0, mLocationListener);
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
throwLocationPermissionMissing(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mWatchedProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 static 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emitError(String error) {
|
||||||
|
getReactApplicationContext().getJSModule(RCTDeviceEventEmitter.class)
|
||||||
|
.emit("geolocationError", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 final Handler mHandler = new Handler();
|
||||||
|
private final Runnable mTimeoutRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (SingleUpdateRequest.this) {
|
||||||
|
if (!mTriggered) {
|
||||||
|
mError.invoke("Location request timed out");
|
||||||
|
mLocationManager.removeUpdates(mLocationListener);
|
||||||
|
mTriggered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private final LocationListener mLocationListener = new LocationListener() {
|
||||||
|
@Override
|
||||||
|
public void onLocationChanged(Location location) {
|
||||||
|
synchronized (SingleUpdateRequest.this) {
|
||||||
|
if (!mTriggered) {
|
||||||
|
mSuccess.invoke(locationToMap(location));
|
||||||
|
mHandler.removeCallbacks(mTimeoutRunnable);
|
||||||
|
mTriggered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
mLocationManager.requestSingleUpdate(mProvider, mLocationListener, null);
|
||||||
|
mHandler.postDelayed(mTimeoutRunnable, SystemClock.currentTimeMillis() + mTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import com.facebook.react.bridge.NativeModule;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.modules.fresco.FrescoModule;
|
import com.facebook.react.modules.fresco.FrescoModule;
|
||||||
import com.facebook.react.modules.intent.IntentModule;
|
import com.facebook.react.modules.intent.IntentModule;
|
||||||
|
import com.facebook.react.modules.location.LocationModule;
|
||||||
import com.facebook.react.modules.network.NetworkingModule;
|
import com.facebook.react.modules.network.NetworkingModule;
|
||||||
import com.facebook.react.modules.storage.AsyncStorageModule;
|
import com.facebook.react.modules.storage.AsyncStorageModule;
|
||||||
import com.facebook.react.modules.toast.ToastModule;
|
import com.facebook.react.modules.toast.ToastModule;
|
||||||
|
@ -50,6 +51,7 @@ public class MainReactPackage implements ReactPackage {
|
||||||
new AsyncStorageModule(reactContext),
|
new AsyncStorageModule(reactContext),
|
||||||
new FrescoModule(reactContext),
|
new FrescoModule(reactContext),
|
||||||
new IntentModule(reactContext),
|
new IntentModule(reactContext),
|
||||||
|
new LocationModule(reactContext),
|
||||||
new NetworkingModule(reactContext),
|
new NetworkingModule(reactContext),
|
||||||
new WebSocketModule(reactContext),
|
new WebSocketModule(reactContext),
|
||||||
new ToastModule(reactContext));
|
new ToastModule(reactContext));
|
||||||
|
|
Loading…
Reference in New Issue