com.facebook.react.modules.location.LocationModule.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.react.modules.location.LocationModule.java

Source

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.react.modules.location;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import com.facebook.common.logging.FLog;
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.ReactConstants;
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.
 */
@SuppressLint("MissingPermission")
@ReactModule(name = LocationModule.NAME)
public class LocationModule extends ReactContextBaseJavaModule {

    public static final String NAME = "LocationObserver";
    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 NAME;
    }

    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(PositionError.buildError(PositionError.POSITION_UNAVAILABLE,
                        "No location provider available."));
                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(location);
        } 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.POSITION_UNAVAILABLE, "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 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;
            }
        }
        // If it's an enabled provider, but we don't have permissions, ignore it
        int finePermission = ContextCompat.checkSelfPermission(getReactApplicationContext(),
                android.Manifest.permission.ACCESS_FINE_LOCATION);
        if (provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) {
            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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            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 Location mOldLocation;
        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);
                        FLog.i(ReactConstants.TAG, "LocationModule: Location request timed out");
                        mTriggered = true;
                    }
                }
            }
        };
        private final LocationListener mLocationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                synchronized (SingleUpdateRequest.this) {
                    if (!mTriggered && isBetterLocation(location, mOldLocation)) {
                        mSuccess.invoke(locationToMap(location));
                        mHandler.removeCallbacks(mTimeoutRunnable);
                        mTriggered = true;
                        mLocationManager.removeUpdates(mLocationListener);
                    }

                    mOldLocation = location;
                }
            }

            @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(Location location) {
            mOldLocation = location;
            mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener);
            mHandler.postDelayed(mTimeoutRunnable, mTimeout);
        }

        private static final int TWO_MINUTES = 1000 * 60 * 2;

        /** Determines whether one Location reading is better than the current Location fix
        * taken from Android Examples https://developer.android.com/guide/topics/location/strategies.html
        *
        * @param location  The new Location that you want to evaluate
        * @param currentBestLocation  The current Location fix, to which you want to compare the new one
        */
        private boolean isBetterLocation(Location location, Location currentBestLocation) {
            if (currentBestLocation == null) {
                // A new location is always better than no location
                return true;
            }

            // Check whether the new location fix is newer or older
            long timeDelta = location.getTime() - currentBestLocation.getTime();
            boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
            boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
            boolean isNewer = timeDelta > 0;

            // If it's been more than two minutes since the current location, use the new location
            // because the user has likely moved
            if (isSignificantlyNewer) {
                return true;
                // If the new location is more than two minutes older, it must be worse
            } else if (isSignificantlyOlder) {
                return false;
            }

            // Check whether the new location fix is more or less accurate
            int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
            boolean isLessAccurate = accuracyDelta > 0;
            boolean isMoreAccurate = accuracyDelta < 0;
            boolean isSignificantlyLessAccurate = accuracyDelta > 200;

            // Check if the old and new location are from the same provider
            boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());

            // Determine location quality using a combination of timeliness and accuracy
            if (isMoreAccurate) {
                return true;
            } else if (isNewer && !isLessAccurate) {
                return true;
            } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
                return true;
            }

            return false;
        }

        /** Checks whether two providers are the same */
        private boolean isSameProvider(String provider1, String provider2) {
            if (provider1 == null) {
                return provider2 == null;
            }
            return provider1.equals(provider2);
        }
    }
}