// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.jstasks;
import java.lang.ref.WeakReference;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
import android.os.Handler;
import android.util.SparseArray;
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.modules.appregistry.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 WeakReference<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 = new WeakReference<ReactContext>(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();
ReactContext reactContext = Assertions.assertNotNull(
mReactContext.get(),
"Tried to start a task on a react context that has already been destroyed");
if (reactContext.getLifecycleState() == LifecycleState.RESUMED &&
!taskConfig.isAllowedInForeground()) {
throw new IllegalStateException(
"Tried to start task " + taskConfig.getTaskKey() +
" while in foreground, but this is not allowed.");
}
final int taskId = mLastTaskId.incrementAndGet();
mActiveTasks.add(taskId);
reactContext.getJSModule(AppRegistry.class)
.startHeadlessTask(taskId, taskConfig.getTaskKey(), taskConfig.getData());
if (taskConfig.getTimeout() > 0) {
scheduleTaskTimeout(taskId, taskConfig.getTimeout());
}
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. A task can only be finished once.
*
* @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);
}
}
});
}
/**
* Check if a given task is currently running. A task is stopped if either {@link #finishTask} is
* called or it times out.
*/
public synchronized boolean isTaskRunning(final int taskId) {
return mActiveTasks.contains(taskId);
}
private void scheduleTaskTimeout(final int taskId, long timeout) {
Runnable runnable = new Runnable() {
@Override
public void run() {
finishTask(taskId);
}
};
mTaskTimeouts.append(taskId, runnable);
mHandler.postDelayed(runnable, timeout);
}
}