@onekeyfe/react-native-background-thread 1.1.46 → 1.1.48
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/BackgroundThread.podspec +4 -2
- package/android/CMakeLists.txt +29 -0
- package/android/build.gradle +42 -0
- package/android/src/main/cpp/cpp-adapter.cpp +222 -0
- package/android/src/main/java/com/backgroundthread/BTLogger.kt +55 -0
- package/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt +258 -0
- package/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt +17 -17
- package/cpp/SharedRPC.cpp +223 -0
- package/cpp/SharedRPC.h +50 -0
- package/cpp/SharedStore.cpp +184 -0
- package/cpp/SharedStore.h +28 -0
- package/ios/BackgroundRunnerReactNativeDelegate.h +0 -17
- package/ios/BackgroundRunnerReactNativeDelegate.mm +83 -123
- package/ios/BackgroundThread.h +1 -0
- package/ios/BackgroundThread.mm +6 -22
- package/ios/BackgroundThreadManager.h +6 -12
- package/ios/BackgroundThreadManager.mm +39 -30
- package/lib/module/NativeBackgroundThread.js.map +1 -1
- package/lib/module/SharedRPC.js +6 -0
- package/lib/module/SharedRPC.js.map +1 -0
- package/lib/module/SharedStore.js +6 -0
- package/lib/module/SharedStore.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeBackgroundThread.d.ts +2 -4
- package/lib/typescript/src/NativeBackgroundThread.d.ts.map +1 -1
- package/lib/typescript/src/SharedRPC.d.ts +12 -0
- package/lib/typescript/src/SharedRPC.d.ts.map +1 -0
- package/lib/typescript/src/SharedStore.d.ts +14 -0
- package/lib/typescript/src/SharedStore.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/NativeBackgroundThread.ts +2 -4
- package/src/SharedRPC.ts +16 -0
- package/src/SharedStore.ts +18 -0
- package/src/index.tsx +4 -0
package/BackgroundThread.podspec
CHANGED
|
@@ -13,8 +13,10 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
14
|
s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-background-thread.git", :tag => "#{s.version}" }
|
|
15
15
|
|
|
16
|
-
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
-
s.public_header_files = "ios
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "cpp/**/*.{h,cpp}"
|
|
17
|
+
s.public_header_files = "ios/BackgroundThreadManager.h", "ios/BTLogger.h"
|
|
18
|
+
s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"', 'DEFINES_MODULE' => 'YES' }
|
|
19
|
+
s.user_target_xcconfig = { 'SWIFT_INCLUDE_PATHS' => '"$(PODS_ROOT)/Headers/Public/BackgroundThread"' }
|
|
18
20
|
|
|
19
21
|
s.dependency 'ReactNativeNativeLogger'
|
|
20
22
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.13)
|
|
2
|
+
project(background_thread)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
6
|
+
|
|
7
|
+
find_package(fbjni REQUIRED CONFIG)
|
|
8
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
9
|
+
|
|
10
|
+
add_library(${PROJECT_NAME} SHARED
|
|
11
|
+
src/main/cpp/cpp-adapter.cpp
|
|
12
|
+
../cpp/SharedStore.cpp
|
|
13
|
+
../cpp/SharedRPC.cpp
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
target_include_directories(${PROJECT_NAME} PRIVATE
|
|
17
|
+
src/main/cpp
|
|
18
|
+
../cpp
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
find_library(LOG_LIB log)
|
|
22
|
+
|
|
23
|
+
target_link_libraries(${PROJECT_NAME}
|
|
24
|
+
${LOG_LIB}
|
|
25
|
+
fbjni::fbjni
|
|
26
|
+
ReactAndroid::jsi
|
|
27
|
+
ReactAndroid::reactnative
|
|
28
|
+
android
|
|
29
|
+
)
|
package/android/build.gradle
CHANGED
|
@@ -25,6 +25,11 @@ def getExtOrIntegerDefault(name) {
|
|
|
25
25
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["BackgroundThread_" + name]).toInteger()
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
def reactNativeArchitectures() {
|
|
29
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
30
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
android {
|
|
29
34
|
namespace "com.backgroundthread"
|
|
30
35
|
|
|
@@ -33,10 +38,47 @@ android {
|
|
|
33
38
|
defaultConfig {
|
|
34
39
|
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
35
40
|
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
41
|
+
|
|
42
|
+
externalNativeBuild {
|
|
43
|
+
cmake {
|
|
44
|
+
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
|
|
45
|
+
arguments "-DANDROID_STL=c++_shared",
|
|
46
|
+
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
47
|
+
abiFilters(*reactNativeArchitectures())
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
externalNativeBuild {
|
|
53
|
+
cmake {
|
|
54
|
+
path "CMakeLists.txt"
|
|
55
|
+
}
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
buildFeatures {
|
|
39
59
|
buildConfig true
|
|
60
|
+
prefab true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
packagingOptions {
|
|
64
|
+
excludes = [
|
|
65
|
+
"META-INF",
|
|
66
|
+
"META-INF/**",
|
|
67
|
+
"**/libc++_shared.so",
|
|
68
|
+
"**/libfbjni.so",
|
|
69
|
+
"**/libjsi.so",
|
|
70
|
+
"**/libfolly_json.so",
|
|
71
|
+
"**/libfolly_runtime.so",
|
|
72
|
+
"**/libglog.so",
|
|
73
|
+
"**/libhermes.so",
|
|
74
|
+
"**/libhermes-executor-debug.so",
|
|
75
|
+
"**/libhermes_executor.so",
|
|
76
|
+
"**/libreactnative.so",
|
|
77
|
+
"**/libreactnativejni.so",
|
|
78
|
+
"**/libturbomodulejsijni.so",
|
|
79
|
+
"**/libreact_nativemodule_core.so",
|
|
80
|
+
"**/libjscexecutor.so"
|
|
81
|
+
]
|
|
40
82
|
}
|
|
41
83
|
|
|
42
84
|
buildTypes {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#include <jni.h>
|
|
2
|
+
#include <jsi/jsi.h>
|
|
3
|
+
#include <android/log.h>
|
|
4
|
+
#include <memory>
|
|
5
|
+
#include <mutex>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <unordered_map>
|
|
8
|
+
|
|
9
|
+
#include "SharedStore.h"
|
|
10
|
+
#include "SharedRPC.h"
|
|
11
|
+
|
|
12
|
+
#define LOG_TAG "BackgroundThread"
|
|
13
|
+
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
14
|
+
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
15
|
+
|
|
16
|
+
namespace jsi = facebook::jsi;
|
|
17
|
+
|
|
18
|
+
static JavaVM *gJavaVM = nullptr;
|
|
19
|
+
|
|
20
|
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
|
|
21
|
+
gJavaVM = vm;
|
|
22
|
+
return JNI_VERSION_1_6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static JNIEnv *getJNIEnv() {
|
|
26
|
+
JNIEnv *env = nullptr;
|
|
27
|
+
if (gJavaVM) {
|
|
28
|
+
gJavaVM->AttachCurrentThread(&env, nullptr);
|
|
29
|
+
}
|
|
30
|
+
return env;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Stub a JSI function on an object (replaces it with a no-op).
|
|
34
|
+
static void stubJsiFunction(jsi::Runtime &runtime, jsi::Object &object, const char *name) {
|
|
35
|
+
object.setProperty(
|
|
36
|
+
runtime,
|
|
37
|
+
name,
|
|
38
|
+
jsi::Function::createFromHostFunction(
|
|
39
|
+
runtime, jsi::PropNameID::forUtf8(runtime, name), 1,
|
|
40
|
+
[](auto &, const auto &, const auto *, size_t) {
|
|
41
|
+
return jsi::Value::undefined();
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name) {
|
|
46
|
+
try {
|
|
47
|
+
auto fnValue = runtime.global().getProperty(runtime, name);
|
|
48
|
+
if (!fnValue.isObject() || !fnValue.asObject(runtime).isFunction(runtime)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
auto fn = fnValue.asObject(runtime).asFunction(runtime);
|
|
53
|
+
fn.call(runtime);
|
|
54
|
+
} catch (const jsi::JSError &e) {
|
|
55
|
+
LOGE("JSError calling global function %s: %s", name, e.getMessage().c_str());
|
|
56
|
+
} catch (const std::exception &e) {
|
|
57
|
+
LOGE("Error calling global function %s: %s", name, e.what());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Pending work map for cross-runtime executor ───────────────────────
|
|
62
|
+
static std::mutex gWorkMutex;
|
|
63
|
+
static std::unordered_map<int64_t, std::function<void(jsi::Runtime &)>> gPendingWork;
|
|
64
|
+
static int64_t gNextWorkId = 0;
|
|
65
|
+
|
|
66
|
+
// Called from Kotlin after runOnJSQueueThread dispatches to the correct thread.
|
|
67
|
+
extern "C" JNIEXPORT void JNICALL
|
|
68
|
+
Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
|
|
69
|
+
JNIEnv *env, jobject thiz, jlong runtimePtr, jlong workId) {
|
|
70
|
+
LOGI("nativeExecuteWork: runtimePtr=%ld, workId=%ld", (long)runtimePtr, (long)workId);
|
|
71
|
+
auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
|
|
72
|
+
if (!rt) return;
|
|
73
|
+
|
|
74
|
+
std::function<void(jsi::Runtime &)> work;
|
|
75
|
+
{
|
|
76
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
77
|
+
auto it = gPendingWork.find(workId);
|
|
78
|
+
if (it == gPendingWork.end()) return;
|
|
79
|
+
work = std::move(it->second);
|
|
80
|
+
gPendingWork.erase(it);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
work(*rt);
|
|
84
|
+
} catch (const jsi::JSError &e) {
|
|
85
|
+
LOGE("JSError in nativeExecuteWork: %s", e.getMessage().c_str());
|
|
86
|
+
} catch (const std::exception &e) {
|
|
87
|
+
LOGE("Error in nativeExecuteWork: %s", e.what());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── nativeInstallSharedBridge ───────────────────────────────────────────
|
|
92
|
+
// Install SharedStore and SharedRPC into a runtime.
|
|
93
|
+
extern "C" JNIEXPORT void JNICALL
|
|
94
|
+
Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
|
|
95
|
+
JNIEnv *env, jobject thiz, jlong runtimePtr, jboolean isMain) {
|
|
96
|
+
|
|
97
|
+
auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
|
|
98
|
+
if (!rt) return;
|
|
99
|
+
|
|
100
|
+
SharedStore::install(*rt);
|
|
101
|
+
|
|
102
|
+
// Create executor that schedules work on this runtime's JS thread via Kotlin
|
|
103
|
+
jobject ref = env->NewGlobalRef(thiz);
|
|
104
|
+
bool capturedIsMain = static_cast<bool>(isMain);
|
|
105
|
+
|
|
106
|
+
RPCRuntimeExecutor executor = [ref, capturedIsMain](std::function<void(jsi::Runtime &)> work) {
|
|
107
|
+
JNIEnv *env = getJNIEnv();
|
|
108
|
+
if (!env || !ref) {
|
|
109
|
+
LOGE("executor: env=%p, ref=%p — aborting", env, ref);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
int64_t workId;
|
|
114
|
+
{
|
|
115
|
+
std::lock_guard<std::mutex> lock(gWorkMutex);
|
|
116
|
+
workId = gNextWorkId++;
|
|
117
|
+
gPendingWork[workId] = std::move(work);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
jclass cls = env->GetObjectClass(ref);
|
|
121
|
+
jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)V");
|
|
122
|
+
if (mid) {
|
|
123
|
+
LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", capturedIsMain, (long)workId);
|
|
124
|
+
env->CallVoidMethod(ref, mid, static_cast<jboolean>(capturedIsMain), static_cast<jlong>(workId));
|
|
125
|
+
if (env->ExceptionCheck()) {
|
|
126
|
+
LOGE("executor: JNI exception after scheduleOnJSThread");
|
|
127
|
+
env->ExceptionDescribe();
|
|
128
|
+
env->ExceptionClear();
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
LOGE("executor: scheduleOnJSThread method not found!");
|
|
132
|
+
if (env->ExceptionCheck()) {
|
|
133
|
+
env->ExceptionDescribe();
|
|
134
|
+
env->ExceptionClear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
env->DeleteLocalRef(cls);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
std::string runtimeId = isMain ? "main" : "background";
|
|
141
|
+
SharedRPC::install(*rt, std::move(executor), runtimeId);
|
|
142
|
+
LOGI("SharedStore and SharedRPC installed (isMain=%d)", static_cast<int>(isMain));
|
|
143
|
+
if (!capturedIsMain) {
|
|
144
|
+
invokeOptionalGlobalFunction(*rt, "__setupBackgroundRPCHandler");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── nativeSetupErrorHandler ─────────────────────────────────────────────
|
|
149
|
+
// Wrap the global error handler in the background runtime.
|
|
150
|
+
// Mirrors iOS BackgroundRunnerReactNativeDelegate.setupErrorHandler:.
|
|
151
|
+
extern "C" JNIEXPORT void JNICALL
|
|
152
|
+
Java_com_backgroundthread_BackgroundThreadManager_nativeSetupErrorHandler(
|
|
153
|
+
JNIEnv *env, jobject thiz, jlong runtimePtr) {
|
|
154
|
+
|
|
155
|
+
auto *runtime = reinterpret_cast<jsi::Runtime *>(runtimePtr);
|
|
156
|
+
if (!runtime) return;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
jsi::Object global = runtime->global();
|
|
160
|
+
jsi::Value errorUtilsVal = global.getProperty(*runtime, "ErrorUtils");
|
|
161
|
+
if (!errorUtilsVal.isObject()) {
|
|
162
|
+
LOGE("ErrorUtils is not available on global object");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
jsi::Object errorUtils = errorUtilsVal.asObject(*runtime);
|
|
167
|
+
|
|
168
|
+
// Capture the current global error handler
|
|
169
|
+
auto originalHandler = std::make_shared<jsi::Value>(
|
|
170
|
+
errorUtils.getProperty(*runtime, "getGlobalHandler")
|
|
171
|
+
.asObject(*runtime).asFunction(*runtime).call(*runtime));
|
|
172
|
+
|
|
173
|
+
// Create a custom handler that delegates to the original
|
|
174
|
+
auto handlerFunc = jsi::Function::createFromHostFunction(
|
|
175
|
+
*runtime,
|
|
176
|
+
jsi::PropNameID::forAscii(*runtime, "customGlobalErrorHandler"),
|
|
177
|
+
2,
|
|
178
|
+
[originalHandler](
|
|
179
|
+
jsi::Runtime &rt, const jsi::Value &,
|
|
180
|
+
const jsi::Value *args, size_t count) -> jsi::Value {
|
|
181
|
+
if (count < 2) {
|
|
182
|
+
return jsi::Value::undefined();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (originalHandler->isObject() &&
|
|
186
|
+
originalHandler->asObject(rt).isFunction(rt)) {
|
|
187
|
+
jsi::Function original =
|
|
188
|
+
originalHandler->asObject(rt).asFunction(rt);
|
|
189
|
+
original.call(rt, args, count);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return jsi::Value::undefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Set the new global error handler
|
|
196
|
+
jsi::Function setHandler =
|
|
197
|
+
errorUtils.getProperty(*runtime, "setGlobalHandler")
|
|
198
|
+
.asObject(*runtime).asFunction(*runtime);
|
|
199
|
+
setHandler.call(*runtime, {std::move(handlerFunc)});
|
|
200
|
+
|
|
201
|
+
// Disable further setGlobalHandler from background JS
|
|
202
|
+
stubJsiFunction(*runtime, errorUtils, "setGlobalHandler");
|
|
203
|
+
|
|
204
|
+
LOGI("Error handler installed in background runtime");
|
|
205
|
+
} catch (const jsi::JSError &e) {
|
|
206
|
+
LOGE("JSError setting up error handler: %s", e.getMessage().c_str());
|
|
207
|
+
} catch (const std::exception &e) {
|
|
208
|
+
LOGE("Error setting up error handler: %s", e.what());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── nativeDestroy ───────────────────────────────────────────────────────
|
|
213
|
+
// Clean up native resources.
|
|
214
|
+
// Called from BackgroundThreadManager.destroy().
|
|
215
|
+
extern "C" JNIEXPORT void JNICALL
|
|
216
|
+
Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy(
|
|
217
|
+
JNIEnv *env, jobject thiz) {
|
|
218
|
+
|
|
219
|
+
SharedRPC::reset();
|
|
220
|
+
|
|
221
|
+
LOGI("Native resources cleaned up");
|
|
222
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.backgroundthread
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight logging wrapper that dynamically dispatches to OneKeyLog.
|
|
5
|
+
* Uses reflection to avoid a hard dependency on the native-logger module.
|
|
6
|
+
* Falls back to android.util.Log when OneKeyLog is not available.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors iOS BTLogger.
|
|
9
|
+
*/
|
|
10
|
+
object BTLogger {
|
|
11
|
+
private const val TAG = "BackgroundThread"
|
|
12
|
+
|
|
13
|
+
private val logClass: Class<*>? by lazy {
|
|
14
|
+
try {
|
|
15
|
+
Class.forName("com.margelo.nitro.nativelogger.OneKeyLog")
|
|
16
|
+
} catch (_: ClassNotFoundException) {
|
|
17
|
+
null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private val methods by lazy {
|
|
22
|
+
val cls = logClass ?: return@lazy null
|
|
23
|
+
mapOf(
|
|
24
|
+
"debug" to cls.getMethod("debug", String::class.java, String::class.java),
|
|
25
|
+
"info" to cls.getMethod("info", String::class.java, String::class.java),
|
|
26
|
+
"warn" to cls.getMethod("warn", String::class.java, String::class.java),
|
|
27
|
+
"error" to cls.getMethod("error", String::class.java, String::class.java),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@JvmStatic
|
|
32
|
+
fun debug(message: String) = log("debug", message, android.util.Log.DEBUG)
|
|
33
|
+
|
|
34
|
+
@JvmStatic
|
|
35
|
+
fun info(message: String) = log("info", message, android.util.Log.INFO)
|
|
36
|
+
|
|
37
|
+
@JvmStatic
|
|
38
|
+
fun warn(message: String) = log("warn", message, android.util.Log.WARN)
|
|
39
|
+
|
|
40
|
+
@JvmStatic
|
|
41
|
+
fun error(message: String) = log("error", message, android.util.Log.ERROR)
|
|
42
|
+
|
|
43
|
+
private fun log(level: String, message: String, androidLogLevel: Int) {
|
|
44
|
+
val method = methods?.get(level)
|
|
45
|
+
if (method != null) {
|
|
46
|
+
try {
|
|
47
|
+
method.invoke(null, TAG, message)
|
|
48
|
+
return
|
|
49
|
+
} catch (_: Exception) {
|
|
50
|
+
// Fall through to android.util.Log
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
android.util.Log.println(androidLogLevel, TAG, message)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
package com.backgroundthread
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import com.facebook.react.ReactPackage
|
|
5
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
|
+
import com.facebook.react.ReactInstanceEventListener
|
|
7
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.ReactContext
|
|
10
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
11
|
+
import com.facebook.react.defaults.DefaultComponentsRegistry
|
|
12
|
+
import com.facebook.react.defaults.DefaultReactHostDelegate
|
|
13
|
+
import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
|
|
14
|
+
import com.facebook.react.fabric.ComponentFactory
|
|
15
|
+
import com.facebook.react.runtime.ReactHostImpl
|
|
16
|
+
import com.facebook.react.runtime.hermes.HermesInstance
|
|
17
|
+
import com.facebook.react.shell.MainReactPackage
|
|
18
|
+
import java.io.File
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Singleton manager for the background React Native runtime.
|
|
22
|
+
* Mirrors iOS BackgroundThreadManager.
|
|
23
|
+
*
|
|
24
|
+
* Responsibilities:
|
|
25
|
+
* - Manages background ReactHostImpl lifecycle
|
|
26
|
+
* - Installs SharedBridge into main and background runtimes
|
|
27
|
+
* - Cross-runtime communication via SharedRPC onWrite notifications
|
|
28
|
+
*/
|
|
29
|
+
class BackgroundThreadManager private constructor() {
|
|
30
|
+
|
|
31
|
+
private var bgReactHost: ReactHostImpl? = null
|
|
32
|
+
private var reactPackages: List<ReactPackage> = emptyList()
|
|
33
|
+
|
|
34
|
+
@Volatile
|
|
35
|
+
private var bgRuntimePtr: Long = 0
|
|
36
|
+
|
|
37
|
+
@Volatile
|
|
38
|
+
private var mainRuntimePtr: Long = 0
|
|
39
|
+
private var mainReactContext: ReactApplicationContext? = null
|
|
40
|
+
private var isStarted = false
|
|
41
|
+
|
|
42
|
+
companion object {
|
|
43
|
+
private const val MODULE_NAME = "background"
|
|
44
|
+
|
|
45
|
+
init {
|
|
46
|
+
System.loadLibrary("background_thread")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Volatile
|
|
50
|
+
private var instance: BackgroundThreadManager? = null
|
|
51
|
+
|
|
52
|
+
@JvmStatic
|
|
53
|
+
fun getInstance(): BackgroundThreadManager {
|
|
54
|
+
return instance ?: synchronized(this) {
|
|
55
|
+
instance ?: BackgroundThreadManager().also { instance = it }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── JNI declarations ────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean)
|
|
63
|
+
private external fun nativeSetupErrorHandler(runtimePtr: Long)
|
|
64
|
+
private external fun nativeDestroy()
|
|
65
|
+
private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
|
|
66
|
+
|
|
67
|
+
// ── SharedBridge ────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Install SharedBridge HostObject into the main (UI) runtime.
|
|
71
|
+
* Call this from installSharedBridge().
|
|
72
|
+
*/
|
|
73
|
+
fun setReactPackages(packages: List<ReactPackage>) {
|
|
74
|
+
reactPackages = packages.toList()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun installSharedBridgeInMainRuntime(context: ReactApplicationContext) {
|
|
78
|
+
mainReactContext = context
|
|
79
|
+
context.runOnJSQueueThread {
|
|
80
|
+
try {
|
|
81
|
+
val ptr = context.javaScriptContextHolder?.get() ?: 0L
|
|
82
|
+
if (ptr != 0L) {
|
|
83
|
+
mainRuntimePtr = ptr
|
|
84
|
+
nativeInstallSharedBridge(ptr, true)
|
|
85
|
+
BTLogger.info("SharedBridge installed in main runtime")
|
|
86
|
+
} else {
|
|
87
|
+
BTLogger.warn("Main runtime pointer is 0, cannot install SharedBridge")
|
|
88
|
+
}
|
|
89
|
+
} catch (e: Exception) {
|
|
90
|
+
BTLogger.error("Error installing SharedBridge in main runtime: ${e.message}")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Background runner lifecycle ─────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
private fun isRemoteBundleUrl(entryURL: String): Boolean {
|
|
98
|
+
return entryURL.startsWith("http://") || entryURL.startsWith("https://")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun resolveLocalBundlePath(entryURL: String): String? {
|
|
102
|
+
if (entryURL.startsWith("file://")) {
|
|
103
|
+
return Uri.parse(entryURL).path
|
|
104
|
+
}
|
|
105
|
+
if (entryURL.startsWith("/")) {
|
|
106
|
+
return entryURL
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun createDownloadedBundleLoader(appContext: android.content.Context, entryURL: String): JSBundleLoader {
|
|
112
|
+
return object : JSBundleLoader() {
|
|
113
|
+
override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
|
|
114
|
+
val tempFile = File(appContext.cacheDir, "background.bundle")
|
|
115
|
+
try {
|
|
116
|
+
java.net.URL(entryURL).openStream().use { input ->
|
|
117
|
+
tempFile.outputStream().use { output ->
|
|
118
|
+
input.copyTo(output)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
BTLogger.info("Background bundle downloaded to ${tempFile.absolutePath}")
|
|
122
|
+
} catch (e: Exception) {
|
|
123
|
+
BTLogger.error("Failed to download background bundle: ${e.message}")
|
|
124
|
+
throw RuntimeException("Failed to download background bundle from $entryURL", e)
|
|
125
|
+
}
|
|
126
|
+
delegate.loadScriptFromFile(tempFile.absolutePath, entryURL, false)
|
|
127
|
+
return entryURL
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun createLocalFileBundleLoader(localPath: String, sourceURL: String): JSBundleLoader {
|
|
133
|
+
return object : JSBundleLoader() {
|
|
134
|
+
override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
|
|
135
|
+
val bundleFile = File(localPath)
|
|
136
|
+
if (!bundleFile.exists()) {
|
|
137
|
+
BTLogger.error("Background bundle file does not exist: $localPath")
|
|
138
|
+
throw RuntimeException("Background bundle file does not exist: $localPath")
|
|
139
|
+
}
|
|
140
|
+
delegate.loadScriptFromFile(bundleFile.absolutePath, sourceURL, false)
|
|
141
|
+
return sourceURL
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
147
|
+
fun startBackgroundRunnerWithEntryURL(context: ReactApplicationContext, entryURL: String) {
|
|
148
|
+
if (isStarted) {
|
|
149
|
+
BTLogger.warn("Background runner already started")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
BTLogger.info("Starting background runner with entryURL: $entryURL")
|
|
153
|
+
|
|
154
|
+
val appContext = context.applicationContext
|
|
155
|
+
val packages =
|
|
156
|
+
if (reactPackages.isNotEmpty()) {
|
|
157
|
+
reactPackages
|
|
158
|
+
} else {
|
|
159
|
+
BTLogger.warn("No ReactPackages registered for background runtime; call setReactPackages(...) from host before start. Falling back to MainReactPackage only.")
|
|
160
|
+
listOf(MainReactPackage())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
val localBundlePath = resolveLocalBundlePath(entryURL)
|
|
164
|
+
val bundleLoader =
|
|
165
|
+
when {
|
|
166
|
+
isRemoteBundleUrl(entryURL) -> createDownloadedBundleLoader(appContext, entryURL)
|
|
167
|
+
localBundlePath != null -> createLocalFileBundleLoader(localBundlePath, entryURL)
|
|
168
|
+
entryURL.startsWith("assets://") -> JSBundleLoader.createAssetLoader(appContext, entryURL, true)
|
|
169
|
+
else -> JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
val delegate = DefaultReactHostDelegate(
|
|
173
|
+
jsMainModulePath = MODULE_NAME,
|
|
174
|
+
jsBundleLoader = bundleLoader,
|
|
175
|
+
reactPackages = packages,
|
|
176
|
+
jsRuntimeFactory = HermesInstance(),
|
|
177
|
+
turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
val componentFactory = ComponentFactory()
|
|
181
|
+
DefaultComponentsRegistry.register(componentFactory)
|
|
182
|
+
|
|
183
|
+
val host = ReactHostImpl(
|
|
184
|
+
appContext,
|
|
185
|
+
delegate,
|
|
186
|
+
componentFactory,
|
|
187
|
+
true, /* allowPackagerServerAccess */
|
|
188
|
+
false, /* useDevSupport */
|
|
189
|
+
)
|
|
190
|
+
bgReactHost = host
|
|
191
|
+
|
|
192
|
+
host.addReactInstanceEventListener(object : ReactInstanceEventListener {
|
|
193
|
+
override fun onReactContextInitialized(context: ReactContext) {
|
|
194
|
+
BTLogger.info("Background ReactContext initialized")
|
|
195
|
+
context.runOnJSQueueThread {
|
|
196
|
+
try {
|
|
197
|
+
val ptr = context.javaScriptContextHolder?.get() ?: 0L
|
|
198
|
+
if (ptr != 0L) {
|
|
199
|
+
bgRuntimePtr = ptr
|
|
200
|
+
nativeInstallSharedBridge(ptr, false)
|
|
201
|
+
nativeSetupErrorHandler(ptr)
|
|
202
|
+
BTLogger.info("SharedBridge and error handler installed in background runtime")
|
|
203
|
+
} else {
|
|
204
|
+
BTLogger.error("Background runtime pointer is 0")
|
|
205
|
+
}
|
|
206
|
+
} catch (e: Exception) {
|
|
207
|
+
BTLogger.error("Error installing bindings in background runtime: ${e.message}")
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
host.start()
|
|
214
|
+
isStarted = true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Called from C++ RuntimeExecutor to schedule work on the correct JS thread.
|
|
219
|
+
* Routes to main or background runtime's JS queue thread, then calls nativeExecuteWork.
|
|
220
|
+
*/
|
|
221
|
+
@DoNotStrip
|
|
222
|
+
fun scheduleOnJSThread(isMain: Boolean, workId: Long) {
|
|
223
|
+
val context = if (isMain) mainReactContext else bgReactHost?.currentReactContext
|
|
224
|
+
BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}")
|
|
225
|
+
if (context == null) {
|
|
226
|
+
BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}")
|
|
227
|
+
}
|
|
228
|
+
context?.runOnJSQueueThread {
|
|
229
|
+
// Re-read ptr inside the block — if a reload happened between
|
|
230
|
+
// scheduling and execution, the old ptr may be stale.
|
|
231
|
+
val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr
|
|
232
|
+
BTLogger.info("scheduleOnJSThread runOnJSQueueThread: isMain=$isMain, workId=$workId, ptr=$ptr")
|
|
233
|
+
if (ptr != 0L) {
|
|
234
|
+
try {
|
|
235
|
+
nativeExecuteWork(ptr, workId)
|
|
236
|
+
} catch (e: Exception) {
|
|
237
|
+
BTLogger.error("Error executing work on JS thread: ${e.message}")
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
BTLogger.error("scheduleOnJSThread: ptr is 0! isMain=$isMain")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
val isBackgroundStarted: Boolean get() = isStarted
|
|
248
|
+
|
|
249
|
+
fun destroy() {
|
|
250
|
+
nativeDestroy()
|
|
251
|
+
bgRuntimePtr = 0
|
|
252
|
+
mainRuntimePtr = 0
|
|
253
|
+
mainReactContext = null
|
|
254
|
+
bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
|
|
255
|
+
bgReactHost = null
|
|
256
|
+
isStarted = false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -3,27 +3,27 @@ package com.backgroundthread
|
|
|
3
3
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
4
|
import com.facebook.react.module.annotations.ReactModule
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* TurboModule entry point for BackgroundThread.
|
|
8
|
+
* Delegates all heavy lifting to [BackgroundThreadManager] singleton.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors iOS BackgroundThread.mm.
|
|
11
|
+
*/
|
|
6
12
|
@ReactModule(name = BackgroundThreadModule.NAME)
|
|
7
13
|
class BackgroundThreadModule(reactContext: ReactApplicationContext) :
|
|
8
|
-
|
|
14
|
+
NativeBackgroundThreadSpec(reactContext) {
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
companion object {
|
|
17
|
+
const val NAME = "BackgroundThread"
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
// TODO: Implement initBackgroundThread
|
|
16
|
-
}
|
|
20
|
+
override fun getName(): String = NAME
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
override fun installSharedBridge() {
|
|
23
|
+
BackgroundThreadManager.getInstance().installSharedBridgeInMainRuntime(reactApplicationContext)
|
|
24
|
+
}
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
companion object {
|
|
27
|
-
const val NAME = "BackgroundThread"
|
|
28
|
-
}
|
|
26
|
+
override fun startBackgroundRunnerWithEntryURL(entryURL: String) {
|
|
27
|
+
BackgroundThreadManager.getInstance().startBackgroundRunnerWithEntryURL(reactApplicationContext, entryURL)
|
|
28
|
+
}
|
|
29
29
|
}
|