@onekeyfe/react-native-background-thread 3.0.18 → 3.0.20
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.
|
@@ -129,6 +129,7 @@ static std::atomic<int64_t> gNextTimerId{1};
|
|
|
129
129
|
static std::atomic<bool> gTimerWorkerStarted{false};
|
|
130
130
|
static std::atomic<bool> gTimerWorkerStop{false};
|
|
131
131
|
static RPCRuntimeExecutor gBgTimerExecutor;
|
|
132
|
+
static std::thread gTimerWorkerThread;
|
|
132
133
|
|
|
133
134
|
static long long nowMs() {
|
|
134
135
|
using namespace std::chrono;
|
|
@@ -160,6 +161,7 @@ static void timerWorkerLoop() {
|
|
|
160
161
|
// their callbacks. Captured under the lock; callbacks are invoked on
|
|
161
162
|
// the JS thread (not here).
|
|
162
163
|
std::vector<std::pair<int64_t, std::shared_ptr<jsi::Function>>> toFire;
|
|
164
|
+
RPCRuntimeExecutor executor;
|
|
163
165
|
{
|
|
164
166
|
std::unique_lock<std::mutex> lock(gTimerMutex);
|
|
165
167
|
if (gTimers.empty()) {
|
|
@@ -218,9 +220,9 @@ static void timerWorkerLoop() {
|
|
|
218
220
|
++it;
|
|
219
221
|
}
|
|
220
222
|
}
|
|
223
|
+
executor = gBgTimerExecutor;
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
RPCRuntimeExecutor executor = gBgTimerExecutor;
|
|
224
226
|
if (!executor) {
|
|
225
227
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
226
228
|
continue;
|
|
@@ -238,7 +240,8 @@ static void timerWorkerLoop() {
|
|
|
238
240
|
static void ensureTimerWorkerStarted() {
|
|
239
241
|
bool expected = false;
|
|
240
242
|
if (gTimerWorkerStarted.compare_exchange_strong(expected, true)) {
|
|
241
|
-
|
|
243
|
+
gTimerWorkerStop.store(false);
|
|
244
|
+
gTimerWorkerThread = std::thread(timerWorkerLoop);
|
|
242
245
|
LOGI("Timer worker thread started");
|
|
243
246
|
}
|
|
244
247
|
}
|
|
@@ -593,5 +596,41 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy(
|
|
|
593
596
|
SharedRPC::reset();
|
|
594
597
|
SharedStore::reset();
|
|
595
598
|
|
|
599
|
+
// Stop and join the timer worker before touching any shared state it
|
|
600
|
+
// reads. Without this, the worker could finish one last dispatch after
|
|
601
|
+
// we clear gTimers / gBgTimerExecutor and enqueue stale work against
|
|
602
|
+
// the about-to-be-destroyed runtime.
|
|
603
|
+
gTimerWorkerStop.store(true);
|
|
604
|
+
gTimerCv.notify_all();
|
|
605
|
+
if (gTimerWorkerThread.joinable()) {
|
|
606
|
+
gTimerWorkerThread.join();
|
|
607
|
+
}
|
|
608
|
+
gTimerWorkerStarted.store(false);
|
|
609
|
+
|
|
610
|
+
// Intentionally leak remaining jsi::Function callbacks (same rationale
|
|
611
|
+
// as SharedRPC::reset): destroying them here would run ~jsi::Function
|
|
612
|
+
// on the torn-down runtime and crash.
|
|
613
|
+
{
|
|
614
|
+
std::lock_guard<std::mutex> lock(gTimerMutex);
|
|
615
|
+
for (auto &entry : gTimers) {
|
|
616
|
+
if (entry.second.callback) {
|
|
617
|
+
new std::shared_ptr<jsi::Function>(std::move(entry.second.callback));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
gTimers.clear();
|
|
621
|
+
gBgTimerExecutor = nullptr;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Drain pending cross-runtime work. Each std::function may capture a
|
|
625
|
+
// shared_ptr<jsi::Function> tied to the destroyed runtime; leak them
|
|
626
|
+
// for the same reason as above.
|
|
627
|
+
{
|
|
628
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
629
|
+
for (auto &entry : gPendingWork) {
|
|
630
|
+
new std::function<void(jsi::Runtime &)>(std::move(entry.second));
|
|
631
|
+
}
|
|
632
|
+
gPendingWork.clear();
|
|
633
|
+
}
|
|
634
|
+
|
|
596
635
|
LOGI("Native resources cleaned up");
|
|
597
636
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
package com.backgroundthread
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
3
5
|
import android.net.Uri
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
4
8
|
import com.facebook.react.ReactPackage
|
|
5
9
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
10
|
import com.facebook.react.ReactInstanceEventListener
|
|
@@ -16,6 +20,7 @@ import com.facebook.react.runtime.ReactHostImpl
|
|
|
16
20
|
import com.facebook.react.runtime.hermes.HermesInstance
|
|
17
21
|
import com.facebook.react.shell.MainReactPackage
|
|
18
22
|
import java.io.File
|
|
23
|
+
import java.lang.ref.WeakReference
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Singleton manager for the background React Native runtime.
|
|
@@ -31,6 +36,11 @@ class BackgroundThreadManager private constructor() {
|
|
|
31
36
|
private var bgReactHost: ReactHostImpl? = null
|
|
32
37
|
private var reactPackages: List<ReactPackage> = emptyList()
|
|
33
38
|
|
|
39
|
+
// Tracks the last resumed Activity so we can replay it onto the bg
|
|
40
|
+
// ReactContext as soon as the bg host finishes initializing (covers the
|
|
41
|
+
// cold-start race where Activity resumes before bg host is ready).
|
|
42
|
+
private var lastResumedActivityRef: WeakReference<Activity> = WeakReference(null)
|
|
43
|
+
|
|
34
44
|
@Volatile
|
|
35
45
|
private var bgRuntimePtr: Long = 0
|
|
36
46
|
|
|
@@ -329,6 +339,11 @@ class BackgroundThreadManager private constructor() {
|
|
|
329
339
|
override fun onReactContextInitialized(context: ReactContext) {
|
|
330
340
|
val initMs = (System.nanoTime() - bgStartTime) / 1_000_000.0
|
|
331
341
|
BTLogger.info("[SplitBundle] background ReactContext initialized in ${String.format("%.1f", initMs)}ms")
|
|
342
|
+
// Replay the most recent Activity resume so TurboModules on the
|
|
343
|
+
// bg host can see getCurrentActivity()/ActivityEventListeners
|
|
344
|
+
// from the very first call, even when the bg host finishes
|
|
345
|
+
// initializing after the Activity is already resumed.
|
|
346
|
+
replayLastResumedActivityOnUi()
|
|
332
347
|
context.runOnJSQueueThread {
|
|
333
348
|
try {
|
|
334
349
|
val ptr = context.javaScriptContextHolder?.get() ?: 0L
|
|
@@ -417,22 +432,328 @@ class BackgroundThreadManager private constructor() {
|
|
|
417
432
|
return
|
|
418
433
|
}
|
|
419
434
|
|
|
420
|
-
|
|
435
|
+
// Use ReactContext.registerSegment which works in both bridge
|
|
436
|
+
// and bridgeless modes.
|
|
437
|
+
try {
|
|
438
|
+
context.registerSegment(segmentId, path) {
|
|
439
|
+
BTLogger.info("Segment registered in background runtime: id=$segmentId, path=$path")
|
|
440
|
+
onComplete(null)
|
|
441
|
+
}
|
|
442
|
+
} catch (e: Exception) {
|
|
443
|
+
BTLogger.error("Failed to register segment in background runtime: ${e.message}")
|
|
444
|
+
onComplete(e)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── Activity lifecycle bridge (selective, reflection-based) ─────────────
|
|
449
|
+
//
|
|
450
|
+
// Goal: let a small, explicit allowlist of native modules on the bg
|
|
451
|
+
// ReactHost observe Activity-related state (getCurrentActivity(),
|
|
452
|
+
// onActivityResult, onNewIntent, and — for the same allowlist —
|
|
453
|
+
// onHostResume/onHostPause/onHostDestroy) WITHOUT triggering any side
|
|
454
|
+
// effects on other modules that happen to live on the same ReactContext.
|
|
455
|
+
//
|
|
456
|
+
// Why not use ReactHost.onHostResume / onActivityResult directly?
|
|
457
|
+
// Those internally fan out to ALL LifecycleEventListeners and
|
|
458
|
+
// ActivityEventListeners registered on the ReactContext, which would
|
|
459
|
+
// cause every bg TurboModule that tracks host lifecycle (BroadcastReceivers,
|
|
460
|
+
// sensors, keyboard observers, etc.) to receive a second set of callbacks
|
|
461
|
+
// in addition to the UI host. That doubles resource registration, opens
|
|
462
|
+
// requestCode collisions on Activity results, and can tear down the bg
|
|
463
|
+
// ReactContext during rotation. We need fine-grained control, which RN
|
|
464
|
+
// does not expose, so we operate on the underlying fields directly.
|
|
465
|
+
//
|
|
466
|
+
// What we actually do:
|
|
467
|
+
// - Reflect-write bg ReactContext's `mCurrentActivity` so
|
|
468
|
+
// getCurrentActivity() returns the correct Activity for modules that
|
|
469
|
+
// bother to query it. Modules that don't query it are unaffected.
|
|
470
|
+
// - Read `mActivityEventListeners` via reflection and invoke ONLY the
|
|
471
|
+
// listeners whose class FQCN matches an entry in
|
|
472
|
+
// `bgActivityBridgeListenerClassAllowlist` (for onActivityResult/onNewIntent).
|
|
473
|
+
// - Read `mLifecycleEventListeners` via reflection and invoke
|
|
474
|
+
// onHostResume/onHostPause/onHostDestroy on listeners whose class
|
|
475
|
+
// FQCN matches the same allowlist. Non-allowlisted listeners are
|
|
476
|
+
// never fired on the bg host — preserving the pre-existing baseline
|
|
477
|
+
// that bg was "never resumed".
|
|
478
|
+
// - Deliberately do NOT touch `mLifecycleState` — setting it to RESUMED
|
|
479
|
+
// would cause `addLifecycleEventListener(...)` to auto-fire
|
|
480
|
+
// `onHostResume()` on EVERY newly-registered listener (including
|
|
481
|
+
// non-allowlisted ones), reintroducing the double-dispatch we're
|
|
482
|
+
// trying to avoid. Instead we fire onHostResume manually on each
|
|
483
|
+
// dispatchActivityResumed, which covers the common case; a listener
|
|
484
|
+
// registered strictly between two resume events will catch up on
|
|
485
|
+
// the next cycle.
|
|
486
|
+
//
|
|
487
|
+
// Trade-off:
|
|
488
|
+
// We rely on RN-internal field names `mCurrentActivity`,
|
|
489
|
+
// `mActivityEventListeners`, `mLifecycleEventListeners` on
|
|
490
|
+
// `com.facebook.react.bridge.ReactContext`. An RN upgrade that
|
|
491
|
+
// renames or restructures these fields will cause reflection to fail
|
|
492
|
+
// (caught and logged via BTLogger → OneKeyLog). Mitigate with a
|
|
493
|
+
// dev-build smoke assertion at the call site.
|
|
494
|
+
//
|
|
495
|
+
// Thread safety:
|
|
496
|
+
// ReactContext.onHostResume / onActivityResult are @ThreadConfined(UI);
|
|
497
|
+
// we match that by bouncing to the main looper. The underlying sets
|
|
498
|
+
// are CopyOnWriteArraySet, so iterating them off-thread would still be
|
|
499
|
+
// safe, but UI-thread is the documented contract and we stick to it.
|
|
500
|
+
|
|
501
|
+
// ── Allowlist (registered externally by the host app) ───────────────────
|
|
502
|
+
//
|
|
503
|
+
// FQCN prefixes of ActivityEventListener / LifecycleEventListener
|
|
504
|
+
// implementations that are allowed to receive Activity-bound events
|
|
505
|
+
// (onActivityResult, onNewIntent, onHostResume/Pause/Destroy) on the
|
|
506
|
+
// bg ReactHost. Anything not matching a registered prefix is fully
|
|
507
|
+
// ignored on bg, preserving the pre-existing baseline.
|
|
508
|
+
//
|
|
509
|
+
// The bg-thread module deliberately ships an EMPTY default — the host
|
|
510
|
+
// application is the only place that knows which third-party modules
|
|
511
|
+
// are bg-eligible. Register entries early in Application.onCreate via
|
|
512
|
+
// addBgActivityBridgeListenerClassPrefix(...) so the allowlist is populated
|
|
513
|
+
// before the first Activity lifecycle callback can fire.
|
|
514
|
+
//
|
|
515
|
+
// Adding a prefix is a cross-runtime change. Verify:
|
|
516
|
+
// 1. The module's listener is idempotent and safe to fire.
|
|
517
|
+
// 2. ActivityResult requestCodes do not collide with other modules.
|
|
518
|
+
// 3. Any LifecycleEventListener side-effects are double-host safe.
|
|
519
|
+
@Volatile
|
|
520
|
+
private var bgActivityBridgeListenerClassAllowlist: Set<String> = emptySet()
|
|
521
|
+
|
|
522
|
+
/** Add a single FQCN-prefix entry to the allowlist (idempotent). */
|
|
523
|
+
@Synchronized
|
|
524
|
+
fun addBgActivityBridgeListenerClassPrefix(prefix: String) {
|
|
525
|
+
if (prefix.isEmpty()) return
|
|
526
|
+
if (bgActivityBridgeListenerClassAllowlist.contains(prefix)) return
|
|
527
|
+
bgActivityBridgeListenerClassAllowlist = bgActivityBridgeListenerClassAllowlist + prefix
|
|
528
|
+
BTLogger.info("addBgActivityBridgeListenerClassPrefix: $prefix " +
|
|
529
|
+
"(total=${bgActivityBridgeListenerClassAllowlist.size})")
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Replace the allowlist wholesale. Intended for test setup / reload. */
|
|
533
|
+
@Synchronized
|
|
534
|
+
fun setBgActivityBridgeListenerClassAllowlist(prefixes: Set<String>) {
|
|
535
|
+
bgActivityBridgeListenerClassAllowlist = prefixes.toSet()
|
|
536
|
+
BTLogger.info("setBgActivityBridgeListenerClassAllowlist: ${prefixes.size} entries")
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Inspect the current allowlist (snapshot). */
|
|
540
|
+
fun getBgActivityBridgeListenerClassAllowlist(): Set<String> =
|
|
541
|
+
bgActivityBridgeListenerClassAllowlist
|
|
542
|
+
|
|
543
|
+
fun dispatchActivityResumed(activity: Activity) {
|
|
544
|
+
lastResumedActivityRef = WeakReference(activity)
|
|
545
|
+
runOnUiThread {
|
|
546
|
+
writeBgCurrentActivity(activity)
|
|
547
|
+
dispatchLifecycleEventToAllowlisted(LifecycleEvent.RESUME)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
fun dispatchActivityPaused(activity: Activity) {
|
|
552
|
+
// Keep mCurrentActivity — Activity is still valid until destroyed,
|
|
553
|
+
// and clearing it between pause/resume would flap getCurrentActivity().
|
|
554
|
+
// LifecycleEventListener.onHostPause() IS fired for allowlisted
|
|
555
|
+
// modules so they can quiesce work (mirrors RN's UI-host behaviour).
|
|
556
|
+
runOnUiThread {
|
|
557
|
+
dispatchLifecycleEventToAllowlisted(LifecycleEvent.PAUSE)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fun dispatchActivityDestroyed(activity: Activity) {
|
|
562
|
+
if (lastResumedActivityRef.get() === activity) {
|
|
563
|
+
lastResumedActivityRef = WeakReference(null)
|
|
564
|
+
}
|
|
565
|
+
runOnUiThread {
|
|
566
|
+
val ctx = bgReactHost?.currentReactContext ?: return@runOnUiThread
|
|
567
|
+
// Only clear if the bg context's tracked Activity IS the one
|
|
568
|
+
// being destroyed. Guards against a stale clear when the host
|
|
569
|
+
// Activity is swapped (e.g. multi-Activity deep-link flows).
|
|
570
|
+
if (ctx.currentActivity === activity) {
|
|
571
|
+
writeBgCurrentActivity(null)
|
|
572
|
+
dispatchLifecycleEventToAllowlisted(LifecycleEvent.DESTROY)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private enum class LifecycleEvent { RESUME, PAUSE, DESTROY }
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Iterate bg ReactContext's LifecycleEventListener set and fire the
|
|
581
|
+
* requested callback on listeners whose class FQCN matches the
|
|
582
|
+
* allowlist. Modules outside the allowlist are completely unaffected
|
|
583
|
+
* (they stay on the pre-existing "never-resumed on bg" baseline).
|
|
584
|
+
*
|
|
585
|
+
* Note on edge case: a LifecycleEventListener registered AFTER the
|
|
586
|
+
* corresponding dispatch event will miss that event, because we don't
|
|
587
|
+
* touch mLifecycleState (doing so would cause RN's
|
|
588
|
+
* addLifecycleEventListener to auto-fire onHostResume on EVERY new
|
|
589
|
+
* listener, including non-allowlisted ones). The next resume/pause/
|
|
590
|
+
* destroy cycle picks it up. This is consistent with how modules that
|
|
591
|
+
* register late behave against the UI host anyway.
|
|
592
|
+
*/
|
|
593
|
+
private fun dispatchLifecycleEventToAllowlisted(event: LifecycleEvent) {
|
|
594
|
+
val listeners = readBgLifecycleListeners() ?: return
|
|
595
|
+
for (l in listeners) {
|
|
596
|
+
if (!isBgListenerAllowed(l)) continue
|
|
421
597
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
598
|
+
when (event) {
|
|
599
|
+
LifecycleEvent.RESUME -> l.onHostResume()
|
|
600
|
+
LifecycleEvent.PAUSE -> l.onHostPause()
|
|
601
|
+
LifecycleEvent.DESTROY -> l.onHostDestroy()
|
|
602
|
+
}
|
|
603
|
+
} catch (t: Throwable) {
|
|
604
|
+
BTLogger.error("bg lifecycle ${event.name} dispatch (${l.javaClass.name}): ${t.message}")
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
fun dispatchActivityResult(
|
|
610
|
+
activity: Activity,
|
|
611
|
+
requestCode: Int,
|
|
612
|
+
resultCode: Int,
|
|
613
|
+
data: Intent?
|
|
614
|
+
) {
|
|
615
|
+
runOnUiThread {
|
|
616
|
+
val listeners = readBgActivityListeners() ?: return@runOnUiThread
|
|
617
|
+
for (l in listeners) {
|
|
618
|
+
if (!isBgListenerAllowed(l)) continue
|
|
619
|
+
try {
|
|
620
|
+
l.onActivityResult(activity, requestCode, resultCode, data)
|
|
621
|
+
} catch (t: Throwable) {
|
|
622
|
+
BTLogger.error("bg onActivityResult dispatch (${l.javaClass.name}): ${t.message}")
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
fun dispatchNewIntent(intent: Intent) {
|
|
629
|
+
runOnUiThread {
|
|
630
|
+
val listeners = readBgActivityListeners() ?: return@runOnUiThread
|
|
631
|
+
for (l in listeners) {
|
|
632
|
+
if (!isBgListenerAllowed(l)) continue
|
|
633
|
+
try {
|
|
634
|
+
l.onNewIntent(intent)
|
|
635
|
+
} catch (t: Throwable) {
|
|
636
|
+
BTLogger.error("bg onNewIntent dispatch (${l.javaClass.name}): ${t.message}")
|
|
428
637
|
}
|
|
429
|
-
} catch (e: Exception) {
|
|
430
|
-
BTLogger.error("Failed to register segment in background runtime: ${e.message}")
|
|
431
|
-
onComplete(e)
|
|
432
638
|
}
|
|
433
639
|
}
|
|
434
640
|
}
|
|
435
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Called when the bg ReactContext becomes available so we can install
|
|
644
|
+
* the last resumed Activity right away, covering the window where the
|
|
645
|
+
* host Activity resumes before the bg host finishes initializing.
|
|
646
|
+
*/
|
|
647
|
+
private fun replayLastResumedActivityOnUi() {
|
|
648
|
+
val activity = lastResumedActivityRef.get() ?: return
|
|
649
|
+
runOnUiThread {
|
|
650
|
+
if (writeBgCurrentActivity(activity)) {
|
|
651
|
+
BTLogger.info("replayLastResumedActivityOnUi: mCurrentActivity=${activity.javaClass.simpleName}")
|
|
652
|
+
}
|
|
653
|
+
// Fire RESUME for allowlisted modules whose listener was
|
|
654
|
+
// registered before the bg ReactContext finished initializing.
|
|
655
|
+
dispatchLifecycleEventToAllowlisted(LifecycleEvent.RESUME)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Reflect-write `mCurrentActivity` on the bg ReactContext so
|
|
661
|
+
* TurboModules on that host see a non-null getCurrentActivity().
|
|
662
|
+
*
|
|
663
|
+
* Returns true on success.
|
|
664
|
+
*/
|
|
665
|
+
private fun writeBgCurrentActivity(activity: Activity?): Boolean {
|
|
666
|
+
val ctx = bgReactHost?.currentReactContext ?: return false
|
|
667
|
+
return try {
|
|
668
|
+
val field = findField(ctx.javaClass, "mCurrentActivity") ?: run {
|
|
669
|
+
BTLogger.error("writeBgCurrentActivity: mCurrentActivity field not found (RN upgrade?)")
|
|
670
|
+
return false
|
|
671
|
+
}
|
|
672
|
+
field.isAccessible = true
|
|
673
|
+
field.set(ctx, if (activity != null) WeakReference(activity) else null)
|
|
674
|
+
true
|
|
675
|
+
} catch (t: Throwable) {
|
|
676
|
+
BTLogger.error("writeBgCurrentActivity: ${t.message}")
|
|
677
|
+
false
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Reflect-read bg ReactContext's `mActivityEventListeners` set so we
|
|
683
|
+
* can iterate it without going through ReactContext.onActivityResult
|
|
684
|
+
* (which would fan out to every listener unconditionally).
|
|
685
|
+
*/
|
|
686
|
+
@Suppress("UNCHECKED_CAST")
|
|
687
|
+
private fun readBgActivityListeners(): Collection<com.facebook.react.bridge.ActivityEventListener>? {
|
|
688
|
+
val ctx = bgReactHost?.currentReactContext ?: return null
|
|
689
|
+
return try {
|
|
690
|
+
val field = findField(ctx.javaClass, "mActivityEventListeners") ?: run {
|
|
691
|
+
BTLogger.error("readBgActivityListeners: field not found (RN upgrade?)")
|
|
692
|
+
return null
|
|
693
|
+
}
|
|
694
|
+
field.isAccessible = true
|
|
695
|
+
field.get(ctx) as? Collection<com.facebook.react.bridge.ActivityEventListener>
|
|
696
|
+
} catch (t: Throwable) {
|
|
697
|
+
BTLogger.error("readBgActivityListeners: ${t.message}")
|
|
698
|
+
null
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Reflect-read bg ReactContext's `mLifecycleEventListeners` set. Same
|
|
704
|
+
* rationale as readBgActivityListeners — we iterate ourselves so we
|
|
705
|
+
* can apply the allowlist filter, avoiding a fan-out to every
|
|
706
|
+
* LifecycleEventListener on the bg ReactContext.
|
|
707
|
+
*/
|
|
708
|
+
@Suppress("UNCHECKED_CAST")
|
|
709
|
+
private fun readBgLifecycleListeners(): Collection<com.facebook.react.bridge.LifecycleEventListener>? {
|
|
710
|
+
val ctx = bgReactHost?.currentReactContext ?: return null
|
|
711
|
+
return try {
|
|
712
|
+
val field = findField(ctx.javaClass, "mLifecycleEventListeners") ?: run {
|
|
713
|
+
BTLogger.error("readBgLifecycleListeners: field not found (RN upgrade?)")
|
|
714
|
+
return null
|
|
715
|
+
}
|
|
716
|
+
field.isAccessible = true
|
|
717
|
+
field.get(ctx) as? Collection<com.facebook.react.bridge.LifecycleEventListener>
|
|
718
|
+
} catch (t: Throwable) {
|
|
719
|
+
BTLogger.error("readBgLifecycleListeners: ${t.message}")
|
|
720
|
+
null
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Walks the class hierarchy to find a declared field by name.
|
|
726
|
+
* `mCurrentActivity` and `mActivityEventListeners` live on
|
|
727
|
+
* `ReactContext`, but bg runs `BridgelessReactContext` which subclasses
|
|
728
|
+
* `ReactApplicationContext` which subclasses `ReactContext`, so we
|
|
729
|
+
* can't use getDeclaredField directly on the runtime class.
|
|
730
|
+
*/
|
|
731
|
+
private fun findField(cls: Class<*>, name: String): java.lang.reflect.Field? {
|
|
732
|
+
var c: Class<*>? = cls
|
|
733
|
+
while (c != null) {
|
|
734
|
+
try { return c.getDeclaredField(name) } catch (_: NoSuchFieldException) {}
|
|
735
|
+
c = c.superclass
|
|
736
|
+
}
|
|
737
|
+
return null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private fun isBgListenerAllowed(listener: Any): Boolean {
|
|
741
|
+
val fqcn = listener.javaClass.name
|
|
742
|
+
val list = bgActivityBridgeListenerClassAllowlist
|
|
743
|
+
for (prefix in list) {
|
|
744
|
+
if (fqcn.startsWith(prefix)) return true
|
|
745
|
+
}
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private fun runOnUiThread(block: () -> Unit) {
|
|
750
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
751
|
+
block()
|
|
752
|
+
} else {
|
|
753
|
+
Handler(Looper.getMainLooper()).post(block)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
436
757
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
437
758
|
|
|
438
759
|
val isBackgroundStarted: Boolean get() = isStarted
|
|
@@ -445,5 +766,6 @@ class BackgroundThreadManager private constructor() {
|
|
|
445
766
|
bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
|
|
446
767
|
bgReactHost = null
|
|
447
768
|
isStarted = false
|
|
769
|
+
lastResumedActivityRef = WeakReference(null)
|
|
448
770
|
}
|
|
449
771
|
}
|
|
@@ -107,7 +107,12 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
|
|
|
107
107
|
self.reactNativeFactoryDelegate = [[BackgroundReactNativeDelegate alloc] init];
|
|
108
108
|
self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self.reactNativeFactoryDelegate];
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// Only set jsBundleSource for debug HTTP URLs or explicit OTA overrides.
|
|
111
|
+
// Leaving the default release name ("background.bundle") unset lets the
|
|
112
|
+
// delegate fall back to split-bundle mode (common.jsbundle + entry).
|
|
113
|
+
if (![entryURL isEqualToString:@"background.bundle"]) {
|
|
114
|
+
[self.reactNativeFactoryDelegate setJsBundleSource:std::string([entryURL UTF8String])];
|
|
115
|
+
}
|
|
111
116
|
|
|
112
117
|
[self.reactNativeFactory.rootViewFactory viewWithModuleName:MODULE_NAME
|
|
113
118
|
initialProperties:initialProperties
|