transistorsoft / react-native-background-geolocation

Sophisticated, battery-conscious background-geolocation with motion-detection
http://shop.transistorsoft.com/pages/react-native-background-geolocation
MIT License
2.57k stars 424 forks source link

Schedule is not working as expected in IOS #2075

Open williamFportillo opened 1 week ago

williamFportillo commented 1 week ago

Your Environment

Expected Behavior

When a schedule is configured and the plug-in is started that schedule will always be running.

Actual Behavior

When a schedule is configured and the plug-in is initialized it starts working as expected. But sometimes after a certain number of days or even hours the schedule stops working. It seems that it does not detect positions or something like that.

To set up a schedule I am currently doing this:

const resetSchedule = async (workdays?: Workdays): Promise<void> => {
    try {
      const schedule = workdays ? getSchedule(workdays) : []; // getSchedule(workdays) returns a string array like this: ["1 00:00-18:00"]
      await BackgroundGeolocation.stopSchedule();
      await BackgroundGeolocation.stop();
      await BackgroundGeolocation.setConfig({
        schedule,
      });
    } catch (error) {
      Alert.alert('Something went wrong while reset schedule');
    }
  };

  const configureSchedule = async ({
    workdays, isTrackingAlwaysActive = isTrackingAlways,
  }: ConfigureScheduleParamsType): Promise<void> => {
    try {
      await resetSchedule(workdays);
      if (toggleStatus && isTrackingAlwaysActive) {
        await BackgroundGeolocation.start();
      }

      if (toggleStatus && !isTrackingAlwaysActive) {
        await BackgroundGeolocation.startSchedule();
      }
    } catch (error) {
      // error
    }
  };

Steps to Reproduce

  1. Configure a schedule with BackgroundGeolocation.setConfig()
  2. Trigger . startSchedule
  3. Left the app in background.

Context

Wednesday, 26 June

The schedule was set up and worked as expected, tracking the day from 00:00 to 18:00. Screenshot 2024-06-27 at 10 26 23 AM copy

As you can see, the tracking service stopped sending the location at 17:59:34 to the server. The user continued moving but was out of schedule, this behavior is expected. On the map, you can see an image with the information of the last position sent to the server.

Screenshot 2024-06-27 at 10 30 06 AM copy

Thursday, 27 June

  1. This is the last log from Wednesday, 26 June.
  2. This is the first log for Thursday, 27 June.

Screenshot 2024-06-27 at 11 27 20 AM

The user continued moving, but none of the positions were sent to the server, and we are unable to see them in the logs as well. The last thing we saw on the logs was something related to geofences but we are not using the functionality (just in case).

Screenshot 2024-06-27 at 10 33 17 AM

One thing I noticed in the documentation is that the schedule property receives an array. However, in the logs, the schedule property is using () for the array. Thats something is just happening on iOS and it could just be how the log is being printed, but I thought I'd mention it in case it helps you as well.

Screenshot 2024-06-27 at 10 37 19 AM

Debug logs

