mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-03 17:45:12 +08: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:
committed by
Facebook Github Bot
parent
542ab8643e
commit
3080b8d26c
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user