-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
HeadlessJS crashes on apps targeting newer Android SDKs #36816
Comments
@valeri-terziyski Hi, in your sample code you call the Did your code work if you pass this parameter to |
For anyone having this same issue, I was able to integrate the headless js service with WorkManager using In the |
Unfortunately even after setting it to |
@david-arteaga do you have any sample code you could share for this 🙏? |
@mrbrentkelly sure 😁 This assumes any given instance of BackgroundUploadTaskService will only run 1 headless task at a time (given how it uses the onFinish “callback”). I wasn’t sure what the best way to handle that part was but the callback works. class BackgroundUploadTaskWorker(
appContext: Context, workerParameters: WorkerParameters
) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork(): Result {
delay(500) // delaying because sometimes when the app is being backgrounded, isAppInForeground still returns true
// Headless JS task crashes if in foreground
if (isAppInForeground(applicationContext)) {
Log.i(
"BackgroundTasksModule",
"App is currently in the foreground, will not start background upload task"
)
return Result.success()
}
Log.i(
"BackgroundTasksModule", "App is not in the foreground, will attempt to run headless js task"
)
return runHeadlessJsTask()
}
private suspend fun runHeadlessJsTask(): Result {
val intent = Intent(
applicationContext, BackgroundUploadTaskService::class.java
)
val bundle = Bundle()
bundle.putLong(
BACKGROUND_TASK_INTERVAL_MS_PARAM, inputData.getLong(BACKGROUND_TASK_INTERVAL_MS_PARAM, 0)
)
intent.putExtras(bundle)
Log.i(
"BackgroundTasksModule",
"Will start executing Headless JS BackgroundUploadTaskService service"
)
suspendCoroutine { continuation ->
var hasUnbound = false // needed because sometimes onAfterServiceDone is called more than once
val connection = object : ServiceConnection {
private fun onAfterServiceDone(fromCallback: String) {
if (hasUnbound) {
Log.i(
"BackgroundTasksModule",
"$fromCallback - Will not unbind from headless js service because already unbound previously"
)
return
}
hasUnbound = true
Log.i("BackgroundTasksModule", "$fromCallback - Will unbind from headless js service")
applicationContext.unbindService(this)
continuation.resume(null)
}
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d("BackgroundTasksModule", "Service connected")
val binder = service as BackgroundUploadTaskService.LocalBinder
binder.startRunning(intent) {
onAfterServiceDone("onServiceConnected:startRunning:onFinish")
}
}
override fun onServiceDisconnected(arg0: ComponentName) {
onAfterServiceDone("onServiceDisconnected")
}
}
Log.d("BackgroundTasksModule", "Will bind to service")
applicationContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
Log.d("BackgroundTasksModule", "Will return from BackgroundUploadTaskWorker")
return Result.success()
}
}
class BackgroundUploadTaskService : HeadlessJsTaskService() {
companion object {
const val HEADLESS_TASK_NAME = "AndroidBackgroundUploadTask"
}
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig {
Log.d("BackgroundTasksModule", "Will get task config for BackgroundUploadTaskService")
val defaultTimeoutMs = 60_000 * 15L
val timeoutMs =
intent?.getLongExtra(BACKGROUND_TASK_INTERVAL_MS_PARAM, defaultTimeoutMs) ?: defaultTimeoutMs
val allowedInForeground = false
return HeadlessJsTaskConfig(
HEADLESS_TASK_NAME, Arguments.createMap(), timeoutMs, allowedInForeground
)
}
inner class LocalBinder : Binder() {
fun startRunning(intent: Intent, onFinish: () -> Unit) {
Log.d("BackgroundTasksModule", "LocalBinder startRunning")
val service = this@BackgroundUploadTaskService
val config = getTaskConfig(intent)
service.startTask(config)
service.onFinish = onFinish
}
}
private val binder = LocalBinder()
override fun onBind(intent: Intent): IBinder {
return binder
}
var onFinish: () -> Unit = {}
override fun onHeadlessJsTaskFinish(taskId: Int) {
Log.d("BackgroundTasksModule", "Will finish headless js task with id $taskId")
super.onHeadlessJsTaskFinish(taskId)
onFinish()
}
}
fun isAppInForeground(context: Context): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
val packageName = context.packageName
for (appProcess in appProcesses) {
if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) {
return true
}
}
return false
} |
@david-arteaga this is great, thanks so much for sharing! |
We've also encountered the same issue and implemented our own import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.os.PowerManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.facebook.infer.annotation.Assertions
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceEventListener
import com.facebook.react.ReactNativeHost
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.jstasks.HeadlessJsTaskConfig
import com.facebook.react.jstasks.HeadlessJsTaskContext
import com.facebook.react.jstasks.HeadlessJsTaskEventListener
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* Base class for running JS without a UI. Generally, you only need to override {@link
* #getTaskConfig}, which is called for every {@link #doWork}. The result, if not {@code
* null}, is used to run a JS task.
*
* <p>If you need more fine-grained control over how tasks are run, you can override {@link
* #doWork} and call {@link #startTask} depending on your custom logic.
*
* <p>If you're starting a {@code HeadlessJsTaskWorker} from a {@code BroadcastReceiver} (e.g.
* handling push notifications), make sure to call {@link #acquireWakeLockNow} before returning from
* {@link BroadcastReceiver#onReceive}, to make sure the device doesn't go to sleep before the
* service is started.
*/
abstract class HeadlessJsTaskWorker(private val notificationId: Int, appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams), HeadlessJsTaskEventListener {
private val mActiveTasks = CopyOnWriteArraySet<Int>()
private var workFinishedCont: Continuation<Result>? = null
companion object {
private var sWakeLock: PowerManager.WakeLock? = null
/**
* Acquire a wake lock to ensure the device doesn't go to sleep while processing background tasks.
*/
@SuppressLint("WakelockTimeout")
fun acquireWakeLockNow(context: Context) {
if (sWakeLock?.isHeld != true) {
val powerManager = Assertions.assertNotNull(context.getSystemService(Context.POWER_SERVICE) as PowerManager);
sWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, HeadlessJsTaskWorker::class.java.canonicalName).apply {
setReferenceCounted(false)
acquire()
}
}
}
}
final override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(notificationId, createNotification())
}
/*
* Called from {@link #getForegroundInfo} to create a {@link Notification}.
*
* @return a {@link Notification} to be displayed when your background JS task
* is being executed.
*/
protected abstract fun createNotification(): Notification
override suspend fun doWork(): Result = suspendCoroutine { cont ->
workFinishedCont = cont
when (val cfg = getTaskConfig(inputData.keyValueMap)) {
null -> finishWork()
else -> UiThreadUtil.runOnUiThread {
startTask(cfg)
}
}
}
/**
* Called from {@link #doWork} to create a {@link HeadlessJsTaskConfig} for this writableMap.
*
* @param writableMap the {@link WritableMap} received in {@link #doWork}.
* @return a {@link HeadlessJsTaskConfig} to be used with {@link #startTask}, or {@code null} to
* ignore this command.
*/
protected open fun getTaskConfig(extras: Map<String, Any>): HeadlessJsTaskConfig? {
return null
}
/**
* Start a task. This method handles starting a new React instance if required.
*
* <p>Has to be called on the UI thread.
*
* @param taskConfig describes what task to start and the parameters to pass to it
*/
protected open fun startTask(taskConfig: HeadlessJsTaskConfig) {
UiThreadUtil.assertOnUiThread()
acquireWakeLockNow(applicationContext);
val reactInstanceManager = getReactNativeHost().reactInstanceManager;
val reactContext = reactInstanceManager.currentReactContext;
when (reactContext) {
null -> {
reactInstanceManager.addReactInstanceEventListener(
object : ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext?) {
invokeStartTask(context, taskConfig)
reactInstanceManager.removeReactInstanceEventListener(this)
}
});
reactInstanceManager.createReactContextInBackground()
}
else -> {
invokeStartTask(reactContext, taskConfig)
}
}
}
private fun invokeStartTask(reactContext: ReactContext?, taskConfig: HeadlessJsTaskConfig) {
val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext)
headlessJsTaskContext.addTaskEventListener(this)
UiThreadUtil.runOnUiThread {
val taskId = headlessJsTaskContext.startTask(taskConfig)
mActiveTasks.add(taskId)
}
}
private fun finishWork() {
if (getReactNativeHost().hasInstance()) {
val reactInstanceManager = getReactNativeHost().reactInstanceManager;
reactInstanceManager.currentReactContext?.let {
val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(it);
headlessJsTaskContext.removeTaskEventListener(this);
}
}
sWakeLock?.release();
workFinishedCont?.resume(Result.success())
}
override fun onHeadlessJsTaskStart(taskId: Int) {}
override fun onHeadlessJsTaskFinish(taskId: Int) {
mActiveTasks.remove(taskId)
if (mActiveTasks.size == 0) {
finishWork()
}
}
/**
* Get the {@link ReactNativeHost} used by this app. By default, assumes {@link #getApplication()}
* is an instance of {@link ReactApplication} and calls {@link
* ReactApplication#getReactNativeHost()}. Override this method if your application class does not
* implement {@code ReactApplication} or you simply have a different mechanism for storing a
* {@code ReactNativeHost}, e.g. as a static field somewhere.
*/
protected open fun getReactNativeHost(): ReactNativeHost {
return (applicationContext as ReactApplication).reactNativeHost
}
}
And from now - scheduling our HeadlessJS tasks using the class MyAPI(appContext: Context, workerParams: WorkerParameters) : HeadlessJsTaskWorker(NOTIFICATION_ID, appContext, workerParams) {
override fun createNotification(): Notification {
val builder = NotificationCompat.Builder(applicationContext, "My App")
.setContentTitle("My App")
.setContentText("My App")
.setSmallIcon(R.mipmap.ic_launcher)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel("My App", "My App").also {
builder.setChannelId(it.id)
}
}
return builder.build()
}
@TargetApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(
channelId: String,
name: String
): NotificationChannel {
return NotificationChannel(
channelId, name, NotificationManager.IMPORTANCE_LOW
).also { channel ->
(applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)
}
}
override fun getTaskConfig(extras: Map<String, Any>): HeadlessJsTaskConfig? {
try {
return HeadlessJsTaskConfig(
"MyAPITask",
Arguments.makeNativeMap(extras),
50000,
true)
} catch (e: Throwable) { }
return null
}
companion object {
private const val TAG = "MyAPI"
private const val NOTIFICATION_ID = 9999
private const val EXTRAS_API_ID = "apiId"
private const val EXTRAS_AUTH20_TOKEN = "auth20_token"
private enum class API private constructor(val value: Int) {
AUTH_20(0)
}
fun auth20(context: Context, auth20_token: String) {
invokeAPITask(
context,
API.AUTH_20,
EXTRAS_AUTH20_TOKEN to auth20_token
)
}
private fun invokeAPITask(context: Context, api: API, vararg params: Pair<String, Any?>) {
val apiData = arrayOf(EXTRAS_API_ID to api.value)
WorkManager.getInstance(context)
.beginUniqueWork(
TAG,
ExistingWorkPolicy.APPEND_OR_REPLACE,
getExpeditedWork<MyAPI>(TAG, *apiData, *params)
)
.enqueue()
}
private inline fun <reified T: ListenableWorker> getExpeditedWork(tag: String, vararg params: Pair<String, Any?>): OneTimeWorkRequest {
val bundle = workDataOf(*params)
return OneTimeWorkRequestBuilder<T>()
.setInputData(bundle)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
.addTag(tag)
.build()
}
}
} |
Hi I work at Google, and I'm a SME on Android on background work. Our recommendation if you need to do background work (where the application is no longer in the foreground) with a react native application is to continue create a custom HeadlessJsTaskService to ensure that you follow best practice guidance. I'm happy to discuss with the owners of react native library if they would like to improve the react native library to better support background work on Android. At this moment the HeadlessJsTaskService will not run in the background after certain Android OS versions unless the developers write custom Android code as depicted above utilizing either WorkManager or Foreground Service. Please note: you do not need to acquire a wakelock except in very specific circumstances, we do not recommend adding the logic to acquire a wakelock as depicted in by @vfedosieievdish. Also, please consider using WorkManager to schedule tasks that do not need user interaction/ notification. |
Hey @AliceYuan, Nicola from the React Native team at Meta. Happy to chat about it. Agree that we would love to expose other implementation of |
@cortinico we had the same experience when enabling the New Architecture (setting On the matter of HeadlessTask on react-native 0.75.x - starting it from WorkManager no longer works (it did on earlier versions) and we are faced with this error:
We'd appreciate any feedback on this one, thanks! |
We're probably going to deprecate & remove HeadlessJSTasks in a future version of RN so you should not be using it. You should instead use JobScheduler, WorkManager and other Android APIs to schedule background work. |
Are you able to launch the task in background with this ? I'm currently having this error |
Description
The current
HeadlessJS
recomendation is to callstartService
on the passed context as described hereThis is problematic since Android 8 (SDK 26), there are limitations placed on the background services.
So up until Android 12 (SDK 31) this could have been fixed with something like
But this will cause issues on apps targeting higher SDKs, since there are additional resrtictions there.
So the latest recommended approach for starting a background service is by using either
WorkManager
orCoroutines
- https://developer.android.com/guide/background#recommended-approachesReact Native Version
0.71.6
Output of
npx react-native info
System:
OS: Linux 5.15 Ubuntu 22.04.1 LTS 22.04.1 LTS (Jammy Jellyfish)
CPU: (8) x64 AMD Ryzen 7 4700U with Radeon Graphics
Memory: 21.18 GB / 38.41 GB
Shell: 5.1.16 - /bin/bash
Binaries:
Node: 16.14.2 - /usr/local/bin/node
Yarn: 1.22.17 - /usr/local/bin/yarn
npm: 8.5.0 - /usr/local/bin/npm
Watchman: 2022.08.15.00 - /home/linuxbrew/.linuxbrew/bin/watchman
SDKs:
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Languages:
Java: 11.0.18 - /usr/bin/javac
npmPackages:
@react-native-community/cli: Not Found
react: 18.2.0 => 18.2.0
react-native: 0.71.6 => 0.71.6
npmGlobalPackages:
react-native: Not Found
Steps to reproduce
yarn start
andyarn android
Snack, code example, screenshot, or link to a repository
https://github.com/valeri-terziyski/react-native-headless-js-example
The text was updated successfully, but these errors were encountered: