/** * 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 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.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.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. */ @ReactModule(name = "LocationObserver") public class LocationModule extends ReactContextBaseJavaModule { 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 "LocationObserver"; } 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("No available location provider."); 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(); } 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.PERMISSION_DENIED, "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 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()); if (android.os.Build.VERSION.SDK_INT >= 18) { 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 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); 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, mTimeout); } } }