Logs I share with you the logs file because is too large. ![Screenshot 2024-06-28 at 12 22 04 PM](https://github.com/transistorsoft/react-native-background-geolocation/assets/17710256/bbd14a79-54a2-40e2-bce3-3a48f26f0b94) [background-geolocation.log-14.gz](https://github.com/user-attachments/files/16034667/background-geolocation.log-14.gz)
christocracy commented 1 week ago

One thing I noticed in the documentation is that the schedule property receives an array. However, in the logs, the schedule property is using () for the array.

This is nothing to worry about. This is merely how an Obj-c NSArray#as_string method prints its contents to the logs.

williamFportillo commented 4 days ago

1. I opened the app on Thursday, 27 June, and then I stopped and started the tracking again (This was after getting the logs that I already shared at the beginning of the issue).

Screenshot 2024-07-01 at 9 52 45 AM

2. You can see the tracking started working

Screenshot 2024-07-01 at 9 54 21 AM

3. The service worked as expected the whole day, respecting the schedule that was set up.

Screenshot 2024-07-01 at 9 58 50 AM

4. The next day (Friday 28th ) the service got a location update (as expected)

Screenshot 2024-07-01 at 9 59 33 AM


5. ⚠️ Keep an eye on this: The config has the Schedule array set up.

Screenshot 2024-07-01 at 10 03 03 AM


6. Same as I described at the beginning of the issue, the tracking stopped working but the last thing printed on the logs was this Geofences table creation. (We aren't using the geofence feature, maybe is just a part of the process but I mentioned it because may be a hint)

Screenshot 2024-07-01 at 10 06 50 AM


7. The service didn't report any location the whole day of Friday 28th, then The service started again the next day on Saturday 29th.

Screenshot 2024-07-01 at 10 10 40 AM


8. ⚠️ But notice the service settings for the schedule, is empty, and the last logs were the geofences (again).

Screenshot 2024-07-01 at 10 12 38 AM

9. Sunday 30th, the same thing as Saturday, the service logged the settings, the schedule array was empty and the last thing printed on the logs was the Geofence table.

Screenshot 2024-07-01 at 10 15 56 AM

I share with you the full logs background-geolocation.log-15.gz

@christocracy

christocracy commented 4 days ago

Are you absolutely sure that your app is calling .ready(config) each time your app is launched, no matter what — even if your app is automatically launched by the OS in the background?

williamFportillo commented 4 days ago

1. index.js

import { AppRegistry } from 'react-native';
import BackgroundGeolocation from 'react-native-background-geolocation';
import App from './App';

AppRegistry.registerComponent(appName, () => App);

BackgroundGeolocation.registerHeadlessTask(HeadlessTask);

2. App.tsx

In my App.tsx exist a global component <Geolocator /> and this component execute once a function that i use to do BackgroundGeolocation.ready

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import ErrorBoundary from './src/ErrorBoundary';
import Geolocator from '.src/Geolocator';

const App = () => (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
          <ErrorBoundary>
            <Geolocator />
            <MainStack />  {/* Handle my screens views */}
          </ErrorBoundary>
      </PersistGate>
    </Provider>
    );

export default App;

3. Geolocator.tsx

In this component i use a custom hook related with all the backgroungGeolocation functionality.

geolocatorConfig() execute BackgroundGeolocation.ready()

import { useEffect } from 'react';
import { useBackgroundGeolocation } from '@hooks';

const Geolocator = () => {
    const { geolocatorConfig } = useBackgroundGeolocation();

    useEffect(() => {
      /*
       ℹ️ The function that executes the BackgroundGeolocation.ready() 
       I think is called each time the app is launched, but I'm not sure 
       if it is because that is inside of this useEffect is prevented from being executed, 
       and I have to take it away from here. 
      */
      geolocatorConfig();
    }, []);

    return <></>;
};

export default Geolocator;

4. @hooks/useBackgroundGeolocation.ts

  1. geolocatorConfig execute BackgroundGeolocation.ready and this contain my current configuration.
  2. I have a button in the app that start/stop geolocation plug-in. Thus startGeolocation function start the plug-in.
  3. stopGeolocation function stop the plug-in,
  4. I have a section in the app where users can select if they want to use schedule or not. Thus configureSchedule execute a reset of the schedule and make a BackgroundGeolocation.setConfig to set the new schedule. This function also evaluate if is neccesary start the plug-in again.
    
    import BackgroundGeolocation, { Location, State } from 'react-native-background-geolocation';
    import { useAppDispatch, useAppSelector } from './useRedux.hook';

} export const useBackgroundGeolocation = () => { const dispatch = useAppDispatch(); const geolocatorSettings = useAppSelector((state) => state.geolocator.geolocatorSettings); const isTrackingAlways = useAppSelector((state) => state.geolocator.trackingAlways); const toggleStatus = useAppSelector((state) => state.geolocator.toggleStatus);

const geolocatorConfig = ( config: GeolocatorConfig = { userIdParam: undefined, sessionId: '' }, ) => { const { userIdParam, sessionId } = config; // ℹ️ Here I'm calling the method .ready() from the plugin BackgroundGeolocation.ready({ desiredAccuracy: geolocatorSettings.desiredAccuracy, distanceFilter: geolocatorSettings.distanceFilter, stopTimeout: geolocatorSettings.stopTimeOut, debug: false, logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE, logMaxDays: 4, stopOnTerminate: geolocatorSettings.stopOnTerminate, startOnBoot: true, url: ${API_URL}/v2/location/${userIdParam || userId}, showsBackgroundLocationIndicator: true, disableLocationAuthorizationAlert: true, maxDaysToPersist: 5, maxRecordsToPersist: -1, heartbeatInterval: 60 geolocatorSettings.heartBeatInterval, preventSuspend: true, foregroundService: true, disableElasticity: geolocatorSettings.disableElasticity, elasticityMultiplier: geolocatorSettings.elasticityMultiplier, stopOnStationary: geolocatorSettings.stopOnStationary, disableMotionActivityUpdates: geolocatorSettings.disableMotionActivityUpdates, disableProviderChangeRecord: false, allowIdenticalLocations: geolocatorSettings.allowIdenticalLocations, locationUpdateInterval: geolocatorSettings.locationUpdateInterval 1000, fastestLocationUpdateInterval: 1000 * geolocatorSettings.fastestLocationUpdateInterval, enableHeadless: true, locationsOrderDirection: 'ASC', headers: { 'x-app-secret': envVariables.APP_SECRET_HEADER, }, extras: { sessionId: sessionId || uniqueId, os: Platform.OS === 'ios' ? 'ios' : 'android', }, locationAuthorizationRequest: 'Always', scheduleUseAlarmManager: true, stationaryRadius: 250, isMoving: true, activityType: BackgroundGeolocation.ACTIVITY_TYPE_OTHER, backgroundPermissionRationale: { title: translate('backgroundPermissionRationaleTitle'), message: translate('backgroundPermissionRationaleMessage'), positiveAction: translate('backgroundPermissionRationalePositiveAction'), negativeAction: translate('cancel'), }, }).catch(); };

const stopGeolocation = (navigateTo?: () => void) => { BackgroundGeolocation.getCurrentPosition({ timeout: 30, maximumAge: 5000, desiredAccuracy: 10, }).then((position) => { const lastPosition = { ...position, event: 'gps_off', }; sendLocations(lastPosition); }); dispatch(changeToggleStatus(false)); BackgroundGeolocation.stopSchedule(); BackgroundGeolocation.stop(); BackgroundGeolocation.destroyLocations(); BackgroundGeolocation.removeListeners(); navigateTo?.(); };

const initBackgroundGeolocation = async () => { const location = await BackgroundGeolocation.getCurrentPosition({ timeout: 30, maximumAge: 5000, desiredAccuracy: 10, }); const firstPosition = { ...location, event: 'gps_on', }; sendLocations(firstPosition); };

const startGeolocation = async (cb?: () => void) => { if (isTrackingAlways) { await BackgroundGeolocation.start(); } else { await BackgroundGeolocation.startSchedule(); } initBackgroundGeolocation(); dispatch(changeToggleStatus(true)); cb?.(); };

const resetSchedule = async (workdays?: Workdays): Promise => { try { const schedule = workdays ? getSchedule(workdays) : []; await BackgroundGeolocation.stopSchedule(); await BackgroundGeolocation.stop(); await BackgroundGeolocation.setConfig({ schedule, }); } catch (error) { Alert.alert('Something went wrong while reset schedule'); } };

// ℹ️ This function is called when the user change the schedule on the app const configureSchedule = async ({ workdays, isTrackingAlwaysActive = isTrackingAlways, }): Promise => { try { await resetSchedule(workdays); if (toggleStatus && isTrackingAlwaysActive) { await BackgroundGeolocation.start(); }

  if (toggleStatus && !isTrackingAlwaysActive) {
    await BackgroundGeolocation.startSchedule();
  }
} catch (error) {
  // error
}

};

return { stopGeolocation, geolocatorConfig, startGeolocation, getGeolocatorState, saveGeolocatorConfigOnDb, resetSchedule, configureSchedule, isSavingGeolocatorConfig: loading, }; };

