MK_Pierce
MK_Pierce

Reputation: 966

Expo Background Permissions Async is not working

I am developing an app where my users will sometimes need to be tracked as they are outside of the app. I have gone through the expo docs and it appears I am supposed to work with Foreground and Background Permissions Async functions but my expo app does not recognize either version. Has anyone else encountered this? How do I fix this? Below is some of my code:

import * as  Permissions from 'expo-permissions'; 
import * as Request from 'expo-permissions';
import * as Location from 'expo-location';
import Constants from 'expo-constants'


useEffect(() => {
    (async () => {
      if (Platform.OS === 'android' && !Constants.isDevice) {
        setErrorMsg(
          'Oops, this will not work on Snack in an Android emulator. Try it on your device!'
        );
        return;
      }
      let { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') {
        setErrorMsg('Permission to access location was denied');
        return;
      }
      let location = await Location.getCurrentPositionAsync({});
      setLocation(location);
    })();
  }, []);

  useEffect (() =>{
    (async () => {
      if (Platform.OS === 'android' && !Constants.isDevice){
        setErrorMsg2(
          'Whoops'
        );
        return;
      }
      let { status2 } = await Location.requestBackgroundPermissionsAsync();
      if (status2 !== 'granted') {
        setErrorMsg2('Permission to access background location was denied');
        return;
      }
      let location2 = await Location.getCurrentPositionAsync({})
      setLocation2(location2)
    })()
  },[])

  let text = 'Waiting..';
  if (errorMsg) {
    text = errorMsg;
  }
   else if (location) {
    text = JSON.stringify(location);
  }

  let text2 = 'Also Wating...';
    if (errorMsg2){
      text2 = errorMsg2;
    }
    else if (location) {
      text2 = JSON.stringify(location)
    }

I am running Expo in managed workflow, currently using Expo version 3.23.2, but have also tested it on version 4.3.2, and have been thrown the same errors.

Upvotes: 2

Views: 8876

Answers (2)

Pencilcheck
Pencilcheck

Reputation: 2922

Instead of task runner from expo, I use notifee for dedicated foreground service to have more granular control. Then I simply create a simple function with setTimeout to run things periodically and use expo-location to grab the location.

import * as Location from 'expo-location';
import Constants from 'expo-constants';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { startActivityAsync, ActivityAction } from 'expo-intent-launcher';
import notifee, { type Event, AndroidColor, EventType } from '@notifee/react-native';
import type { LocationObject } from 'expo-location';

export { register, unregister }

async function waitUntil(
  conditionPromise: () => Promise<boolean>,
  loopFunc: () => Promise<any>,
  timeout: number
) {
  return await new Promise(resolve => {
    console.log(`Creating interval task at date: ${new Date().toISOString()}`);

    const interval = setInterval(
      async () => {
        const condition = await conditionPromise();
        if (condition) {
          console.log(`Stopping interval task at date: ${new Date().toISOString()}`);
          resolve('stopping');
          clearInterval(interval);
          return;
        } else {
          return loopFunc();
        }
      },
      timeout
    );
  });
}

