react-native-background-geolocation 4.18.0 → 4.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +1 -3
- package/android/src/main/java/com/transistorsoft/rnbackgroundgeolocation/HeadlessTask.java +22 -301
- package/android/src/main/java/com/transistorsoft/rnbackgroundgeolocation/HeadlessTaskManager.java +460 -0
- package/android/src/main/java/com/transistorsoft/rnbackgroundgeolocation/RNBackgroundGeolocationModule.java +6 -0
- package/package.json +1 -1
- package/src/NativeModule.js +1 -1
- package/src/declarations/BackgroundGeolocation.d.ts +8 -50
- package/src/declarations/interfaces/Config.d.ts +0 -1
- package/src/declarations/interfaces/HeadlessEvent.d.ts +49 -0
- package/src/declarations/types.d.ts +3 -0
- package/src/index.js +26 -7
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
2
|
package="com.transistorsoft.rnbackgroundgeolocation">
|
|
3
|
-
|
|
4
3
|
<application android:label="@string/app_name"></application>
|
|
5
|
-
|
|
6
4
|
</manifest>
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
package com.transistorsoft.rnbackgroundgeolocation;
|
|
2
2
|
|
|
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;
|
|
17
3
|
import com.facebook.react.bridge.WritableMap;
|
|
18
4
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
19
|
-
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
|
20
|
-
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
|
21
5
|
|
|
22
6
|
import org.greenrobot.eventbus.Subscribe;
|
|
23
7
|
|
|
24
|
-
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
|
25
8
|
import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation;
|
|
26
9
|
import com.transistorsoft.locationmanager.event.FinishHeadlessTaskEvent;
|
|
27
10
|
import com.transistorsoft.locationmanager.event.HeadlessEvent;
|
|
@@ -31,65 +14,18 @@ import org.greenrobot.eventbus.ThreadMode;
|
|
|
31
14
|
import org.json.JSONException;
|
|
32
15
|
import org.json.JSONObject;
|
|
33
16
|
|
|
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
|
-
|
|
40
17
|
/**
|
|
41
18
|
* The BackgroundGeolocation SDK creates a single instance of this class (via reflection upon Config.headlessJobService)
|
|
42
19
|
* The SDK delivers events to this instance by dropping them onto EventBus (see @Subscribe). Because this instance is subscribed
|
|
43
20
|
* into EventBus, it's protected from GC.
|
|
44
21
|
*
|
|
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
|
-
*
|
|
76
22
|
* Created by chris on 2018-01-23.
|
|
77
23
|
*/
|
|
78
24
|
|
|
79
25
|
public class HeadlessTask {
|
|
80
|
-
private static String HEADLESS_TASK_NAME = "BackgroundGeolocation";
|
|
26
|
+
private static final String HEADLESS_TASK_NAME = "BackgroundGeolocation";
|
|
81
27
|
// 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;
|
|
93
29
|
|
|
94
30
|
/**
|
|
95
31
|
* EventBus receiver for a HeadlessTask HeadlessEvent
|
|
@@ -153,14 +89,28 @@ public class HeadlessTask {
|
|
|
153
89
|
clientEvent.putNull("params");
|
|
154
90
|
clientEvent.putString("error", e.getMessage());
|
|
155
91
|
TSLog.logger.error(TSLog.error(e.getMessage()), e);
|
|
156
|
-
e.printStackTrace();
|
|
157
92
|
}
|
|
158
93
|
}
|
|
159
94
|
|
|
160
95
|
try {
|
|
161
|
-
startTask(event.getContext(), new
|
|
162
|
-
|
|
163
|
-
|
|
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) {
|
|
112
|
+
TSLog.logger.warn(TSLog.warn("Failed invoke HeadlessTask " + name + ". Task ignored: " + e.getMessage()));
|
|
113
|
+
e.printStackTrace();
|
|
164
114
|
}
|
|
165
115
|
}
|
|
166
116
|
|
|
@@ -171,239 +121,10 @@ public class HeadlessTask {
|
|
|
171
121
|
*/
|
|
172
122
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
|
173
123
|
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);
|
|
229
124
|
try {
|
|
230
|
-
|
|
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);
|
|
125
|
+
HeadlessTaskManager.getInstance().finishTask(event.getContext(), event.getTaskId());
|
|
381
126
|
} catch (Exception e) {
|
|
382
|
-
|
|
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);
|
|
127
|
+
TSLog.logger.warn(TSLog.warn(e.getMessage()));
|
|
407
128
|
}
|
|
408
129
|
}
|
|
409
130
|
}
|
package/android/src/main/java/com/transistorsoft/rnbackgroundgeolocation/HeadlessTaskManager.java
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
package com.transistorsoft.rnbackgroundgeolocation;
|
|
2
|
+
|
|
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;
|
|
17
|
+
import com.facebook.react.bridge.WritableMap;
|
|
18
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
|
19
|
+
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
|
20
|
+
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
|
21
|
+
import com.transistorsoft.locationmanager.logger.TSLog;
|
|
22
|
+
|
|
23
|
+
import java.lang.reflect.Method;
|
|
24
|
+
|
|
25
|
+
import java.util.Set;
|
|
26
|
+
import java.util.concurrent.CopyOnWriteArraySet;
|
|
27
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
28
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This is a general singleton for managing invoking RN headless-tasks. This class could be copied and implemented in any plugin.
|
|
32
|
+
*
|
|
33
|
+
* RN headless-tasks have no mechanism for the Javascript to signal completion of their tasks: you can either configure a timeout for
|
|
34
|
+
* your tasks (letting each task timeout) or provide no timeout at all, which will cause a memory leak in RN's HeadlessJsContext.java
|
|
35
|
+
* https://github.com/facebook/react-native/blob/bc0b5ca5df5d2cf12c791b0332ef2fb4e29d6d2d/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/jstasks/HeadlessJsTaskContext.java#L53-L55
|
|
36
|
+
*
|
|
37
|
+
* Without providing a timeout for your headless-tasks, those collections in HeadlessJsContext will continue grow and never be cleared.
|
|
38
|
+
*
|
|
39
|
+
* Because there's only one instance of this class, we have to be mindful that we can possibly receive events rapidly,
|
|
40
|
+
* so we store them in a Queue (#mTaskQueue).
|
|
41
|
+
*
|
|
42
|
+
* We also have to be mindful that it's a heavy operation to do the initial launch the ReactNative Host, so several events
|
|
43
|
+
* might build up in the queue before the Host is finally launched, when we drain the queue (see #drainTaskQueue).
|
|
44
|
+
*
|
|
45
|
+
* For finally sending events to the client, we wrap the RN HeadlessJsTaskConfig with our own TaskConfig class. This class
|
|
46
|
+
* adds our own auto-incremented "taskId" field to maintain a mapping between our taskId and RN's. See #invokeStartTask.
|
|
47
|
+
* This class appends our custom taskId into the the event params sent to Javascript, for the following purpose:
|
|
48
|
+
*
|
|
49
|
+
* ```javascript
|
|
50
|
+
* const BackgroundGeolocationHeadlessTask = async (event) => {
|
|
51
|
+
* console.log('[HeadlessTask] taskId: ', event.taskId); // <-- here's our custom taskId.
|
|
52
|
+
*
|
|
53
|
+
* await doWork(); // <-- perform some arbitrarily long process (eg: http request).
|
|
54
|
+
*
|
|
55
|
+
* BackgroundGeolocation.finishHeadlessTask(event.taskId); // <-- $$$ Here's the money $$$
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* See "Here's the $money" above: We want to signal back to this native HeadlessTask instance , that our JS task is now complete.
|
|
60
|
+
* This is a pretty easy task to do via EventBus -- Just create a new instance of FinishHeadlessTaskEvent(params.taskId);
|
|
61
|
+
*
|
|
62
|
+
* The code then looks up a TaskConfig from mEventQueue using the given event.taskId.
|
|
63
|
+
*
|
|
64
|
+
* All this extra fussing with taking care to finish our RN HeadlessTasks seems to be more important with RN's "new architecture", where before,
|
|
65
|
+
* RN seemed to automatically complete its tasks seemingly when the Javascript function stopped executing. This is why it was always so
|
|
66
|
+
* important to await one's Promises and do all the work before the the last line of the function executed.
|
|
67
|
+
*
|
|
68
|
+
* Now, the Javascript must explicitly call back to the native side to say "I'M DONE" -- BackgroundGeolocation.finishHeadlessTask(taskId)
|
|
69
|
+
*/
|
|
70
|
+
public class HeadlessTaskManager implements HeadlessJsTaskEventListener {
|
|
71
|
+
private static final String TAG = "TSLocationManager";
|
|
72
|
+
|
|
73
|
+
private static HeadlessTaskManager sInstance;
|
|
74
|
+
public static HeadlessTaskManager getInstance() {
|
|
75
|
+
if (sInstance == null) {
|
|
76
|
+
sInstance = getInstanceSynchronized();
|
|
77
|
+
}
|
|
78
|
+
return sInstance;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static synchronized HeadlessTaskManager getInstanceSynchronized() {
|
|
82
|
+
if (sInstance == null) sInstance = new HeadlessTaskManager();
|
|
83
|
+
return sInstance;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private final Set<Task> mTaskQueue = new CopyOnWriteArraySet<>();
|
|
87
|
+
private final AtomicBoolean mIsReactContextInitialized = new AtomicBoolean(false);
|
|
88
|
+
private final AtomicBoolean mIsInitializingReactContext = new AtomicBoolean(false);
|
|
89
|
+
private final AtomicBoolean mIsHeadlessJsTaskListenerRegistered = new AtomicBoolean(false);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Start a task. This method handles starting a new React instance if required.
|
|
93
|
+
*
|
|
94
|
+
* Has to be called on the UI thread.
|
|
95
|
+
*
|
|
96
|
+
* @param context Application Context
|
|
97
|
+
* @param task The Task instance
|
|
98
|
+
*/
|
|
99
|
+
public void startTask(Context context, Task task) throws AssertionError {
|
|
100
|
+
UiThreadUtil.assertOnUiThread();
|
|
101
|
+
|
|
102
|
+
addTask(task);
|
|
103
|
+
|
|
104
|
+
if (!mIsReactContextInitialized.get()) {
|
|
105
|
+
createReactContextAndScheduleTask(context);
|
|
106
|
+
} else {
|
|
107
|
+
boolean success = invokeStartTask(getReactContext(context), task);
|
|
108
|
+
if (!success) {
|
|
109
|
+
removeTask(task);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public void finishTask(Context context, int taskId) throws TaskNotFoundError, ContextError {
|
|
115
|
+
if (!mIsReactContextInitialized.get()) {
|
|
116
|
+
throw new ContextError(getClass().getName() + ".finishTask: ReactContext not initialized");
|
|
117
|
+
}
|
|
118
|
+
ReactContext reactContext = getReactContext(context.getApplicationContext());
|
|
119
|
+
if (reactContext != null) {
|
|
120
|
+
Task task = findTask(taskId);
|
|
121
|
+
if (task != null) {
|
|
122
|
+
HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
|
|
123
|
+
if (headlessJsTaskContext.isTaskRunning(task.getReactTaskId())) {
|
|
124
|
+
headlessJsTaskContext.finishTask(task.getReactTaskId());
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
throw new TaskNotFoundError(taskId);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
throw new ContextError(getClass().getName() + ".finishTask ReactContext is null");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private boolean invokeStartTask(ReactContext reactContext, final Task task) {
|
|
135
|
+
final HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
|
|
136
|
+
if (mIsHeadlessJsTaskListenerRegistered.compareAndSet(false, true)) {
|
|
137
|
+
// Register the RN HeadlessJSTaskEventListener just once.
|
|
138
|
+
// TODO we might use a LifeCycle event-listener here to remove the listener when the app is launched to foreground.
|
|
139
|
+
headlessJsTaskContext.addTaskEventListener(this);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return task.invoke(reactContext);
|
|
143
|
+
} catch (Exception e) {
|
|
144
|
+
task.onError(e);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Override
|
|
150
|
+
public void onHeadlessJsTaskStart(int taskId) {}
|
|
151
|
+
|
|
152
|
+
// 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.
|
|
153
|
+
// WARNING: this listener seems to receive events from ANY OTHER plugin's Headless events.
|
|
154
|
+
@Override
|
|
155
|
+
public void onHeadlessJsTaskFinish(int taskId) {
|
|
156
|
+
Task task = findTaskByReactId(taskId);
|
|
157
|
+
if (task == null) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
removeTask(task);
|
|
161
|
+
task.onFinish();
|
|
162
|
+
|
|
163
|
+
TSLog.logger.debug("[onHeadlessJsTaskFinish] taskId: " + taskId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private ReactNativeHost getReactNativeHost(Context context) {
|
|
167
|
+
return ((ReactApplication) context.getApplicationContext()).getReactNativeHost();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get the {ReactHost} used by this app. ure and returns null if not.
|
|
172
|
+
*/
|
|
173
|
+
private @Nullable Object getReactHost(Context context) {
|
|
174
|
+
context = context.getApplicationContext();
|
|
175
|
+
try {
|
|
176
|
+
Method getReactHost = context.getClass().getMethod("getReactHost");
|
|
177
|
+
return getReactHost.invoke(context);
|
|
178
|
+
} catch (Exception e) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private ReactContext getReactContext(Context context) {
|
|
184
|
+
if (isBridglessArchitectureEnabled()) {
|
|
185
|
+
Object reactHost = getReactHost(context);
|
|
186
|
+
Assertions.assertNotNull(reactHost, "getReactHost() is null in New Architecture");
|
|
187
|
+
try {
|
|
188
|
+
Method getCurrentReactContext = reactHost.getClass().getMethod("getCurrentReactContext");
|
|
189
|
+
return (ReactContext) getCurrentReactContext.invoke(reactHost);
|
|
190
|
+
} catch (Exception e) {
|
|
191
|
+
TSLog.logger.error(TSLog.error( "Reflection error getCurrentReactContext: " + e.getMessage()), e);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
final ReactInstanceManager reactInstanceManager = getReactNativeHost(context).getReactInstanceManager();
|
|
195
|
+
return reactInstanceManager.getCurrentReactContext();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private void createReactContextAndScheduleTask(Context context) {
|
|
199
|
+
// ReactContext could have already been initialized by another plugin (eg: background-fetch).
|
|
200
|
+
// If we get a non-null ReactContext here, we're good to go!
|
|
201
|
+
ReactContext reactContext = getReactContext(context);
|
|
202
|
+
if (reactContext != null && !mIsInitializingReactContext.get()) {
|
|
203
|
+
mIsReactContextInitialized.set(true);
|
|
204
|
+
drainTaskQueue(reactContext);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (mIsInitializingReactContext.compareAndSet(false, true)) {
|
|
208
|
+
TSLog.logger.debug( "[createReactContextAndScheduleTask] initialize ReactContext");
|
|
209
|
+
final Object reactHost = getReactHost(context);
|
|
210
|
+
if (isBridglessArchitectureEnabled()) { // NEW arch
|
|
211
|
+
ReactInstanceEventListener callback = new ReactInstanceEventListener() {
|
|
212
|
+
@Override
|
|
213
|
+
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
|
|
214
|
+
mIsReactContextInitialized.set(true);
|
|
215
|
+
drainTaskQueue(reactContext);
|
|
216
|
+
try {
|
|
217
|
+
Method removeReactInstanceEventListener = reactHost.getClass().getMethod("removeReactInstanceEventListener", ReactInstanceEventListener.class);
|
|
218
|
+
removeReactInstanceEventListener.invoke(reactHost, this);
|
|
219
|
+
} catch (Exception e) {
|
|
220
|
+
TSLog.logger.error(TSLog.error("HeadlessTask reflection error removeReactInstanceEventListener: ") + e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
Method addReactInstanceEventListener = reactHost.getClass().getMethod("addReactInstanceEventListener", ReactInstanceEventListener.class);
|
|
226
|
+
addReactInstanceEventListener.invoke(reactHost, callback);
|
|
227
|
+
Method startReactHost = reactHost.getClass().getMethod("start");
|
|
228
|
+
startReactHost.invoke(reactHost);
|
|
229
|
+
} catch (Exception e) {
|
|
230
|
+
TSLog.logger.error(TSLog.error("HeadlessTask reflection error ReactHost start: " + e.getMessage()), e);
|
|
231
|
+
}
|
|
232
|
+
} else { // OLD arch
|
|
233
|
+
final ReactInstanceManager reactInstanceManager = getReactNativeHost(context).getReactInstanceManager();
|
|
234
|
+
reactInstanceManager.addReactInstanceEventListener(new ReactInstanceEventListener() {
|
|
235
|
+
@Override
|
|
236
|
+
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
|
|
237
|
+
mIsReactContextInitialized.set(true);
|
|
238
|
+
drainTaskQueue(reactContext);
|
|
239
|
+
reactInstanceManager.removeReactInstanceEventListener(this);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
reactInstanceManager.createReactContextInBackground();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Return true if this app is running with RN's bridgeless architecture.
|
|
249
|
+
* Cheers to @mikehardy for this idea.
|
|
250
|
+
* @return
|
|
251
|
+
*/
|
|
252
|
+
private boolean isBridglessArchitectureEnabled() {
|
|
253
|
+
try {
|
|
254
|
+
Class<?> entryPoint = Class.forName("com.facebook.react.defaults.DefaultNewArchitectureEntryPoint");
|
|
255
|
+
Method bridgelessEnabled = entryPoint.getMethod("getBridgelessEnabled");
|
|
256
|
+
Object result = bridgelessEnabled.invoke(null);
|
|
257
|
+
return (result == Boolean.TRUE);
|
|
258
|
+
} catch (Exception e) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Invokes HeadlessEvents queued while waiting for the ReactContext to initialize.
|
|
265
|
+
* @param reactContext
|
|
266
|
+
*/
|
|
267
|
+
private void drainTaskQueue(final ReactContext reactContext) {
|
|
268
|
+
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
|
269
|
+
mTaskQueue.forEach((Task task) -> {
|
|
270
|
+
boolean success = invokeStartTask(reactContext, task);
|
|
271
|
+
if (!success) {
|
|
272
|
+
removeTask(task);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}, 250);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Find a task in the queue.
|
|
279
|
+
public Task findTask(int taskId) {
|
|
280
|
+
Task found = null;
|
|
281
|
+
// Locate the TaskConfig instance by our local taskId.
|
|
282
|
+
synchronized (mTaskQueue) {
|
|
283
|
+
for (Task task : mTaskQueue) {
|
|
284
|
+
if (task.getId() == taskId) {
|
|
285
|
+
found = task;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return found;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private Task findTaskByReactId(int reactTaskId) {
|
|
294
|
+
Task found = null;
|
|
295
|
+
// Locate the TaskConfig instance by our local taskId.
|
|
296
|
+
synchronized (mTaskQueue) {
|
|
297
|
+
for (Task config : mTaskQueue) {
|
|
298
|
+
if (config.getReactTaskId() == reactTaskId) {
|
|
299
|
+
found = config;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return found;
|
|
305
|
+
}
|
|
306
|
+
// Add a task to queue.
|
|
307
|
+
private void addTask(Task config) {
|
|
308
|
+
// push this HeadlessEvent onto the taskQueue, to be drained once the React context is finished initializing,
|
|
309
|
+
// or executed immediately if Context exists currently.
|
|
310
|
+
synchronized (mTaskQueue) {
|
|
311
|
+
mTaskQueue.add(config);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Remove TaskConfig from queue.
|
|
316
|
+
private void removeTask(Task task) {
|
|
317
|
+
synchronized (mTaskQueue) {
|
|
318
|
+
mTaskQueue.remove(task);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Any tasks in the queue?
|
|
323
|
+
private boolean hasTasks() {
|
|
324
|
+
synchronized (mTaskQueue) {
|
|
325
|
+
return !mTaskQueue.isEmpty();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Wrapper for a client event. Inserts our custom taskId into the RN ClientEvent params.
|
|
331
|
+
*/
|
|
332
|
+
public static class Task {
|
|
333
|
+
private static final AtomicInteger sLastTaskId = new AtomicInteger(0);
|
|
334
|
+
synchronized static int getNextTaskId() {
|
|
335
|
+
return sLastTaskId.incrementAndGet();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private final int mId;
|
|
339
|
+
private final String mTaskName;
|
|
340
|
+
private int mReactTaskId;
|
|
341
|
+
private final OnInvokeCallback mOnInvokeCallback;
|
|
342
|
+
private final OnFinishCallback mOnFinishCallback;
|
|
343
|
+
private final OnErrorCallback mOnErrorCallback;
|
|
344
|
+
private final int mTimeout;
|
|
345
|
+
private final WritableMap mParams;
|
|
346
|
+
|
|
347
|
+
Task(Builder builder) {
|
|
348
|
+
mTaskName = builder.name;
|
|
349
|
+
mId = getNextTaskId();
|
|
350
|
+
mOnInvokeCallback = builder.onInvokeCallback;
|
|
351
|
+
mOnFinishCallback = builder.onFinishCallback;
|
|
352
|
+
mOnErrorCallback = builder.onErrorCallback;
|
|
353
|
+
mTimeout = builder.timeout;
|
|
354
|
+
mParams = builder.params;
|
|
355
|
+
// append our custom headless taskId.
|
|
356
|
+
mParams.putInt("_transistorHeadlessTaskId", mId);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
boolean invoke(ReactContext reactContext) throws IllegalStateException {
|
|
361
|
+
HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
|
|
362
|
+
// Provide the RN taskId to our private TaskConfig instance, mapping the RN taskId to our TaskConfig's internal taskId.
|
|
363
|
+
|
|
364
|
+
mReactTaskId = headlessJsTaskContext.startTask(buildTaskConfig());
|
|
365
|
+
if (mOnInvokeCallback != null) {
|
|
366
|
+
mOnInvokeCallback.onInvoke(reactContext, this);
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
public int getId() {
|
|
372
|
+
return mId;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private int getReactTaskId() {
|
|
376
|
+
return mReactTaskId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private HeadlessJsTaskConfig buildTaskConfig() {
|
|
380
|
+
return new HeadlessJsTaskConfig(mTaskName, mParams, mTimeout);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
void onFinish() {
|
|
384
|
+
if (mOnFinishCallback != null) {
|
|
385
|
+
mOnFinishCallback.onFinish(mId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
void onError(Exception e) {
|
|
390
|
+
if (mOnErrorCallback != null) {
|
|
391
|
+
mOnErrorCallback.onError(this, e);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
public String toString() {
|
|
396
|
+
return "[HeadlessTaskManager.Task name: " + mTaskName + " " + mParams + "]";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
public static class Builder {
|
|
400
|
+
private static final int DEFAULT_TIMEOUT = 60000;
|
|
401
|
+
|
|
402
|
+
private String name;
|
|
403
|
+
private OnInvokeCallback onInvokeCallback;
|
|
404
|
+
private OnFinishCallback onFinishCallback;
|
|
405
|
+
private OnErrorCallback onErrorCallback;
|
|
406
|
+
private WritableMap params;
|
|
407
|
+
private int timeout = DEFAULT_TIMEOUT;
|
|
408
|
+
|
|
409
|
+
public Builder setName(String name) {
|
|
410
|
+
this.name = name;
|
|
411
|
+
return this;
|
|
412
|
+
}
|
|
413
|
+
public Builder setOnInvokeCallback(OnInvokeCallback callback) {
|
|
414
|
+
this.onInvokeCallback = callback;
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
public Builder setOnFinishCallback(OnFinishCallback callback) {
|
|
418
|
+
this.onFinishCallback = callback;
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
public Builder setOnErrorCallback(OnErrorCallback callback) {
|
|
422
|
+
this.onErrorCallback = callback;
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
public Builder setParams(WritableMap params) {
|
|
426
|
+
this.params = params;
|
|
427
|
+
return this;
|
|
428
|
+
}
|
|
429
|
+
public Builder setTimeout(int timeout) {
|
|
430
|
+
this.timeout = timeout;
|
|
431
|
+
return this;
|
|
432
|
+
}
|
|
433
|
+
public Task build() {
|
|
434
|
+
return new Task(this);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public static class TaskNotFoundError extends Exception {
|
|
440
|
+
public TaskNotFoundError(int taskId) {
|
|
441
|
+
super(HeadlessTaskManager.class.getName() + " failed to find task: " + taskId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public static class ContextError extends Exception {
|
|
446
|
+
public ContextError(String message) {
|
|
447
|
+
super(message);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
public interface OnInvokeCallback {
|
|
452
|
+
void onInvoke(ReactContext reactContext, Task task);
|
|
453
|
+
}
|
|
454
|
+
public interface OnFinishCallback {
|
|
455
|
+
void onFinish(int taskId);
|
|
456
|
+
}
|
|
457
|
+
public interface OnErrorCallback {
|
|
458
|
+
void onError(Task task, Exception e);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
@@ -825,6 +825,12 @@ public class RNBackgroundGeolocationModule extends ReactContextBaseJavaModule im
|
|
|
825
825
|
success.invoke(taskId);
|
|
826
826
|
}
|
|
827
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Called by user's registered HeadlessTask when they call BackgroundGeolocation.finishHeadlessTask(event.taskId)
|
|
830
|
+
* @param taskId
|
|
831
|
+
* @param success
|
|
832
|
+
* @param failure
|
|
833
|
+
*/
|
|
828
834
|
@ReactMethod
|
|
829
835
|
public void finishHeadlessTask(int taskId, Callback success, Callback failure) {
|
|
830
836
|
EventBus eventBus = EventBus.getDefault();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-background-geolocation",
|
|
3
|
-
"version": "4.18.
|
|
3
|
+
"version": "4.18.2",
|
|
4
4
|
"description": "The most sophisticated cross-platform background location-tracking & geofencing module with battery-conscious motion-detection intelligence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "yarn run build:expo",
|
package/src/NativeModule.js
CHANGED
|
@@ -327,7 +327,7 @@ export default class NativeModule {
|
|
|
327
327
|
reject('INVALID_TASK_ID: ' + taskId);
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
|
-
let success = (
|
|
330
|
+
let success = () => { resolve() }
|
|
331
331
|
let failure = (error) => { reject(error) }
|
|
332
332
|
RNBackgroundGeolocation.finishHeadlessTask(taskId, success, failure);
|
|
333
333
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
/// <reference path="interfaces/GeofenceEvent.d.ts" />
|
|
7
7
|
/// <reference path="interfaces/GeofencesChangeEvent.d.ts" />
|
|
8
8
|
/// <reference path="interfaces/HeartbeatEvent.d.ts" />
|
|
9
|
+
/// <reference path="interfaces/HeadlessEvent.d.ts" />
|
|
9
10
|
/// <reference path="interfaces/HttpEvent.d.ts" />
|
|
10
11
|
/// <reference path="interfaces/Location.d.ts" />
|
|
11
12
|
/// <reference path="interfaces/LocationAuthorizationAlert.d.ts" />
|
|
@@ -676,7 +677,7 @@ declare module "react-native-background-geolocation" {
|
|
|
676
677
|
* - You __must__ `registerHeadlessTask` in your application root file (eg: `index.js`).
|
|
677
678
|
*
|
|
678
679
|
* ### ⚠️ Warning:
|
|
679
|
-
* - Your `function` __must__ be declared as `async`.
|
|
680
|
+
* - Your `function` __must__ be declared as `async`. You must `await` all work within your task. Your headless-task will automatically be terminated after executing the last line of your function.
|
|
680
681
|
*
|
|
681
682
|
* @example
|
|
682
683
|
* ```typescript
|
|
@@ -685,7 +686,7 @@ declare module "react-native-background-geolocation" {
|
|
|
685
686
|
* console.log("[BackgroundGeolocation HeadlessTask] -", event.name, params);
|
|
686
687
|
*
|
|
687
688
|
* switch (event.name) {
|
|
688
|
-
* case "
|
|
689
|
+
* case "terminate":
|
|
689
690
|
* // Use await for async tasks
|
|
690
691
|
* const location = await BackgroundGeolocation.getCurrentPosition({
|
|
691
692
|
* samples: 1,
|
|
@@ -694,8 +695,9 @@ declare module "react-native-background-geolocation" {
|
|
|
694
695
|
* console.log("[BackgroundGeolocation HeadlessTask] - getCurrentPosition:", location);
|
|
695
696
|
* break;
|
|
696
697
|
* }
|
|
697
|
-
* //
|
|
698
|
-
*
|
|
698
|
+
* // You must await all work you do in your task.
|
|
699
|
+
* // Headless-tasks are automatically terminated after executing the last line of your function.
|
|
700
|
+
* await doWork();
|
|
699
701
|
* }
|
|
700
702
|
*
|
|
701
703
|
* BackgroundGeolocation.registerHeadlessTask(BackgroundGeolocationHeadlessTask);
|
|
@@ -740,11 +742,10 @@ declare module "react-native-background-geolocation" {
|
|
|
740
742
|
*
|
|
741
743
|
* ### ℹ️ See also:
|
|
742
744
|
* - 📘 [Android Headless Mode](github:wiki/Android-Headless-Mode).
|
|
743
|
-
* - [[BackgroundGeolocation.finishHeadlessTask]]
|
|
744
745
|
* - [[Config.enableHeadless]]
|
|
745
746
|
*
|
|
746
747
|
*/
|
|
747
|
-
static registerHeadlessTask(callback:(event:
|
|
748
|
+
static registerHeadlessTask(callback:(event:HeadlessEvent)=> Promise<void>): void;
|
|
748
749
|
|
|
749
750
|
/**
|
|
750
751
|
*
|
|
@@ -1077,51 +1078,8 @@ declare module "react-native-background-geolocation" {
|
|
|
1077
1078
|
static finish(taskId: number, success?: Function, failure?: Function): Promise<number>;
|
|
1078
1079
|
|
|
1079
1080
|
/**
|
|
1081
|
+
* @private
|
|
1080
1082
|
* __[Android-only]__ Signals completion of an Android headless-task (see [[Config.enableHeadless]])
|
|
1081
|
-
*
|
|
1082
|
-
* @example
|
|
1083
|
-
* ```typescript
|
|
1084
|
-
* const bgGeoHeadlessTask = async (event) => {
|
|
1085
|
-
* // the name of this event (eg: "location", "motionchange", "http")
|
|
1086
|
-
* const eventName = event.name;
|
|
1087
|
-
* // our event-data from the BG Geo SDK.
|
|
1088
|
-
* const params = event.params;
|
|
1089
|
-
* // IMPORTANT: This task's ID
|
|
1090
|
-
* const taskId = event.taskId;
|
|
1091
|
-
*
|
|
1092
|
-
* console.log('[BGGeoHeadlessTask] ', eventName, taskId, params);
|
|
1093
|
-
*
|
|
1094
|
-
* // Fake a long-running task (eg: HTTP request).
|
|
1095
|
-
* await doWork();
|
|
1096
|
-
*
|
|
1097
|
-
* // Signal completion of our RN HeadlessTask.
|
|
1098
|
-
* BackgroundGeolocation.finishHeadlessTask(taskId);
|
|
1099
|
-
* }
|
|
1100
|
-
*
|
|
1101
|
-
* // Example "work" function where you might perform a long-running task (such as an HTTP request).
|
|
1102
|
-
* // Uses a simple JS setTimeout timer to simulate work.
|
|
1103
|
-
* const doWork = async () => {
|
|
1104
|
-
* return new Promise(async (resolve, reject) => {
|
|
1105
|
-
* // Start a BGGeo backgroundTask for performing long-running task (such as HTTP request)
|
|
1106
|
-
* const bgTaskId = await BackgroundGeolocation.startBackgroundTask();
|
|
1107
|
-
* console.log('*** [doWork] START');
|
|
1108
|
-
* setTimeout(() => {
|
|
1109
|
-
* console.log('*** [doWork] FINISH');
|
|
1110
|
-
* // Signal completion of our bg-task.
|
|
1111
|
-
* BackgroundGeolocation.stopBackgroundTask(bgTaskId);
|
|
1112
|
-
* resolve();
|
|
1113
|
-
* }, 5000);
|
|
1114
|
-
* });
|
|
1115
|
-
* }
|
|
1116
|
-
*
|
|
1117
|
-
* // Register the HeadlessTask with RN.
|
|
1118
|
-
* BackgroundGeolocation.registerHeadlessTask(bgGeoHeadlessTask);
|
|
1119
|
-
*
|
|
1120
|
-
* ```
|
|
1121
|
-
* ### ℹ️ See also:
|
|
1122
|
-
* - 📘 [Android Headless Mode](github:wiki/Android-Headless-Mode).
|
|
1123
|
-
* - [[BackgroundGeolocation.registerHeadlessTask]]
|
|
1124
|
-
* - [[Config.enableHeadless]]
|
|
1125
1083
|
*/
|
|
1126
1084
|
static finishHeadlessTask(taskId: number, success?: Function, failure?: Function): Promise<number>;
|
|
1127
1085
|
|
|
@@ -2259,7 +2259,6 @@ declare module "react-native-background-geolocation" {
|
|
|
2259
2259
|
* ### ℹ️ See also:
|
|
2260
2260
|
* - 📘 [Android Headless Mode](github:wiki/Android-Headless-Mode).
|
|
2261
2261
|
* - [[BackgroundGeolocation.registerHeadlessTask]]
|
|
2262
|
-
* - [[BackgroundGeolocation.finishHeadlessTask]]
|
|
2263
2262
|
*/
|
|
2264
2263
|
enableHeadless?: boolean;
|
|
2265
2264
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/// <reference path="../types.d.ts" />
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
declare module "react-native-background-geolocation" {
|
|
5
|
+
/**
|
|
6
|
+
* This is the event-object provided to Android headless-tasks registered via [[BackgroundGeolocation.registerHeadlessTask]].
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const BackgroundGeolocationHeadlessTask = async (event) => {
|
|
11
|
+
* const params = event.params;
|
|
12
|
+
* console.log("[BackgroundGeolocation HeadlessTask] -", event.name, params);
|
|
13
|
+
*
|
|
14
|
+
* switch (event.name) {
|
|
15
|
+
* case "terminate":
|
|
16
|
+
* // Use await for async tasks
|
|
17
|
+
* const location = await BackgroundGeolocation.getCurrentPosition({
|
|
18
|
+
* samples: 1,
|
|
19
|
+
* persist: false
|
|
20
|
+
* });
|
|
21
|
+
* console.log("[BackgroundGeolocation HeadlessTask] - getCurrentPosition:", location);
|
|
22
|
+
* break;
|
|
23
|
+
* }
|
|
24
|
+
* // You must await all work you do in your task.
|
|
25
|
+
* // Headless-tasks are automatically terminated after executing the last line of your function.
|
|
26
|
+
* await doWork();
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* BackgroundGeolocation.registerHeadlessTask(BackgroundGeolocationHeadlessTask);
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ### ℹ️ See also:
|
|
33
|
+
* - 📘 [Android Headless Mode](github:wiki/Android-Headless-Mode).
|
|
34
|
+
* - [[BackgroundGeolocation.registerHeadlessTask]]
|
|
35
|
+
* - [[Config.enableHeadless]]
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
interface HeadlessEvent {
|
|
39
|
+
/**
|
|
40
|
+
* BackgroundGeolocation event name (eg: `location`, `motionchange`, `terminate`, `http`, etc).
|
|
41
|
+
*/
|
|
42
|
+
name: Event;
|
|
43
|
+
/**
|
|
44
|
+
* General event params according to the context of the event name.
|
|
45
|
+
*/
|
|
46
|
+
params: Map;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -157,6 +157,8 @@ declare module "react-native-background-geolocation" {
|
|
|
157
157
|
|
|
158
158
|
type PersistMode = -1 | 0 | 1 | 2;
|
|
159
159
|
|
|
160
|
+
type Map = {[key: string]: string|null|number|boolean|Map|string[]|number[]|number[][]|boolean[]|Map[]};
|
|
161
|
+
|
|
160
162
|
type Extras = {[key: string]: string|null|number|boolean|Extras|string[]|number[]|number[][]|boolean[]|Extras[]};
|
|
161
163
|
|
|
162
164
|
/**
|
|
@@ -185,4 +187,5 @@ declare module "react-native-background-geolocation" {
|
|
|
185
187
|
* - See [[onActivityChange]] and [[Location.activity]].
|
|
186
188
|
*/
|
|
187
189
|
type MotionActivityType = "unknown" | "still" | "walking" | "on_foot" | "running" | "on_bicycle" | "in_vehicle";
|
|
190
|
+
|
|
188
191
|
}
|
package/src/index.js
CHANGED
|
@@ -131,13 +131,6 @@ export default class BackgroundGeolocation {
|
|
|
131
131
|
return _deviceSettingsInstance;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/**
|
|
135
|
-
* Register HeadlessTask
|
|
136
|
-
*/
|
|
137
|
-
static registerHeadlessTask(task) {
|
|
138
|
-
AppRegistry.registerHeadlessTask(TAG, () => task);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
134
|
/**
|
|
142
135
|
* Core Plugin Control Methods
|
|
143
136
|
*/
|
|
@@ -380,7 +373,33 @@ export default class BackgroundGeolocation {
|
|
|
380
373
|
static finish(taskId, success, failure) {
|
|
381
374
|
this.stopBackgroundTask.apply(this, arguments);
|
|
382
375
|
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Register HeadlessTask
|
|
379
|
+
* Have to wrap execution to finish our own headless-task because there's an unknown issue with TurboModules in new arch
|
|
380
|
+
* where RN's HeadlessJsTaskSupport is null. That module can automatically finish RN headless-tasks.
|
|
381
|
+
*/
|
|
382
|
+
static registerHeadlessTask(taskProvider) {
|
|
383
|
+
AppRegistry.registerHeadlessTask(TAG, () => {
|
|
384
|
+
// return to RN's AppRegistry a Function that returns a Promise.
|
|
385
|
+
return (event) => {
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const taskId = event._transistorHeadlessTaskId;
|
|
388
|
+
delete(event._transistorHeadlessTaskId);
|
|
389
|
+
taskProvider(event).then(() => {
|
|
390
|
+
BackgroundGeolocation.finishHeadlessTask(taskId);
|
|
391
|
+
resolve();
|
|
392
|
+
}).catch(reason => {
|
|
393
|
+
BackgroundGeolocation.finishHeadlessTask(taskId);
|
|
394
|
+
reject(reason);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
383
401
|
/**
|
|
402
|
+
* @private
|
|
384
403
|
* Signal completion of a react-native headless-task. This helps RN free-up resources allocated to the headless-task execution.
|
|
385
404
|
*/
|
|
386
405
|
static finishHeadlessTask(taskId, success, failure) {
|