diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 3f3ac5877c957b..82915571da411c 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -27,18 +27,22 @@ public class com/facebook/react/DebugCorePackage$$ReactModuleInfoProvider : com/ } public abstract class com/facebook/react/HeadlessJsTaskService : android/app/Service, com/facebook/react/jstasks/HeadlessJsTaskEventListener { + public static final field Companion Lcom/facebook/react/HeadlessJsTaskService$Companion; public fun ()V - public static fun acquireWakeLockNow (Landroid/content/Context;)V - protected fun getReactContext ()Lcom/facebook/react/bridge/ReactContext; - protected fun getReactHost ()Lcom/facebook/react/ReactHost; - protected fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost; + protected final fun getReactContext ()Lcom/facebook/react/bridge/ReactContext; + protected final fun getReactHost ()Lcom/facebook/react/ReactHost; + protected final fun getReactNativeHost ()Lcom/facebook/react/ReactNativeHost; protected fun getTaskConfig (Landroid/content/Intent;)Lcom/facebook/react/jstasks/HeadlessJsTaskConfig; public fun onBind (Landroid/content/Intent;)Landroid/os/IBinder; public fun onDestroy ()V public fun onHeadlessJsTaskFinish (I)V public fun onHeadlessJsTaskStart (I)V public fun onStartCommand (Landroid/content/Intent;II)I - protected fun startTask (Lcom/facebook/react/jstasks/HeadlessJsTaskConfig;)V + protected final fun startTask (Lcom/facebook/react/jstasks/HeadlessJsTaskConfig;)V +} + +public final class com/facebook/react/HeadlessJsTaskService$Companion { + public final fun acquireWakeLockNow (Landroid/content/Context;)V } public final class com/facebook/react/JSEngineResolutionAlgorithm : java/lang/Enum { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java deleted file mode 100644 index b47a4e9f07e25f..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags; -import com.facebook.react.jstasks.HeadlessJsTaskConfig; -import com.facebook.react.jstasks.HeadlessJsTaskContext; -import com.facebook.react.jstasks.HeadlessJsTaskEventListener; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Base class for running JS without a UI. Generally, you only need to override {@link - * #getTaskConfig}, which is called for every {@link #onStartCommand}. The result, if not {@code - * null}, is used to run a JS task. - * - *

If you need more fine-grained control over how tasks are run, you can override {@link - * #onStartCommand} and call {@link #startTask} depending on your custom logic. - * - *

If you're starting a {@code HeadlessJsTaskService} 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. - */ -public abstract class HeadlessJsTaskService extends Service implements HeadlessJsTaskEventListener { - - private final Set mActiveTasks = new CopyOnWriteArraySet<>(); - private static @Nullable PowerManager.WakeLock sWakeLock; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - HeadlessJsTaskConfig taskConfig = getTaskConfig(intent); - if (taskConfig != null) { - startTask(taskConfig); - return START_REDELIVER_INTENT; - } - return START_NOT_STICKY; - } - - /** - * Called from {@link #onStartCommand} to create a {@link HeadlessJsTaskConfig} for this intent. - * - * @param intent the {@link Intent} received in {@link #onStartCommand}. - * @return a {@link HeadlessJsTaskConfig} to be used with {@link #startTask}, or {@code null} to - * ignore this command. - */ - protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) { - return null; - } - - /** - * Acquire a wake lock to ensure the device doesn't go to sleep while processing background tasks. - */ - @SuppressLint("WakelockTimeout") - public static void acquireWakeLockNow(Context context) { - if (sWakeLock == null || !sWakeLock.isHeld()) { - PowerManager powerManager = - Assertions.assertNotNull((PowerManager) context.getSystemService(POWER_SERVICE)); - sWakeLock = - powerManager.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, HeadlessJsTaskService.class.getCanonicalName()); - sWakeLock.setReferenceCounted(false); - sWakeLock.acquire(); - } - } - - @Override - public @Nullable IBinder onBind(Intent intent) { - return 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 void startTask(final HeadlessJsTaskConfig taskConfig) { - UiThreadUtil.assertOnUiThread(); - acquireWakeLockNow(this); - - ReactContext reactContext = getReactContext(); - - if (reactContext == null) { - createReactContextAndScheduleTask(taskConfig); - } else { - invokeStartTask(reactContext, taskConfig); - } - } - - private void invokeStartTask(ReactContext reactContext, final HeadlessJsTaskConfig taskConfig) { - final HeadlessJsTaskContext headlessJsTaskContext = - HeadlessJsTaskContext.getInstance(reactContext); - headlessJsTaskContext.addTaskEventListener(this); - - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - int taskId = headlessJsTaskContext.startTask(taskConfig); - mActiveTasks.add(taskId); - } - }); - } - - @Override - public void onDestroy() { - super.onDestroy(); - ReactContext reactContext = getReactContext(); - - if (reactContext != null) { - HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext); - headlessJsTaskContext.removeTaskEventListener(this); - } - if (sWakeLock != null) { - sWakeLock.release(); - } - } - - @Override - public void onHeadlessJsTaskStart(int taskId) {} - - @Override - public void onHeadlessJsTaskFinish(int taskId) { - mActiveTasks.remove(taskId); - if (mActiveTasks.size() == 0) { - stopSelf(); - } - } - - /** - * 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 ReactNativeHost getReactNativeHost() { - return ((ReactApplication) getApplication()).getReactNativeHost(); - } - - /** - * Get the {@link ReactHost} used by this app. By default, assumes {@link #getApplication()} is an - * instance of {@link ReactApplication} and calls {@link ReactApplication#getReactHost()}. This - * method assumes it is called in new architecture and returns null if not. - */ - protected @Nullable ReactHost getReactHost() { - return ((ReactApplication) getApplication()).getReactHost(); - } - - protected ReactContext getReactContext() { - if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { - ReactHost reactHost = getReactHost(); - Assertions.assertNotNull(reactHost, "getReactHost() is null in New Architecture"); - return reactHost.getCurrentReactContext(); - } else { - final ReactInstanceManager reactInstanceManager = - getReactNativeHost().getReactInstanceManager(); - return reactInstanceManager.getCurrentReactContext(); - } - } - - private void createReactContextAndScheduleTask(final HeadlessJsTaskConfig taskConfig) { - if (ReactNativeFeatureFlags.enableBridgelessArchitecture()) { - final ReactHost reactHost = getReactHost(); - reactHost.addReactInstanceEventListener( - new ReactInstanceEventListener() { - @Override - public void onReactContextInitialized(@NonNull ReactContext reactContext) { - invokeStartTask(reactContext, taskConfig); - reactHost.removeReactInstanceEventListener(this); - } - }); - reactHost.start(); - } else { - final ReactInstanceManager reactInstanceManager = - getReactNativeHost().getReactInstanceManager(); - - reactInstanceManager.addReactInstanceEventListener( - new ReactInstanceEventListener() { - @Override - public void onReactContextInitialized(@NonNull ReactContext reactContext) { - invokeStartTask(reactContext, taskConfig); - reactInstanceManager.removeReactInstanceEventListener(this); - } - }); - reactInstanceManager.createReactContextInBackground(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.kt new file mode 100644 index 00000000000000..56f1af64c52f32 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/HeadlessJsTaskService.kt @@ -0,0 +1,184 @@ +/* + * 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.ReactNativeFeatureFlags +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. + */ + protected 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.getReactHost]. This method assumes it is called + * in new architecture and returns null if not. + */ + protected val reactHost: ReactHost? + get() = (application as ReactApplication).reactHost + + protected val reactContext: ReactContext? + get() { + if (ReactNativeFeatureFlags.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 (ReactNativeFeatureFlags.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. + */ + @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() + } + } + } + } +}