@onekeyfe/react-native-range-downloader 3.0.39

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/ReactNativeRangeDownloader.podspec +30 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +132 -0
  6. package/android/gradle.properties +4 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ConcurrentRangeDownloader.kt +340 -0
  10. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +233 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloaderPackage.kt +24 -0
  12. package/ios/ReactNativeRangeDownloader.swift +732 -0
  13. package/lib/module/ReactNativeRangeDownloader.nitro.js +4 -0
  14. package/lib/module/ReactNativeRangeDownloader.nitro.js.map +1 -0
  15. package/lib/module/index.js +15 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +35 -0
  20. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/index.d.ts +9 -0
  22. package/lib/typescript/src/index.d.ts.map +1 -0
  23. package/nitro.json +17 -0
  24. package/nitrogen/generated/android/c++/JDownloadChannel.hpp +62 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_RangeDownloadEvent.hpp +80 -0
  26. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +117 -0
  27. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +69 -0
  28. package/nitrogen/generated/android/c++/JRangeDownloadEvent.hpp +75 -0
  29. package/nitrogen/generated/android/c++/JRangeDownloadOutcome.hpp +59 -0
  30. package/nitrogen/generated/android/c++/JRangeDownloadParams.hpp +84 -0
  31. package/nitrogen/generated/android/c++/JRangeDownloadResult.hpp +68 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/DownloadChannel.kt +22 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/Func_void_RangeDownloadEvent.kt +80 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +79 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadEvent.kt +50 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadOutcome.kt +21 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadParams.kt +56 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/RangeDownloadResult.kt +44 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/reactnativerangedownloaderOnLoad.kt +35 -0
  40. package/nitrogen/generated/android/reactnativerangedownloader+autolinking.cmake +81 -0
  41. package/nitrogen/generated/android/reactnativerangedownloader+autolinking.gradle +27 -0
  42. package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.cpp +46 -0
  43. package/nitrogen/generated/android/reactnativerangedownloaderOnLoad.hpp +25 -0
  44. package/nitrogen/generated/ios/ReactNativeRangeDownloader+autolinking.rb +60 -0
  45. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.cpp +65 -0
  46. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Bridge.hpp +246 -0
  47. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +62 -0
  48. package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.mm +33 -0
  49. package/nitrogen/generated/ios/ReactNativeRangeDownloaderAutolinking.swift +25 -0
  50. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.cpp +11 -0
  51. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +123 -0
  52. package/nitrogen/generated/ios/swift/DownloadChannel.swift +44 -0
  53. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  54. package/nitrogen/generated/ios/swift/Func_void_RangeDownloadEvent.swift +47 -0
  55. package/nitrogen/generated/ios/swift/Func_void_RangeDownloadResult.swift +47 -0
  56. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  57. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +60 -0
  58. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +197 -0
  59. package/nitrogen/generated/ios/swift/RangeDownloadEvent.swift +80 -0
  60. package/nitrogen/generated/ios/swift/RangeDownloadOutcome.swift +40 -0
  61. package/nitrogen/generated/ios/swift/RangeDownloadParams.swift +145 -0
  62. package/nitrogen/generated/ios/swift/RangeDownloadResult.swift +77 -0
  63. package/nitrogen/generated/shared/c++/DownloadChannel.hpp +80 -0
  64. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +25 -0
  65. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +79 -0
  66. package/nitrogen/generated/shared/c++/RangeDownloadEvent.hpp +93 -0
  67. package/nitrogen/generated/shared/c++/RangeDownloadOutcome.hpp +76 -0
  68. package/nitrogen/generated/shared/c++/RangeDownloadParams.hpp +102 -0
  69. package/nitrogen/generated/shared/c++/RangeDownloadResult.hpp +86 -0
  70. package/package.json +169 -0
  71. package/src/ReactNativeRangeDownloader.nitro.ts +60 -0
  72. package/src/index.tsx +20 -0
