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:
Felix Oghina 2016-09-29 03:43:59 -07:00 committed by Facebook Github Bot
parent 542ab8643e
commit 3080b8d26c
14 changed files with 650 additions and 31 deletions

View File

@ -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(

View File

@ -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'),

View File

@ -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

View File

@ -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();
}
}

View File

@ -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();
}

View 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',
)

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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',

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);