async function register(func: (locations: LocationObject[]) => Promise<void>) {
  // Request permissions (required for iOS)
  await notifee.requestPermission()

  // Create a channel (required for Android)
  const channelId = await notifee.createChannel({
    id: 'default',
    name: 'Default Channel',
  });

  notifee.registerForegroundService((notification) => {
    console.log('register foreground', notification);
    return new Promise(() => {
      waitUntil(async () => {
        const value = await AsyncStorage.getItem('@Notification_id');
        //console.log(`AsyncStorage @Notification_id: ${value}`);
        return !value;
      }, async () => {
        let locationObject;

        try {
          locationObject = await Location.getCurrentPositionAsync({
            accuracy: Location.Accuracy.Highest,
          });
          //console.log(`Got background fetch call at date: ${new Date().toISOString()}`);

          const prevLocations = JSON.parse((await AsyncStorage.getItem('@Locations')) || '[]') as LocationObject[];
          const locations = [...prevLocations, locationObject]

          await func(locations); // callback to application layer

          // cleanup
          await AsyncStorage.removeItem('@Locations');

          // update our live status of notification to indicate it is working
          const notificationId = await AsyncStorage.getItem('@Notification_id');
          if (notificationId) {
            await notifee.displayNotification({
              id: notificationId,
              title: notification.title,
              body: notification.body,
              android: {
                ...notification.android,
                color: AndroidColor.NAVY,
                ongoing: true,
                progress: {
                  indeterminate: true,
                },
                pressAction: {
                  id: 'stop',
                },
              },
            });
          }
        } catch (e) {
          //console.log('Inside try catch', e);
          if (e instanceof Error) {
            const notificationId = await AsyncStorage.getItem('@Notification_id');
            // update our foreground notification to indicate error
            if (notificationId) {
              await notifee.displayNotification({
                id: notificationId,
                title: notification.title,
                body: e.message,
                android: {
                  ...notification.android,
                  color: AndroidColor.OLIVE,
                  ongoing: true,
                  progress: undefined,
                  pressAction: {
                    id: 'error',
                  },
                }
              });
            }

            if (/network request failed/i.test(e.message)) {
              // save
              await AsyncStorage.setItem('@Locations', JSON.stringify(locations));
            }
          }
        }
      }, 30000).catch(console.log)

      const eventCallback = async ({ type, detail }: Event) => {
        if (type === EventType.ACTION_PRESS || type === EventType.PRESS) {
          if (detail?.pressAction?.id === 'stop') {
            await notifee.stopForegroundService();
            await AsyncStorage.removeItem('@Notification_id');
          } else if (detail?.pressAction?.id === 'error') {
            // Go to location sources settings page, regardless of error
            await startActivityAsync(ActivityAction.LOCATION_SOURCE_SETTINGS);
          }
        }
      }

      notifee.onForegroundEvent(async ({ type, detail }) => {
        //console.log(`onForegroundEvent`, type, JSON.stringify(detail, null, 4));
        await eventCallback({ type, detail }).catch(console.log);
      });

      notifee.onBackgroundEvent(async ({ type, detail }) => {
        //console.log(`onBackgroundEvent`, type, JSON.stringify(detail, null, 4));
        await eventCallback({ type, detail });
      });
    });
  });

  const notificationId = await notifee.displayNotification({
    title: 'App',
    body: 'Tracking your location in the background. Click notification to disable.',
    android: {
      channelId,
      asForegroundService: true,
      color: AndroidColor.NAVY,
      ongoing: true,
      colorized: true,
      pressAction: {
        id: 'stop',
      },
    },
  });

  await AsyncStorage.setItem('@Notification_id', notificationId)

  console.log(`Registered notification at date: ${new Date().toISOString()}`);

  return notificationId;
}

async function unregister(notificationId?: string) {
  await notifee.stopForegroundService();
  notificationId && await notifee.cancelNotification(notificationId);

  await AsyncStorage.removeItem('@Notification_id');

  console.log(`Unregister notification at date: ${new Date().toISOString()}`);
}

Upvotes: 0

Kartikey
Kartikey

Reputation: 4992

While using expo-location, you have to keep these points in mind :

  1. Location.requestBackgroundPermissionsAsync() Asks the user to grant permissions for location while the app is in the background. On Android 11 or higher: this method will open the system settings page - before that happens you should explain to the user why your application needs background location permission.

  2. Foreground permissions should be granted before asking for the background permissions (your app can't obtain background permission without foreground permission).

Refer to the docs here

Note : This has been tested on SDK 41 and expo-location's version 12.2.1

Working Example for Background Location here

How I've implemented it

import { useState, useEffect } from "react";
import { Text, View, StyleSheet } from "react-native";
import * as Location from "expo-location";

export default function App() {
  const [location, setLocation] = useState(null);
  const [errorMsg, setErrorMsg] = useState(null);

  useEffect(() => {
    (async () => {
      let { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== "granted") {
        setErrorMsg("Permission to access location was denied");
        return;
      }

      let location = await Location.getCurrentPositionAsync({});
      setLocation(location);

      let backPerm = await Location.requestBackgroundPermissionsAsync();
      console.log(backPerm);
    })();
  }, []);

  let text = "Waiting..";
  if (errorMsg) {
    text = errorMsg;
  } else if (location) {
    text = JSON.stringify(location);
  }

  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>{text}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    paddingTop: 50,
    backgroundColor: "#ecf0f1",
    padding: 8,
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    fontWeight: "bold",
    textAlign: "center",
  },
});

First, it asks me the foreground Permission, I tapped on While Using the app

Foregorund Permission Prompt

Then, for background location permission it takes me to the device settings page to allow location access in the background

Background Location Permission Settings Page

If by any chance this doesn't work as expected, try these steps

  1. Clear all the permission given to Expo Go app.
  2. Clear Cache and app data
  3. Uninstall and restart your device, and then again download Expo go app

Upvotes: 7

Related Questions