Skip to content

Commit ea924cb

Browse files
committed
Merge branch 'headless-task'
2 parents 8f27ef2 + 1766298 commit ea924cb

File tree

3 files changed

+478
-300
lines changed

3 files changed

+478
-300
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Change Log
22

3+
## Unreleased
4+
* [Android] Re-factor HeadlessTask.java. Introduce HeadlessTaskManager class.
5+
36
## 4.18.0 — 2024-12-02
47
* [Android] implement support for new "Bridgeless Architecture"
58
* [Android] Introduce new android-only method for signalling completion of your headless-tasks registered with `BackgroundGeolocation.registerHeadlessTask(bgGeoHeadlessTask)`. This allows RN to quickly free-up resources when your task is complete, signalling good background behaviour with the OS.
Lines changed: 20 additions & 300 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
package com.transistorsoft.rnbackgroundgeolocation;
22

3-
import android.content.Context;
4-
import android.os.Handler;
5-
import android.os.Looper;
6-
7-
import androidx.annotation.NonNull;
8-
import androidx.annotation.Nullable;
9-
10-
import com.facebook.infer.annotation.Assertions;
11-
import com.facebook.react.ReactApplication;
12-
import com.facebook.react.ReactInstanceEventListener;
13-
import com.facebook.react.ReactInstanceManager;
14-
import com.facebook.react.ReactNativeHost;
15-
import com.facebook.react.bridge.ReactContext;
16-
import com.facebook.react.bridge.UiThreadUtil;
173
import com.facebook.react.bridge.WritableMap;
184
import com.facebook.react.bridge.WritableNativeMap;
19-
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
20-
import com.facebook.react.jstasks.HeadlessJsTaskContext;
215

226
import org.greenrobot.eventbus.Subscribe;
237

24-
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
258
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
269
import com.transistorsoft.locationmanager.event.FinishHeadlessTaskEvent;
2710
import com.transistorsoft.locationmanager.event.HeadlessEvent;
@@ -31,65 +14,18 @@
3114
import org.json.JSONException;
3215
import org.json.JSONObject;
3316

34-
import java.lang.reflect.Method;
35-
import java.util.ArrayList;
36-
import java.util.List;
37-
import java.util.concurrent.atomic.AtomicBoolean;
38-
import java.util.concurrent.atomic.AtomicInteger;
39-
4017
/**
4118
* The BackgroundGeolocation SDK creates a single instance of this class (via reflection upon Config.headlessJobService)
4219
* The SDK delivers events to this instance by dropping them onto EventBus (see @Subscribe). Because this instance is subscribed
4320
* into EventBus, it's protected from GC.
4421
*
45-
* Because there's only one instance of this class, we have to be mindful that we can possibly receive events rapidly,
46-
* so we store them in a Queue (#mTaskQueue).
47-
*
48-
* We also have to be mindful that it's a heavy operation to do the initial launch the ReactNative Host, so several events
49-
* might build up in the queue before the Host is finally launched, when we drain the queue (see #drainTaskQueue).
50-
*
51-
* For finally sending events to the client, we wrap the RN HeadlessJsTaskConfig with our own TaskConfig class. This class
52-
* adds our own auto-incremented "taskId" field to maintain a mapping between our taskId and RN's. See #invokeStartTask.
53-
* This class appends our custom taskId into the the event params sent to Javascript, for the following purpose:
54-
*
55-
* ```javascript
56-
* const BackgroundGeolocationHeadlessTask = async (event) => {
57-
* console.log('[HeadlessTask] taskId: ', event.taskId); // <-- here's our custom taskId.
58-
*
59-
* await doWork(); // <-- perform some arbitrarily long process (eg: http request).
60-
*
61-
* BackgroundGeolocation.finishHeadlessTask(event.taskId); // <-- $$$ Here's the money $$$
62-
* }
63-
* ```
64-
*
65-
* See "Here's the $money" above: We want to signal back to this native HeadlessTask instance , that our JS task is now complete.
66-
* This is a pretty easy task to do via EventBus -- Just create a new instance of FinishHeadlessTaskEvent(params.taskId);
67-
*
68-
* The code then looks up a TaskConfig from mEventQueue using the given event.taskId.
69-
*
70-
* All this extra fussing with taking care to finish our RN HeadlessTasks seems to be more important with RN's "new architecture", where before,
71-
* RN seemed to automatically complete its tasks seemingly when the Javascript function stopped executing. This is why it was always so
72-
* important to await one's Promises and do all the work before the the last line of the function executed.
73-
*
74-
* Now, the Javascript must explicitly call back to the native side to say "I'M DONE" -- BackgroundGeolocation.finishHeadlessTask(taskId)
75-
*
7622
* Created by chris on 2018-01-23.
7723
*/
7824

