From ac7ec4602fd4d8ec033a084df6a92437ea2d3a8a Mon Sep 17 00:00:00 2001 From: Joshua Ong Date: Thu, 6 Jun 2019 11:54:08 -0700 Subject: [PATCH] Allow headless JS tasks to retry (#23231) Summary: `setTimeout` inside a headless JS task does not always works; the function does not get invoked until the user starts an `Activity`. This was attempted to be used in the context of widgets. When the widget update or user interaction causes the process and React context to be created, the headless JS task may run before other app-specific JS initialisation logic has completed. If it's not possible to change the behaviour of the pre-requisites to be synchronous, then the headless JS task blocks such asynchronous JS work that it may depend on. A primitive solution is the use of `setTimeout` in order to wait for the pre-conditions to be met before continuing with the rest of the headless JS task. But as the function passed to `setTimeout` is not always called, the task will not run to completion. This PR solves this scenario by allowing the task to be retried again with a delay. If the task returns a promise that resolves to a `{'timeout': number}` object, `AppRegistry.js` will not notify that the task has finished as per master, instead it will tell `HeadlessJsContext` to `startTask` again (cleaning up any posted `Runnable`s beforehand) via a `Handler` within the `HeadlessJsContext`. Documentation also updated here: https://github.com/facebook/react-native-website/pull/771 ### AppRegistry.js If the task provider does not return any data, or if the data it returns does not contain `timeout` as a number, then it behaves as `master`; notifies that the task has finished. If the response does contain `{timeout: number}`, then it will attempt to queue a retry. If that fails, then it will behaves as if the task provider returned no response i.e. behaves as `master` again. If the retry was successfully queued, then there is nothing to do as we do not want the `Service` to stop itself. ### HeadlessJsTaskSupportModule.java Similar to notify start/finished, we simply check if the context is running, and if so, pass the request onto `HeadlessJsTaskContext`. The only difference here is that we return a `Promise`, so that `AppRegistry`, as above, knows whether the enqueuing failed and thus needs to perform the usual task clean-up. ### HeadlessJsTaskContext.java Before retrying, we need to clean-up any timeout `Runnable`'s posted for the first attempt. Then we need to copy the task config so that if this retry (second attempt) also fails, then on the third attempt (second retry) we do not run into a consumed exception. This is also why in `startTask` we copy the config before putting it in the `Map`, so that the initial attempt does leave the config's in the map as consumed. Then we post a `Runnable` to call `startTask` on the main thread's `Handler`. We use the same `taskId` because the `Service` is keeping track of active task IDs in order to calculate whether it needs to `stopSelf`. This negates the need to inform the `Service` of a new task id and us having to remove the old one. ## Changelog [Android][added] - Allow headless JS tasks to return a promise that will cause the task to be retried again with the specified delay Pull Request resolved: https://github.com/facebook/react-native/pull/23231 Differential Revision: D15646870 fbshipit-source-id: 4440f4b4392f1fa5c69aab7908b51b7007ba2c40 --- Libraries/ReactNative/AppRegistry.js | 15 +++- Libraries/ReactNative/HeadlessJsTaskError.js | 12 +++ .../NativeHeadlessJsTaskSupport.js | 1 + .../tests/core/WritableNativeMapTest.java | 26 +++++- .../facebook/react/bridge/JavaOnlyMap.java | 7 ++ .../facebook/react/bridge/UiThreadUtil.java | 9 +- .../facebook/react/bridge/WritableMap.java | 1 + .../react/bridge/WritableNativeMap.java | 7 ++ .../react/jstasks/HeadlessJsTaskConfig.java | 46 ++++++++++ .../react/jstasks/HeadlessJsTaskContext.java | 83 ++++++++++++++++--- .../jstasks/HeadlessJsTaskRetryPolicy.java | 16 ++++ .../jstasks/LinearCountingRetryPolicy.java | 38 +++++++++ .../facebook/react/jstasks/NoRetryPolicy.java | 30 +++++++ .../core/HeadlessJsTaskSupportModule.java | 17 ++++ 14 files changed, 290 insertions(+), 18 deletions(-) create mode 100644 Libraries/ReactNative/HeadlessJsTaskError.js create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.java diff --git a/Libraries/ReactNative/AppRegistry.js b/Libraries/ReactNative/AppRegistry.js index 2d517ce6dd0f10..d7e9fc788a8adb 100644 --- a/Libraries/ReactNative/AppRegistry.js +++ b/Libraries/ReactNative/AppRegistry.js @@ -21,6 +21,7 @@ const createPerformanceLogger = require('../Utilities/createPerformanceLogger'); import type {IPerformanceLogger} from '../Utilities/createPerformanceLogger'; import NativeHeadlessJsTaskSupport from './NativeHeadlessJsTaskSupport'; +import HeadlessJsTaskError from './HeadlessJsTaskError'; type Task = (taskData: any) => Promise; type TaskProvider = () => Task; @@ -275,8 +276,18 @@ const AppRegistry = { }) .catch(reason => { console.error(reason); - if (NativeHeadlessJsTaskSupport) { - NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId); + + if ( + NativeHeadlessJsTaskSupport && + reason instanceof HeadlessJsTaskError + ) { + NativeHeadlessJsTaskSupport.notifyTaskRetry(taskId).then( + retryPosted => { + if (!retryPosted) { + NativeHeadlessJsTaskSupport.notifyTaskFinished(taskId); + } + }, + ); } }); }, diff --git a/Libraries/ReactNative/HeadlessJsTaskError.js b/Libraries/ReactNative/HeadlessJsTaskError.js new file mode 100644 index 00000000000000..0f85e138bb5a5f --- /dev/null +++ b/Libraries/ReactNative/HeadlessJsTaskError.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ +'use strict'; + +export default class HeadlessJsTaskError extends Error {} diff --git a/Libraries/ReactNative/NativeHeadlessJsTaskSupport.js b/Libraries/ReactNative/NativeHeadlessJsTaskSupport.js index 686ababaa9ae4f..d3256f49e10046 100644 --- a/Libraries/ReactNative/NativeHeadlessJsTaskSupport.js +++ b/Libraries/ReactNative/NativeHeadlessJsTaskSupport.js @@ -15,6 +15,7 @@ import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; export interface Spec extends TurboModule { +notifyTaskFinished: (taskId: number) => void; + +notifyTaskRetry: (taskId: number) => Promise; } export default TurboModuleRegistry.get('HeadlessJsTaskSupport'); diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/WritableNativeMapTest.java b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/WritableNativeMapTest.java index 5199156393d99e..e709799f66e7dd 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/WritableNativeMapTest.java +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/tests/core/WritableNativeMapTest.java @@ -5,6 +5,8 @@ import androidx.test.runner.AndroidJUnit4; import com.facebook.react.bridge.NoSuchKeyException; import com.facebook.react.bridge.UnexpectedNativeTypeException; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; import org.junit.Assert; @@ -16,6 +18,8 @@ @RunWith(AndroidJUnit4.class) public class WritableNativeMapTest { + private static final String ARRAY = "array"; + private static final String MAP = "map"; private WritableNativeMap mMap; @Before @@ -25,8 +29,8 @@ public void setup() { mMap.putDouble("double", 1.2); mMap.putInt("int", 1); mMap.putString("string", "abc"); - mMap.putMap("map", new WritableNativeMap()); - mMap.putArray("array", new WritableNativeArray()); + mMap.putMap(MAP, new WritableNativeMap()); + mMap.putArray(ARRAY, new WritableNativeArray()); mMap.putBoolean("dvacca", true); } @@ -100,4 +104,22 @@ public void testErrorMessageContainsKey() { assertThat(e.getMessage()).contains(key); } } + + @Test + public void testCopy() { + final WritableMap copy = mMap.copy(); + + assertThat(copy).isNotSameAs(mMap); + assertThat(copy.getMap(MAP)).isNotSameAs(mMap.getMap(MAP)); + assertThat(copy.getArray(ARRAY)).isNotSameAs(mMap.getArray(ARRAY)); + } + + @Test + public void testCopyModification() { + final WritableMap copy = mMap.copy(); + copy.putString("string", "foo"); + + assertThat(copy.getString("string")).isEqualTo("foo"); + assertThat(mMap.getString("string")).isEqualTo("abc"); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index 139f824611338a..ddf450f814ccfb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -206,6 +206,13 @@ public void merge(@Nonnull ReadableMap source) { mBackingMap.putAll(((JavaOnlyMap) source).mBackingMap); } + @Override + public WritableMap copy() { + final JavaOnlyMap target = new JavaOnlyMap(); + target.merge(this); + return target; + } + @Override public void putArray(@Nonnull String key, @Nullable WritableArray value) { mBackingMap.put(key, value); diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java index 3adf9b8359e6f3..9b320097d69e56 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/UiThreadUtil.java @@ -44,11 +44,18 @@ public static void assertNotOnUiThread() { * Runs the given {@code Runnable} on the UI thread. */ public static void runOnUiThread(Runnable runnable) { + runOnUiThread(runnable, 0); + } + + /** + * Runs the given {@code Runnable} on the UI thread with the specified delay. + */ + public static void runOnUiThread(Runnable runnable, long delayInMs) { synchronized (UiThreadUtil.class) { if (sMainHandler == null) { sMainHandler = new Handler(Looper.getMainLooper()); } } - sMainHandler.post(runnable); + sMainHandler.postDelayed(runnable, delayInMs); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java index 1aa2d7a108f683..9338c19ae96903 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java @@ -24,4 +24,5 @@ public interface WritableMap extends ReadableMap { void putMap(@Nonnull String key, @Nullable WritableMap value); void merge(@Nonnull ReadableMap source); + WritableMap copy(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index ec632940b47e86..997f4058783336 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -61,6 +61,13 @@ public void merge(@Nonnull ReadableMap source) { mergeNativeMap((ReadableNativeMap) source); } + @Override + public WritableMap copy() { + final WritableNativeMap target = new WritableNativeMap(); + target.merge(this); + return target; + } + public WritableNativeMap() { super(initHybrid()); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java index f0e15451f7cd7e..1c441300ba1a40 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java +++ b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskConfig.java @@ -15,6 +15,7 @@ public class HeadlessJsTaskConfig { private final WritableMap mData; private final long mTimeout; private final boolean mAllowedInForeground; + private final HeadlessJsTaskRetryPolicy mRetryPolicy; /** * Create a HeadlessJsTaskConfig. Equivalent to calling @@ -55,10 +56,51 @@ public HeadlessJsTaskConfig( 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() { @@ -76,4 +118,8 @@ public HeadlessJsTaskConfig( /* package */ boolean isAllowedInForeground() { return mAllowedInForeground; } + + /* package */ HeadlessJsTaskRetryPolicy getRetryPolicy() { + return mRetryPolicy; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java index fbf38f1a41ee4a..e69505f396a9cd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java +++ b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java @@ -5,12 +5,6 @@ package com.facebook.react.jstasks; -import java.lang.ref.WeakReference; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.atomic.AtomicInteger; - import android.os.Handler; import android.util.SparseArray; @@ -20,6 +14,14 @@ 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. @@ -51,6 +53,7 @@ public static HeadlessJsTaskContext getInstance(ReactContext context) { private final AtomicInteger mLastTaskId = new AtomicInteger(0); private final Handler mHandler = new Handler(); private final Set mActiveTasks = new CopyOnWriteArraySet<>(); + private final Map mActiveTaskConfigs = new ConcurrentHashMap<>(); private final SparseArray mTaskTimeouts = new SparseArray<>(); private HeadlessJsTaskContext(ReactContext reactContext) { @@ -85,6 +88,16 @@ public boolean hasActiveTasks() { * @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(), @@ -95,8 +108,8 @@ public synchronized int startTask(final HeadlessJsTaskConfig taskConfig) { "Tried to start task " + taskConfig.getTaskKey() + " while in foreground, but this is not allowed."); } - final int taskId = mLastTaskId.incrementAndGet(); mActiveTasks.add(taskId); + mActiveTaskConfigs.put(taskId, new HeadlessJsTaskConfig(taskConfig)); reactContext.getJSModule(AppRegistry.class) .startHeadlessTask(taskId, taskConfig.getTaskKey(), taskConfig.getData()); if (taskConfig.getTimeout() > 0) { @@ -105,7 +118,44 @@ public synchronized int startTask(final HeadlessJsTaskConfig taskConfig) { for (HeadlessJsTaskEventListener listener : mHeadlessJsTaskEventListeners) { listener.onHeadlessJsTaskStart(taskId); } - return 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; } /** @@ -118,11 +168,10 @@ public synchronized void finishTask(final int taskId) { Assertions.assertCondition( mActiveTasks.remove(taskId), "Tried to finish non-existent task with id " + taskId + "."); - Runnable timeout = mTaskTimeouts.get(taskId); - if (timeout != null) { - mHandler.removeCallbacks(timeout); - mTaskTimeouts.remove(taskId); - } + Assertions.assertCondition( + mActiveTaskConfigs.remove(taskId) != null, + "Tried to remove non-existent task config with id " + taskId + "."); + removeTimeout(taskId); UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { @@ -133,6 +182,14 @@ public void run() { }); } + private void removeTimeout(int taskId) { + Runnable timeout = mTaskTimeouts.get(taskId); + if (timeout != null) { + mHandler.removeCallbacks(timeout); + 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. diff --git a/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java new file mode 100644 index 00000000000000..215f5b8b44cb8a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskRetryPolicy.java @@ -0,0 +1,16 @@ +package com.facebook.react.jstasks; + +import javax.annotation.CheckReturnValue; + +public interface HeadlessJsTaskRetryPolicy { + + boolean canRetry(); + + int getDelay(); + + @CheckReturnValue + HeadlessJsTaskRetryPolicy update(); + + HeadlessJsTaskRetryPolicy copy(); + +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.java b/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.java new file mode 100644 index 00000000000000..b319d35435ead1 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jstasks/LinearCountingRetryPolicy.java @@ -0,0 +1,38 @@ +package com.facebook.react.jstasks; + +public class LinearCountingRetryPolicy implements HeadlessJsTaskRetryPolicy { + + private final int mRetryAttempts; + private final int mDelayBetweenAttemptsInMs; + + public LinearCountingRetryPolicy(int retryAttempts, int delayBetweenAttemptsInMs) { + mRetryAttempts = retryAttempts; + mDelayBetweenAttemptsInMs = delayBetweenAttemptsInMs; + } + + @Override + public boolean canRetry() { + return mRetryAttempts > 0; + } + + @Override + public int getDelay() { + return mDelayBetweenAttemptsInMs; + } + + @Override + public HeadlessJsTaskRetryPolicy update() { + final int remainingRetryAttempts = mRetryAttempts - 1; + + if (remainingRetryAttempts > 0) { + return new LinearCountingRetryPolicy(remainingRetryAttempts, mDelayBetweenAttemptsInMs); + } else { + return NoRetryPolicy.INSTANCE; + } + } + + @Override + public HeadlessJsTaskRetryPolicy copy() { + return new LinearCountingRetryPolicy(mRetryAttempts, mDelayBetweenAttemptsInMs); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.java b/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.java new file mode 100644 index 00000000000000..ac707ab70d7789 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jstasks/NoRetryPolicy.java @@ -0,0 +1,30 @@ +package com.facebook.react.jstasks; + +public class NoRetryPolicy implements HeadlessJsTaskRetryPolicy { + + public static final NoRetryPolicy INSTANCE = new NoRetryPolicy(); + + private NoRetryPolicy() { + } + + @Override + public boolean canRetry() { + return false; + } + + @Override + public int getDelay() { + throw new IllegalStateException("Should not retrieve delay as canRetry is: " + canRetry()); + } + + @Override + public HeadlessJsTaskRetryPolicy update() { + throw new IllegalStateException("Should not update as canRetry is: " + canRetry()); + } + + @Override + public HeadlessJsTaskRetryPolicy copy() { + // Class is immutable so no need to copy + return this; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/HeadlessJsTaskSupportModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/HeadlessJsTaskSupportModule.java index 2d783585a77a6d..e9c2cae2a69c9a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/HeadlessJsTaskSupportModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/HeadlessJsTaskSupportModule.java @@ -8,6 +8,7 @@ package com.facebook.react.modules.core; import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -32,6 +33,22 @@ public String getName() { return NAME; } + @ReactMethod + public void notifyTaskRetry(int taskId, Promise promise) { + HeadlessJsTaskContext headlessJsTaskContext = + HeadlessJsTaskContext.getInstance(getReactApplicationContext()); + if (headlessJsTaskContext.isTaskRunning(taskId)) { + final boolean retryPosted = headlessJsTaskContext.retryTask(taskId); + promise.resolve(retryPosted); + } else { + FLog.w( + HeadlessJsTaskSupportModule.class, + "Tried to retry non-active task with id %d. Did it time out?", + taskId); + promise.resolve(false); + } + } + @ReactMethod public void notifyTaskFinished(int taskId) { HeadlessJsTaskContext headlessJsTaskContext =