From 494930afb22bdced610d6089ea6146a7b8a3c5a0 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Wed, 18 Nov 2015 09:03:51 -0800 Subject: [PATCH] Open source the Android Location module Reviewed By: foghina Differential Revision: D2658581 fb-gh-sync-id: e95b21c5c7c06f3332d2a7c9fab8be9a2e6441cb --- Examples/UIExplorer/UIExplorerList.android.js | 1 + Libraries/Geolocation/Geolocation.js | 11 +- .../modules/location/LocationModule.java | 285 ++++++++++++++++++ .../react/shell/MainReactPackage.java | 2 + 4 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/modules/location/LocationModule.java diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 7024779a4..5a370a01c 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -38,6 +38,7 @@ var COMPONENTS = [ var APIS = [ require('./AccessibilityAndroidExample.android'), require('./BorderExample'), + require('./GeolocationExample'), require('./IntentAndroidExample.android'), require('./LayoutEventsExample'), require('./LayoutExample'), diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js index 80dbfa19b..b6752e3f0 100644 --- a/Libraries/Geolocation/Geolocation.js +++ b/Libraries/Geolocation/Geolocation.js @@ -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 * in Info.plist to enable geolocation. Geolocation is enabled by default * when you create a project with `react-native init`. * - * Geolocation follows the MDN specification: - * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation + * ### Android + * To request access to location, you need to add the following line to your + * app's `AndroidManifest.xml`: + * + * `` */ var Geolocation = { diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/location/LocationModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/location/LocationModule.java new file mode 100644 index 000000000..188a45905 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/location/LocationModule.java @@ -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" + + "", 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); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 2f0da533e..115c33ea6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -19,6 +19,7 @@ import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.modules.fresco.FrescoModule; 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.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; @@ -50,6 +51,7 @@ public class MainReactPackage implements ReactPackage { new AsyncStorageModule(reactContext), new FrescoModule(reactContext), new IntentModule(reactContext), + new LocationModule(reactContext), new NetworkingModule(reactContext), new WebSocketModule(reactContext), new ToastModule(reactContext));