react-native-background-geolocation icon indicating copy to clipboard operation
react-native-background-geolocation copied to clipboard

Don`t have background tracking (dot trail) with location setting 4 (While using the app) (Android)

Open denisvely opened this issue 3 years ago • 6 comments

Your Environment

  • Plugin version: "^4.6.0"
  • Platform: Android
  • OS version: Android version 11
  • Device manufacturer / model: Samsung / A12
  • React Native version (react-native -v): "^0.66.3"
  • Plugin config

What settings should we use in order to have background tracking (dot trail) with location setting 4 (While using the app)? How can we have pins on the map the whole path from begining to end without gaps? We wan’t to have pins while the app is minimised without activating the location setting on 5 (Always). Technically the app is activated and then minimised (still working on background) not killed.

// Starting the plugin

import BackgroundGeolocation from 'react-native-background-geolocation';

import LocationPermissions from '../../components/LocationPermissions/LocationPermissions';
import saveLocationService from '../../screens/Activity/Activity/components/MapComponent/services/SaveLocationService';
import {setBackgroundLocationPermission} from '../../store/backgroundGeolocation';

const sendLocation = saveLocationService.saveLocation();

let lastLatitude = null;
let lastLongtitude = null;
let isFirstLoad = true;
let shouldSkipProvider = true; // Provider change triggers when app is started and background location inits twice ( this is to prevent doubled initilization)

export const getCurrentUserLocation = () => {
	return BackgroundGeolocation.getCurrentPosition({
		timeout: 5,
		persist: true,
		maximumAge: 5000,
		desiredAccuracy: 10,
		samples: 1,
	});
};

const backgroundLocation = (dispatch) => {
	LocationPermissions.checkPermission().then(async ({permissionGranted}) => {
		if (permissionGranted) {
			BackgroundGeolocation.stop();
			// Set if we have permission or not
			setBackgroundLocationPermission(dispatch, true);
			// Start the plugin
			initBackgroundGeolocation();
		} else {
			// Set if we have permission or not
			BackgroundGeolocation.stop();
			BackgroundGeolocation.onProviderChange(onProviderChange);
			setBackgroundLocationPermission(dispatch, false);
		}
	});

	const initBackgroundGeolocation = () => {
		BackgroundGeolocation.onProviderChange(onProviderChange);
		BackgroundGeolocation.onLocation(onLocation, onLocationError);
		BackgroundGeolocation.onHeartbeat(onHeartbeat);
		BackgroundGeolocation.ready({
			locationAuthorizationRequest: 'WhenInUse',
			desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
			distanceFilter: 500,
			activityRecognitionInterval: 10000, // Android
			fastestLocationUpdateInterval: 10000,
			forceReloadOnGeofence: true,
			stopOnTerminate: true,
			startOnBoot: true,
			foregroundService: true,
			heartbeatInterval: 60 * 10,
			logLevel: BackgroundGeolocation.LOG_LEVEL_OFF,
			autoSync: false,
			reset: true,
			extras: {
				route_id: 1,
				filterDistance: 500,
			},
			disableLocationAuthorizationAlert: true,
		})
			.then((state) => {
				console.log('- BackgroundGeolocation is ready: ', state.enabled);
				if (!state.enabled) {
					BackgroundGeolocation.start(async () => {
						if (isFirstLoad) {
							const userLocation = await getCurrentUserLocation();
							if (userLocation && !userLocation.sample) {
								// Sending location coords to the server
								sendLocation.fetch(userLocation);
							}
							isFirstLoad = false;
						}
					});
				}
			})
			.catch((error) => {
				console.log('- BackgroundGeolocation error: ', error);
			});
	};

	const onLocation = (location) => {
		if (location && !location.sample && location.extras.route_id === 1) {
			if (
				lastLongtitude &&
				lastLongtitude.toFixed(7) !== location.coords.longitude.toFixed(7) &&
				lastLatitude &&
				lastLatitude.toFixed(7) !== location.coords.latitude.toFixed(7)
			) {
				// Sending location coords to the server
				sendLocation.fetch(location);
				lastLongtitude = location.coords.longitude;
				lastLatitude = location.coords.latitude;
			} else {
				if (lastLongtitude === null && lastLatitude === null) {
					lastLongtitude = location.coords.longitude;
					lastLatitude = location.coords.latitude;
					// Sending location coords to the server
					sendLocation.fetch(location);
				} else {
					lastLongtitude = location.coords.longitude;
					lastLatitude = location.coords.latitude;
				}
			}
		}
	};

	const onHeartbeat = (location) => {
		console.log('Heartbeat', location);
	};
	const onLocationError = (event) => {
		console.warn('[event] location ERROR: ', event);
	};

	const onProviderChange = (provider) => {
		if (shouldSkipProvider) {
			shouldSkipProvider = false;
			return;
		}
		switch (provider.status) {
			case BackgroundGeolocation.AUTHORIZATION_STATUS_ALWAYS:
				BackgroundGeolocation.setConfig({locationAuthorizationRequest: 'WhenInUse'}, () => {
					// Set if we have permission or not
					setBackgroundLocationPermission(dispatch, true);
					// Start the plugin
					initBackgroundGeolocation('WhenInUse');
				});
				break;
			case BackgroundGeolocation.AUTHORIZATION_STATUS_WHEN_IN_USE:
				BackgroundGeolocation.setConfig({locationAuthorizationRequest: 'WhenInUse'}, () => {
					// Set if we have permission or not
					setBackgroundLocationPermission(dispatch, true);
					// Start the plugin
					initBackgroundGeolocation('WhenInUse');
				});
				break;
			case BackgroundGeolocation.AUTHORIZATION_STATUS_DENIED:
				BackgroundGeolocation.stop();
				BackgroundGeolocation.setConfig({locationAuthorizationRequest: 'Never'}, () => {
					// Set if we have permission or not
					setBackgroundLocationPermission(dispatch, false);
				});
				break;
			default:
				BackgroundGeolocation.stop();
				// Set if we have permission or not
				setBackgroundLocationPermission(dispatch, false);
				break;
		}
	};
};

export default backgroundLocation;

Here is our New Workout Component: // Starting the workout.

import React, {useRef, useEffect} from 'react';
import {View, Pressable} from 'react-native';
import PropTypes from 'prop-types';
import {useTranslation} from 'react-i18next';
import MapView, {Marker, Polyline} from 'react-native-maps';
import BackgroundGeolocation from 'react-native-background-geolocation';
import {deviceWidth, deviceHeight} from 'utils';
import {Typography, GradientButton} from 'components';
import {SvgXml} from 'react-native-svg';
import {userPin, mapCenter} from '../../../Activity/components/MapIcons/MapIcons';
import {
	convertToTimeString,
	calculateDisplacementDifference,
} from '../../../Activity/components/MapComponent/helpers/helper';
import {useDispatch} from 'react-redux';
import {backgroundLocation} from '../../../../../utils';
import mapStyles from '../../../Activity/components/MapComponent/map.json';
import {getBackgroundGeolocationPermission} from '../../../../../store/backgroundGeolocation';
import {useSelector} from 'react-redux';
import HikingMapOverlay from '../HikingMapOverlay/HikingMapOverlay';

import styles from './hikingMapStyles';

const LATITUDE_DELTA = 0.00922;
const LONGITUDE_DELTA = LATITUDE_DELTA * (deviceWidth / deviceHeight);

const distanceFilterMetres = 10;

const HikingMap = ({state, setState}) => {
	const {t} = useTranslation();
	const mapView = useRef(null);
	const dispatch = useDispatch();
	const isPermmissionGranteed = useSelector((state) => getBackgroundGeolocationPermission(state));

	const setCenter = async (location, isFirstTime) => {
		if (location === null && isFirstTime) {
			location = await BackgroundGeolocation.getCurrentPosition({
				timeout: 30,
				persist: true,
				maximumAge: 5000,
				desiredAccuracy: 10,
				samples: 3,
			});
			addMarker(location);
		}

		if (!mapView) {
			return;
		}

		if (location) {
			mapView.current.animateToRegion({
				latitude: location.coords.latitude,
				longitude: location.coords.longitude,
				latitudeDelta: LATITUDE_DELTA,
				longitudeDelta: LONGITUDE_DELTA,
			});
		}
	};

	const centerMap = async () => {
		location = await BackgroundGeolocation.getCurrentPosition({
			timeout: 30,
			persist: true,
			maximumAge: 5000,
			desiredAccuracy: 10,
			samples: 1,
		});

		if (location) {
			mapView.current.animateToRegion({
				latitude: location.coords.latitude,
				longitude: location.coords.longitude,
				latitudeDelta: LATITUDE_DELTA,
				longitudeDelta: LONGITUDE_DELTA,
			});
		}
	};

	const backgroundGeolocationSevices = async () => {
		await BackgroundGeolocation.changePace(true);
		BackgroundGeolocation.resetOdometer();
		BackgroundGeolocation.setConfig({
			locationAuthorizationRequest: 'WhenInUse',
			distanceFilter: distanceFilterMetres,
			extras: {
				route_id: 2,
				filterDistance: 10,
			},
		}).then(async (state) => {
			setState((prevState) => ({
				...prevState,
				enabled: state.enabled,
				isMoving: state.isMoving,
				showsUserLocation: state.enabled,
				hikeStartTimestamp: Date.now(),
			}));
			BackgroundGeolocation.onLocation(onLocation, onLocationError);
		});
	};

	const onLocation = (location) => {
		if (location.error === 0) {
			BackgroundGeolocation.stop();
			return;
		}
		if (!location.sample && location.extras.route_id === 2) {
			addMarker(location);
			setState((prevState) => ({
				...prevState,
				lastLocation: location,
			}));
			setCenter(location);
		}
	};

	// Handle Location Data
	useEffect(() => {
		if (state.lastLocation !== null) {
			let displacementDifference = 0;
			const altitudeValue = state.lastLocation.coords.altitude;
			if (state.altitude !== null) {
				displacementDifference = parseInt(
					state.displacementDiff +
						calculateDisplacementDifference(
							parseFloat(state.displacement),
							state.lastLocation.coords.altitude,
						),
				);
			}
			setState((prevState) => ({
				...prevState,
				distance: parseInt(state.lastLocation.odometer).toFixed(0),
				altitude: altitudeValue,
				displacement: state.altitude === null ? state.lastLocation.coords.altitude : state.altitude,
				displacementDiff: displacementDifference,
			}));
		}
	}, [state.lastLocation]);

	const onLocationError = (event) => {
		console.warn('[event] location ERROR: ', event);
	};

	const addMarker = (location) => {
		const marker = {
			key: location.uuid,
			title: location.timestamp,
			heading: location.coords.heading,
			coordinate: {
				latitude: location.coords.latitude,
				longitude: location.coords.longitude,
			},
		};

		setState((prevState) => ({
			...prevState,
			markers: [...prevState.markers, marker],
			coordinates: [
				...prevState.coordinates,
				{
					latitude: location.coords.latitude,
					longitude: location.coords.longitude,
				},
			],
		}));
	};

	useEffect(() => {
		setCenter(null, true);
		backgroundGeolocationSevices();

		return () => {
			BackgroundGeolocation.stop();
			backgroundLocation(dispatch, false);
		};
	}, []);

	useEffect(() => {
		if (state.hikeStartTimestamp) {
			var workoutTimer = setInterval(() => {
				const timeString = convertToTimeString(state.hikeStartTimestamp);
				setState((prevState) => ({...prevState, timeString: timeString}));
			}, 1000);
		}
		return () => clearInterval(workoutTimer);
	}, [state.hikeStartTimestamp]);

	return (
		<>
			<View style={styles.finishHiking}>
				<GradientButton
					text={t('activity.end')}
					size="small"
					onPress={() => setState((prevState) => ({...prevState, isFinishWorkoutModalVisible: true}))}
				/>
			</View>
			{!isPermmissionGranteed && <HikingMapOverlay />}
			<MapView
				customMapStyle={mapStyles}
				ref={mapView}
				style={styles.map}
				showsUserLocation={true}
				scrollEnabled={true}
				showsMyLocationButton={false}
				showsPointsOfInterest={false}
				showsScale={false}
				showsTraffic={false}
				toolbarEnabled={false}>
				<Polyline
					key="polyline"
					coordinates={state.coordinates}
					geodesic={true}
					strokeColor="transparent"
					strokeWidth={0}
					zIndex={0}
				/>
				{state.markers?.map((marker, index) => (
					<Marker
						key={`markerKey${index}`}
						coordinate={marker.coordinate}
						anchor={{x: 0, y: 0.1}}
						title={marker.title}>
						{index === 0 ? (
							<View style={{width: 36.4, height: 49}}>
								<SvgXml xml={userPin} width="36" height="49" />
							</View>
						) : (
							<View style={styles.markerIcon}></View>
						)}
					</Marker>
				))}
			</MapView>
			<View style={styles.centerButton}>
				<Pressable onPress={() => centerMap()}>
					<SvgXml xml={mapCenter} width="25" height="25" />
				</Pressable>
			</View>
			<View style={styles.workoutInfo}>
				<View style={styles.leftView}>
					<Typography name="normal" text={t('activity.distance')} style={styles.text} />
					<Typography
						name="extraLargeNormal"
						text={`${state.distance} ${t('activity.metersShort')}`}
						style={styles.text}
					/>
				</View>
				<View>
					<View style={styles.rightView}>
						<Typography name="tiny" text={t('activity.time')} style={styles.text} />
						<Typography name="large" text={state.timeString} style={styles.text} />
					</View>
					<View style={styles.rightView}>
						<Typography name="tiny" text={t('activity.displacement')} style={styles.text} />
						<Typography
							name="large"
							text={`${state.displacementDiff.toFixed(0)} ${t('activity.metersShort')}`}
							style={styles.text}
						/>
					</View>
				</View>
			</View>
		</>
	);
};

HikingMap.propTypes = {
	state: PropTypes.object.isRequired,
	setState: PropTypes.func.isRequired,
};

export default HikingMap;

Expected Behavior

We have pins on the map the whole path from begining to end without gaps.

Actual Behavior

The app is not drawing on background and we have gaps.

Steps to Reproduce

The scenario is the following:

  1. We start the app
  2. We initiate “tracking mode”
  3. We start walking while the app is on the screen and the app is placing pins on the map
  4. We minimise the app, lock the phone and put it in our pocket.
  5. We walk for another 3 km and then we take the phone out of the pocket, unlock the screen, navigate between all open apps and chose the tracking app to see the map and pins.
  6. The result is that there are no pins from the time when we minimised the app and locked the phone and the time we unlocked the phone and viewed on screen the tracking app.
  7. After that we continue the walk with the app on screen for another 3 km and again we have pins on the map and everything works perfectly.

In the end we have tracking for in the begining then gap (no tracking) in the middle and again tracking in the end.

Context

New Workout functionality

Debug logs

Logs
PASTE_YOUR_LOGS_HERE

denisvely avatar May 09 '22 07:05 denisvely

Why have you posted two issues (#1475

christocracy avatar May 09 '22 12:05 christocracy

  • OS version: ??

  • See Wiki Debugging. Learn how to observe the plugin's logs. See API docs Config.logLevel, Config.debug
  • See https://dontkillmyapp.com

christocracy avatar May 09 '22 12:05 christocracy

Why do you call .start() immediately after .ready() is called?

				if (!state.enabled) {
					BackgroundGeolocation.start(async () => {
						if (isFirstLoad) {
							const userLocation = await getCurrentUserLocation();
							if (userLocation && !userLocation.sample) {
								// Sending location coords to the server
								sendLocation.fetch(userLocation);
							}
							isFirstLoad = false;
						}
					});
				}			
  • Call .ready() as soon as your app boots, regardless of whether you don't want to start tracking. Calling .ready() does not imply "start tracking". .ready() is a signal to the plugin that your app has launched. You do not call .ready() only when you're ready to start location tracking.

  • When you wish to start a "New Workout", then you do this:

await BackgroundGeolocation.start();
await BackgroundGeolocation.changePace();
  • When you wish to stop a "Workout", you do this:
BackgroundGeolocation.stop()

christocracy avatar May 13 '22 12:05 christocracy

Also, did you follow this instruction in the Setup Guide?

christocracy avatar May 13 '22 12:05 christocracy

.ready() is designed to be called only once, at the boot of your app.

Your logs are full of:

⚠️ #ready already called. Redirecting to #setConfig

christocracy avatar May 13 '22 12:05 christocracy

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

stale[bot] avatar Sep 21 '22 03:09 stale[bot]