package com.agontuk.RNFusedLocation; import android.app.Activity; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.location.Location; import android.util.Log; import androidx.annotation.NonNull; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.BaseActivityEventListener; 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.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.ResolvableApiException; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationAvailability; import com.google.android.gms.location.LocationCallback; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationServices; import com.google.android.gms.location.LocationSettingsRequest; import com.google.android.gms.location.LocationSettingsResponse; import com.google.android.gms.location.LocationSettingsStatusCodes; import com.google.android.gms.location.SettingsClient; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import java.lang.RuntimeException; public class RNFusedLocationModule extends ReactContextBaseJavaModule { public static final String TAG = "RNFusedLocation"; private static final int REQUEST_SETTINGS_SINGLE_UPDATE = 11403; private static final int REQUEST_SETTINGS_CONTINUOUS_UPDATE = 11404; private static final float DEFAULT_DISTANCE_FILTER = 100; private static final int DEFAULT_ACCURACY = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; private static final long DEFAULT_INTERVAL = 10 * 1000; /* 10 secs */ private static final long DEFAULT_FASTEST_INTERVAL = 5 * 1000; /* 5 sec */ private boolean mShowLocationDialog = true; private boolean mForceRequestLocation = false; private int mLocationPriority = DEFAULT_ACCURACY; private long mUpdateInterval = DEFAULT_INTERVAL; private long mFastestInterval = DEFAULT_FASTEST_INTERVAL; private double mMaximumAge = Double.POSITIVE_INFINITY; private long mTimeout = Long.MAX_VALUE; private float mDistanceFilter = DEFAULT_DISTANCE_FILTER; private Callback mSuccessCallback; private Callback mErrorCallback; private FusedLocationProviderClient mFusedProviderClient; private SettingsClient mSettingsClient; private LocationRequest mLocationRequest; private LocationCallback mLocationCallback; private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { if (requestCode == REQUEST_SETTINGS_SINGLE_UPDATE) { if (resultCode == Activity.RESULT_OK) { // Location settings changed successfully, request user location. getUserLocation(); return; } if (!mForceRequestLocation) { invokeError( LocationError.SETTINGS_NOT_SATISFIED.getValue(), "Location settings are not satisfied.", true ); return; } if (!LocationUtils.isLocationEnabled(getContext())) { invokeError( LocationError.POSITION_UNAVAILABLE.getValue(), "No location provider available.", true ); return; } getUserLocation(); } else if (requestCode == REQUEST_SETTINGS_CONTINUOUS_UPDATE) { if (resultCode == Activity.RESULT_OK) { // Location settings changed successfully, request user location. getLocationUpdates(); return; } if (!mForceRequestLocation) { invokeError( LocationError.SETTINGS_NOT_SATISFIED.getValue(), "Location settings are not satisfied.", false ); return; } if (!LocationUtils.isLocationEnabled(getContext())) { invokeError( LocationError.POSITION_UNAVAILABLE.getValue(), "No location provider available.", false ); return; } getLocationUpdates(); } } }; public RNFusedLocationModule(ReactApplicationContext reactContext) { super(reactContext); mFusedProviderClient = LocationServices.getFusedLocationProviderClient(reactContext); mSettingsClient = LocationServices.getSettingsClient(reactContext); reactContext.addActivityEventListener(mActivityEventListener); Log.i(TAG, TAG + " initialized"); } @NonNull @Override public String getName() { return TAG; } /** * 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), * highAccuracy (boolean), distanceFilter (double) and showLocationDialog (boolean) * @param success success callback * @param error error callback */ @ReactMethod public void getCurrentPosition(ReadableMap options, final Callback success, final Callback error) { ReactApplicationContext context = getContext(); mSuccessCallback = success; mErrorCallback = error; if (!LocationUtils.hasLocationPermission(context)) { invokeError( LocationError.PERMISSION_DENIED.getValue(), "Location permission not granted.", true ); return; } if (!LocationUtils.isGooglePlayServicesAvailable(context)) { invokeError( LocationError.PLAY_SERVICE_NOT_AVAILABLE.getValue(), "Google play service is not available.", true ); return; } boolean highAccuracy = options.hasKey("enableHighAccuracy") && options.getBoolean("enableHighAccuracy"); // TODO: Make other PRIORITY_* constants available to the user mLocationPriority = highAccuracy ? LocationRequest.PRIORITY_HIGH_ACCURACY : DEFAULT_ACCURACY; mTimeout = options.hasKey("timeout") ? (long) options.getDouble("timeout") : Long.MAX_VALUE; mMaximumAge = options.hasKey("maximumAge") ? options.getDouble("maximumAge") : Double.POSITIVE_INFINITY; mDistanceFilter = options.hasKey("distanceFilter") ? (float) options.getDouble("distanceFilter") : 0; mShowLocationDialog = options.hasKey("showLocationDialog") ? options.getBoolean("showLocationDialog") : true; mForceRequestLocation = options.hasKey("forceRequestLocation") ? options.getBoolean("forceRequestLocation") : false; LocationSettingsRequest locationSettingsRequest = buildLocationSettingsRequest(); if (mSettingsClient != null) { mSettingsClient.checkLocationSettings(locationSettingsRequest) .addOnCompleteListener(new OnCompleteListener<LocationSettingsResponse>() { @Override public void onComplete(@NonNull Task<LocationSettingsResponse> task) { onLocationSettingsResponse(task, true); } }); } } /** * 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), distanceFilter (double), * interval (millis), fastestInterval (millis) */ @ReactMethod public void startObserving(ReadableMap options) { ReactApplicationContext context = getContext(); if (!LocationUtils.hasLocationPermission(context)) { invokeError( LocationError.PERMISSION_DENIED.getValue(), "Location permission not granted.", false ); return; } if (!LocationUtils.isGooglePlayServicesAvailable(context)) { invokeError( LocationError.PLAY_SERVICE_NOT_AVAILABLE.getValue(), "Google play service is not available.", false ); return; } boolean highAccuracy = options.hasKey("enableHighAccuracy") && options.getBoolean("enableHighAccuracy"); // TODO: Make other PRIORITY_* constants available to the user mLocationPriority = highAccuracy ? LocationRequest.PRIORITY_HIGH_ACCURACY : DEFAULT_ACCURACY; mDistanceFilter = options.hasKey("distanceFilter") ? (float) options.getDouble("distanceFilter") : DEFAULT_DISTANCE_FILTER; mUpdateInterval = options.hasKey("interval") ? (long) options.getDouble("interval") : DEFAULT_INTERVAL; mFastestInterval = options.hasKey("fastestInterval") ? (long) options.getDouble("fastestInterval") : DEFAULT_INTERVAL; mShowLocationDialog = options.hasKey("showLocationDialog") ? options.getBoolean("showLocationDialog") : true; mForceRequestLocation = options.hasKey("forceRequestLocation") ? options.getBoolean("forceRequestLocation") : false; LocationSettingsRequest locationSettingsRequest = buildLocationSettingsRequest(); if (mSettingsClient != null) { mSettingsClient.checkLocationSettings(locationSettingsRequest) .addOnCompleteListener(new OnCompleteListener<LocationSettingsResponse>() { @Override public void onComplete(@NonNull Task<LocationSettingsResponse> task) { onLocationSettingsResponse(task, false); } }); } } /** * Stop listening for location updates. */ @ReactMethod public void stopObserving() { if (mFusedProviderClient != null && mLocationCallback != null) { mFusedProviderClient.removeLocationUpdates(mLocationCallback); mLocationCallback = null; } } /** * Build location setting request using current configuration */ private LocationSettingsRequest buildLocationSettingsRequest() { mLocationRequest = new LocationRequest(); mLocationRequest.setPriority(mLocationPriority) .setInterval(mUpdateInterval) .setFastestInterval(mFastestInterval) .setSmallestDisplacement(mDistanceFilter); LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); builder.addLocationRequest(mLocationRequest); return builder.build(); } /** * Check location setting response and decide whether to proceed with * location request or not. */ private void onLocationSettingsResponse( Task<LocationSettingsResponse> task, boolean isSingleUpdate ) { try { LocationSettingsResponse response = task.getResult(ApiException.class); // All location settings are satisfied, start location request. if (isSingleUpdate) { getUserLocation(); } else { getLocationUpdates(); } } catch (ApiException exception) { switch (exception.getStatusCode()) { case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: /** * Location settings are not satisfied. But could be fixed by showing the * user a dialog. It means either location service is not enabled or * default location mode is not enough to perform the request. */ if (!mShowLocationDialog) { invokeError( LocationError.SETTINGS_NOT_SATISFIED.getValue(), "Location settings are not satisfied.", isSingleUpdate ); break; } try { ResolvableApiException resolvable = (ResolvableApiException) exception; Activity activity = getCurrentActivity(); if (activity == null) { invokeError( LocationError.INTERNAL_ERROR.getValue(), "Tried to open location dialog while not attached to an Activity", isSingleUpdate ); break; } resolvable.startResolutionForResult( activity, isSingleUpdate ? REQUEST_SETTINGS_SINGLE_UPDATE : REQUEST_SETTINGS_CONTINUOUS_UPDATE ); } catch (SendIntentException e) { invokeError( LocationError.INTERNAL_ERROR.getValue(), "Internal error occurred", isSingleUpdate ); } catch (ClassCastException e) { invokeError( LocationError.INTERNAL_ERROR.getValue(), "Internal error occurred", isSingleUpdate ); } break; default: // TODO: we may have to handle other use case here. // For now just say that settings are not ok. invokeError( LocationError.SETTINGS_NOT_SATISFIED.getValue(), "Location settings are not satisfied.", isSingleUpdate ); break; } } } /** * Get last known location if it exists, otherwise request a new update. */ private void getUserLocation() { if (mFusedProviderClient != null) { mFusedProviderClient.getLastLocation().addOnCompleteListener(new OnCompleteListener<Location>() { @Override public void onComplete(@NonNull Task<Location> task) { Location location = null; try { location = task.getResult(ApiException.class); } catch (ApiException exception) { Log.w(TAG, "getLastLocation error: " + exception.getMessage()); } if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < mMaximumAge) { invokeSuccess(LocationUtils.locationToMap(location), true); return; } // Last location is not available, request location update. new SingleLocationUpdate( mFusedProviderClient, mLocationRequest, mTimeout, mSuccessCallback, mErrorCallback ).getLocation(); } }); } } /** * Get periodic location updates based on the current location request. */ private void getLocationUpdates() { if (mFusedProviderClient != null && mLocationRequest != null) { mLocationCallback = new LocationCallback() { @Override public void onLocationAvailability(LocationAvailability locationAvailability) { if (!locationAvailability.isLocationAvailable()) { invokeError( LocationError.POSITION_UNAVAILABLE.getValue(), "Unable to retrieve location", false ); } } @Override public void onLocationResult(LocationResult locationResult) { Location location = locationResult.getLastLocation(); invokeSuccess(LocationUtils.locationToMap(location), false); } }; mFusedProviderClient.requestLocationUpdates(mLocationRequest, mLocationCallback, null); } } /** * Get react context */ private ReactApplicationContext getContext() { return getReactApplicationContext(); } /** * Clear the JS callbacks */ private void clearCallbacks() { mSuccessCallback = null; mErrorCallback = null; } /** * Helper method to invoke success callback */ private void invokeSuccess(WritableMap data, boolean isSingleUpdate) { if (!isSingleUpdate) { getContext().getJSModule(RCTDeviceEventEmitter.class) .emit("geolocationDidChange", data); return; } try { if (mSuccessCallback != null) { mSuccessCallback.invoke(data); } clearCallbacks(); } catch (RuntimeException e) { // Illegal callback invocation Log.w(TAG, e.getMessage()); } } /** * Helper method to invoke error callback */ private void invokeError(int code, String message, boolean isSingleUpdate) { if (!isSingleUpdate) { getContext().getJSModule(RCTDeviceEventEmitter.class) .emit("geolocationError", LocationUtils.buildError(code, message)); return; } try { if (mErrorCallback != null) { mErrorCallback.invoke(LocationUtils.buildError(code, message)); } clearCallbacks(); } catch (RuntimeException e) { // Illegal callback invocation Log.w(TAG, e.getMessage()); } } }