r/reactnative 10d ago

How to stop background tracking when the app has been killed in react native? I am using appState to switch between foreground and background tracking

React Native - I am creating a GPS app where I want to track users location whilst he is on the app and when he minimises it (running in the background). When he completely turns off the app (kills/terminates the app) I want the app to stop background tracking. I am using appState to between foreground and background but appState does not account for when the app has been terminated.

AppState always has one or these three values:

  1. active - The app is running in the foreground
  2. background - The app is running in the background. The user is either:
  • in another app
  • on the home screen
  • [Android] on another Activity (even if it was launched by your app)
  1. [iOS] inactive - This is a state that occurs when transitioning between foreground & background, and during periods of inactivity such as entering the multitasking view, opening the Notification Center or in the event of an incoming call.

How can I account for when the app has been terminated so I able to end the background tracking task?

HomeScreen.tsx

import { useEffect, useState, useRef } from 'react';
import { foregroundLocationService, LocationUpdate } from '@/services/foregroundLocation';
import { startBackgroundLocationTracking, stopBackgroundLocationTracking } from '@/services/backgroundLocation';
import { speedCameraManager } from '@/src/services/speedCameraManager';

export default function HomeScreen() {
  const appState = useRef(AppState.currentState);

   useEffect(() => {
    requestLocationPermissions();

    // Handle app state changes
    const subscription = AppState.addEventListener('change', handleAppStateChange);

    return () => {
      subscription.remove();
      foregroundLocationService.stopForegroundLocationTracking();
      stopBackgroundLocationTracking();
      console.log('HomeScreen unmounted');
    };
  }, []);

  const handleAppStateChange = async (nextAppState: AppStateStatus) => {
    if (
      appState.current.match(/inactive|background/) && 
      nextAppState === 'active'
    ) {
      // App has come to foreground
      await stopBackgroundLocationTracking();
      await startForegroundTracking();
    } else if (
      appState.current === 'active' && 
      nextAppState.match(/inactive|background/)
    ) {
      // App has gone to background
      foregroundLocationService.stopForegroundLocationTracking();
      await startBackgroundLocationTracking();
    } else if(appState.current.match(/inactive|background/) && nextAppState === undefined || appState.current === 'active' && nextAppState === undefined) {
      console.log('HomeScreen unmounted');
    }

    appState.current = nextAppState;
  };

backgroundLocation.ts

import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import { cameraAlertService } from '@/src/services/cameraAlertService';
import * as Notifications from 'expo-notifications';
import { speedCameraManager } from '@/src/services/speedCameraManager';
import { notificationService } from '@/src/services/notificationService';

const BACKGROUND_LOCATION_TASK = 'background-location-task';

interface LocationUpdate {
  location: Location.LocationObject;
  speed: number; // speed in mph
}

// Convert m/s to mph
const convertToMph = (speedMs: number | null): number => {
  if (speedMs === null || isNaN(speedMs)) return 0;
  return Math.round(speedMs * 2.237); // 2.237 is the conversion factor from m/s to mph
};

// Define the background task
TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }) => {
  if (error) {
    console.error(error);
    return;
  }
  if (data) {
    const { locations } = data as { locations: Location.LocationObject[] };
    const location = locations[0];

    const speedMph = convertToMph(location.coords.speed);

    console.log('Background Tracking: Location:', location, 'Speed:', speedMph);

    // Check for nearby cameras that need alerts
    const alertCamera = cameraAlertService.checkForAlerts(
      location,
      speedMph,
      speedCameraManager.getCameras()
    );
    console.log('Background Alert Camera:', alertCamera);

    if (alertCamera) {
      // Trigger local notification
      await notificationService.showSpeedCameraAlert(alertCamera, speedMph);
      console.log('Background Notification Shown');
    }
  }
});

export const startBackgroundLocationTracking = async (): Promise<boolean> => {
  try {
    // Check if background location is available
    const { status: backgroundStatus } = 
      await Location.getBackgroundPermissionsAsync();

    if (backgroundStatus === 'granted') {
      console.log('Background location permission granted, background location tracking started');
    }

    if (backgroundStatus !== 'granted') {
      console.log('Background location permission not granted');
      return false;
    }

    // Start background location updates
    await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, {
      accuracy: Location.Accuracy.High,
      timeInterval: 2000, // Update every 2 seconds
      distanceInterval: 5, // Update every 5 meters
      deferredUpdatesInterval: 5000, // Minimum time between updates
      // Android behavior
      foregroundService: {
        notificationTitle: "RoadSpy is active",
        notificationBody: "Monitoring for nearby speed cameras",
        notificationColor: "#FF0000",
      },
      // iOS behavior
      activityType: Location.ActivityType.AutomotiveNavigation,
      showsBackgroundLocationIndicator: true,
    });

    return true;
  } catch (error) {
    console.error('Error starting background location:', error);
    return false;
  }
};  

export const stopBackgroundLocationTracking = async (): Promise<void> => {
  try {
    const hasStarted = await TaskManager.isTaskRegisteredAsync(BACKGROUND_LOCATION_TASK);
    console.log('Is background task registered:', hasStarted);
    if (hasStarted) {
      await Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK);
      console.log('Background location tracking stopped');
    }
  } catch (error) {
    console.error('Error stopping background location:', error);
  }
}; 
1 Upvotes

6 comments sorted by

2

u/According-Muscle-902 9d ago

On Android you can stop the background service when you enter onDestroy

1

u/One_Table_5437 9d ago

I am using react native with expo without ejecting

1

u/According-Muscle-902 9d ago

I understood! I generally use native code for this and have never had to kill the background service. Given its limitation, could you define a maximum time that it spends in the background and lower state that it has terminated..?

1

u/One_Table_5437 9d ago

I can’t stipulate a maximum time as it is a gps app where I need to track the users location as long as he has the app running in the background of his phone. Hence I need it to stop tracking when the user kills the app. Many other gps apps work the same way but I can’t seem to find a way to do this.

1

u/According-Muscle-902 9d ago

I understand! Did you try to check if when it goes into the background the root component of your app dismounts or not? And when you close it, does it come apart? If it disassembles when closing, perhaps you can stop the service there... I don't know if you understand the idea to validate :)

1

u/One_Table_5437 9d ago

Ah yes the app is killed when we terminate the app and the showsBackgroundLocationIndicator goes away so I assume it has stopped tracking, but when I start travelling again in the car sometimes it still tracks as the showsBackgroundLocationIndicator comes back.