revtel / react-native-nfc-manager

React Native NFC module for Android & iOS
MIT License
1.38k stars 317 forks source link

[Android] AppState background triggered while scanning nfc chip #437

Closed buekera closed 2 years ago

buekera commented 3 years ago

Hey there, I just noticed some odd behavior while scanning nfc chips. The moment you put your nfc chip to the phone and the scanner detects it the AppState changes from active to background back to active again.

Due to this behavior some other parts which listen to the AppStage change behave strangely. Is there any way to work around this or might this even be a bug?

OS: Android tested nfc manager versions: 3.7.0 - 3.10.0

Code:

AppState.addEventListener("change", (nextAppState: AppStateStatus) => {
  log.debug("state changed to -> ", nextAppState);
});

[...]

try {
  setWaiting(true);
  await NfcManager.requestTechnology([NfcTech.Ndef]);

  const tag = await NfcManager.getTag();

  handleNfcScan(tag?.id as unknown as string);
  NfcManager.cancelTechnologyRequest();
} catch (error) {
  // handle error
}

So when putting a nfc chip to the scanner the following logs within a split second:

DEBUG state changed to -> background
DEBUG state changed to -> active

Regards

ognjenmarcheta commented 3 years ago

@buekera That is happening because of how react native sees/defines background and foreground states. React native uses your app activity as the owner of the UI thread. When something is in front of your app (alerts, dialogs, file pickers, etc.) onPause callback will be called and trigger "background" event.

That false report happens really fast, what you could do is to measure time between background/foreground and to ignore those false reports. But to measure it in background you need this package since when react native app goes to background state it is paused and code will not execute until you are back in the app again, so measuring is not possible without running some code in background. For example setTimeout or setInterval.

Package for running background code: https://github.com/ocetnik/react-native-background-timer

I've already recently reported this to react-native community repo and it was already reported couple years ago, I think in 2018 but old report was closed due to inactivity.

You can see they are not interested in this issue: https://github.com/facebook/react-native/issues/32036 https://github.com/react-native-community/discussions-and-proposals/issues/399

whitedogg13 commented 3 years ago

@buekera might relate to #438 please try the latest v3.10.1. Thanks!

NqCompiler commented 3 years ago

I "fixed" it by measuring how long "background" state was on until "active" state was back on. If it's less than 1 second, I assume it's from nfc reading. I had to use reference variables because my state variable was not updated in my callback function in AppState.addEventListener("change", onChangeState).

  const blockTsRef = useRef<number>(null);

  const onChangeState = (newState) => {
    setCurrAppState(newState);
    if(newState === "background") {
      // set timestamp
      blockTsRef.current = new Date().getTime();
    }
    else if(newState === "active") {
      if(typeof blockTsRef.current === "number") {
        const tsNow = new Date().getTime();
        // calculate difference
        if(tsNow - blockTsRef.current <= 1000) {
          // reset blockTsRef
          blockTsRef.current = null;
          // don't do
          return;
        }
      }
      // do
    }
  };
github-actions[bot] commented 2 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] commented 2 years ago

This issue was closed because it has been stalled for 5 days with no activity.

neiracausevic1 commented 9 months ago

I fixed it similarly to the solution provided above but just set the difference to 500 ms because it usually takes 20-40 ms if its the NFC triggering the state changes and it works for me.

const appState = useRef(AppState.current);
const [lastBackgroundTime, setLastBackgroundTime] = useState(null);

useEffect(() => {
    const handleAppStateChange = nextAppState => {
      if (
        appState.current.match(/inactive|background/) &&
        nextAppState === ‘active’
      ) {
        const currentTime = new Date().getTime();
        if (lastBackgroundTime && currentTime - lastBackgroundTime > 500) {
           // App was in background
        } else {
          // App state falsely triggered by NFC
          return;
        }
      }
      if (nextAppState === ‘background’) {
        setLastBackgroundTime(new Date().getTime());
      }
      appState.current = nextAppState;
    };
    // Subscribe to app state changes
    const appStateSubscription = AppState.addEventListener(
      ‘change’,
      handleAppStateChange,
    );
    return () => {
      // Clean up the subscription on unmount
      appStateSubscription.remove();
    };
  }, [lastBackgroundTime]);
douglasjunior commented 5 months ago

On Android, the background state means that the React Native Activity is in background, and not necessary the entire app.

To handle this, I created a package that implements the Android Lifecycle API for React Native: https://github.com/douglasjunior/react-native-applifecycle

Why Use This?

The original AppState API provided by React Native behaves differently between Android and iOS, particularly regarding the background state:

  • On iOS, the background state signifies that the entire app is in the background.
  • On Android, the background state indicates that the React Native Activity is in the background, which might not necessarily mean the entire app is in the background.

By using react-native-applifecycle, you can handle these differences seamlessly across both platforms, ensuring that the state background will be dispatched only when the entire app is in background.