mirror of
https://github.com/status-im/react-native.git
synced 2025-02-26 08:05:34 +00:00
Android: add support for headless js tasks
Summary: Provide a base `HeadlessJsTaskService` class that can be extended to run JS in headless mode in response to some event. Added `HeadlessJsTaskEventListener` for modules that are interested in background lifecycle events, and `HeadlessJsTaskContext` that basically extends `ReactContext` without touching it. The react instance is shared with the rest of the app (e.g. activities) through the `ReactNativeHost`. Reviewed By: astreet Differential Revision: D3225753 fbshipit-source-id: 2c5e7679636f31e0e7842d8a67aeb95baf47c563
This commit is contained in:
parent
542ab8643e
commit
3080b8d26c
@ -15,9 +15,11 @@ var BatchedBridge = require('BatchedBridge');
|
||||
var BugReporting = require('BugReporting');
|
||||
var ReactNative = require('react/lib/ReactNative');
|
||||
|
||||
const infoLog = require('infoLog');
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
var renderApplication = require('renderApplication');
|
||||
const infoLog = require('infoLog');
|
||||
|
||||
const { HeadlessJsTaskSupport } = require('NativeModules');
|
||||
|
||||
if (__DEV__) {
|
||||
// In order to use Cmd+P to record/dump perf data, we need to make sure
|
||||
@ -25,8 +27,12 @@ if (__DEV__) {
|
||||
require('RCTRenderingPerf');
|
||||
}
|
||||
|
||||
type Task = (taskData: any) => Promise<void>;
|
||||
type TaskProvider = () => Task;
|
||||
|
||||
var runnables = {};
|
||||
var runCount = 1;
|
||||
const tasks: Map<string, TaskProvider> = new Map();
|
||||
|
||||
type ComponentProvider = () => ReactClass<any>;
|
||||
|
||||
@ -103,6 +109,40 @@ var AppRegistry = {
|
||||
ReactNative.unmountComponentAtNodeAndRemoveContainer(rootTag);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a headless task. A headless task is a bit of code that runs without a UI.
|
||||
* @param taskKey the key associated with this task
|
||||
* @param task a promise returning function that takes some data passed from the native side as
|
||||
* the only argument; when the promise is resolved or rejected the native side is
|
||||
* notified of this event and it may decide to destroy the JS context.
|
||||
*/
|
||||
registerHeadlessTask: function(taskKey: string, task: TaskProvider): void {
|
||||
if (tasks.has(taskKey)) {
|
||||
console.warn(`registerHeadlessTask called multiple times for same key '${taskKey}'`);
|
||||
}
|
||||
tasks.set(taskKey, task);
|
||||
},
|
||||
|
||||
/**
|
||||
* Only called from native code. Starts a headless task.
|
||||
*
|
||||
* @param taskId the native id for this task instance to keep track of its execution
|
||||
* @param taskKey the key for the task to start
|
||||
* @param data the data to pass to the task
|
||||
*/
|
||||
startHeadlessTask: function(taskId: number, taskKey: string, data: any): void {
|
||||
const taskProvider = tasks.get(taskKey);
|
||||
if (!taskProvider) {
|
||||
throw new Error(`No task registered for key ${taskKey}`);
|
||||
}
|
||||
taskProvider()(data)
|
||||
.then(() => HeadlessJsTaskSupport.notifyTaskFinished(taskId))
|
||||
.catch(reason => {
|
||||
console.error(reason);
|
||||
HeadlessJsTaskSupport.notifyTaskFinished(taskId);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
BatchedBridge.registerCallableModule(
|
||||
|
@ -5,6 +5,7 @@ DEPS = [
|
||||
react_native_target('java/com/facebook/react/bridge:bridge'),
|
||||
react_native_target('java/com/facebook/react/common:common'),
|
||||
react_native_target('java/com/facebook/react/devsupport:devsupport'),
|
||||
react_native_target('java/com/facebook/react/jstasks:jstasks'),
|
||||
react_native_target('java/com/facebook/react/module/annotations:annotations'),
|
||||
react_native_target('java/com/facebook/react/module/model:model'),
|
||||
react_native_target('java/com/facebook/react/modules/core:core'),
|
||||
|
@ -26,6 +26,7 @@ import com.facebook.react.devsupport.HMRClient;
|
||||
import com.facebook.react.devsupport.JSCHeapCapture;
|
||||
import com.facebook.react.devsupport.JSCSamplingProfiler;
|
||||
import com.facebook.react.module.annotations.ReactModuleList;
|
||||
import com.facebook.react.modules.core.HeadlessJsTaskSupportModule;
|
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.modules.core.ExceptionsManagerModule;
|
||||
@ -97,6 +98,13 @@ import static com.facebook.react.bridge.ReactMarkerConstants.CREATE_UI_MANAGER_M
|
||||
return new AndroidInfoModule();
|
||||
}
|
||||
}));
|
||||
moduleSpecList
|
||||
.add(new ModuleSpec(HeadlessJsTaskSupportModule.class, new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return new HeadlessJsTaskSupportModule(reactContext);
|
||||
}
|
||||
}));
|
||||
moduleSpecList.add(
|
||||
new ModuleSpec(DeviceEventManagerModule.class, new Provider<NativeModule>() {
|
||||
@Override
|
||||
|
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
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 com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
||||
|
||||
/**
|
||||
* 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<Integer> 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.
|
||||
*/
|
||||
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.getSimpleName());
|
||||
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);
|
||||
final ReactInstanceManager reactInstanceManager =
|
||||
getReactNativeHost().getReactInstanceManager();
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager
|
||||
.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
invokeStartTask(reactContext, taskConfig);
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
}
|
||||
});
|
||||
if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
|
||||
reactInstanceManager.createReactContextInBackground();
|
||||
}
|
||||
} else {
|
||||
invokeStartTask(reactContext, taskConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeStartTask(ReactContext reactContext, HeadlessJsTaskConfig taskConfig) {
|
||||
HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
|
||||
headlessJsTaskContext.addTaskEventListener(this);
|
||||
int taskId = headlessJsTaskContext.startTask(taskConfig);
|
||||
mActiveTasks.add(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (getReactNativeHost().hasInstance()) {
|
||||
ReactInstanceManager reactInstanceManager = getReactNativeHost().getReactInstanceManager();
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
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();
|
||||
}
|
||||
}
|
@ -12,9 +12,9 @@ package com.facebook.react.bridge;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@ -47,6 +47,8 @@ public class ReactContext extends ContextWrapper {
|
||||
private final CopyOnWriteArraySet<ActivityEventListener> mActivityEventListeners =
|
||||
new CopyOnWriteArraySet<>();
|
||||
|
||||
private LifecycleState mLifecycleState = LifecycleState.BEFORE_CREATE;
|
||||
|
||||
private @Nullable CatalystInstance mCatalystInstance;
|
||||
private @Nullable LayoutInflater mInflater;
|
||||
private @Nullable MessageQueueThread mUiMessageQueueThread;
|
||||
@ -54,7 +56,6 @@ public class ReactContext extends ContextWrapper {
|
||||
private @Nullable MessageQueueThread mJSMessageQueueThread;
|
||||
private @Nullable NativeModuleCallExceptionHandler mNativeModuleCallExceptionHandler;
|
||||
private @Nullable WeakReference<Activity> mCurrentActivity;
|
||||
private LifecycleState mLifecycleState = LifecycleState.BEFORE_RESUME;
|
||||
|
||||
public ReactContext(Context base) {
|
||||
super(base);
|
||||
@ -145,6 +146,10 @@ public class ReactContext extends ContextWrapper {
|
||||
return mCatalystInstance != null && !mCatalystInstance.isDestroyed();
|
||||
}
|
||||
|
||||
public LifecycleState getLifecycleState() {
|
||||
return mLifecycleState;
|
||||
}
|
||||
|
||||
public void addLifecycleEventListener(final LifecycleEventListener listener) {
|
||||
mLifecycleEventListeners.add(listener);
|
||||
if (hasActiveCatalystInstance()) {
|
||||
@ -197,6 +202,7 @@ public class ReactContext extends ContextWrapper {
|
||||
*/
|
||||
public void onHostResume(@Nullable Activity activity) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mLifecycleState = LifecycleState.RESUMED;
|
||||
mCurrentActivity = new WeakReference(activity);
|
||||
mLifecycleState = LifecycleState.RESUMED;
|
||||
for (LifecycleEventListener listener : mLifecycleEventListeners) {
|
||||
@ -228,6 +234,7 @@ public class ReactContext extends ContextWrapper {
|
||||
*/
|
||||
public void onHostDestroy() {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
mLifecycleState = LifecycleState.BEFORE_CREATE;
|
||||
for (LifecycleEventListener listener : mLifecycleEventListeners) {
|
||||
listener.onHostDestroy();
|
||||
}
|
||||
|
23
ReactAndroid/src/main/java/com/facebook/react/jstasks/BUCK
Normal file
23
ReactAndroid/src/main/java/com/facebook/react/jstasks/BUCK
Normal file
@ -0,0 +1,23 @@
|
||||
include_defs('//ReactAndroid/DEFS')
|
||||
|
||||
DEPS = [
|
||||
react_native_target('java/com/facebook/react/bridge:bridge'),
|
||||
react_native_target('java/com/facebook/react/common:common'),
|
||||
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
||||
react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'),
|
||||
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
|
||||
react_native_dep('third-party/java/jsr-305:jsr-305'),
|
||||
]
|
||||
|
||||
android_library(
|
||||
name = 'jstasks',
|
||||
srcs = glob(['*.java']),
|
||||
deps = DEPS,
|
||||
visibility = [
|
||||
'PUBLIC',
|
||||
],
|
||||
)
|
||||
|
||||
project_config(
|
||||
src_target = ':jstasks',
|
||||
)
|
@ -0,0 +1,76 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
mTaskKey = taskKey;
|
||||
mData = data;
|
||||
mTimeout = timeout;
|
||||
mAllowedInForeground = allowedInForeground;
|
||||
}
|
||||
|
||||
/* package */ String getTaskKey() {
|
||||
return mTaskKey;
|
||||
}
|
||||
|
||||
/* package */ WritableMap getData() {
|
||||
return mData;
|
||||
}
|
||||
|
||||
/* package */ long getTimeout() {
|
||||
return mTimeout;
|
||||
}
|
||||
|
||||
/* package */ boolean isAllowedInForeground() {
|
||||
return mAllowedInForeground;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.jstasks;
|
||||
|
||||
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;
|
||||
|
||||
import com.facebook.infer.annotation.Assertions;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.uimanager.AppRegistry;
|
||||
|
||||
/**
|
||||
* 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<ReactContext, HeadlessJsTaskContext> INSTANCES =
|
||||
new WeakHashMap<>();
|
||||
|
||||
/**
|
||||
* Get the task helper instance for a particular {@link ReactContext}. There is only one instance
|
||||
* per context.
|
||||
* <p>
|
||||
* <strong>Note:</strong> 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 ReactContext mReactContext;
|
||||
private final Set<HeadlessJsTaskEventListener> mHeadlessJsTaskEventListeners =
|
||||
new CopyOnWriteArraySet<>();
|
||||
private final AtomicInteger mLastTaskId = new AtomicInteger(0);
|
||||
private final Handler mHandler = new Handler();
|
||||
private final Set<Integer> mActiveTasks = new CopyOnWriteArraySet<>();
|
||||
private final SparseArray<Runnable> mTaskTimeouts = new SparseArray<>();
|
||||
|
||||
private HeadlessJsTaskContext(ReactContext reactContext) {
|
||||
mReactContext = reactContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a task lifecycle event listener.
|
||||
*/
|
||||
public void addTaskEventListener(HeadlessJsTaskEventListener listener) {
|
||||
mHeadlessJsTaskEventListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
UiThreadUtil.assertOnUiThread();
|
||||
if (mReactContext.getLifecycleState() == LifecycleState.RESUMED &&
|
||||
!taskConfig.isAllowedInForeground()) {
|
||||
throw new IllegalStateException(
|
||||
"Tried to start task " + taskConfig.getTaskKey() +
|
||||
" while in foreground, but this is not allowed.");
|
||||
}
|
||||
final int taskId = mLastTaskId.incrementAndGet();
|
||||
mReactContext.getJSModule(AppRegistry.class)
|
||||
.startHeadlessTask(taskId, taskConfig.getTaskKey(), taskConfig.getData());
|
||||
if (taskConfig.getTimeout() > 0) {
|
||||
scheduleTaskTimeout(taskId, taskConfig.getTimeout());
|
||||
}
|
||||
mActiveTasks.add(taskId);
|
||||
for (HeadlessJsTaskEventListener listener : mHeadlessJsTaskEventListeners) {
|
||||
listener.onHeadlessJsTaskStart(taskId);
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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);
|
||||
}
|
||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (HeadlessJsTaskEventListener listener : mHeadlessJsTaskEventListeners) {
|
||||
listener.onHeadlessJsTaskFinish(taskId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleTaskTimeout(final int taskId, long timeout) {
|
||||
Runnable runnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finishTask(taskId);
|
||||
}
|
||||
};
|
||||
mTaskTimeouts.append(taskId, runnable);
|
||||
mHandler.postDelayed(runnable, timeout);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.jstasks;
|
||||
|
||||
/**
|
||||
* Listener interface for task lifecycle events.
|
||||
*/
|
||||
public interface HeadlessJsTaskEventListener {
|
||||
|
||||
/**
|
||||
* Called when a JS task is started, on the UI thread.
|
||||
*
|
||||
* @param taskId the unique identifier of this task instance
|
||||
*/
|
||||
void onHeadlessJsTaskStart(int taskId);
|
||||
|
||||
/**
|
||||
* Called when a JS task finishes (i.e. when
|
||||
* {@link HeadlessJsTaskSupportModule#notifyTaskFinished} is called, or when it times out), on the
|
||||
* UI thread.
|
||||
*/
|
||||
void onHeadlessJsTaskFinish(int taskId);
|
||||
}
|
@ -12,6 +12,7 @@ android_library(
|
||||
react_native_target('java/com/facebook/react/devsupport:devsupport'),
|
||||
react_native_target('java/com/facebook/react/module/annotations:annotations'),
|
||||
react_native_target('java/com/facebook/react/uimanager:uimanager'),
|
||||
react_native_target('java/com/facebook/react/jstasks:jstasks'),
|
||||
],
|
||||
visibility = [
|
||||
'PUBLIC',
|
||||
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.core;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
||||
|
||||
/**
|
||||
* Simple native module that allows JS to notify native of having completed some task work, so that
|
||||
* it can e.g. release any resources, stop timers etc.
|
||||
*/
|
||||
public class HeadlessJsTaskSupportModule extends ReactContextBaseJavaModule {
|
||||
|
||||
public HeadlessJsTaskSupportModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HeadlessJsTaskSupport";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void notifyTaskFinished(int taskId) {
|
||||
HeadlessJsTaskContext headlessJsTaskContext =
|
||||
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
|
||||
headlessJsTaskContext.finishTask(taskId);
|
||||
}
|
||||
}
|
@ -9,6 +9,18 @@
|
||||
|
||||
package com.facebook.react.modules.core;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import android.view.Choreographer;
|
||||
|
||||
@ -24,27 +36,17 @@ import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.common.SystemClock;
|
||||
import com.facebook.react.devsupport.DevSupportManager;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.uimanager.ReactChoreographer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Native module for JS timer execution. Timers fire on frame boundaries.
|
||||
*/
|
||||
@ReactModule(name = "RCTTiming", supportsWebWorkers = true)
|
||||
public final class Timing extends ReactContextBaseJavaModule implements LifecycleEventListener,
|
||||
OnExecutorUnregisteredListener {
|
||||
OnExecutorUnregisteredListener, HeadlessJsTaskEventListener {
|
||||
|
||||
// These timing contants should be kept in sync with the ones in `JSTimersExecution.js`.
|
||||
// The minimum time in milliseconds left in the frame to call idle callbacks.
|
||||
@ -88,7 +90,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
*/
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
if (isPaused.get()) {
|
||||
if (isPaused.get() && !isRunningTasks.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -132,7 +134,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
if (isPaused.get()) {
|
||||
if (isPaused.get() && !isRunningTasks.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -197,6 +199,7 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
private final PriorityQueue<Timer> mTimers;
|
||||
private final Map<ExecutorToken, SparseArray<Timer>> mTimerIdsToTimers;
|
||||
private final AtomicBoolean isPaused = new AtomicBoolean(true);
|
||||
private final AtomicBoolean isRunningTasks = new AtomicBoolean(false);
|
||||
private final TimerFrameCallback mTimerFrameCallback = new TimerFrameCallback();
|
||||
private final IdleFrameCallback mIdleFrameCallback = new IdleFrameCallback();
|
||||
private @Nullable IdleCallbackRunnable mCurrentIdleCallbackRunnable;
|
||||
@ -236,19 +239,22 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
// Safe to acquire choreographer here, as initialize() is invoked from UI thread.
|
||||
mReactChoreographer = ReactChoreographer.getInstance();
|
||||
getReactApplicationContext().addLifecycleEventListener(this);
|
||||
HeadlessJsTaskContext headlessJsTaskContext =
|
||||
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
|
||||
headlessJsTaskContext.addTaskEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
isPaused.set(true);
|
||||
clearChoreographerCallback();
|
||||
clearChoreographerIdleCallback();
|
||||
maybeClearChoreographerIdleCallback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
clearChoreographerCallback();
|
||||
clearChoreographerIdleCallback();
|
||||
maybeClearChoreographerIdleCallback();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -257,11 +263,25 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
// TODO(5195192) Investigate possible problems related to restarting all tasks at the same
|
||||
// moment
|
||||
setChoreographerCallback();
|
||||
maybeSetChoreographerIdleCallback();
|
||||
}
|
||||
|
||||
synchronized (mIdleCallbackGuard) {
|
||||
if (mSendIdleEventsExecutorTokens.size() > 0) {
|
||||
setChoreographerIdleCallback();
|
||||
}
|
||||
@Override
|
||||
public void onHeadlessJsTaskStart(int taskId) {
|
||||
if (!isRunningTasks.getAndSet(true)) {
|
||||
setChoreographerCallback();
|
||||
maybeSetChoreographerIdleCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadlessJsTaskFinish(int taskId) {
|
||||
HeadlessJsTaskContext headlessJsTaskContext =
|
||||
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
|
||||
if (!headlessJsTaskContext.hasActiveTasks()) {
|
||||
isRunningTasks.set(false);
|
||||
clearChoreographerCallback();
|
||||
maybeClearChoreographerIdleCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,6 +289,23 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
public void onCatalystInstanceDestroy() {
|
||||
clearChoreographerCallback();
|
||||
clearChoreographerIdleCallback();
|
||||
HeadlessJsTaskContext headlessJsTaskContext =
|
||||
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
|
||||
headlessJsTaskContext.removeTaskEventListener(this);
|
||||
}
|
||||
|
||||
private void maybeSetChoreographerIdleCallback() {
|
||||
synchronized (mIdleCallbackGuard) {
|
||||
if (mSendIdleEventsExecutorTokens.size() > 0) {
|
||||
setChoreographerIdleCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeClearChoreographerIdleCallback() {
|
||||
if (isPaused.get() && !isRunningTasks.get()) {
|
||||
clearChoreographerCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private void setChoreographerCallback() {
|
||||
@ -281,7 +318,10 @@ public final class Timing extends ReactContextBaseJavaModule implements Lifecycl
|
||||
}
|
||||
|
||||
private void clearChoreographerCallback() {
|
||||
if (mFrameCallbackPosted) {
|
||||
HeadlessJsTaskContext headlessJsTaskContext =
|
||||
HeadlessJsTaskContext.getInstance(getReactApplicationContext());
|
||||
if (mFrameCallbackPosted && isPaused.get() &&
|
||||
!headlessJsTaskContext.hasActiveTasks()) {
|
||||
Assertions.assertNotNull(mReactChoreographer).removeFrameCallback(
|
||||
ReactChoreographer.CallbackType.TIMERS_EVENTS,
|
||||
mTimerFrameCallback);
|
||||
|
@ -19,5 +19,5 @@ public interface AppRegistry extends JavaScriptModule {
|
||||
|
||||
void runApplication(String appKey, WritableMap appParameters);
|
||||
void unmountApplicationComponentAtRootTag(int rootNodeTag);
|
||||
|
||||
void startHeadlessTask(int taskId, String taskKey, WritableMap data);
|
||||
}
|
||||
|
@ -22,22 +22,17 @@ import com.facebook.react.common.SystemClock;
|
||||
import com.facebook.react.modules.core.JSTimersExecution;
|
||||
import com.facebook.react.modules.core.Timing;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ -189,6 +184,62 @@ public class TimingModuleTest {
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadlessJsTaskInBackground() {
|
||||
mTiming.onHostPause();
|
||||
mTiming.onHeadlessJsTaskStart(42);
|
||||
mTiming.createTimer(mExecutorTokenMock, 41, 1, 0, true);
|
||||
|
||||
stepChoreographerFrame();
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
|
||||
reset(mJSTimersMock);
|
||||
mTiming.onHeadlessJsTaskFinish(42);
|
||||
stepChoreographerFrame();
|
||||
verifyNoMoreInteractions(mJSTimersMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadlessJsTaskInForeground() {
|
||||
mTiming.onHostResume();
|
||||
mTiming.onHeadlessJsTaskStart(42);
|
||||
mTiming.createTimer(mExecutorTokenMock, 41, 1, 0, true);
|
||||
|
||||
stepChoreographerFrame();
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
|
||||
reset(mJSTimersMock);
|
||||
mTiming.onHeadlessJsTaskFinish(42);
|
||||
stepChoreographerFrame();
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
|
||||
reset(mJSTimersMock);
|
||||
mTiming.onHostPause();
|
||||
verifyNoMoreInteractions(mJSTimersMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadlessJsTaskIntertwine() {
|
||||
mTiming.onHostResume();
|
||||
mTiming.onHeadlessJsTaskStart(42);
|
||||
mTiming.createTimer(mExecutorTokenMock, 41, 1, 0, true);
|
||||
mTiming.onHostPause();
|
||||
|
||||
stepChoreographerFrame();
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
|
||||
reset(mJSTimersMock);
|
||||
mTiming.onHostResume();
|
||||
mTiming.onHeadlessJsTaskFinish(42);
|
||||
stepChoreographerFrame();
|
||||
verify(mJSTimersMock).callTimers(JavaOnlyArray.of(41));
|
||||
|
||||
reset(mJSTimersMock);
|
||||
mTiming.onHostPause();
|
||||
stepChoreographerFrame();
|
||||
verifyNoMoreInteractions(mJSTimersMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetTimeoutZero() {
|
||||
mTiming.createTimer(mExecutorTokenMock, 100, 0, 0, false);
|
||||
|
Loading…
x
Reference in New Issue
Block a user