7925
public class HeadlessTask {
80-
private static String HEADLESS_TASK_NAME = "BackgroundGeolocation";
26+
private static final String HEADLESS_TASK_NAME = "BackgroundGeolocation";
8127
// Hard-coded time-limit for headless-tasks is 60000 @todo configurable?
82-
private static final int TASK_TIMEOUT = 60000;
83-
private static final AtomicInteger sLastTaskId = new AtomicInteger(0);
84-
85-
synchronized static int getNextTaskId() {
86-
return sLastTaskId.incrementAndGet();
87-
}
88-
89-
private final List<TaskConfig> mTaskQueue = new ArrayList<>();
90-
private final AtomicBoolean mIsReactContextInitialized = new AtomicBoolean(false);
91-
private final AtomicBoolean mIsInitializingReactContext = new AtomicBoolean(false);
92-
private final AtomicBoolean mIsHeadlessJsTaskListenerRegistered = new AtomicBoolean(false);
28+
private static final int TASK_TIMEOUT = 60000 * 2;
9329

9430
/**
9531
* EventBus receiver for a HeadlessTask HeadlessEvent
@@ -153,13 +89,26 @@ public void onHeadlessEvent(HeadlessEvent event) {
15389
clientEvent.putNull("params");
15490
clientEvent.putString("error", e.getMessage());
15591
TSLog.logger.error(TSLog.error(e.getMessage()), e);
156-
e.printStackTrace();
15792
}
15893
}
15994

16095
try {
161-
startTask(event.getContext(), new TaskConfig(clientEvent));
162-
} catch (AssertionError e) {
96+
HeadlessTaskManager.getInstance().startTask(event.getContext(), new HeadlessTaskManager.Task.Builder()
97+
.setName(HEADLESS_TASK_NAME)
98+
.setParams(clientEvent)
99+
.setTimeout(TASK_TIMEOUT)
100+
.setOnInvokeCallback((reactContext, task) -> {
101+
//TSLog.logger.debug("*** onInvoke: " + task.getId());
102+
})
103+
.setOnFinishCallback(taskId -> {
104+
//TSLog.logger.debug("*** onFinish: " + taskId);
105+
})
106+
.setOnErrorCallback((task, e) -> {
107+
TSLog.logger.warn("⚠\uFE0F HeadlessTaskError: " + e.getMessage() + ": " + task.toString());
108+
})
109+
.build()
110+
);
111+
} catch (Exception e) {
163112
TSLog.logger.warn(TSLog.warn("Failed invoke HeadlessTask " + name + ". Task ignored." + e.getMessage()));
164113
}
165114
}
@@ -171,239 +120,10 @@ public void onHeadlessEvent(HeadlessEvent event) {
171120
*/
172121
@Subscribe(threadMode = ThreadMode.MAIN)
173122
public void onFinishHeadlessTask(FinishHeadlessTaskEvent event) {
174-
if (!mIsReactContextInitialized.get()) {
175-
TSLog.logger.warn(TSLog.warn("BackgroundGeolocation.finishHeadlessTask:" + event.getTaskId() + " found no ReactContext"));
176-
return;
177-
}
178-
ReactContext reactContext = getReactContext(event.getContext());
179-
if (reactContext != null) {
180-
int taskId = event.getTaskId();
181-
182-
synchronized (mTaskQueue) {
183-
// Locate the TaskConfig instance by our local taskId.
184-
TaskConfig taskConfig = null;
185-
for (TaskConfig config : mTaskQueue) {
186-
if (config.getTaskId() == taskId) {
187-
taskConfig = config;
188-
break;
189-
}
190-
}
191-
if (taskConfig != null) {
192-
HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
193-
// Tell RN we're done using the mapped getReactTaskId().
194-
headlessJsTaskContext.finishTask(taskConfig.getReactTaskId());
195-
} else {
196-
TSLog.logger.warn(TSLog.warn("Failed to find task: " + taskId));
197-
}
198-
}
199-
} else {
200-
TSLog.logger.warn(TSLog.warn("Failed to finishHeadlessTask: " + event.getTaskId() + " -- HeadlessTask onFinishHeadlessTask failed to find a ReactContext. This is unexpected"));
201-
}
202-
}
203-
204-
/**
205-
* Start a task. This method handles starting a new React instance if required.
206-
*
207-
* Has to be called on the UI thread.
208-
*
209-
* @param taskConfig describes what task to start and the parameters to pass to it
210-
*/
211-
private void startTask(Context context, final TaskConfig taskConfig) throws AssertionError {
212-
UiThreadUtil.assertOnUiThread();
213-
214-
// push this HeadlessEvent onto the taskQueue, to be drained once the React context is finished initializing,
215-
// or executed immediately if Context exists currently.
216-
synchronized (mTaskQueue) {
217-
mTaskQueue.add(taskConfig);
218-
}
219-
220-
if (!mIsReactContextInitialized.get()) {
221-
createReactContextAndScheduleTask(context);
222-
} else {
223-
invokeStartTask(getReactContext(context), taskConfig);
224-
}
225-
}
226-
227-
synchronized private void invokeStartTask(ReactContext reactContext, final TaskConfig taskConfig) {
228-
final HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
229123
try {
230-
if (mIsHeadlessJsTaskListenerRegistered.compareAndSet(false, true)) {
231-
// Register the RN HeadlessJSTaskEventListener just once.
232-
// This inline-listener is handy here as a closure around the HeadlessJsTaskContext.
233-
// Otherwise, we'd have to store this Context to an instance var.
234-
// The only purpose of this listener is to clear events from the queue. The RN task is assumed to have had its .stopTask(taskId) method called, which is why this event has fired.
235-
// WARNING: this listener seems to receive events from ANY OTHER plugin's Headless events.
236-
// TODO we might use a LifeCycle event-listener here to remove the listener when the app is launched to foreground.
237-
headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
238-
@Override public void onHeadlessJsTaskStart(int taskId) {}
239-
@Override public void onHeadlessJsTaskFinish(int taskId) {
240-
synchronized (mTaskQueue) {
241-
if (mTaskQueue.isEmpty()) {
242-
// Nothing in queue? This events cannot be for us.
243-
return;
244-
}
245-
// Query our queue for this event...
246-
TaskConfig taskConfig = null;
247-
for (TaskConfig config : mTaskQueue) {
248-
if (config.getReactTaskId() == taskId) {
249-
taskConfig = config;
250-
break;
251-
}
252-
}
253-
if (taskConfig != null) {
254-
// Clear it from the Queue.
255-
TSLog.logger.debug("taskId: " + taskConfig.getTaskId());
256-
mTaskQueue.remove(taskConfig);
257-
} else {
258-
TSLog.logger.warn(TSLog.warn("Failed to find taskId: " + taskId));
259-
}
260-
}
261-
}
262-
});
263-
}
264-
// Finally: the actual launch of the RN headless-task!
265-
int taskId = headlessJsTaskContext.startTask(taskConfig.getTaskConfig());
266-
// Provide the RN taskId to our private TaskConfig instance, mapping the RN taskId to our TaskConfig's internal taskId.
267-
taskConfig.setReactTaskId(taskId);
268-
TSLog.logger.debug("taskId: " + taskId);
269-
} catch (IllegalStateException e) {
270-
TSLog.logger.error(TSLog.error(e.getMessage()), e);
271-
}
272-
}
273-
274-
protected ReactNativeHost getReactNativeHost(Context context) {
275-
return ((ReactApplication) context.getApplicationContext()).getReactNativeHost();
276-
}
277-
278-
/**
279-
* Get the {ReactHost} used by this app. ure and returns null if not.
280-
*/
281-
private @Nullable Object getReactHost(Context context) {
282-
context = context.getApplicationContext();
283-
try {
284-
Method getReactHost = context.getClass().getMethod("getReactHost");
285-
return getReactHost.invoke(context);
286-
} catch (Exception e) {
287-
return null;
288-
}
289-
}
290-
291-
private ReactContext getReactContext(Context context) {
292-
if (isBridglessArchitectureEnabled()) {
293-
Object reactHost = getReactHost(context);
294-
Assertions.assertNotNull(reactHost, "getReactHost() is null in New Architecture");
295-
try {
296-
Method getCurrentReactContext = reactHost.getClass().getMethod("getCurrentReactContext");
297-
return (ReactContext) getCurrentReactContext.invoke(reactHost);
298-
} catch (Exception e) {
299-
TSLog.logger.error(TSLog.error("[HeadlessTask] Reflection error getCurrentReactContext: " + e.getMessage()), e);
300-
}
301-
}
302-
final ReactInstanceManager reactInstanceManager = getReactNativeHost(context).getReactInstanceManager();
303-
return reactInstanceManager.getCurrentReactContext();
304-
}
305-
306-
private void createReactContextAndScheduleTask(Context context) {
307-
308-
// ReactContext could have already been initialized by another plugin (eg: background-fetch).
309-
// If we get a non-null ReactContext here, we're good to go!
310-
ReactContext reactContext = getReactContext(context);
311-
if (reactContext != null && !mIsInitializingReactContext.get()) {
312-
mIsReactContextInitialized.set(true);
313-
drainTaskQueue(reactContext);
314-
return;
315-
}
316-
if (mIsInitializingReactContext.compareAndSet(false, true)) {
317-
TSLog.logger.debug("initialize ReactContext");
318-
final Object reactHost = getReactHost(context);
319-
if (isBridglessArchitectureEnabled()) { // NEW arch
320-
ReactInstanceEventListener callback = new ReactInstanceEventListener() {
321-
@Override
322-
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
323-
mIsReactContextInitialized.set(true);
324-
drainTaskQueue(reactContext);
325-
try {
326-
Method removeReactInstanceEventListener = reactHost.getClass().getMethod("removeReactInstanceEventListener", ReactInstanceEventListener.class);
327-
removeReactInstanceEventListener.invoke(reactHost, this);
328-
} catch (Exception e) {
329-
TSLog.logger.error(TSLog.error("HeadlessTask reflection error A: " + e), e);
330-
}
331-
}
332-
};
333-
try {
334-
Method addReactInstanceEventListener = reactHost.getClass().getMethod("addReactInstanceEventListener", ReactInstanceEventListener.class);
335-
addReactInstanceEventListener.invoke(reactHost, callback);
336-
Method startReactHost = reactHost.getClass().getMethod("start");
337-
startReactHost.invoke(reactHost);
338-
} catch (Exception e) {
339-
TSLog.logger.error(TSLog.error("HeadlessTask reflection error ReactHost start: " + e.getMessage()), e);
340-
}
341-
} else { // OLD arch
342-
final ReactInstanceManager reactInstanceManager = getReactNativeHost(context).getReactInstanceManager();
343-
reactInstanceManager.addReactInstanceEventListener(new ReactInstanceEventListener() {
344-
@Override
345-
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
346-
mIsReactContextInitialized.set(true);
347-
drainTaskQueue(reactContext);
348-
reactInstanceManager.removeReactInstanceEventListener(this);
349-
}
350-
});
351-
reactInstanceManager.createReactContextInBackground();
352-
}
353-
}
354-
}
355-
356-
/**
357-
* Invokes HeadlessEvents queued while waiting for the ReactContext to initialize.
358-
* @param reactContext
359-
*/
360-
private void drainTaskQueue(final ReactContext reactContext) {
361-
new Handler(Looper.getMainLooper()).postDelayed(() -> {
362-
synchronized (mTaskQueue) {
363-
for (TaskConfig taskConfig : mTaskQueue) {
364-
invokeStartTask(reactContext, taskConfig);
365-
}
366-
}
367-
}, 500);
368-
}
369-
370-
/**
371-
* Return true if this app is running with RN's bridgeless architecture.
372-
* Cheers to @mikehardy for this idea.
373-
* @return
374-
*/
375-
private boolean isBridglessArchitectureEnabled() {
376-
try {
377-
Class<?> entryPoint = Class.forName("com.facebook.react.defaults.DefaultNewArchitectureEntryPoint");
378-
Method bridgelessEnabled = entryPoint.getMethod("getBridgelessEnabled");
379-
Object result = bridgelessEnabled.invoke(null);
380-
return (result == Boolean.TRUE);
124+
HeadlessTaskManager.getInstance().finishTask(event.getContext(), event.getTaskId());
381125
} catch (Exception e) {
382-
return false;
383-
}
384-
}
385-
386-
/**
387-
* Wrapper for a client event. Inserts our custom taskId into the RN ClientEvent params.
388-
*/
389-
private static class TaskConfig {
390-
private final int mTaskId;
391-
private int mReactTaskId;
392-
private final WritableMap mParams;
393-
public TaskConfig(WritableMap params) {
394-
mTaskId = getNextTaskId();
395-
// Insert our custom taskId for users to call BackgroundGeolocation.finishHeadlessTask(taskId) with.
396-
params.putInt("taskId", mTaskId);
397-
mParams = params;
398-
}
399-
public void setReactTaskId(int taskId) {
400-
mReactTaskId = taskId;
401-
}
402-
public int getTaskId() { return mTaskId; }
403-
public int getReactTaskId() { return mReactTaskId; }
404-
405-
public HeadlessJsTaskConfig getTaskConfig() {
406-
return new HeadlessJsTaskConfig(HEADLESS_TASK_NAME, mParams, TASK_TIMEOUT);
126+
TSLog.logger.warn(TSLog.warn(e.getMessage()));
407127
}
408128
}
409129
}

0 commit comments

Comments
 (0)