@@ -0,0 +1,233 @@
1
+ package com.margelo.nitro.reactnativerangedownloader
2
+
3
+ import com.facebook.proguard.annotations.DoNotStrip
4
+ import com.margelo.nitro.NitroModules
5
+ import com.margelo.nitro.core.Promise
6
+ import com.margelo.nitro.nativelogger.OneKeyLog
7
+ import java.io.File
8
+ import java.security.MessageDigest
9
+ import java.util.concurrent.CopyOnWriteArrayList
10
+ import java.util.concurrent.atomic.AtomicLong
11
+
12
+ // P1: faithful migration of the Android concurrent multi-range downloader.
13
+ //
14
+ // The core algorithm (.partial preallocation + .progress manifest resume +
15
+ // 8-segment thread pool + If-Range/200 fallback) lives unchanged in the
16
+ // in-module ConcurrentRangeDownloader helper, copied byte-for-byte from
17
+ // react-native-bundle-update. This class is the Nitro adapter: it builds the
18
+ // HTTPS-only OkHttpClient, drives the helper, finalizes on COMPLETED (promote
19
+ // .partial -> dest + optional SHA256 self-check), and bridges progress to the
20
+ // shared listener registry as RangeDownloadEvent (tagged with channel/taskId).
21
+ //
22
+ // Android has no background-session concept: `channel` is only an event label
23
+ // + artifact-directory tag and does not change the download mechanism. The
24
+ // on-disk format (.partial + .progress) is kept exactly as shipped so existing
25
+ // interrupted downloads resume cleanly.
26
+ @DoNotStrip
27
+ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
28
+
29
+ private class Listener(val id: Double, val callback: (RangeDownloadEvent) -> Unit)
30
+
31
+ private val listeners = CopyOnWriteArrayList<Listener>()
32
+ private val nextListenerId = AtomicLong(1)
33
+
34
+ // HTTPS-only client: reject any redirect to a non-HTTPS hop. Mirrors the
35
+ // existing react-native-bundle-update configuration verbatim.
36
+ private val httpClient = okhttp3.OkHttpClient.Builder()
37
+ .addNetworkInterceptor { chain ->
38
+ val req = chain.request()
39
+ if (!req.url.isHttps) {
40
+ throw java.io.IOException("Redirect to non-HTTPS URL is not allowed: ${req.url}")
41
+ }
42
+ chain.proceed(req)
43
+ }
44
+ .build()
45
+
46
+ override fun download(params: RangeDownloadParams): Promise<RangeDownloadResult> {
47
+ return Promise.async {
48
+ val channel = params.channel
49
+ val taskId = params.taskId
50
+ val downloadUrl = params.url
51
+ val destFilePath = params.destFilePath
52
+ val expectedSha256 = params.expectedSha256
53
+
54
+ // HTTPS-only, same gate as the source module.
55
+ if (!downloadUrl.startsWith("https://")) {
56
+ OneKeyLog.error("RangeDownloader", "download: URL is not HTTPS: $downloadUrl")
57
+ sendEvent(channel, taskId, type = "error", message = "Download URL must use HTTPS")
58
+ throw Exception("Download URL must use HTTPS")
59
+ }
60
+
61
+ // Resume support: download to <dest>.partial, promote to <dest> on success.
62
+ val partialFilePath = "$destFilePath.partial"
63
+ val destFile = File(destFilePath)
64
+
65
+ // Optional tuning knobs, defaulting to the shipped behavior (8 segments / 2MB).
66
+ val segmentCount = params.segmentCount?.toInt()?.takeIf { it > 0 } ?: 8
67
+ val minConcurrentBytes = params.minConcurrentBytes?.toLong()?.takeIf { it > 0 }
68
+ ?: (2L * 1024 * 1024)
69
+
70
+ sendEvent(channel, taskId, type = "start")
71
+
72
+ var lastProgress = -1
73
+ val outcome = ConcurrentRangeDownloader(
74
+ httpClient = httpClient,
75
+ segmentCount = segmentCount,
76
+ minConcurrentBytes = minConcurrentBytes,
77
+ log = { msg -> OneKeyLog.info("RangeDownloader", msg) },
78
+ ).download(downloadUrl, partialFilePath) { transferred, total ->
79
+ if (total > 0) {
80
+ val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
81
+ if (p != lastProgress) {
82
+ sendEvent(channel, taskId, type = "progress", progress = p)
83
+ lastProgress = p
84
+ }
85
+ }
86
+ }
87
+
88
+ if (outcome == ConcurrentRangeDownloader.Outcome.FALLBACK) {
89
+ // Concurrency unusable. The helper has already cleaned up its own
90
+ // artifacts where appropriate; the caller runs its single-stream path.
91
+ OneKeyLog.info("RangeDownloader", "download: concurrent not used, returning fallback")
92
+ sendEvent(channel, taskId, type = "fallback", message = "concurrent unavailable")
93
+ return@async RangeDownloadResult(
94
+ outcome = RangeDownloadOutcome.FALLBACK,
95
+ filePath = destFilePath,
96
+ fallbackReason = "concurrent unavailable (range unsupported / 200 / too small / single-stream partial)",
97
+ )
98
+ }
99
+
100
+ // COMPLETED: `.partial` is fully on disk. Promote -> final, then run the
101
+ // optional in-module SHA256 self-check (mirrors the source finalize path).
102
+ if (destFile.exists()) destFile.delete()
103
+ if (!File(partialFilePath).renameTo(destFile)) {
104
+ OneKeyLog.error("RangeDownloader", "download: rename .partial -> final failed")
105
+ sendEvent(channel, taskId, type = "error", message = "Failed to finalize download")
106
+ throw Exception("Failed to finalize download")
107
+ }
108
+
109
+ if (!expectedSha256.isNullOrEmpty()) {
110
+ OneKeyLog.info("RangeDownloader", "download: concurrent finished, verifying SHA256...")
111
+ if (!verifySHA256(destFilePath, expectedSha256)) {
112
+ destFile.delete()
113
+ OneKeyLog.error("RangeDownloader", "download: SHA256 verification failed")
114
+ sendEvent(channel, taskId, type = "error", message = "SHA256 verification failed")
115
+ throw Exception("SHA256 verification failed")
116
+ }
117
+ }
118
+
119
+ sendEvent(channel, taskId, type = "complete", progress = 100)
120
+ OneKeyLog.info("RangeDownloader", "download: completed channel=$channel taskId=$taskId")
121
+ RangeDownloadResult(
122
+ outcome = RangeDownloadOutcome.COMPLETED,
123
+ filePath = destFilePath,
124
+ fallbackReason = null,
125
+ )
126
+ }
127
+ }
128
+
129
+ override fun discardArtifacts(
130
+ channel: DownloadChannel,
131
+ taskId: String,
132
+ destFilePath: String
133
+ ): Promise<Unit> {
134
+ return Promise.async {
135
+ // Manifest first so it never outlives the partial it describes.
136
+ File("$destFilePath.partial.progress").delete()
137
+ File("$destFilePath.partial").delete()
138
+ OneKeyLog.info("RangeDownloader", "discardArtifacts: channel=$channel taskId=$taskId")
139
+ Unit
140
+ }
141
+ }
142
+
143
+ override fun addDownloadListener(callback: (event: RangeDownloadEvent) -> Unit): Double {
144
+ val id = nextListenerId.getAndIncrement().toDouble()
145
+ listeners.add(Listener(id, callback))
146
+ OneKeyLog.debug("RangeDownloader", "addDownloadListener: id=$id, totalListeners=${listeners.size}")
147
+ return id
148
+ }
149
+
150
+ override fun removeDownloadListener(id: Double) {
151
+ listeners.removeAll { it.id == id }
152
+ OneKeyLog.debug("RangeDownloader", "removeDownloadListener: id=$id, totalListeners=${listeners.size}")
153
+ }
154
+
155
+ // App cache directory — an app-owned, writable absolute path resolved at
156
+ // runtime (no hardcoded sandbox path).
157
+ override fun getDownloadsDir(): String {
158
+ val ctx = NitroModules.applicationContext ?: return ""
159
+ return ctx.cacheDir.absolutePath
160
+ }
161
+
162
+ // Broadcast one event to every registered listener. Listeners filter by
163
+ // channel/taskId on their side (shared registry, per the design).
164
+ private fun sendEvent(
165
+ channel: DownloadChannel,
166
+ taskId: String,
167
+ type: String,
168
+ progress: Int = 0,
169
+ message: String = "",
170
+ ) {
171
+ val event = RangeDownloadEvent(
172
+ channel = channel,
173
+ taskId = taskId,
174
+ type = type,
175
+ progress = progress.toDouble(),
176
+ message = message,
177
+ )
178
+ for (listener in listeners) {
179
+ try {
180
+ listener.callback(event)
181
+ } catch (e: Exception) {
182
+ OneKeyLog.error("RangeDownloader", "Error sending event: ${e.message}")
183
+ }
184
+ }
185
+ }
186
+
187
+ private fun verifySHA256(filePath: String, expected: String): Boolean {
188
+ val calculated = calculateSHA256(filePath) ?: return false
189
+ return secureCompare(calculated, expected)
190
+ }
191
+
192
+ private fun calculateSHA256(filePath: String): String? {
193
+ val file = File(filePath)
194
+ if (!file.exists()) {
195
+ OneKeyLog.error("RangeDownloader", "calculateSHA256: file not found: $filePath")
196
+ return null
197
+ }
198
+ return try {
199
+ val digest = MessageDigest.getInstance("SHA-256")
200
+ java.io.BufferedInputStream(java.io.FileInputStream(filePath)).use { bis ->
201
+ val buffer = ByteArray(8192)
202
+ var count: Int
203
+ while (bis.read(buffer).also { count = it } > 0) {
204
+ digest.update(buffer, 0, count)
205
+ }
206
+ }
207
+ bytesToHex(digest.digest())
208
+ } catch (e: Exception) {
209
+ OneKeyLog.error("RangeDownloader", "calculateSHA256: ${e.javaClass.simpleName}: ${e.message}")
210
+ null
211
+ }
212
+ }
213
+
214
+ private fun bytesToHex(bytes: ByteArray): String {
215
+ val sb = StringBuilder(bytes.size * 2)
216
+ for (b in bytes) {
217
+ sb.append(String.format("%02x", b))
218
+ }
219
+ return sb.toString()
220
+ }
221
+
222
+ // Constant-time comparison of two hex SHA256 strings (lower-cased).
223
+ private fun secureCompare(a: String, b: String): Boolean {
224
+ val x = a.lowercase()
225
+ val y = b.lowercase()
226
+ if (x.length != y.length) return false
227
+ var result = 0
228
+ for (i in x.indices) {
229
+ result = result or (x[i].code xor y[i].code)
230
+ }
231
+ return result == 0
232
+ }
233
+ }
@@ -0,0 +1,24 @@
1
+ package com.margelo.nitro.reactnativerangedownloader
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class ReactNativeRangeDownloaderPackage : BaseReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("reactnativerangedownloader")
20
+ }
21
+ }
22
+ }
23
+
24
+