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
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,13 +89,26 @@ public void onHeadlessEvent(HeadlessEvent event) {
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 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 ) {
163
112
TSLog .logger .warn (TSLog .warn ("Failed invoke HeadlessTask " + name + ". Task ignored." + e .getMessage ()));
164
113
}
165
114
}
@@ -171,239 +120,10 @@ public void onHeadlessEvent(HeadlessEvent event) {
171
120
*/
172
121
@ Subscribe (threadMode = ThreadMode .MAIN )
173
122
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
123
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 ());
381
125
} 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 ()));
407
127
}
408
128
}
409
129
}
0 commit comments