@onekeyfe/react-native-app-update 3.0.53 → 3.0.55
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/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ConcurrentRangeDownloader.kt
CHANGED
|
@@ -18,8 +18,14 @@ import java.util.concurrent.atomic.AtomicReference
|
|
|
18
18
|
*
|
|
19
19
|
* Mirrors the desktop DesktopApiBundleUpdate concurrent path. This class is
|
|
20
20
|
* intentionally free of Android/OneKey dependencies (logging is injected) so
|
|
21
|
-
* it can be unit/type-checked standalone
|
|
22
|
-
*
|
|
21
|
+
* it can be unit/type-checked standalone.
|
|
22
|
+
*
|
|
23
|
+
* LOAD-BEARING: the caller's whole-file SHA256 + GPG verify performed AFTER
|
|
24
|
+
* promotion is the final correctness backstop for this concurrent path and must
|
|
25
|
+
* NEVER be skipped. Resume here is best-effort (ETag-gated; no ETag => fresh
|
|
26
|
+
* start), and concurrent stitching of ranges has no per-segment integrity
|
|
27
|
+
* check — a mismatched or mixed file is only ever caught by that downstream
|
|
28
|
+
* whole-file verify. Removing it would let a corrupt/mixed APK reach install.
|
|
23
29
|
*
|
|
24
30
|
* Invariant: the manifest is only meaningful as metadata for an existing
|
|
25
31
|
* `.partial`. Either both exist (resume) or neither does (fresh) — any other
|
|
@@ -147,8 +153,13 @@ internal class ConcurrentRangeDownloader(
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
// Single round-trip probe: a one-byte Range request that confirms Range
|
|
150
|
-
// support and captures total size + ETag. OkHttp
|
|
151
|
-
//
|
|
156
|
+
// support and captures total size + ETag. The caller's OkHttp client has
|
|
157
|
+
// redirects DISABLED (followRedirects(false)/followSslRedirects(false)), so
|
|
158
|
+
// a 3xx is not a 200/206 and probe() returns null -> Outcome.FALLBACK; the
|
|
159
|
+
// single-stream path (also redirect-disabled) then handles it.
|
|
160
|
+
// TODO: if APK origins ever start issuing redirects, decide deliberately
|
|
161
|
+
// whether to enable followSslRedirects(true) (HTTPS-only) here; until then
|
|
162
|
+
// redirecting origins simply fall back to single-stream.
|
|
152
163
|
private fun probe(url: String): Probe? {
|
|
153
164
|
return try {
|
|
154
165
|
val req = Request.Builder().url(url).addHeader("Range", "bytes=0-0").build()
|
|
@@ -249,7 +260,18 @@ internal class ConcurrentRangeDownloader(
|
|
|
249
260
|
val savedEtag = head.getOrNull(1)?.takeIf { it.isNotEmpty() }
|
|
250
261
|
// Object must be identical to what's on disk and on the CDN.
|
|
251
262
|
if (savedSize != total || partialSize != total) return null
|
|
252
|
-
|
|
263
|
+
// Without an ETag we have no strong validator that the bytes already
|
|
264
|
+
// on disk belong to the SAME build as the one the CDN is serving now.
|
|
265
|
+
// A same-size-but-different build (common during a staged rollout)
|
|
266
|
+
// would otherwise resume by stitching old + new bytes into one file.
|
|
267
|
+
// The downstream whole-file SHA256 + GPG verify WOULD reject that
|
|
268
|
+
// mixed file, but only after we've wasted the whole download. So treat
|
|
269
|
+
// an ETag-less manifest as untrustworthy across restarts: bail out
|
|
270
|
+
// here and let the caller start fresh.
|
|
271
|
+
// (If a weaker validator is ever wanted, persist+compare Last-Modified
|
|
272
|
+
// instead of dropping outright.)
|
|
273
|
+
if (etag == null || savedEtag == null) return null
|
|
274
|
+
if (etag != savedEtag) return null
|
|
253
275
|
val parts = ArrayList<Part>()
|
|
254
276
|
for (idx in 1 until lines.size) {
|
|
255
277
|
val cols = lines[idx].split(',')
|
package/android/src/main/java/com/margelo/nitro/reactnativeappupdate/ReactNativeAppUpdate.kt
CHANGED
|
@@ -25,6 +25,7 @@ import java.io.InputStreamReader
|
|
|
25
25
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
26
26
|
import java.util.concurrent.TimeUnit
|
|
27
27
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
28
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
28
29
|
import java.util.concurrent.atomic.AtomicLong
|
|
29
30
|
import com.margelo.nitro.reactnativebundlecrypto.BundleCryptoCore
|
|
30
31
|
|
|
@@ -518,8 +519,24 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
|
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
// Phase 2 — pick up an in-flight partial.
|
|
522
|
+
//
|
|
523
|
+
// A concurrent (multi-range) partial is pre-allocated to the FULL
|
|
524
|
+
// size up front (RandomAccessFile.setLength(total)) and tracks the
|
|
525
|
+
// real, durably-written cursor only in its sidecar
|
|
526
|
+
// "<partial>.progress" manifest — the .partial itself is zero-filled
|
|
527
|
+
// past the real data. The size-based classification below is blind
|
|
528
|
+
// to that: it would see partialSize == expectedSize and try to
|
|
529
|
+
// promote+SHA-verify a mostly-zeroed file, hit HashMismatch, and
|
|
530
|
+
// DELETE it — nuking every interrupted concurrent download back to
|
|
531
|
+
// byte 0. So when the manifest exists, skip the size-based
|
|
532
|
+
// promote/discard branches entirely and let the concurrent
|
|
533
|
+
// downloader below own the file: it resumes from the manifest (or
|
|
534
|
+
// returns FALLBACK and hands the bytes back to single-stream).
|
|
535
|
+
// The path must match exactly what ConcurrentRangeDownloader writes:
|
|
536
|
+
// File("$partialFilePath.progress").
|
|
537
|
+
val hasConcurrentManifest = buildFile("$partialFilePath.progress").exists()
|
|
521
538
|
var partialBytes = 0L
|
|
522
|
-
if (partialFile.exists()) {
|
|
539
|
+
if (partialFile.exists() && !hasConcurrentManifest) {
|
|
523
540
|
val partialSize = partialFile.length()
|
|
524
541
|
when {
|
|
525
542
|
expectedSize > 0 && partialSize == expectedSize -> {
|
|
@@ -575,23 +592,36 @@ class ReactNativeAppUpdate : HybridReactNativeAppUpdateSpec() {
|
|
|
575
592
|
.followRedirects(false)
|
|
576
593
|
.followSslRedirects(false)
|
|
577
594
|
.build()
|
|
578
|
-
|
|
595
|
+
// onProgress is invoked concurrently from all 8 worker
|
|
596
|
+
// threads. concurrentProgress must be an AtomicInteger so the
|
|
597
|
+
// "did the percent advance?" check is race-free, and the
|
|
598
|
+
// NotificationCompat.Builder (shared, NOT thread-safe) plus the
|
|
599
|
+
// notify() call must be guarded by a single lock so two threads
|
|
600
|
+
// never mutate/build it at once. We emit at most once per
|
|
601
|
+
// percent step: getAndSet returns the previous value, and only
|
|
602
|
+
// the thread that actually moved the percent forward proceeds.
|
|
603
|
+
val concurrentProgress = AtomicInteger(-1)
|
|
604
|
+
val notifyLock = Any()
|
|
579
605
|
val concurrentOutcome = ConcurrentRangeDownloader(
|
|
580
606
|
httpClient = concurrentClient,
|
|
581
607
|
log = { msg -> OneKeyLog.info("AppUpdate", msg) },
|
|
582
608
|
).download(url, partialFilePath) { transferred, total ->
|
|
583
609
|
if (total > 0) {
|
|
584
610
|
val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
|
|
585
|
-
|
|
611
|
+
// Only the thread that advances the percent emits; a
|
|
612
|
+
// stale/lower p (out-of-order delivery) is dropped.
|
|
613
|
+
val prev = concurrentProgress.get()
|
|
614
|
+
if (p > prev && concurrentProgress.compareAndSet(prev, p)) {
|
|
586
615
|
sendEvent("update/downloading", progress = p)
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
616
|
+
synchronized(notifyLock) {
|
|
617
|
+
builder.setProgress(100, p, false)
|
|
618
|
+
if (ActivityCompat.checkSelfPermission(
|
|
619
|
+
context, android.Manifest.permission.POST_NOTIFICATIONS
|
|
620
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
621
|
+
) {
|
|
622
|
+
notifyManager.notify(NOTIFICATION_ID, builder.build())
|
|
623
|
+
}
|
|
593
624
|
}
|
|
594
|
-
concurrentProgress = p
|
|
595
625
|
}
|
|
596
626
|
}
|
|
597
627
|
}
|