/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package com.facebook.react import android.annotation.SuppressLint import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.IBinder import android.os.PowerManager import android.os.PowerManager.WakeLock import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags import com.facebook.react.jstasks.HeadlessJsTaskConfig import com.facebook.react.jstasks.HeadlessJsTaskContext.Companion.getInstance import com.facebook.react.jstasks.HeadlessJsTaskEventListener import java.util.concurrent.CopyOnWriteArraySet /** * Base class for running JS without a UI. Generally, you only need to override [getTaskConfig], * which is called for every [onStartCommand]. The result, if not `null`, is used to run a JS task. * * If you need more fine-grained control over how tasks are run, you can override [onStartCommand] * and call [startTask] depending on your custom logic. * * If you're starting a `HeadlessJsTaskService` from a `BroadcastReceiver` (e.g. handling push * notifications), make sure to call [acquireWakeLockNow] before returning from * [BroadcastReceiver.onReceive], to make sure the device doesn't go to sleep before the service is * started. */ public abstract class HeadlessJsTaskService : Service(), HeadlessJsTaskEventListener { private val activeTasks: MutableSet = CopyOnWriteArraySet() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val taskConfig = getTaskConfig(intent) return if (taskConfig != null) { startTask(taskConfig) START_REDELIVER_INTENT } else { START_NOT_STICKY } } /** * Called from [onStartCommand] to create a [HeadlessJsTaskConfig] for this intent. * * @return a [HeadlessJsTaskConfig] to be used with [startTask], or `null` to ignore this command. */ protected open fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? = null override fun onBind(intent: Intent): IBinder? = null /** * Start a task. This method handles starting a new React instance if required. * * Has to be called on the UI thread. * * @param taskConfig describes what task to start and the parameters to pass to it */ protected fun startTask(taskConfig: HeadlessJsTaskConfig) { UiThreadUtil.assertOnUiThread() acquireWakeLockNow(this) val context = reactContext if (context == null) { createReactContextAndScheduleTask(taskConfig) } else { invokeStartTask(context, taskConfig) } } private fun invokeStartTask(reactContext: ReactContext, taskConfig: HeadlessJsTaskConfig) { val headlessJsTaskContext = getInstance(reactContext) headlessJsTaskContext.addTaskEventListener(this) UiThreadUtil.runOnUiThread { val taskId = headlessJsTaskContext.startTask(taskConfig) activeTasks.add(taskId) } } override fun onDestroy() { super.onDestroy() reactContext?.let { context -> val headlessJsTaskContext = getInstance(context) headlessJsTaskContext.removeTaskEventListener(this) } wakeLock?.release() } override fun onHeadlessJsTaskStart(taskId: Int): Unit = Unit override fun onHeadlessJsTaskFinish(taskId: Int) { activeTasks.remove(taskId) if (activeTasks.isEmpty()) { stopSelf() } } /** * Get the [ReactNativeHost] used by this app. By default, assumes [getApplication] is an instance * of [ReactApplication] and calls [ReactApplication.reactNativeHost]. * * Override this method if your application class does not implement `ReactApplication` or you * simply have a different mechanism for storing a `ReactNativeHost`, e.g. as a static field * somewhere. */ @Suppress("DEPRECATION") protected open val reactNativeHost: ReactNativeHost get() = (application as ReactApplication).reactNativeHost /** * Get the [ReactHost] used by this app. By default, assumes [getApplication] is an instance of * [ReactApplication] and calls [ReactApplication.reactHost]. This method assumes it is called in * new architecture and returns null if not. */ protected open val reactHost: ReactHost? get() = (application as ReactApplication).reactHost protected val reactContext: ReactContext? get() { if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { val reactHost = checkNotNull(reactHost) { "ReactHost is not initialized in New Architecture" } return reactHost.currentReactContext } else { val reactInstanceManager = reactNativeHost.reactInstanceManager return reactInstanceManager.currentReactContext } } private fun createReactContextAndScheduleTask(taskConfig: HeadlessJsTaskConfig) { if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { val reactHost = checkNotNull(reactHost) reactHost.addReactInstanceEventListener( object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { invokeStartTask(context, taskConfig) reactHost.removeReactInstanceEventListener(this) } } ) reactHost.start() } else { val reactInstanceManager = reactNativeHost.reactInstanceManager reactInstanceManager.addReactInstanceEventListener( object : ReactInstanceEventListener { override fun onReactContextInitialized(context: ReactContext) { invokeStartTask(context, taskConfig) reactInstanceManager.removeReactInstanceEventListener(this) } } ) reactInstanceManager.createReactContextInBackground() } } public companion object { private var wakeLock: WakeLock? = null /** * Acquire a wake lock to ensure the device doesn't go to sleep while processing background * tasks. */ @JvmStatic @SuppressLint("WakelockTimeout") public fun acquireWakeLockNow(context: Context) { if (wakeLock == null || wakeLock?.isHeld == false) { val powerManager = checkNotNull(context.getSystemService(POWER_SERVICE) as PowerManager) wakeLock = powerManager .newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, HeadlessJsTaskService::class.java.canonicalName, ) .also { lock -> lock.setReferenceCounted(false) lock.acquire() } } } } }