Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0f2cfa
docs: add OneKey Concurrent Download Standard (OCDS) v1.1
huhuanming Jun 20, 2026
ace9896
feat: OCDS v1.1 conformance — iOS + Android concurrent downloader
huhuanming Jun 20, 2026
d729a3a
feat: forward typed OCDS outcome + tuning params across the Nitro JS …
huhuanming Jun 20, 2026
537bf03
fix(android): await worker termination on cancel before delete (OCDS …
huhuanming Jun 20, 2026
e50860b
test(android): unit-test the OCDS §4 HTTP-status classifier
huhuanming Jun 20, 2026
e6f3e32
test(android): OCDS §6 conformance suite for ConcurrentRangeDownloade…
huhuanming Jun 20, 2026
94ccb76
test(ios): SwiftPM unit tests for RangeDownloader OCDS logic (+ extra…
huhuanming Jun 20, 2026
a032f08
chore(example): regenerate iOS Podfile.lock after pod install
huhuanming Jun 21, 2026
11e5d48
chore(ios-tests): stop tracking SwiftPM .build artifacts (196MB), git…
huhuanming Jun 21, 2026
e18a6b5
docs(range-downloader): consolidate OCDS verification (Node/Android/i…
huhuanming Jun 21, 2026
77962d2
docs(range-downloader): document the synthetic-task scope of the iOS …
huhuanming Jun 21, 2026
3998829
spec(OCDS 1.2): drop the single-stream retry-once on checksum mismatch
huhuanming Jun 21, 2026
cc57f49
test(android): close OCDS audit coverage gaps (5xx/416/multipart/bad-…
huhuanming Jun 22, 2026
363af24
test(android): add the missing BAD_TOTAL_206 fault mode to FaultServer
huhuanming Jun 22, 2026
42405f8
docs(conformance): record the audit-driven coverage delta + mutation-…
huhuanming Jun 22, 2026
2bc8fed
test(android): make the adapter unit-testable via a behavior-preservi…
huhuanming Jun 22, 2026
241f4f1
docs(conformance): Android adapter gap closed via behavior-preserving…
huhuanming Jun 22, 2026
213acf0
fix(android-apk): wire a CancelHandle so clearCache can't race in-fli…
huhuanming Jun 22, 2026
24d5973
fix(android-apk): don't fire update/error when a download is intentio…
huhuanming Jun 22, 2026
011c24a
fix(android-apk): don't fire update/error when ASC verification is me…
huhuanming Jun 22, 2026
b9caf71
test(android-apk): extract pure logic + JVM unit tests for the zero-c…
huhuanming Jun 22, 2026
42171dd
docs(conformance): record the Android APK-updater audit + fixes + ext…
huhuanming Jun 22, 2026
4c49e9c
docs(conformance): correct verifyAPK + desktop-installer scope claims
huhuanming Jun 22, 2026
f483ee8
fix(android-apk): surface non-I/O verify failures instead of deferrin…
huhuanming Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 68 additions & 68 deletions example/react-native/ios/Podfile.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions native-modules/react-native-app-update/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,9 @@ dependencies {

// MMKV for reading DevSettings (compileOnly: provided by the host app via react-native-mmkv)
compileOnly "io.github.zhongwuzw:mmkv:2.2.4"

// Pure-logic unit tests (AppUpdateLogic) run on the local JVM via
// :onekeyfe_react-native-app-update:testDebugUnitTest. No Robolectric: the
// extracted logic is dependency-free (no Context / Nitro / OkHttp).
testImplementation "junit:junit:4.13.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.margelo.nitro.reactnativeappupdate

/**
* Dependency-free pure logic extracted from ReactNativeAppUpdate.
*
* This object intentionally pulls in NO Android / Nitro / OkHttp / File-I/O
* dependencies: it holds only constants and pure string-derivation helpers so
* the values can be unit-tested under plain JVM JUnit (the adapter itself
* cannot, because it needs a live Context / FileProvider / network).
*/
object AppUpdateLogic {
// Must match ConcurrentRangeDownloader's default segmentCount: the
// concurrent downloader writes sibling segment files
// "<partial>.seg0".."<partial>.seg${N-1}", and Phase 2 below scans this
// range to detect an in-flight concurrent download.
const val CONCURRENT_SEGMENT_COUNT = 8

/**
* Derive the sibling segment-file name the concurrent downloader writes for
* a given partial path and segment index, i.e. "<partial>.seg<index>".
* This MUST match exactly what ConcurrentRangeDownloader writes.
*/
fun segmentFileName(partialFilePath: String, index: Int): String =
"$partialFilePath.seg$index"

/**
* All [CONCURRENT_SEGMENT_COUNT] segment-file names for a given partial
* path, ordered seg0..seg${N-1}.
*/
fun segmentFileNames(partialFilePath: String): List<String> =
(0 until CONCURRENT_SEGMENT_COUNT).map { segmentFileName(partialFilePath, it) }

/**
* Parse the total size out of a 416 (Range Not Satisfiable)
* `Content-Range: bytes * /<total>` header. Returns null when the header is
* absent or is not the "unsatisfiable range" form (e.g. a normal
* `bytes start-end/total`).
*
* Regex body copied VERBATIM from ReactNativeAppUpdate.downloadAPK (the 416
* branch); wrapped here with no behavior change so the parse step is pure
* and unit-testable. The adapter's `total == partialBytes` comparison stays
* in the adapter (it needs runtime `partialBytes`); see [is416Complete].
*/
fun parse416Total(contentRange: String?): Long? =
contentRange
?.let { Regex("""bytes\s+\*\s*/\s*(\d+)""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() }

/**
* Parse a 206 `Content-Range: bytes start-end/total` header into
* (start, end, total). `total` is null when the server sends `*`
* (unknown total). Returns null when the header is absent or malformed.
*
* `rangeRegex` body copied VERBATIM from ReactNativeAppUpdate.downloadAPK
* (the 206 sanity-check). The adapter's `start != partialBytes`
* CDN-misalignment guard stays in the adapter; see [is206StartAligned].
*/
fun parse206ContentRange(contentRange: String?): Triple<Long?, Long?, Long?>? {
val match = contentRange
?.let { Regex("""bytes\s+(\d+)\s*-\s*(\d+)\s*/\s*(\d+|\*)""").find(it) }
?: return null
val start = match.groupValues.getOrNull(1)?.toLongOrNull()
val end = match.groupValues.getOrNull(2)?.toLongOrNull()
// group 3 is either digits or literal "*"; "*" → unknown total → null.
val total = match.groupValues.getOrNull(3)?.toLongOrNull()
return Triple(start, end, total)
}

/**
* 416 recovery predicate: the server-reported total exactly equals the
* bytes we already have on disk, meaning our `.partial` IS the whole APK
* and just needs verify + rename rather than a wipe. Pure mirror of the
* adapter's `totalFromHeader != null && totalFromHeader == partialBytes`.
*/
fun is416Complete(total: Long?, partialBytes: Long): Boolean =
total != null && total == partialBytes

/**
* 206 start-alignment guard: the server's body starts exactly where we
* asked it to resume from. When false (CDN/proxy rewrote the range), the
* 206 body is a mis-aligned slice and must NOT be appended. Pure mirror of
* the adapter's `rangeStart != null && rangeStart == partialBytes` check.
*/
fun is206StartAligned(start: Long?, partialBytes: Long): Boolean =
start != null && start == partialBytes
}
Loading
Loading