transistorsoft / react-native-background-fetch

Periodic callbacks in the background for both IOS and Android
MIT License
1.44k stars 191 forks source link

Android app is crashing (in background) when headlessJS job is fired in release variant #165

Closed wanschi closed 4 years ago

wanschi commented 4 years ago

Your Environment

minimumFetchInterval: 15, stopOnTerminate: false, startOnBoot: true, enableHeadless: true,

Expected Behavior

Android app shouldn't crash whenever Headless JS Task is fired.

Actual Behavior

Android app is crashing in release build whenever Headless JS Task is fired.

Steps to Reproduce

  1. Configure bgfetch
  2. Instal release variant of android app
  3. Run adb shell cmd jobscheduler run -f com.xxx 999
  4. App is crashing

Code

componentDidMount() {
  setUpBackgroundTask()
  BackgroundFetch.registerHeadlessTask(setUpBackgroundTask)
}

const setUpBackgroundTask = () => {
  BackgroundFetch.configure(
    {
      minimumFetchInterval: 15,
      stopOnTerminate: false,
      startOnBoot: true,
      enableHeadless: true,
    },
    () => {
      _checkTerminatingAndTerminate()
    }
  )
  BackgroundFetch.status(status => {
    switch (status) {
      case BackgroundFetch.STATUS_RESTRICTED:
        showAlertMessage(
          determineErrorMessage(
            {
              response: {
                err_code: "ERROR_BACKGROUND_APP_REFRESH_RESTRICTED",
              },
            },
            () => {
              store.dispatch(setDrivingModeOff())
            }
          )
        )
        break
      case BackgroundFetch.STATUS_DENIED:
        showAlertMessage(
          determineErrorMessage(
            {
              response: {
                err_code: "ERROR_BACKGROUND_APP_REFRESH_DENIED",
              },
            },
            () => {
              store.dispatch(setDrivingModeOff())
            }
          )
        )
        break
    }
  })
}

Debug logs

07-23 14:03:00.277  5812  5812 D TSBackgroundFetch: HeadlessJobService onStartJob
07-23 14:03:00.351  5812  5812 D ReactNative: ReactInstanceManager.createReactContextInBackground()
07-23 14:03:00.351  5812  5812 D ReactNative: ReactInstanceManager.recreateReactContextInBackgroundInner()
07-23 14:03:00.351  5812  5812 D ReactNative: ReactInstanceManager.recreateReactContextInBackgroundFromBundleLoader()
07-23 14:03:00.351  5812  5812 D ReactNative: ReactInstanceManager.recreateReactContextInBackground()
07-23 14:03:00.351  5812  5812 D ReactNative: ReactInstanceManager.runCreateReactContextOnNewThread()
07-23 14:03:00.362  5812  5834 D ReactNative: ReactInstanceManager.createReactContext()
07-23 14:03:00.441  5812  5834 D ReactNative: Initializing React Xplat Bridge.
07-23 14:03:00.446  5812  5834 D ReactNative: Initializing React Xplat Bridge before initializeBridge
07-23 14:03:00.461  5812  5834 D ReactNative: Initializing React Xplat Bridge after initializeBridge
07-23 14:03:00.461  5812  5834 D ReactNative: CatalystInstanceImpl.runJSBundle()
07-23 14:03:00.479  5812  5839 D ReactNative: ReactInstanceManager.setupReactContext()
07-23 14:03:00.479  5812  5839 D ReactNative: CatalystInstanceImpl.initialize()
07-23 14:03:00.985  5812  5812 D TSBackgroundFetch: onHeadlessJsTaskStart: 1
christocracy commented 4 years ago

If you're seeing a crash, don't filter the logcat with :S*. You're filtering out the exception stacktrace.

$ adb logcat
wanschi commented 4 years ago

Does that mean anything to you?

07-23 16:27:44.440 17367 17392 E ReactNativeJS: Module AppRegistry is not a registered callable module (calling startHeadlessTask)
07-23 16:27:44.479 17367 17393 E AndroidRuntime: FATAL EXCEPTION: mqt_native_modules
07-23 16:27:44.479 17367 17393 E AndroidRuntime: Process: com.brive_app, PID: 17367
07-23 16:27:44.479 17367 17393 E AndroidRuntime: com.facebook.react.common.JavascriptException: Module AppRegistry is not a registered callable module (calling startHeadlessTask), stack:
07-23 16:27:44.479 17367 17393 E AndroidRuntime: value@37:3170
07-23 16:27:44.479 17367 17393 E AndroidRuntime: <unknown>@37:822
07-23 16:27:44.479 17367 17393 E AndroidRuntime: value@37:2565
07-23 16:27:44.479 17367 17393 E AndroidRuntime: value@37:794
07-23 16:27:44.479 17367 17393 E AndroidRuntime: value@-1
07-23 16:27:44.479 17367 17393 E AndroidRuntime: 
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.modules.core.ExceptionsManagerModule.showOrThrowError(ExceptionsManagerModule.java:54)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.modules.core.ExceptionsManagerModule.reportFatalException(ExceptionsManagerModule.java:38)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at java.lang.reflect.Method.invoke(Native Method)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at android.os.Handler.handleCallback(Handler.java:751)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at android.os.Handler.dispatchMessage(Handler.java:95)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at android.os.Looper.loop(Looper.java:154)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:232)
07-23 16:27:44.479 17367 17393 E AndroidRuntime:    at java.lang.Thread.run(Thread.java:762)
christocracy commented 4 years ago

