@dr33m/react-native-litert-lm 0.5.0
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/LICENSE +21 -0
- package/README.md +627 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLM.kt +888 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/HybridModelStore.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/LiteRTLMRegistry.kt +32 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt +15 -0
- package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt +89 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMInitProvider.kt +57 -0
- package/android/src/main/java/dev/litert/litertlm/LiteRTLMPackage.kt +43 -0
- package/android/src/test/java/com/margelo/nitro/core/Promise.kt +46 -0
- package/android/src/test/java/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMTest.kt +122 -0
- package/app.plugin.js +58 -0
- package/cpp/cpp-adapter.cpp +21 -0
- package/cpp/include/README.md +32 -0
- package/cpp/include/litert_lm_engine.h +516 -0
- package/cpp/include/stb_image.h +7988 -0
- package/ios/HybridLiteRTLM+Execute.swift +287 -0
- package/ios/HybridLiteRTLM+Streaming.swift +153 -0
- package/ios/HybridLiteRTLM.swift +541 -0
- package/ios/HybridModelStore.swift +206 -0
- package/ios/MultimodalPart+Factories.swift +22 -0
- package/ios/Tests/HybridLiteRTLMTests.swift +187 -0
- package/lib/__mocks__/react-native-nitro-modules.d.ts +81 -0
- package/lib/__mocks__/react-native-nitro-modules.js +98 -0
- package/lib/__tests__/hooks.test.d.ts +1 -0
- package/lib/__tests__/hooks.test.js +175 -0
- package/lib/__tests__/inferenceRouting.test.d.ts +1 -0
- package/lib/__tests__/inferenceRouting.test.js +28 -0
- package/lib/__tests__/memoryTracker.test.d.ts +1 -0
- package/lib/__tests__/memoryTracker.test.js +74 -0
- package/lib/__tests__/modelFactory.test.d.ts +1 -0
- package/lib/__tests__/modelFactory.test.js +107 -0
- package/lib/__tests__/modelPath.test.d.ts +1 -0
- package/lib/__tests__/modelPath.test.js +12 -0
- package/lib/__tests__/modelRegistry.test.d.ts +1 -0
- package/lib/__tests__/modelRegistry.test.js +61 -0
- package/lib/hooks.d.ts +46 -0
- package/lib/hooks.js +159 -0
- package/lib/index.d.ts +142 -0
- package/lib/index.js +184 -0
- package/lib/inferenceRouting.d.ts +18 -0
- package/lib/inferenceRouting.js +63 -0
- package/lib/memoryTracker.d.ts +128 -0
- package/lib/memoryTracker.js +155 -0
- package/lib/modelFactory.d.ts +24 -0
- package/lib/modelFactory.js +128 -0
- package/lib/modelPath.d.ts +8 -0
- package/lib/modelPath.js +18 -0
- package/lib/modelRegistry.d.ts +55 -0
- package/lib/modelRegistry.js +82 -0
- package/lib/specs/LiteRTLM.nitro.d.ts +380 -0
- package/lib/specs/LiteRTLM.nitro.js +2 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.cmake +83 -0
- package/nitrogen/generated/android/LiteRTLM+autolinking.gradle +27 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.cpp +74 -0
- package/nitrogen/generated/android/LiteRTLMOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JBackend.hpp +61 -0
- package/nitrogen/generated/android/c++/JExecuteResult.hpp +86 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_bool.hpp +76 -0
- package/nitrogen/generated/android/c++/JGenerationStats.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.cpp +371 -0
- package/nitrogen/generated/android/c++/JHybridLiteRTLMSpec.hpp +83 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.cpp +101 -0
- package/nitrogen/generated/android/c++/JHybridModelStoreSpec.hpp +67 -0
- package/nitrogen/generated/android/c++/JLLMConfig.hpp +125 -0
- package/nitrogen/generated/android/c++/JMemoryUsage.hpp +69 -0
- package/nitrogen/generated/android/c++/JMessage.hpp +63 -0
- package/nitrogen/generated/android/c++/JModelFile.hpp +69 -0
- package/nitrogen/generated/android/c++/JMultimodalPart.hpp +78 -0
- package/nitrogen/generated/android/c++/JPartType.hpp +61 -0
- package/nitrogen/generated/android/c++/JRole.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolCall.hpp +61 -0
- package/nitrogen/generated/android/c++/JToolDefinition.hpp +65 -0
- package/nitrogen/generated/android/c++/JToolResponse.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Backend.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ExecuteResult.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Func_void_std__string_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/GenerationStats.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridLiteRTLMSpec.kt +165 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/HybridModelStoreSpec.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LLMConfig.kt +106 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/LiteRTLMOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MemoryUsage.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Message.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ModelFile.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/MultimodalPart.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/PartType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/Role.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolCall.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolDefinition.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dev/litert/litertlm/ToolResponse.kt +56 -0
- package/nitrogen/generated/ios/LiteRTLM+autolinking.rb +62 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.cpp +98 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Bridge.hpp +585 -0
- package/nitrogen/generated/ios/LiteRTLM-Swift-Cxx-Umbrella.hpp +93 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.mm +41 -0
- package/nitrogen/generated/ios/LiteRTLMAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLiteRTLMSpecSwift.hpp +279 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridModelStoreSpecSwift.hpp +117 -0
- package/nitrogen/generated/ios/swift/Backend.swift +44 -0
- package/nitrogen/generated/ios/swift/ExecuteResult.swift +45 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExecuteResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/GenerationStats.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec.swift +75 -0
- package/nitrogen/generated/ios/swift/HybridLiteRTLMSpec_cxx.swift +516 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridModelStoreSpec_cxx.swift +203 -0
- package/nitrogen/generated/ios/swift/LLMConfig.swift +239 -0
- package/nitrogen/generated/ios/swift/MemoryUsage.swift +44 -0
- package/nitrogen/generated/ios/swift/Message.swift +34 -0
- package/nitrogen/generated/ios/swift/ModelFile.swift +44 -0
- package/nitrogen/generated/ios/swift/MultimodalPart.swift +101 -0
- package/nitrogen/generated/ios/swift/PartType.swift +44 -0
- package/nitrogen/generated/ios/swift/Role.swift +44 -0
- package/nitrogen/generated/ios/swift/ToolCall.swift +34 -0
- package/nitrogen/generated/ios/swift/ToolDefinition.swift +39 -0
- package/nitrogen/generated/ios/swift/ToolResponse.swift +34 -0
- package/nitrogen/generated/shared/c++/Backend.hpp +80 -0
- package/nitrogen/generated/shared/c++/ExecuteResult.hpp +94 -0
- package/nitrogen/generated/shared/c++/GenerationStats.hpp +103 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.cpp +41 -0
- package/nitrogen/generated/shared/c++/HybridLiteRTLMSpec.hpp +109 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridModelStoreSpec.hpp +71 -0
- package/nitrogen/generated/shared/c++/LLMConfig.hpp +134 -0
- package/nitrogen/generated/shared/c++/MemoryUsage.hpp +95 -0
- package/nitrogen/generated/shared/c++/Message.hpp +89 -0
- package/nitrogen/generated/shared/c++/ModelFile.hpp +95 -0
- package/nitrogen/generated/shared/c++/MultimodalPart.hpp +103 -0
- package/nitrogen/generated/shared/c++/PartType.hpp +80 -0
- package/nitrogen/generated/shared/c++/Role.hpp +80 -0
- package/nitrogen/generated/shared/c++/ToolCall.hpp +87 -0
- package/nitrogen/generated/shared/c++/ToolDefinition.hpp +91 -0
- package/nitrogen/generated/shared/c++/ToolResponse.hpp +87 -0
- package/package.json +115 -0
- package/react-native-litert-lm.podspec +62 -0
- package/react-native.config.js +16 -0
- package/scripts/download-ios-frameworks.sh +39 -0
- package/scripts/framework-source.js +46 -0
- package/scripts/postinstall.js +145 -0
- package/src/__mocks__/react-native-nitro-modules.ts +132 -0
- package/src/__tests__/hooks.test.ts +216 -0
- package/src/__tests__/inferenceRouting.test.ts +36 -0
- package/src/__tests__/memoryTracker.test.ts +87 -0
- package/src/__tests__/modelFactory.test.ts +153 -0
- package/src/__tests__/modelPath.test.ts +14 -0
- package/src/__tests__/modelRegistry.test.ts +73 -0
- package/src/hooks.ts +234 -0
- package/src/index.ts +214 -0
- package/src/inferenceRouting.ts +80 -0
- package/src/memoryTracker.ts +268 -0
- package/src/modelFactory.ts +163 -0
- package/src/modelPath.ts +16 -0
- package/src/modelRegistry.ts +104 -0
- package/src/specs/LiteRTLM.nitro.ts +451 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.annotation.Keep
|
|
5
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
|
+
import com.margelo.nitro.core.Promise
|
|
7
|
+
import dev.litert.litertlm.LiteRTLMInitProvider
|
|
8
|
+
import java.io.File
|
|
9
|
+
import java.io.FileOutputStream
|
|
10
|
+
import java.net.HttpURLConnection
|
|
11
|
+
import java.net.URL
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
@DoNotStrip
|
|
15
|
+
@Keep
|
|
16
|
+
class HybridModelStore : HybridModelStoreSpec() {
|
|
17
|
+
|
|
18
|
+
private val tag = "HybridModelStore"
|
|
19
|
+
|
|
20
|
+
private val modelsDirectory: File
|
|
21
|
+
get() {
|
|
22
|
+
val context = LiteRTLMInitProvider.applicationContext
|
|
23
|
+
?: throw RuntimeException("Android Application Context is not available")
|
|
24
|
+
val modelsDir = File(context.filesDir, "models")
|
|
25
|
+
if (!modelsDir.exists()) {
|
|
26
|
+
modelsDir.mkdirs()
|
|
27
|
+
}
|
|
28
|
+
return modelsDir
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun sanitizeFileName(fileName: String) {
|
|
32
|
+
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
|
33
|
+
throw IllegalArgumentException("Invalid filename: path traversal or directory separators are not allowed.")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun isCached(fileName: String): Boolean {
|
|
38
|
+
sanitizeFileName(fileName)
|
|
39
|
+
val file = File(modelsDirectory, fileName)
|
|
40
|
+
return file.exists() && file.length() > 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override fun getFilePath(fileName: String): String {
|
|
44
|
+
sanitizeFileName(fileName)
|
|
45
|
+
return File(modelsDirectory, fileName).absolutePath
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun listCachedFiles(): Array<ModelFile> {
|
|
49
|
+
return try {
|
|
50
|
+
val dir = modelsDirectory
|
|
51
|
+
val files = dir.listFiles() ?: return emptyArray()
|
|
52
|
+
val result = mutableListOf<ModelFile>()
|
|
53
|
+
for (f in files) {
|
|
54
|
+
if (f.isFile && !f.name.endsWith(".tmp")) {
|
|
55
|
+
result.add(
|
|
56
|
+
ModelFile(
|
|
57
|
+
fileName = f.name,
|
|
58
|
+
absolutePath = f.absolutePath,
|
|
59
|
+
sizeBytes = f.length().toDouble(),
|
|
60
|
+
lastModifiedMs = f.lastModified().toDouble()
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
result.toTypedArray()
|
|
66
|
+
} catch (e: Exception) {
|
|
67
|
+
Log.e(tag, "Failed to list cached files", e)
|
|
68
|
+
emptyArray()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun deleteFile(fileName: String) {
|
|
73
|
+
sanitizeFileName(fileName)
|
|
74
|
+
val file = File(modelsDirectory, fileName)
|
|
75
|
+
if (file.exists()) {
|
|
76
|
+
val deleted = file.delete()
|
|
77
|
+
if (!deleted) {
|
|
78
|
+
throw RuntimeException("Failed to delete model file: ${file.absolutePath}")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun downloadFile(
|
|
84
|
+
url: String,
|
|
85
|
+
fileName: String,
|
|
86
|
+
headersJson: String,
|
|
87
|
+
onProgress: (progress: Double) -> Unit
|
|
88
|
+
): Promise<String> {
|
|
89
|
+
return Promise.parallel {
|
|
90
|
+
Log.i(tag, "downloadFile: $url -> $fileName")
|
|
91
|
+
sanitizeFileName(fileName)
|
|
92
|
+
|
|
93
|
+
if (!url.startsWith("https://", ignoreCase = true)) {
|
|
94
|
+
throw IllegalArgumentException("Invalid download URL: HTTPS is required for security.")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
val dir = modelsDirectory
|
|
98
|
+
val modelFile = File(dir, fileName)
|
|
99
|
+
val tempFile = File(dir, "$fileName.tmp")
|
|
100
|
+
|
|
101
|
+
// Fast cache check
|
|
102
|
+
if (modelFile.exists() && modelFile.length() > 0) {
|
|
103
|
+
Log.i(tag, "Model already exists: ${modelFile.absolutePath}")
|
|
104
|
+
onProgress(1.0)
|
|
105
|
+
return@parallel modelFile.absolutePath
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Log.i(tag, "Downloading model to temp file: ${tempFile.absolutePath}")
|
|
109
|
+
onProgress(0.0)
|
|
110
|
+
|
|
111
|
+
// Parse headers
|
|
112
|
+
val headersMap = mutableMapOf<String, String>()
|
|
113
|
+
if (headersJson.isNotEmpty()) {
|
|
114
|
+
try {
|
|
115
|
+
val json = JSONObject(headersJson)
|
|
116
|
+
val keys = json.keys()
|
|
117
|
+
while (keys.hasNext()) {
|
|
118
|
+
val key = keys.next()
|
|
119
|
+
headersMap[key] = json.getString(key)
|
|
120
|
+
}
|
|
121
|
+
} catch (e: Exception) {
|
|
122
|
+
Log.e(tag, "Failed to parse custom headers JSON", e)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
val connection = URL(url).openConnection() as HttpURLConnection
|
|
128
|
+
connection.connectTimeout = 15000 // 15s
|
|
129
|
+
connection.readTimeout = 0 // Infinite read timeout for large files
|
|
130
|
+
connection.doInput = true
|
|
131
|
+
|
|
132
|
+
// Apply headers
|
|
133
|
+
for ((key, value) in headersMap) {
|
|
134
|
+
connection.setRequestProperty(key, value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
connection.connect()
|
|
138
|
+
|
|
139
|
+
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
|
140
|
+
throw RuntimeException("Failed to download model: HTTP ${connection.responseCode}")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
val contentLength = connection.contentLengthLong
|
|
144
|
+
val input = connection.inputStream
|
|
145
|
+
val output = FileOutputStream(tempFile)
|
|
146
|
+
|
|
147
|
+
val buffer = ByteArray(8 * 1024)
|
|
148
|
+
var bytesRead: Int
|
|
149
|
+
var totalBytesRead = 0L
|
|
150
|
+
var lastProgressUpdate = 0L
|
|
151
|
+
|
|
152
|
+
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
153
|
+
output.write(buffer, 0, bytesRead)
|
|
154
|
+
totalBytesRead += bytesRead
|
|
155
|
+
|
|
156
|
+
if (contentLength > 0) {
|
|
157
|
+
val currentTime = System.currentTimeMillis()
|
|
158
|
+
// Update progress roughly every 100ms to prevent bridge flooding
|
|
159
|
+
if (currentTime - lastProgressUpdate > 100) {
|
|
160
|
+
val progress = totalBytesRead.toDouble() / contentLength.toDouble()
|
|
161
|
+
onProgress(progress)
|
|
162
|
+
lastProgressUpdate = currentTime
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output.flush()
|
|
168
|
+
output.close()
|
|
169
|
+
input.close()
|
|
170
|
+
connection.disconnect()
|
|
171
|
+
|
|
172
|
+
// Atomic rename
|
|
173
|
+
if (tempFile.renameTo(modelFile)) {
|
|
174
|
+
Log.i(tag, "Download complete and verified at: ${modelFile.absolutePath}")
|
|
175
|
+
onProgress(1.0)
|
|
176
|
+
modelFile.absolutePath
|
|
177
|
+
} else {
|
|
178
|
+
throw RuntimeException("Failed to rename temporary file to model file")
|
|
179
|
+
}
|
|
180
|
+
} catch (e: Exception) {
|
|
181
|
+
Log.e(tag, "Download failed", e)
|
|
182
|
+
if (tempFile.exists()) {
|
|
183
|
+
tempFile.delete()
|
|
184
|
+
}
|
|
185
|
+
throw RuntimeException("Download failed: ${e.message}", e)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import java.util.Collections
|
|
4
|
+
import java.util.WeakHashMap
|
|
5
|
+
import android.util.Log
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Global registry to track active LiteRTLM instances.
|
|
9
|
+
* Used for memory trimming and cleanup.
|
|
10
|
+
*/
|
|
11
|
+
object LiteRTLMRegistry {
|
|
12
|
+
private const val TAG = "LiteRTLMRegistry"
|
|
13
|
+
|
|
14
|
+
// Use WeakSet-like structure to prevent leaks
|
|
15
|
+
private val instances = Collections.newSetFromMap(WeakHashMap<HybridLiteRTLM, Boolean>())
|
|
16
|
+
|
|
17
|
+
fun register(instance: HybridLiteRTLM) {
|
|
18
|
+
synchronized(instances) {
|
|
19
|
+
instances.add(instance)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fun onTrimMemory(level: Int) {
|
|
24
|
+
Log.w(TAG, "Received memory warning (level=$level). Releasing resources...")
|
|
25
|
+
synchronized(instances) {
|
|
26
|
+
instances.forEach { it.close() }
|
|
27
|
+
// Note: We don't clear the set here, as close() should be idempotent
|
|
28
|
+
// and the instance might still be ref-counted by JS.
|
|
29
|
+
// We just ensure the HEAVY native resources are gone.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/MultimodalPartFactories.kt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors [src/inferenceRouting.ts] — keep shapes in sync for native direct hybrid access.
|
|
5
|
+
*/
|
|
6
|
+
object MultimodalPartFactories {
|
|
7
|
+
fun textPart(text: String): MultimodalPart =
|
|
8
|
+
MultimodalPart(type = PartType.TEXT, text = text, path = null, imageBuffer = null, audioBuffer = null)
|
|
9
|
+
|
|
10
|
+
fun imagePart(path: String): MultimodalPart =
|
|
11
|
+
MultimodalPart(type = PartType.IMAGE, text = null, path = path, imageBuffer = null, audioBuffer = null)
|
|
12
|
+
|
|
13
|
+
fun audioPart(path: String): MultimodalPart =
|
|
14
|
+
MultimodalPart(type = PartType.AUDIO, text = null, path = path, imageBuffer = null, audioBuffer = null)
|
|
15
|
+
}
|
package/android/src/main/java/com/margelo/nitro/dev/litert/litertlm/StreamingCallbackListener.kt
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.google.ai.edge.litertlm.Content
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Named implementation of the LiteRT-LM MessageCallback for streaming inference.
|
|
8
|
+
*
|
|
9
|
+
* Accumulates response chunks, forwards tokens to JS, and appends the final
|
|
10
|
+
* response to the conversation history.
|
|
11
|
+
*/
|
|
12
|
+
internal class StreamingCallbackListener(
|
|
13
|
+
private val onToken: (String, Boolean) -> Unit,
|
|
14
|
+
private val responseBuilder: StringBuilder,
|
|
15
|
+
private val thinkingBuilder: StringBuilder = StringBuilder(),
|
|
16
|
+
private val history: MutableList<Message>,
|
|
17
|
+
private val userMessage: String,
|
|
18
|
+
private val onStatsReady: (GenerationStats) -> Unit,
|
|
19
|
+
private val onFailure: ((Throwable) -> Unit)? = null,
|
|
20
|
+
private val onThinkingToken: ((String) -> Unit)? = null,
|
|
21
|
+
) : com.google.ai.edge.litertlm.MessageCallback {
|
|
22
|
+
|
|
23
|
+
private val startTime = System.nanoTime()
|
|
24
|
+
private var firstTokenTime = 0L
|
|
25
|
+
private var tokenCount = 0
|
|
26
|
+
|
|
27
|
+
override fun onMessage(message: com.google.ai.edge.litertlm.Message) {
|
|
28
|
+
val chunk = message.contents.contents
|
|
29
|
+
.filterIsInstance<Content.Text>()
|
|
30
|
+
.joinToString("") { it.text }
|
|
31
|
+
|
|
32
|
+
// Capture thinking from the "thought" channel
|
|
33
|
+
val thinkingChunk = message.channels["thought"]
|
|
34
|
+
if (!thinkingChunk.isNullOrEmpty()) {
|
|
35
|
+
thinkingBuilder.append(thinkingChunk)
|
|
36
|
+
onThinkingToken?.invoke(thinkingChunk)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (firstTokenTime == 0L && chunk.isNotEmpty()) {
|
|
40
|
+
firstTokenTime = System.nanoTime()
|
|
41
|
+
}
|
|
42
|
+
if (chunk.isNotEmpty()) {
|
|
43
|
+
tokenCount++
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onToken(chunk, false)
|
|
47
|
+
|
|
48
|
+
if (chunk.isNotEmpty()) {
|
|
49
|
+
responseBuilder.append(chunk)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onDone() {
|
|
54
|
+
val fullResponse = responseBuilder.toString()
|
|
55
|
+
history.add(Message(Role.MODEL, fullResponse))
|
|
56
|
+
|
|
57
|
+
// Compute stats using heuristic token counts (~4 chars/token)
|
|
58
|
+
val elapsedMs = (System.nanoTime() - startTime) / 1_000_000.0
|
|
59
|
+
val ttftMs = if (firstTokenTime > 0) (firstTokenTime - startTime) / 1_000_000.0 else 0.0
|
|
60
|
+
val promptTokens = userMessage.length / 4.0
|
|
61
|
+
val completionTokens = fullResponse.length / 4.0
|
|
62
|
+
onStatsReady(GenerationStats(
|
|
63
|
+
promptTokens = promptTokens,
|
|
64
|
+
completionTokens = completionTokens,
|
|
65
|
+
totalTokens = promptTokens + completionTokens,
|
|
66
|
+
timeToFirstToken = ttftMs,
|
|
67
|
+
totalTime = elapsedMs,
|
|
68
|
+
tokensPerSecond = if (elapsedMs > 0) completionTokens / (elapsedMs / 1000.0) else 0.0
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
Log.d("StreamingCallbackListener", "Streaming done. Length: ${fullResponse.length}, TTFT: ${ttftMs.toLong()}ms, Total: ${elapsedMs.toLong()}ms")
|
|
72
|
+
|
|
73
|
+
// Notify JS that streaming is done AFTER updating history and stats
|
|
74
|
+
onToken("", true)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onError(throwable: Throwable) {
|
|
78
|
+
if (throwable is kotlinx.coroutines.CancellationException ||
|
|
79
|
+
throwable.message?.contains("cancel", ignoreCase = true) == true) {
|
|
80
|
+
// Graceful cancellation via stopGeneration() — treat as done with partial content
|
|
81
|
+
Log.i("StreamingCallbackListener", "Inference cancelled, emitting partial result")
|
|
82
|
+
onDone()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
Log.e("StreamingCallbackListener", "Async generation failed", throwable)
|
|
86
|
+
onToken("Error: ${throwable.message}", true)
|
|
87
|
+
onFailure?.invoke(throwable)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import android.content.ContentProvider
|
|
4
|
+
import android.content.ContentValues
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.database.Cursor
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.util.Log
|
|
9
|
+
|
|
10
|
+
class LiteRTLMInitProvider : ContentProvider() {
|
|
11
|
+
companion object {
|
|
12
|
+
private const val TAG = "LiteRTLMInitProvider"
|
|
13
|
+
var applicationContext: Context? = null
|
|
14
|
+
private set
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override fun onCreate(): Boolean {
|
|
18
|
+
applicationContext = context?.applicationContext
|
|
19
|
+
Log.i(TAG, "LiteRTLMInitProvider initialized with context: $applicationContext")
|
|
20
|
+
|
|
21
|
+
applicationContext?.registerComponentCallbacks(object : android.content.ComponentCallbacks2 {
|
|
22
|
+
override fun onTrimMemory(level: Int) {
|
|
23
|
+
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
|
24
|
+
com.margelo.nitro.dev.litert.litertlm.LiteRTLMRegistry.onTrimMemory(level)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {}
|
|
29
|
+
override fun onLowMemory() {
|
|
30
|
+
com.margelo.nitro.dev.litert.litertlm.LiteRTLMRegistry.onTrimMemory(android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun query(
|
|
38
|
+
uri: Uri,
|
|
39
|
+
projection: Array<out String>?,
|
|
40
|
+
selection: String?,
|
|
41
|
+
selectionArgs: Array<out String>?,
|
|
42
|
+
sortOrder: String?
|
|
43
|
+
): Cursor? = null
|
|
44
|
+
|
|
45
|
+
override fun getType(uri: Uri): String? = null
|
|
46
|
+
|
|
47
|
+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
|
48
|
+
|
|
49
|
+
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
|
50
|
+
|
|
51
|
+
override fun update(
|
|
52
|
+
uri: Uri,
|
|
53
|
+
values: ContentValues?,
|
|
54
|
+
selection: String?,
|
|
55
|
+
selectionArgs: Array<out String>?
|
|
56
|
+
): Int = 0
|
|
57
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.facebook.react.TurboReactPackage
|
|
6
|
+
import com.facebook.react.bridge.NativeModule
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
9
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import com.margelo.nitro.dev.litert.litertlm.LiteRTLMOnLoad
|
|
13
|
+
|
|
14
|
+
class LiteRTLMPackage : TurboReactPackage() {
|
|
15
|
+
companion object {
|
|
16
|
+
private const val TAG = "LiteRTLMPackage"
|
|
17
|
+
|
|
18
|
+
private fun isSupportedPrimaryAbi(): Boolean {
|
|
19
|
+
val primaryAbi = Build.SUPPORTED_64_BIT_ABIS.firstOrNull() ?: return false
|
|
20
|
+
return primaryAbi == "arm64-v8a"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
init {
|
|
24
|
+
if (!isSupportedPrimaryAbi()) {
|
|
25
|
+
Log.w(TAG, "Skipping LiteRTLM native init on unsupported primary ABI: ${Build.SUPPORTED_64_BIT_ABIS.firstOrNull()}")
|
|
26
|
+
} else {
|
|
27
|
+
try {
|
|
28
|
+
LiteRTLMOnLoad.initializeNative()
|
|
29
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
30
|
+
Log.e(TAG, "LiteRTLM native init failed; disabling LiteRTLM for this process.", e)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
41
|
+
return ReactModuleInfoProvider { emptyMap<String, ReactModuleInfo>() }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package com.margelo.nitro.core
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Keep
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
|
|
6
|
+
@Keep
|
|
7
|
+
@DoNotStrip
|
|
8
|
+
class Promise<T> {
|
|
9
|
+
companion object {
|
|
10
|
+
@JvmStatic
|
|
11
|
+
fun <T> parallel(block: () -> T): Promise<T> {
|
|
12
|
+
val promise = Promise<T>()
|
|
13
|
+
try {
|
|
14
|
+
val result = block()
|
|
15
|
+
promise.resolve(result)
|
|
16
|
+
} catch (e: Throwable) {
|
|
17
|
+
promise.reject(e)
|
|
18
|
+
}
|
|
19
|
+
return promise
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var result: T? = null
|
|
24
|
+
private set
|
|
25
|
+
var error: Throwable? = null
|
|
26
|
+
private set
|
|
27
|
+
var isCompleted = false
|
|
28
|
+
private set
|
|
29
|
+
private val callbacks = mutableListOf<(T?, Throwable?) -> Unit>()
|
|
30
|
+
|
|
31
|
+
fun resolve(value: T) {
|
|
32
|
+
synchronized(this) {
|
|
33
|
+
result = value
|
|
34
|
+
isCompleted = true
|
|
35
|
+
callbacks.forEach { it(value, null) }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fun reject(exception: Throwable) {
|
|
40
|
+
synchronized(this) {
|
|
41
|
+
error = exception
|
|
42
|
+
isCompleted = true
|
|
43
|
+
callbacks.forEach { it(null, exception) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
package com.margelo.nitro.dev.litert.litertlm
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Before
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.robolectric.RobolectricTestRunner
|
|
9
|
+
import org.robolectric.RuntimeEnvironment
|
|
10
|
+
import dev.litert.litertlm.LiteRTLMInitProvider
|
|
11
|
+
import java.lang.IllegalArgumentException
|
|
12
|
+
|
|
13
|
+
@RunWith(RobolectricTestRunner::class)
|
|
14
|
+
class HybridLiteRTLMTest {
|
|
15
|
+
private lateinit var bridge: HybridLiteRTLM
|
|
16
|
+
|
|
17
|
+
@Before
|
|
18
|
+
fun setUp() {
|
|
19
|
+
// Initialize the static applicationContext inside LiteRTLMInitProvider via reflection
|
|
20
|
+
try {
|
|
21
|
+
val field = LiteRTLMInitProvider::class.java.getDeclaredField("applicationContext")
|
|
22
|
+
field.isAccessible = true
|
|
23
|
+
field.set(null, RuntimeEnvironment.getApplication())
|
|
24
|
+
} catch (e: Exception) {
|
|
25
|
+
e.printStackTrace()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bridge = HybridLiteRTLM()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@After
|
|
32
|
+
fun tearDown() {
|
|
33
|
+
bridge.close()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun testAndroidPathTraversalPrevention() {
|
|
38
|
+
val traversals = arrayOf("../secret", "/etc/hosts", "nested\\..\\file", "..", "../", "..\\")
|
|
39
|
+
for (traversal in traversals) {
|
|
40
|
+
val promise = bridge.deleteModel(traversal)
|
|
41
|
+
assertNotNull("Promise should not be null", promise)
|
|
42
|
+
assertTrue("Promise should be completed", promise.isCompleted)
|
|
43
|
+
assertNotNull("Promise should have rejected with an error for filename: $traversal", promise.error)
|
|
44
|
+
val error = promise.error!!
|
|
45
|
+
val errMsg = error.message ?: error.cause?.message ?: ""
|
|
46
|
+
assertTrue("Expected message to contain traversal warning, got: $errMsg",
|
|
47
|
+
errMsg.contains("path traversal or directory separators are not allowed"))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun testAndroidHTTPSDownloadEnforcement() {
|
|
53
|
+
val promise = bridge.downloadModel("http://insecure.site/model.bin", "model.bin", null)
|
|
54
|
+
assertNotNull("Promise should not be null", promise)
|
|
55
|
+
assertTrue("Promise should be completed", promise.isCompleted)
|
|
56
|
+
assertNotNull("Promise should have rejected with an error", promise.error)
|
|
57
|
+
val error = promise.error!!
|
|
58
|
+
val errMsg = error.message ?: error.cause?.message ?: ""
|
|
59
|
+
assertTrue("Expected message to contain HTTPS warning, got: $errMsg",
|
|
60
|
+
errMsg.contains("HTTPS is required for security"))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun testAndroidMemoryTelemetry() {
|
|
65
|
+
val mem = bridge.getMemoryUsage()
|
|
66
|
+
assertNotNull(mem)
|
|
67
|
+
assertTrue(mem.nativeHeapBytes >= 0.0)
|
|
68
|
+
assertTrue(mem.residentBytes >= 0.0)
|
|
69
|
+
assertTrue(mem.availableMemoryBytes >= 0.0)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
fun testSendMessageWithImageAsyncRejectsWithoutModel() {
|
|
74
|
+
val promise = bridge.sendMessageWithImageAsync("hello", "/tmp/image.jpg") { _, _ -> }
|
|
75
|
+
assertNotNull("Promise should not be null", promise)
|
|
76
|
+
assertTrue("Promise should be completed", promise.isCompleted)
|
|
77
|
+
assertNotNull("Promise should have rejected without model", promise.error)
|
|
78
|
+
val errMsg = promise.error!!.message ?: promise.error!!.cause?.message ?: ""
|
|
79
|
+
assertTrue("Expected no-model error, got: $errMsg",
|
|
80
|
+
errMsg.contains("No model loaded"))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
fun testSendMessageWithAudioAsyncRejectsWithoutModel() {
|
|
85
|
+
val promise = bridge.sendMessageWithAudioAsync("hello", "/tmp/audio.wav") { _, _ -> }
|
|
86
|
+
assertNotNull("Promise should not be null", promise)
|
|
87
|
+
assertTrue("Promise should be completed", promise.isCompleted)
|
|
88
|
+
assertNotNull("Promise should have rejected without model", promise.error)
|
|
89
|
+
val errMsg = promise.error!!.message ?: promise.error!!.cause?.message ?: ""
|
|
90
|
+
assertTrue("Expected no-model error, got: $errMsg",
|
|
91
|
+
errMsg.contains("No model loaded"))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun testAndroidInitialStats() {
|
|
96
|
+
val stats = bridge.getStats()
|
|
97
|
+
assertNotNull(stats)
|
|
98
|
+
assertEquals(0.0, stats.promptTokens, 0.0)
|
|
99
|
+
assertEquals(0.0, stats.completionTokens, 0.0)
|
|
100
|
+
assertEquals(0.0, stats.totalTokens, 0.0)
|
|
101
|
+
assertEquals(0.0, stats.timeToFirstToken, 0.0)
|
|
102
|
+
assertEquals(0.0, stats.totalTime, 0.0)
|
|
103
|
+
assertEquals(0.0, stats.tokensPerSecond, 0.0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
fun testDeleteModelCleanupLogic() {
|
|
108
|
+
val loadedPathField = HybridLiteRTLM::class.java.getDeclaredField("loadedModelPath")
|
|
109
|
+
loadedPathField.isAccessible = true
|
|
110
|
+
loadedPathField.set(bridge, "/path/to/my_loaded_model.litertlm")
|
|
111
|
+
|
|
112
|
+
val promise1 = bridge.deleteModel("other_model.litertlm")
|
|
113
|
+
assertNotNull(promise1)
|
|
114
|
+
while (!promise1.isCompleted) { Thread.sleep(10) }
|
|
115
|
+
assertEquals("/path/to/my_loaded_model.litertlm", loadedPathField.get(bridge))
|
|
116
|
+
|
|
117
|
+
val promise2 = bridge.deleteModel("my_loaded_model.litertlm")
|
|
118
|
+
assertNotNull(promise2)
|
|
119
|
+
while (!promise2.isCompleted) { Thread.sleep(10) }
|
|
120
|
+
assertNull(loadedPathField.get(bridge))
|
|
121
|
+
}
|
|
122
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expo config plugin for react-native-litert-lm.
|
|
3
|
+
*
|
|
4
|
+
* Ensures correct build settings for the LiteRT-LM native module:
|
|
5
|
+
* - Android: minSdkVersion 26, Kotlin 2.3.0 (required by litertlm-android AAR)
|
|
6
|
+
*/
|
|
7
|
+
const {
|
|
8
|
+
withGradleProperties,
|
|
9
|
+
withProjectBuildGradle,
|
|
10
|
+
} = require('@expo/config-plugins');
|
|
11
|
+
|
|
12
|
+
function withLiteRTLM(config) {
|
|
13
|
+
// Android: Ensure minSdkVersion is at least 26
|
|
14
|
+
config = withGradleProperties(config, (config) => {
|
|
15
|
+
const props = config.modResults;
|
|
16
|
+
|
|
17
|
+
// Set minSdkVersion if not already high enough
|
|
18
|
+
const minSdkProp = props.find((p) => p.key === 'android.minSdkVersion');
|
|
19
|
+
if (!minSdkProp) {
|
|
20
|
+
props.push({
|
|
21
|
+
type: 'property',
|
|
22
|
+
key: 'android.minSdkVersion',
|
|
23
|
+
value: '26',
|
|
24
|
+
});
|
|
25
|
+
} else if (parseInt(minSdkProp.value, 10) < 26) {
|
|
26
|
+
minSdkProp.value = '26';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return config;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Android: Pin Kotlin Gradle plugin to 2.3.0
|
|
33
|
+
// The litertlm-android AAR uses Kotlin 2.3.0 metadata (version defined in
|
|
34
|
+
// package.json → litertLm.androidMavenVersion).
|
|
35
|
+
// React Native's default Kotlin version (2.1.0) cannot read this metadata,
|
|
36
|
+
// so we must force the Kotlin Gradle plugin to 2.3.0 in the project-level
|
|
37
|
+
// build.gradle. This ensures the fix survives `expo prebuild --clean`.
|
|
38
|
+
config = withProjectBuildGradle(config, (config) => {
|
|
39
|
+
if (config.modResults.language === 'groovy') {
|
|
40
|
+
const contents = config.modResults.contents;
|
|
41
|
+
|
|
42
|
+
// Only add if not already pinned
|
|
43
|
+
if (!contents.includes("kotlin-gradle-plugin:2.3.0")) {
|
|
44
|
+
// Replace the unversioned kotlin-gradle-plugin classpath with a pinned one
|
|
45
|
+
config.modResults.contents = contents.replace(
|
|
46
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')",
|
|
47
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0')"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return config;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = withLiteRTLM;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// cpp-adapter.cpp
|
|
3
|
+
/// JNI Entry Point - Required by Nitrogen to register Kotlin HybridObjects
|
|
4
|
+
///
|
|
5
|
+
/// Updated for react-native-nitro-modules v0.35+:
|
|
6
|
+
/// Uses facebook::jni::initialize() directly with registerAllNatives().
|
|
7
|
+
///
|
|
8
|
+
|
|
9
|
+
#include <jni.h>
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include "LiteRTLMOnLoad.hpp"
|
|
12
|
+
|
|
13
|
+
// JNI_OnLoad is called when the native library is loaded via System.loadLibrary()
|
|
14
|
+
// This is where we initialize the Nitrogen bridge and register all Kotlin HybridObjects.
|
|
15
|
+
// The new v0.35 API allows registering custom C++ native JNI classes/functions
|
|
16
|
+
// alongside Nitrogen's auto-generated registrations.
|
|
17
|
+
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
|
18
|
+
return facebook::jni::initialize(vm, []() {
|
|
19
|
+
margelo::nitro::litertlm::registerAllNatives();
|
|
20
|
+
});
|
|
21
|
+
}
|