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