Agontuk / react-native-geolocation-service

React native geolocation service for iOS and android
https://www.npmjs.com/package/react-native-geolocation-service
MIT License
1.61k stars 292 forks source link

Concurrent calls to getCurrentPosition #195

Closed mppperez closed 2 years ago

mppperez commented 4 years ago

I just found this comment in a related (closed) issue:

https://github.com/Agontuk/react-native-geolocation-service/issues/41#issuecomment-516406403_

I think this should get some more attention:

BR

hisothreed commented 4 years ago

I'm also experiencing this issue, for a quick fix just use redux to store the latest fetched location and to indicate that there is an active location request that has been triggered in any of the other pages, wait for it to resolve, then return the result after fetching the location and storing it and toggling the indicator in redux. not sure if this is the optimal solution for this problem, I would really appreciate any feedback

import Geolocation from 'react-native-geolocation-service';
import {
  PermissionsAndroid,
  Platform,
  Alert,
  Linking,
  ToastAndroid,
} from 'react-native';
import {useSelector, useDispatch} from 'react-redux';
import {ActionTypes} from '../../constants';
import {useEffect, useRef} from 'react';

function useUserLocation() {
  const {location = null, isFetchingLocation = false} = useSelector(
    ({Shared}: any) => Shared,
  );
  let _isFetchingLocation = useRef(isFetchingLocation);
  let _location = useRef(location);

  const dispatch = useDispatch();

  const setLocation = (value) => {
    dispatch({type: ActionTypes.SET_LOCATION, value});
  };
  const setIsFetching = (value) => {
    dispatch({type: ActionTypes.IS_FETCHING_LOCATION, value});
  };

  useEffect(() => {
    _isFetchingLocation.current = isFetchingLocation;
    _location.current = location;
  }, [isFetchingLocation, location]);

  const hasLocationPermissionIOS = async () => {
    const openSetting = () => {
      Linking.openSettings().catch(() => {
        Alert.alert('Unable to open settings');
      });
    };
    const status = await Geolocation.requestAuthorization('whenInUse');

    if (status === 'granted') {
      return true;
    }

    if (status === 'denied') {
      Alert.alert('Location permission denied');
    }

    if (status === 'disabled') {
      Alert.alert(
        `Turn on Location Services to allow "Kader" to determine your location.`,
        '',
        [
          {text: 'Go to Settings', onPress: openSetting},
          {text: "Don't Use Location", onPress: () => {}},
        ],
      );
    }

    return false;
  };
  const hasLocationPermission = async () => {
    if (Platform.OS === 'ios') {
      const hasPermission = await hasLocationPermissionIOS();
      return hasPermission;
    }

    if (Platform.OS === 'android' && Platform.Version < 23) {
      return true;
    }

    const hasPermission = await PermissionsAndroid.check(
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
    );

    if (hasPermission) {
      return true;
    }

    const status = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
    );

    if (status === PermissionsAndroid.RESULTS.GRANTED) {
      return true;
    }

    if (status === PermissionsAndroid.RESULTS.DENIED) {
      ToastAndroid.show(
        'Location permission denied by user.',
        ToastAndroid.LONG,
      );
    } else if (status === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) {
      ToastAndroid.show(
        'Location permission revoked by user.',
        ToastAndroid.LONG,
      );
    }

    return false;
  };
  const getLocation = async () => {
    return new Promise(async (resolve, reject) => {
      const hasPermission = await hasLocationPermission();
      if (!hasPermission) {
        setLocation(null);
        reject(
          new Error(
              'Location access permission needed'
          ),
        );
      }
      if (_location.current) {
        return resolve(_location.current.coords);
      }
      if (_isFetchingLocation.current) {
        const waitForLocation = () => {
          setTimeout(() => {
            if (!_isFetchingLocation.current) {
              return resolve(
                _location.current ? _location.current.coords : null,
              );
            } else {
              waitForLocation();
            }
          }, 500);
        };
        waitForLocation();
      } else {
        setIsFetching(true);
        Geolocation.getCurrentPosition(
          (position) => {
            setLocation(position);
            setIsFetching(false);
            return resolve(position.coords);
          },
          (error) => {
            console.debug(error);
            setLocation(null);
            setIsFetching(false);
            return reject(error);
          },
          {},
        );
      }
    });
  };

  return {
    getLocation,
  };
}

export default useUserLocation;
mppperez commented 4 years ago

We've solved the issue simply with wrapping the invocation in a "runWithTimeout" method that is rejecting the promise if we don't receive a response within a specific timeframe:

export const runWithTimeout = <P> (timeout: number, promise: Promise<P>) => {
    return new Promise<P>((resolve, reject) => {
        BackgroundTimer.setTimeout(() => {
            reject(new Error("timeout"))
        }, timeout);
        promise.then(resolve, reject)
    })
};

getCurrentPosition must be wrapped therefor:

const requestPosition = (/* your parameters ... */): Promise<GeoPosition> => {
    return new Promise((resolve, reject) => {
        Geolocation.getCurrentPosition(
            (pos) => {
                // success callback
            },
            (error) => {
                // error callback
            },
            {
                // options
            }
        );
    });
};

Then simply invoke it like this:

runWithTimeout(timeoutPos, requestPosition(/* your parameters ... */));

Note that BackgroundTimer from react-native-background-timer is used here to ensure that the this will also work if the app is going to background.

Agontuk commented 4 years ago

Can you provide an use case where parallel location request is needed ? I haven't give it much thought since I did not face any situation where this is needed.

mppperez commented 4 years ago

The use case in the app I'm currently working on (existing app with existing features) is that there are events that may include a geolocation but don't have to. Other events require a geolocation and cannot be send to the server without.

Those that don't require a location but no location could be determined at the time of the event (timeout or accuarcy too high) will be put into a queue. Then (for about 5 mins) it will be tried to add a geolocation afterwards for those objects in the queue by a background task.

This worked fine with the old geolocation library (@react-native-community/geolocation) but we noticed that after changing to this library most of them had no location after these 5 mins since new events were fired that blocked geolocation api or they prevented each other by getting a location.

Expected behavior would be that at least the promise or an error would be returned as expected by a promise rather than dismissing it.

Agontuk commented 3 years ago

Can you try the new implementation from android-rewrite branch ? Let me know if this works for concurrent getCurrentPosition requests.

Agontuk commented 2 years ago

Closing this as the latest version should handle multiple location calls properly.