-
Notifications
You must be signed in to change notification settings - Fork 24.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Open source the Android Location module
Reviewed By: foghina Differential Revision: D2658581 fb-gh-sync-id: e95b21c5c7c06f3332d2a7c9fab8be9a2e6441cb
- Loading branch information
Martin Konicek
authored and
facebook-github-bot-6
committed
Nov 18, 2015
1 parent
99b4901
commit 494930a
Showing
4 changed files
with
297 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
285 changes: 285 additions & 0 deletions
285
ReactAndroid/src/main/java/com/facebook/react/modules/location/LocationModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
494930a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't test it on device yet but it looks like this will fail on Android API 23+ due to the new permission model. See tiagojdf/rn-geolocation#1 for more info. Is there a plan for targetting API 23?