Open valeri-terziyski opened 1 year ago
@valeri-terziyski Hi, in your sample code you call the BackgroundTask
when the app is in foreground, but in the overriden getTaskConfig()
method, the last parameter of HeadlessJsTaskConfig
instanciation, allowedInForeground
, is set to false
.
Did your code work if you pass this parameter to true
?
For anyone having this same issue, I was able to integrate the headless js service with WorkManager using bindService
instead of startService
and a CoroutineWorker
that suspends until the headless task finishes executing (onHeadlessJsTaskFinish
call from HeadlessJsTaskService
)
In the Binder
instance returned from onBind
, I just added a method to start executing the headless js task that just calls getTaskConfig
and startTask
.
@valeri-terziyski Hi, in your sample code you call the
BackgroundTask
when the app is in foreground, but in the overridengetTaskConfig()
method, the last parameter ofHeadlessJsTaskConfig
instanciation,allowedInForeground
, is set tofalse
.Did your code work if you pass this parameter to
true
?
Unfortunately even after setting it to true
, I still encountered a few crashes, so I wouldn't say, that's a viable option.
For anyone having this same issue, I was able to integrate the headless js service with WorkManager using
bindService
instead ofstartService
and aCoroutineWorker
that suspends until the headless task finishes executing (onHeadlessJsTaskFinish
call fromHeadlessJsTaskService
)In the
Binder
instance returned fromonBind
, I just added a method to start executing the headless js task that just callsgetTaskConfig
andstartTask
.
@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 HeadlessJsTaskWorker
(instantiated from a CoroutineWorker
instead of a Service
).
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 WorkManager
, not a startService()
or startForegroundService()
:
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. Please refer to https://developer.android.com/develop/background-work/background-tasks to decide whether you should be using Coroutines, WorkManager, or Foreground Service.
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.
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.
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 HeadlessJsTaskService
that are backed by WM, Coroutines and so on. On the other hand we don't want to do add those external dependencies to the framework as every app consumer will also depend on them, so they're probably better suited for 3rd party library implementations
@cortinico we had the same experience when enabling the New Architecture (setting newArchEnabled=true
), turning it off fixed the issue.
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:
E Work [ id=c54094ac-da2e-48ca-9509-01f71c94667d, tags={ com.testapp75.MyWorker } ] failed because it threw an exception/error
java.util.concurrent.ExecutionException: android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { cmp=com.testapp75/.MyHeadlessJsService (has extras) }: app is in background uid UidRecord{cfc0c33 u0a565 TRNB bg:+396ms idle change:procadj procs:0 seq(6889459,6889455)} caps=-------
at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:515)
at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:474)
at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:316)
at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
Caused by: android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { cmp=com.testapp75/.MyHeadlessJsService (has extras) }: app is in background uid UidRecord{cfc0c33 u0a565 TRNB bg:+396ms idle change:procadj procs:0 seq(6889459,6889455)} caps=-------
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1946)
at android.app.ContextImpl.startService(ContextImpl.java:1901)
at android.content.ContextWrapper.startService(ContextWrapper.java:827)
at com.testapp75.MyWorker.doWork(MyWorker.kt:43)
at androidx.work.Worker$1.run(Worker.java:82)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
@RequiresApi(Build.VERSION_CODES.O)
override fun doWork(): Result {
val reactNativeHost = (applicationContext as ReactApplication).reactNativeHost
val reactInstanceManager: ReactInstanceManager = reactNativeHost.reactInstanceManager
val reactContext = reactInstanceManager.currentReactContext
val service = Intent(applicationContext, MyHeadlessJsService::class.java)
applicationContext.startService(service)
HeadlessJsTaskService.acquireWakeLockNow(applicationContext);
return Result.success()
}
We'd appreciate any feedback on this one, thanks!
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.
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