1

This is one of my first posts ever. When I'm stuck I usually just hit my head against a wall, but today I'm asking for help. I've got a ReactNative app built using Expo which does some realtime location monitoring. Which works when the component is active on the screen, but when the app goes into the background, it stops functioning. I've attempted to implement a background task using expo-task-manager, but for the life of me I can't seem to get it to work. I've implemented it to use haptics to notify me that it's functioning properly while testing in the field, and while it seems to work ok on the simulator (I get the console.log at least), the real device only buzzes when the component is active and on screen. I am able to see the blue indicator when the app is not active, but no haptics.

Can someone figure out what I've missed here? Thank you so much in advance.

Here's my code:

import React, { useState, useEffect } from 'react';
import { Text, View, Button } from 'react-native';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import * as Haptics from 'expo-haptics';

const DESTINATION_COORDS = {
  latitude: 44.0041179865438,
  longitude: -121.68169920997431,
};

const BACKGROUND_LOCATION_TASK = 'background-location-task-buzz-v2';

TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }) => {
  if (error) {
    console.error('Background location task error:', error.message);
    return;
  }
  
  const { locations } = data;
  if (locations && locations.length > 0) {
    const { latitude, longitude } = locations[0].coords;
    const distance = calculateDistance(
      latitude,
      longitude,
      DESTINATION_COORDS.latitude,
      DESTINATION_COORDS.longitude
    );
    if (distance < 10) {
      await triggerHaptics();
      console.log('found it.', Date.now());
    }
  }
});

const calculateDistance = (lat1, lon1, lat2, lon2) => {
  const R = 6371; // Radius of the earth in km
  const dLat = deg2rad(lat2 - lat1);  // deg2rad below
  const dLon = deg2rad(lon2 - lon1);
  const a = 
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * 
    Math.sin(dLon / 2) * Math.sin(dLon / 2)
    ; 
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 
  const d = R * c; // Distance in km
  return d * 1000; // Convert to meters
};

const deg2rad = (deg) => {
  return deg * (Math.PI / 180);
};

const triggerHaptics = async () => {
  await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};

const BuzzOnArrival = () => {
  const [currentLocation, setCurrentLocation] = useState(null);
  const [distanceToDestination, setDistanceToDestination] = useState(null);

  useEffect(() => {
    // Start background location updates when component mounts
    Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, {
      accuracy: Location.Accuracy.BestForNavigation,
      timeInterval: 10000, // Check every 10 seconds
      distanceInterval: 0,
      showsBackgroundLocationIndicator: true,
    });

    // Clean up function to stop background location updates when component unmounts
    return () => {
      Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK);
    };
  }, []);

  useEffect(() => {
    // Fetch current location every second
    const interval = setInterval(() => {
      fetchCurrentLocation();
    }, 1000);

    // Clean up function to clear the interval when component unmounts
    return () => clearInterval(interval);
  }, []);


  const requestPermissions = async () => {
        const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync();
        if (foregroundStatus === 'granted') {
          const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync();
          if (backgroundStatus === 'granted') {
            await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
              accuracy: Location.Accuracy.Balanced,
            });
          }
        }
      };

  const fetchCurrentLocation = async () => {


    const location = await Location.getCurrentPositionAsync({});
    setCurrentLocation(location.coords);

    // Calculate distance to destination
    const distance = calculateDistance(
      location.coords.latitude,
      location.coords.longitude,
      DESTINATION_COORDS.latitude,
      DESTINATION_COORDS.longitude
    );
    setDistanceToDestination(distance);
  };

  const handleButtonPress = async () => {
    await triggerHaptics();
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Listening for arrival...</Text>
      {currentLocation && (
        <Text>Current Location: {currentLocation.latitude}, {currentLocation.longitude}</Text>
      )}
      <Text>Destination: {DESTINATION_COORDS.latitude}, {DESTINATION_COORDS.longitude}</Text>
      <Text>Distance to Destination: {distanceToDestination?.toFixed(2)} meters</Text>
      <Button title="Test Haptics" onPress={handleButtonPress} />
      <Button onPress={requestPermissions} title="Enable background location" />
    </View>
  );
};

export default BuzzOnArrival;

And here is my package.json:

{
  "name": "geocaster",
  "version": "1.0.0",
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@gorhom/bottom-sheet": "^4.6.1",
    "@react-native-async-storage/async-storage": "1.21.0",
    "@reduxjs/toolkit": "^2.0.1",
    "@supabase/supabase-js": "^2.39.3",
    "base64-arraybuffer": "^1.0.2",
    "expo": "~50.0.2",
    "expo-av": "~13.10.3",
    "expo-background-fetch": "^11.8.1",
    "expo-camera": "~14.0.1",
    "expo-constants": "~15.4.5",
    "expo-crypto": "~12.8.0",
    "expo-file-system": "~16.0.6",
    "expo-haptics": "~12.8.1",
    "expo-image-picker": "~14.7.1",
    "expo-linking": "~6.2.2",
    "expo-location": "~16.5.2",
    "expo-media-library": "~15.9.1",
    "expo-notifications": "~0.27.5",
    "expo-router": "~3.4.4",
    "expo-secure-store": "~12.8.1",
    "expo-sensors": "~12.9.1",
    "expo-status-bar": "~1.11.1",
    "expo-task-manager": "~11.7.0",
    "expo-updates": "~0.24.12",
    "expo-web-browser": "~12.8.2",
    "firebase": "^10.7.1",
    "geolib": "^3.3.4",
    "jszip": "^3.10.1",
    "jszip-utils": "^0.1.0",
    "lucide-react-native": "^0.314.0",
    "pullstate": "^1.25.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.73.2",
    "react-native-draggable-flatlist": "^4.0.1",
    "react-native-easy-grid": "^0.2.2",
    "react-native-gesture-handler": "^2.16.0",
    "react-native-maps": "1.8.0",
    "react-native-paper": "^5.12.1",
    "react-native-progress": "^5.0.1",
    "react-native-reanimated": "^3.8.1",
    "react-native-safe-area-context": "4.8.2",
    "react-native-screens": "~3.29.0",
    "react-native-snap-carousel": "^3.9.1",
    "react-native-svg": "14.1.0",
    "react-native-web": "~0.19.6",
    "react-redux": "^9.1.0",
    "redux": "^5.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0"
  },
  "private": true
}

I also am providing what I believe is the relevant parts of my app.json:

{
  "expo": {
    ...
    "ios": {
      ...,
      "infoPlist": {
        "UIBackgroundModes": ["location", "fetch"],
        "NSLocationAlwaysAndWhenInUseUsageDescription": "Placeholder",
        "NSLocationAlwaysUsageDescription": "Placeholder",
        "NSLocationWhenInUseUsageDescription": "Placeholder",
      }
    },
    ...,
    "plugins": [
      "expo-router",
      "expo-secure-store",
      [
        "expo-location",
        {
          "locationAlwaysAndWhenInUsePermission": "Placeholder",
          "isAndroidBackgroundLocationEnabled": true,
          "isIosBackgroundLocationEnabled": true
        }
      ],
    ],
    ...
  }
}

and even my Info.plist:

        <key>NSLocationUsageDescription</key>
    <string>Use of location for determining waypoints and proximity</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Use of location for determing waypoints and proximity</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>The Microphone will be used to record videos for waypoints.</string>
    <key>NSMotionUsageDescription</key>
    <string>The of motion sensors required for the compass.</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>The photo library can be used to select images and videos for upload.</string>
    <key>NSUserActivityTypes</key>
    <array>
        <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
    </array>
    <key>UIBackgroundModes</key>
    <array>
        <string>location</string>
        <string>fetch</string>
    </array>
    

I'm focused on iOS for the moment so have only tested it there. I've tried working with it in the simulator, and on real devices. The code above buzzes when the component is active, but not when it's in the background.

I would expect it to buzz as I get within about ten meters of the DESTINATION_COORDS, whether I have the app in the foreground OR in the background.

Right now it only buzzes when the app is in the foreground and the component is actively on the screen.

2
  • You can't use a background task. If you have always location permission and background location updates then you will receive periodic location updates. You need to perform processing in response to the update. Looking at what you are trying to do, however, you might be much better off using iOS' inbuilt geofencing support. Commented May 3, 2024 at 3:55
  • Thanks for the feedback. I guess I'm also wondering if anyone has a functional demo of something that works in a similar fashion to what I'm trying to do? Commented May 4, 2024 at 18:56

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.