Does that mean anything to you?

Yes.

React Native version (react-native -v):

wanschi commented 4 years ago

Sorry my bad. Version is: 0.59.8

christocracy commented 4 years ago

Show me your root index.js

The plugin is failing to import {AppRegistry} from "react-native"

Causing this line to fail

wanschi commented 4 years ago

I've added import {AppRegistry} from "react-native", but it's still not working.

/**
 * @format
 * @lint-ignore-every XPLATJSCOPYRIGHT1
 */

import { Navigation } from "react-native-navigation"
import { registerScreens } from "./src/navigation"
import AppStateHandler from "./src/AppStateHandler"
import { createStore } from "./src/redux"
import {Store} from 'redux'
import "moment/src/locale/de"
import localization from "moment/locale/de"
import moment from "moment"
import { persistStore as persistStoreRaw } from "redux-persist"
import {AppRegistry} from "react-native"

import {
  dashboard,
  deviceNotSuported,
  driving,
  loginScreen,
  forceUpdate,
  pendingTerms,
} from "./src/AppRootViews"

import Color from "@assets/color"

export const store: Store = createStore()

moment().locale("de", localization)

const persistStore = storeToPersist =>
  new Promise(resolve => {
    let persistor = persistStoreRaw(storeToPersist, undefined, () => {
      resolve(persistor)
    })
  })

async function bootstrap() {
  const persistor = await persistStore(store)
  registerScreens(store, persistor)

  Navigation.setDefaultOptions({
    bottomTabs: { visible: true, drawBehind: false, animate: true },
    topBar: {
      buttonColor: Color.buttonColor,
      drawBehind: false,
      backButton: {
        showTitle: false,
        color: Color.buttonColor,
      },
    },
  })

  const stateHandler = new AppStateHandler({
    store,
    persistor,
    onSignedOut: loginScreen,
    onSignedIn: dashboard,
    onDriving: driving,
    onDeviceNotSupported: deviceNotSuported,
    onForceUpdate: forceUpdate,
    onPendingTerms: pendingTerms
  })
}

Navigation.events().registerAppLaunchedListener(() => {
  bootstrap()
})
christocracy commented 4 years ago

I've added import {AppRegistry} from "react-native", but it's still not working.

Why? You're not referencing AppRegistry in your index.js at all.

Why are you not executing BackgroundFetch.registerHeadlessTask in your index.js?

import BackgroundFetch from "react-native-background-fetch";

let MyHeadlessTask = async () => {
  console.log('[BackgroundFetch HeadlessTask] start');

  // Perform an example HTTP request.
  // Important:  await asychronous tasks when using HeadlessJS.
  let response = await fetch('https://facebook.github.io/react-native/movies.json');
  let responseJson = await response.json();
  console.log('[BackgroundFetch HeadlessTask response: ', responseJson);

  // Required:  Signal to native code that your task is complete.
  // If you don't do this, your app could be terminated and/or assigned
  // battery-blame for consuming too much time in background.
  BackgroundFetch.finish();
}

// Register your BackgroundFetch HeadlessTask
BackgroundFetch.registerHeadlessTask(MyHeadlessTask);
wanschi commented 4 years ago

Ok - so I need to register my headless tasks in the index.js right? I configure BackgroundFetch in the componentDidMount() method in my component. Do I have to do both when I want to use headlessJS? In your example above you don't configure the task.

christocracy commented 4 years ago

Do I have to do both

christocracy commented 4 years ago

Your react-native application does not mount in the headless context. None of your react components' componentDidMount will execute in the headless context. Only index.js runs in the headless context.

adrian1388 commented 4 years ago
  • BackgroundFetch.registerHeadessTask always goes in index.js

@christocracy Can I import any function and register it on index.js? Something like this:

import { myFunction } from "./src/functions/index";
...
BackgroundFetch.registerHeadlessTask(myFunction);
christocracy commented 4 years ago

Of course.

wanschi commented 4 years ago

Hey @christocracy,

thanks for your support! Everything is working as expected now. Should I do a pull request to clarify some things in the documentation?

christocracy commented 4 years ago

Sure

roryabraham commented 2 years ago

Your react-native application does not mount in the headless context. None of your react components' componentDidMount will execute in the headless context. Only index.js runs in the headless context.

Hey @christocracy, I was wondering if you know why this is true? My experience working with Headless JS in a RN application has shown me that it is true, but I would love to know why.

My best guess (and it is purely a guess) is that this limitation exists because headless JS spawns a different background thread to execute some work that's separate from the main application. Like normally RN has these three threads: (1) Native UI, (2) Layout, (3) Main JS/RN code:

image

And I'm guessing that the headless background process is separate from the main JS thread, so won't execute any of the React code that would normally interact with the other two threads.... Something like that....

Anyways, I would be appreciative if you have a more definitive answer. Thanks!

christocracy commented 2 years ago

When you launch a typical Android app from the home-screen, it consists of an Application + Activity. Your RN App instance (the user interface) runs in the Activity.

When an app is launched headless, there is only an Application instance — no Activity, thus no RN App instance, thus no UI.

The Activity is the "Head". The Application is the "body". Another name for headless is activity-less or UI-less.

christocracy commented 2 years ago

My best guess (and it is purely a guess) is that this limitation exists because

It's not a "limitation", it is simply the nature of Android application architecture.

roryabraham commented 2 years ago

Thanks @christocracy!