diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 0bdeb4a882d6cd..674f5f06a962bc 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2902,23 +2902,35 @@ public final class com/facebook/react/jscexecutor/JSCExecutorFactory : com/faceb public fun toString ()Ljava/lang/String; } -public class com/facebook/react/jstasks/HeadlessJsTaskConfig { +public final class com/facebook/react/jstasks/HeadlessJsTaskConfig { public fun (Lcom/facebook/react/jstasks/HeadlessJsTaskConfig;)V public fun (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;)V public fun (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;J)V public fun (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;JZ)V public fun (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;JZLcom/facebook/react/jstasks/HeadlessJsTaskRetryPolicy;)V -} - -public class com/facebook/react/jstasks/HeadlessJsTaskContext { - public fun addTaskEventListener (Lcom/facebook/react/jstasks/HeadlessJsTaskEventListener;)V - public fun finishTask (I)V - public static fun getInstance (Lcom/facebook/react/bridge/ReactContext;)Lcom/facebook/react/jstasks/HeadlessJsTaskContext; - public fun hasActiveTasks ()Z - public fun isTaskRunning (I)Z - public fun removeTaskEventListener (Lcom/facebook/react/jstasks/HeadlessJsTaskEventListener;)V - public fun retryTask (I)Z - public fun startTask (Lcom/facebook/react/jstasks/HeadlessJsTaskConfig;)I + public synthetic fun (Ljava/lang/String;Lcom/facebook/react/bridge/WritableMap;JZLcom/facebook/react/jstasks/HeadlessJsTaskRetryPolicy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getData ()Lcom/facebook/react/bridge/WritableMap; + public final fun getRetryPolicy ()Lcom/facebook/react/jstasks/HeadlessJsTaskRetryPolicy; + public final fun getTaskKey ()Ljava/lang/String; + public final fun getTimeout ()J + public final fun isAllowedInForeground ()Z +} + +public final class com/facebook/react/jstasks/HeadlessJsTaskContext { + public static final field Companion Lcom/facebook/react/jstasks/HeadlessJsTaskContext$Companion; + public synthetic fun (Lcom/facebook/react/bridge/ReactContext;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addTaskEventListener (Lcom/facebook/react/jstasks/HeadlessJsTaskEventListener;)V + public final fun finishTask (I)V + public static final fun getInstance (Lcom/facebook/react/bridge/ReactContext;)Lcom/facebook/react/jstasks/HeadlessJsTaskContext; + public final fun hasActiveTasks ()Z + public final fun isTaskRunning (I)Z + public final fun removeTaskEventListener (Lcom/facebook/react/jstasks/HeadlessJsTaskEventListener;)V + public final fun retryTask (I)Z + public final fun startTask (Lcom/facebook/react/jstasks/HeadlessJsTaskConfig;)I +} + +public final class com/facebook/react/jstasks/HeadlessJsTaskContext$Companion { + public final fun getInstance (Lcom/facebook/react/bridge/ReactContext;)Lcom/facebook/react/jstasks/HeadlessJsTaskContext; } public abstract interface class com/facebook/react/jstasks/HeadlessJsTaskEventListener { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java deleted file mode 100644 index 78b99242b36abd..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java +++ /dev/null @@ -1,121 +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.jstasks; - -import com.facebook.react.bridge.WritableMap; - -/** Class that holds the various parameters needed to start a JS task. */ -public class HeadlessJsTaskConfig { - private final String mTaskKey; - private final WritableMap mData; - private final long mTimeout; - private final boolean mAllowedInForeground; - private final HeadlessJsTaskRetryPolicy mRetryPolicy; - - /** - * Create a HeadlessJsTaskConfig. Equivalent to calling {@link #HeadlessJsTaskConfig(String, - * WritableMap, long, boolean)} with no timeout (0) and {@code false} for {@code - * allowedInBackground}. - */ - public HeadlessJsTaskConfig(String taskKey, WritableMap data) { - this(taskKey, data, 0, false); - } - - /** - * Create a HeadlessJsTaskConfig. Equivalent to calling {@link #HeadlessJsTaskConfig(String, - * WritableMap, long, boolean)} with {@code false} for {@code allowedInBackground}. - */ - public HeadlessJsTaskConfig(String taskKey, WritableMap data, long timeout) { - this(taskKey, data, timeout, false); - } - - /** - * Create a HeadlessJsTaskConfig. - * - * @param taskKey the key for the JS task to execute. This is the same key that you call {@code - * AppRegistry.registerTask} with in JS. - * @param data a map of parameters passed to the JS task executor. - * @param timeout the amount of time (in ms) after which the React instance should be terminated - * regardless of whether the task has completed or not. This is meant as a safeguard against - * accidentally keeping the device awake for long periods of time because JS crashed or some - * request timed out. A value of 0 means no timeout (should only be used for long-running - * tasks such as music playback). - * @param allowedInForeground whether to allow this task to run while the app is in the foreground - * (i.e. there is a host in resumed mode for the current ReactContext). Only set this to true - * if you really need it. Note that tasks run in the same JS thread as UI code, so doing - * expensive operations would degrade user experience. - */ - public HeadlessJsTaskConfig( - String taskKey, WritableMap data, long timeout, boolean allowedInForeground) { - this(taskKey, data, timeout, allowedInForeground, NoRetryPolicy.INSTANCE); - } - - /** - * Create a HeadlessJsTaskConfig. - * - * @param taskKey the key for the JS task to execute. This is the same key that you call {@code - * AppRegistry.registerTask} with in JS. - * @param data a map of parameters passed to the JS task executor. - * @param timeout the amount of time (in ms) after which the React instance should be terminated - * regardless of whether the task has completed or not. This is meant as a safeguard against - * accidentally keeping the device awake for long periods of time because JS crashed or some - * request timed out. A value of 0 means no timeout (should only be used for long-running - * tasks such as music playback). - * @param allowedInForeground whether to allow this task to run while the app is in the foreground - * (i.e. there is a host in resumed mode for the current ReactContext). Only set this to true - * if you really need it. Note that tasks run in the same JS thread as UI code, so doing - * expensive operations would degrade user experience. - * @param retryPolicy the number of times & delays the task should be retried on error. - */ - public HeadlessJsTaskConfig( - String taskKey, - WritableMap data, - long timeout, - boolean allowedInForeground, - HeadlessJsTaskRetryPolicy retryPolicy) { - mTaskKey = taskKey; - mData = data; - mTimeout = timeout; - mAllowedInForeground = allowedInForeground; - mRetryPolicy = retryPolicy; - } - - public HeadlessJsTaskConfig(HeadlessJsTaskConfig source) { - mTaskKey = source.mTaskKey; - mData = source.mData.copy(); - mTimeout = source.mTimeout; - mAllowedInForeground = source.mAllowedInForeground; - - final HeadlessJsTaskRetryPolicy retryPolicy = source.mRetryPolicy; - if (retryPolicy != null) { - mRetryPolicy = retryPolicy.copy(); - } else { - mRetryPolicy = null; - } - } - - /* package */ String getTaskKey() { - return mTaskKey; - } - - /* package */ WritableMap getData() { - return mData; - } - - /* package */ long getTimeout() { - return mTimeout; - } - - /* package */ boolean isAllowedInForeground() { - return mAllowedInForeground; - } - - /* package */ HeadlessJsTaskRetryPolicy getRetryPolicy() { - return mRetryPolicy; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.kt new file mode 100644 index 00000000000000..7b10570dd1c618 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.kt @@ -0,0 +1,55 @@ +/* + * 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.jstasks + +import com.facebook.react.bridge.WritableMap + +/** + * Class that holds the various parameters needed to start a JS task. + * + * @property taskKey the key for the JS task to execute. This is the same key that you call + * `AppRegistry.registerTask` with in JS. + * @property data a map of parameters passed to the JS task executor. + * @property timeout the amount of time (in ms) after which the React instance should be terminated + * regardless of whether the task has completed or not. This is meant as a safeguard against + * accidentally keeping the device awake for long periods of time because JS crashed or some + * request timed out. A value of 0 means no timeout (should only be used for long-running tasks + * such as music playback). + * @property allowedInForeground whether to allow this task to run while the app is in the + * foreground (i.e. there is a host in resumed mode for the current ReactContext). Only set this + * to true if you really need it. Note that tasks run in the same JS thread as UI code, so doing + * expensive operations would degrade user experience. + * @property allowedInForeground whether to allow this task to run while the app is in the + * foreground (i.e. there is a host in resumed mode for the current ReactContext). Only set this + * to true if you really need it. Note that tasks run in the same JS thread as UI code, so doing + * expensive operations would degrade user experience. + * @property retryPolicy the number of times & delays the task should be retried on error. + */ +public class HeadlessJsTaskConfig +@JvmOverloads +constructor( + public val taskKey: String, + public val data: WritableMap, + public val timeout: Long = 0, + public val isAllowedInForeground: Boolean = false, + public val retryPolicy: HeadlessJsTaskRetryPolicy? = NoRetryPolicy.INSTANCE +) { + + /** + * Copy constructor to create a HeadlessJsTaskConfig from an existing one. Equivalent to calling + * [HeadlessJsTaskConfig] with `false` for `allowedInBackground`. + */ + public constructor( + source: HeadlessJsTaskConfig + ) : this( + source.taskKey, + source.data.copy(), + source.timeout, + source.isAllowedInForeground, + source.retryPolicy?.copy()) +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java deleted file mode 100644 index 00ea6098d6305e..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java +++ /dev/null @@ -1,214 +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.jstasks; - -import android.util.SparseArray; -import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.UiThreadUtil; -import com.facebook.react.common.LifecycleState; -import com.facebook.react.modules.appregistry.AppRegistry; -import java.lang.ref.WeakReference; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Helper class for dealing with JS tasks. Handles per-ReactContext active task tracking, starting / - * stopping tasks and notifying listeners. - */ -public class HeadlessJsTaskContext { - - private static final WeakHashMap INSTANCES = - new WeakHashMap<>(); - - /** - * Get the task helper instance for a particular {@link ReactContext}. There is only one instance - * per context. - * - *

Note: do not hold long-lived references to the object returned here, as - * that will cause memory leaks. Instead, just call this method on-demand. - */ - public static HeadlessJsTaskContext getInstance(ReactContext context) { - HeadlessJsTaskContext helper = INSTANCES.get(context); - if (helper == null) { - helper = new HeadlessJsTaskContext(context); - INSTANCES.put(context, helper); - } - return helper; - } - - private final WeakReference mReactContext; - private final Set mHeadlessJsTaskEventListeners = - new CopyOnWriteArraySet<>(); - private final AtomicInteger mLastTaskId = new AtomicInteger(0); - private final Set mActiveTasks = new CopyOnWriteArraySet<>(); - private final Map mActiveTaskConfigs = new ConcurrentHashMap<>(); - private final SparseArray mTaskTimeouts = new SparseArray<>(); - - private HeadlessJsTaskContext(ReactContext reactContext) { - mReactContext = new WeakReference(reactContext); - } - - /** - * Register a task lifecycle event listener. Synchronized in order to prevent race conditions with - * finishTask, as the listener will be invoked for already running tasks. - */ - public synchronized void addTaskEventListener(HeadlessJsTaskEventListener listener) { - mHeadlessJsTaskEventListeners.add(listener); - for (Integer activeTaskId : mActiveTasks) { - listener.onHeadlessJsTaskStart(activeTaskId); - } - } - - /** Unregister a task lifecycle event listener. */ - public void removeTaskEventListener(HeadlessJsTaskEventListener listener) { - mHeadlessJsTaskEventListeners.remove(listener); - } - - /** Get whether there are any running JS tasks at the moment. */ - public boolean hasActiveTasks() { - return mActiveTasks.size() > 0; - } - - /** - * Start a JS task. Handles invoking {@link AppRegistry#startHeadlessTask} and notifying - * listeners. - * - * @return a unique id representing this task instance. - */ - public synchronized int startTask(final HeadlessJsTaskConfig taskConfig) { - final int taskId = mLastTaskId.incrementAndGet(); - startTask(taskConfig, taskId); - return taskId; - } - - /** - * Start a JS task the provided task id. Handles invoking {@link AppRegistry#startHeadlessTask} - * and notifying listeners. - */ - private synchronized void startTask(final HeadlessJsTaskConfig taskConfig, int taskId) { - UiThreadUtil.assertOnUiThread(); - ReactContext reactContext = - Assertions.assertNotNull( - mReactContext.get(), - "Tried to start a task on a react context that has already been destroyed"); - if (reactContext.getLifecycleState() == LifecycleState.RESUMED - && !taskConfig.isAllowedInForeground()) { - throw new IllegalStateException( - "Tried to start task " - + taskConfig.getTaskKey() - + " while in foreground, but this is not allowed."); - } - mActiveTasks.add(taskId); - mActiveTaskConfigs.put(taskId, new HeadlessJsTaskConfig(taskConfig)); - if (reactContext.hasActiveReactInstance()) { - reactContext - .getJSModule(AppRegistry.class) - .startHeadlessTask(taskId, taskConfig.getTaskKey(), taskConfig.getData()); - } else { - ReactSoftExceptionLogger.logSoftException( - "HeadlessJsTaskContext", - new RuntimeException("Cannot start headless task, CatalystInstance not available")); - } - if (taskConfig.getTimeout() > 0) { - scheduleTaskTimeout(taskId, taskConfig.getTimeout()); - } - for (HeadlessJsTaskEventListener listener : mHeadlessJsTaskEventListeners) { - listener.onHeadlessJsTaskStart(taskId); - } - } - - /** - * Retry a running JS task with a delay. Invokes {@link - * HeadlessJsTaskContext#startTask(HeadlessJsTaskConfig, int)} as long as the process does not get - * killed. - * - * @return true if a retry attempt has been posted. - */ - public synchronized boolean retryTask(final int taskId) { - final HeadlessJsTaskConfig sourceTaskConfig = mActiveTaskConfigs.get(taskId); - Assertions.assertCondition( - sourceTaskConfig != null, - "Tried to retrieve non-existent task config with id " + taskId + "."); - - final HeadlessJsTaskRetryPolicy retryPolicy = sourceTaskConfig.getRetryPolicy(); - if (!retryPolicy.canRetry()) { - return false; - } - - removeTimeout(taskId); - final HeadlessJsTaskConfig taskConfig = - new HeadlessJsTaskConfig( - sourceTaskConfig.getTaskKey(), - sourceTaskConfig.getData(), - sourceTaskConfig.getTimeout(), - sourceTaskConfig.isAllowedInForeground(), - retryPolicy.update()); - - final Runnable retryAttempt = - new Runnable() { - @Override - public void run() { - startTask(taskConfig, taskId); - } - }; - - UiThreadUtil.runOnUiThread(retryAttempt, retryPolicy.getDelay()); - return true; - } - - /** - * Finish a JS task. Doesn't actually stop the task on the JS side, only removes it from the list - * of active tasks and notifies listeners. - * - * @param taskId the unique id returned by {@link #startTask}. - */ - public synchronized void finishTask(final int taskId) { - boolean removed = mActiveTasks.remove(taskId); - mActiveTaskConfigs.remove(taskId); - removeTimeout(taskId); - if (removed) { - UiThreadUtil.runOnUiThread( - new Runnable() { - @Override - public void run() { - for (HeadlessJsTaskEventListener listener : mHeadlessJsTaskEventListeners) { - listener.onHeadlessJsTaskFinish(taskId); - } - } - }); - } - } - - private void removeTimeout(int taskId) { - Runnable runnable = mTaskTimeouts.get(taskId); - if (runnable != null) { - UiThreadUtil.removeOnUiThread(runnable); - mTaskTimeouts.remove(taskId); - } - } - - /** - * Check if a given task is currently running. A task is stopped if either {@link #finishTask} is - * called or it times out. - */ - public synchronized boolean isTaskRunning(final int taskId) { - return mActiveTasks.contains(taskId); - } - - private void scheduleTaskTimeout(final int taskId, long timeout) { - Runnable runnable = () -> finishTask(taskId); - mTaskTimeouts.append(taskId, runnable); - UiThreadUtil.runOnUiThread(runnable, timeout); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.kt new file mode 100644 index 00000000000000..c57d3501392bef --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.kt @@ -0,0 +1,185 @@ +/* + * 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.jstasks + +import android.util.SparseArray +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.common.LifecycleState +import com.facebook.react.modules.appregistry.AppRegistry +import java.lang.ref.WeakReference +import java.util.WeakHashMap +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.atomic.AtomicInteger + +/** + * Helper class for dealing with JS tasks. Handles per-ReactContext active task tracking, starting / + * stopping tasks and notifying listeners. + */ +public class HeadlessJsTaskContext private constructor(reactContext: ReactContext) { + private val reactContext = WeakReference(reactContext) + private val headlessJsTaskEventListeners: MutableSet = + CopyOnWriteArraySet() + private val lastTaskId = AtomicInteger(0) + private val activeTasks: MutableSet = CopyOnWriteArraySet() + private val activeTaskConfigs: MutableMap = ConcurrentHashMap() + private val taskTimeouts = SparseArray() + + /** + * Register a task lifecycle event listener. Synchronized in order to prevent race conditions with + * finishTask, as the listener will be invoked for already running tasks. + */ + @Synchronized + public fun addTaskEventListener(listener: HeadlessJsTaskEventListener) { + headlessJsTaskEventListeners.add(listener) + for (activeTaskId in activeTasks) { + listener.onHeadlessJsTaskStart(activeTaskId) + } + } + + /** Unregister a task lifecycle event listener. */ + public fun removeTaskEventListener(listener: HeadlessJsTaskEventListener) { + headlessJsTaskEventListeners.remove(listener) + } + + /** Get whether there are any running JS tasks at the moment. */ + public fun hasActiveTasks(): Boolean = activeTasks.isNotEmpty() + + /** + * Start a JS task. Handles invoking [AppRegistry.startHeadlessTask] and notifying listeners. + * + * @return a unique id representing this task instance. + */ + @Synchronized + public fun startTask(taskConfig: HeadlessJsTaskConfig): Int = + lastTaskId.incrementAndGet().apply { startTask(taskConfig, this) } + + /** + * Start a JS task the provided task id. Handles invoking [AppRegistry.startHeadlessTask] and + * notifying listeners. + */ + @Synchronized + private fun startTask(taskConfig: HeadlessJsTaskConfig, taskId: Int) { + UiThreadUtil.assertOnUiThread() + val reactContext = + Assertions.assertNotNull( + reactContext.get(), + "Tried to start a task on a react context that has already been destroyed") + check( + !(reactContext.lifecycleState == LifecycleState.RESUMED && + !taskConfig.isAllowedInForeground)) { + "Tried to start task ${taskConfig.taskKey} while in foreground, but this is not allowed." + } + activeTasks.add(taskId) + activeTaskConfigs[taskId] = HeadlessJsTaskConfig(taskConfig) + if (reactContext.hasActiveReactInstance()) { + reactContext + .getJSModule(AppRegistry::class.java) + .startHeadlessTask(taskId, taskConfig.taskKey, taskConfig.data) + } else { + logSoftException( + "HeadlessJsTaskContext", + RuntimeException("Cannot start headless task, CatalystInstance not available")) + } + if (taskConfig.timeout > 0) { + scheduleTaskTimeout(taskId, taskConfig.timeout) + } + for (listener in headlessJsTaskEventListeners) { + listener.onHeadlessJsTaskStart(taskId) + } + } + + /** + * Retry a running JS task with a delay. Invokes [ ][HeadlessJsTaskContext.startTask] as long as + * the process does not get killed. + * + * @return true if a retry attempt has been posted. + */ + @Synchronized + public fun retryTask(taskId: Int): Boolean { + val sourceTaskConfig = activeTaskConfigs[taskId] + checkNotNull(sourceTaskConfig) { "Tried to retrieve non-existent task config with id $taskId." } + + val retryPolicy = sourceTaskConfig.retryPolicy + if (retryPolicy == null || !retryPolicy.canRetry()) { + return false + } + + removeTimeout(taskId) + val taskConfig = + HeadlessJsTaskConfig( + sourceTaskConfig.taskKey, + sourceTaskConfig.data, + sourceTaskConfig.timeout, + sourceTaskConfig.isAllowedInForeground, + retryPolicy.update()) + + val retryAttempt = Runnable { startTask(taskConfig, taskId) } + + UiThreadUtil.runOnUiThread(retryAttempt, retryPolicy.delay.toLong()) + return true + } + + /** + * Finish a JS task. Doesn't actually stop the task on the JS side, only removes it from the list + * of active tasks and notifies listeners. + * + * @param taskId the unique id returned by [.startTask]. + */ + @Synchronized + public fun finishTask(taskId: Int) { + val removed = activeTasks.remove(taskId) + activeTaskConfigs.remove(taskId) + removeTimeout(taskId) + if (removed) { + UiThreadUtil.runOnUiThread { + for (listener in headlessJsTaskEventListeners) { + listener.onHeadlessJsTaskFinish(taskId) + } + } + } + } + + private fun removeTimeout(taskId: Int) { + val runnable = taskTimeouts[taskId] + if (runnable != null) { + UiThreadUtil.removeOnUiThread(runnable) + taskTimeouts.remove(taskId) + } + } + + /** + * Check if a given task is currently running. A task is stopped if either [finishTask] is called + * or it times out. + */ + @Synchronized public fun isTaskRunning(taskId: Int): Boolean = taskId in activeTasks + + private fun scheduleTaskTimeout(taskId: Int, timeout: Long) { + val runnable = Runnable { finishTask(taskId) } + taskTimeouts.append(taskId, runnable) + UiThreadUtil.runOnUiThread(runnable, timeout) + } + + public companion object { + private val INSTANCES = WeakHashMap() + + /** + * Get the task helper instance for a particular [ReactContext]. There is only one instance per + * context. + * + * **Note:** do not hold long-lived references to the object returned here, as that will cause + * memory leaks. Instead, just call this method on-demand. + */ + @JvmStatic + public fun getInstance(context: ReactContext): HeadlessJsTaskContext = + INSTANCES.getOrPut(context) { HeadlessJsTaskContext(context) } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.kt similarity index 60% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.java rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.kt index 0de53dbd195cac..ae0ab4cbc15ef3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskEventListener.kt @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.jstasks; +package com.facebook.react.jstasks /** Listener interface for task lifecycle events. */ public interface HeadlessJsTaskEventListener { @@ -15,12 +15,11 @@ public interface HeadlessJsTaskEventListener { * * @param taskId the unique identifier of this task instance */ - void onHeadlessJsTaskStart(int taskId); + public fun onHeadlessJsTaskStart(taskId: Int) /** - * Called when a JS task finishes (i.e. when {@link - * HeadlessJsTaskSupportModule#notifyTaskFinished} is called, or when it times out), on the UI - * thread. + * Called when a JS task finishes (i.e. when [HeadlessJsTaskSupportModule.notifyTaskFinished] is + * called, or when it times out), on the UI thread. */ - void onHeadlessJsTaskFinish(int taskId); + public fun onHeadlessJsTaskFinish(taskId: Int) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java deleted file mode 100644 index 6a8965f64e55a6..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java +++ /dev/null @@ -1,22 +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.jstasks; - -import javax.annotation.CheckReturnValue; - -public interface HeadlessJsTaskRetryPolicy { - - boolean canRetry(); - - int getDelay(); - - @CheckReturnValue - HeadlessJsTaskRetryPolicy update(); - - HeadlessJsTaskRetryPolicy copy(); -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.kt new file mode 100644 index 00000000000000..e2fb82e20a11fa --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.kt @@ -0,0 +1,20 @@ +/* + * 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.jstasks + +import javax.annotation.CheckReturnValue + +public interface HeadlessJsTaskRetryPolicy { + public fun canRetry(): Boolean + + public val delay: Int + + @CheckReturnValue public fun update(): HeadlessJsTaskRetryPolicy? + + public fun copy(): HeadlessJsTaskRetryPolicy? +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.kt index 1ba4ff038f784e..b312b5bbe2c57b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.kt @@ -12,11 +12,11 @@ public class LinearCountingRetryPolicy( private val delayBetweenAttemptsInMs: Int ) : HeadlessJsTaskRetryPolicy { - override public fun canRetry(): Boolean = retryAttempts > 0 + public override fun canRetry(): Boolean = retryAttempts > 0 - override public fun getDelay(): Int = delayBetweenAttemptsInMs + override val delay: Int = delayBetweenAttemptsInMs - override public fun update(): HeadlessJsTaskRetryPolicy { + public override fun update(): HeadlessJsTaskRetryPolicy { val remainingRetryAttempts = retryAttempts - 1 return if (remainingRetryAttempts > 0) { @@ -26,6 +26,6 @@ public class LinearCountingRetryPolicy( } } - override public fun copy(): HeadlessJsTaskRetryPolicy = + public override fun copy(): HeadlessJsTaskRetryPolicy = LinearCountingRetryPolicy(retryAttempts, delayBetweenAttemptsInMs) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.kt index 2e6223c66a92c7..fe505746194070 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.kt @@ -11,9 +11,8 @@ internal class NoRetryPolicy private constructor() : HeadlessJsTaskRetryPolicy { override fun canRetry(): Boolean = false - override fun getDelay(): Int { - throw IllegalStateException("Should not retrieve delay as canRetry is: ${canRetry()}") - } + override val delay: Int + get() = throw IllegalStateException("Should not retrieve delay as canRetry is: ${canRetry()}") override fun update(): HeadlessJsTaskRetryPolicy { throw IllegalStateException("Should not update as canRetry is: ${canRetry()}")