--- 

## Login.tsx
When i do login i call `geolocatorConfig` to set in `BackgroundGeolocation.ready({})` a config with the values of the user.
```tsx
import React, { useEffect, useRef, useState } from 'react';
import { useAppDispatch, useAppSelector, useBackgroundGeolocation, useTranslate } from '@hooks';
import { AxiosError } from 'axios';

export const Login = () => {
  const dispatch = useAppDispatch();
  const workdays = useAppSelector((state) => state.user.workdays);
  const fcmToken = useAppSelector((state) => state.user.fcmToken);
  const { geolocatorConfig } = useBackgroundGeolocation();
  const { execute } = authService.login();

  const validateSession = async (code: string): Promise<void> => {
    try {
      const result = await execute<LoginResponse>({
          phoneNumber,
          areaCode,
          otp: code,
          fcmToken,
          phoneOS: DeviceInfo.getSystemName(),
          phoneModel: DeviceInfo.getModel(),
      });
      if (result.status === 200) {
        /* ℹ️ Here I'm calling the method .ready() from the plugin again, 
        Not sure if this may cause an issue, but I don't think it does
        because the screen only appears once, and then should be called globally 
        in the `Geolocator` component
        */
        geolocatorConfig({
          userIdParam: result.data?.id,
          sessionId: result.data?.uniqueId!,
        });
      }
    } catch (err) {
        showAlert();
    }
  };

  return (<></>);
};

@christocracy This is how I am setting up the plug-in

christocracy commented 4 days ago

It’s a yes or no question. It’s not for me to analyze your code.

Are you absolutely sure that your app is calling .ready(config) each time your app is launched, no matter what — even if your app is automatically launched by the OS in the background?

williamFportillo commented 4 days ago

Yes, i'm calling it here

const Geolocator = () => {
    const { geolocatorConfig } = useBackgroundGeolocation();

    useEffect(() => {
      /*
       ℹ️ The function that executes the BackgroundGeolocation.ready() 
      */
      geolocatorConfig(); // BackgroundGeolocation.ready({ config }) 👈🏻
    }, []);

    return <></>;
};

const App = () => (
        <Geolocator />
    );

export default App;