Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9f3ee25
refactor: extract the pure update-flow decision layer into updateFlow…
sunnylqm Aug 7, 2026
40b5104
docs: native checkUpdate design — declarative endpoint plan, plain-te…
sunnylqm Aug 7, 2026
b19413b
docs: guardian source portability verified — engine choice goes per-p…
sunnylqm Aug 7, 2026
7eb9970
docs: rule out linking the host Hermes on Android — C ABI not exported
sunnylqm Aug 7, 2026
d843213
feat: port the update-flow decision layer to C++ with golden-vector p…
sunnylqm Aug 7, 2026
b945d0b
docs: final verdict — option B, uniform C++ decision layer on all pla…
sunnylqm Aug 7, 2026
a8710f9
fix: harden the flow_json parser against hostile server input
sunnylqm Aug 7, 2026
9a0d113
feat: HandleCheckResponse composition + native orchestration design (…
sunnylqm Aug 7, 2026
e34f390
feat: syncNativeConfig — provisioning layer for the native cold-start…
sunnylqm Aug 7, 2026
95c73c6
feat(ios): native cold-start update check orchestrator
sunnylqm Aug 7, 2026
03450c6
feat(android): native cold-start update check orchestrator
sunnylqm Aug 7, 2026
2a4041d
feat(harmony): native cold-start update check orchestrator
sunnylqm Aug 7, 2026
24f3adf
feat: JS reuses the native check's cached response (§10.3)
sunnylqm Aug 7, 2026
8ee69ae
docs: mark the native checkUpdate implementation complete
sunnylqm Aug 7, 2026
ae5f1ca
feat: forceBoot — per-version remote activation override (§10.7)
sunnylqm Aug 8, 2026
8412b64
docs: record the config placement verdict — forceBoot lives on bindings
sunnylqm Aug 9, 2026
f679e11
fix: harden native cold-start update orchestration
sunnylqm Aug 9, 2026
181952a
fix: address native check review feedback
sunnylqm Aug 9, 2026
3229cd8
docs: record the open follow-ups from the native check review rounds
sunnylqm Aug 9, 2026
f33bd6a
docs: fold the second review round into the follow-ups list
sunnylqm Aug 9, 2026
283dfd4
fix: close native check review follow-ups
sunnylqm Aug 9, 2026
60f5fd2
docs: record the third review round's open items
sunnylqm Aug 10, 2026
33dc6cb
fix: resolve third native check review follow-ups
sunnylqm Aug 10, 2026
845de20
fix: address remaining native check review comments
sunnylqm Aug 11, 2026
0f4651e
fix: address fourth native check review
sunnylqm Aug 11, 2026
5f5d0cb
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Aug 11, 2026
ffe5da3
docs: record the fourth review round and the merge-gate verdict
sunnylqm Aug 11, 2026
41918dc
fix: stop the native check from overriding checkStrategy and resets
sunnylqm Aug 11, 2026
ffa606f
fix: commit native check results atomically against resets
sunnylqm Aug 11, 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
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ jobs:
-lpthread \
-o unit_test
./unit_test

# Golden-vector parity between src/updateFlowCore.ts (reference) and
# the cpp/update_flow_core port. The vectors themselves are kept in
# sync with the TS side by src/__tests__/flowVectors.test.ts (js-test).
- name: Replay flow golden vectors against the C++ port
run: SANITIZE=1 ./scripts/test-update-flow-core.sh
272 changes: 251 additions & 21 deletions NATIVE_CHECKUPDATE_DESIGN.md

Large diffs are not rendered by default.

365 changes: 365 additions & 0 deletions NATIVE_CHECK_FOLLOWUPS.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion android/jni/Android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/HDiffPatch \
$(LOCAL_PATH)/HDiffPatch/libHDiffPatch/HPatch \
$(LOCAL_PATH)/lzma/C \
$(LOCAL_PATH)/../../cpp/patch_core
$(LOCAL_PATH)/../../cpp/patch_core \
$(LOCAL_PATH)/../../cpp/update_flow_core

Hdp_Files := \
hpatch.c \
Expand All @@ -28,6 +29,9 @@ LOCAL_SRC_FILES := \
../../cpp/patch_core/patch_core_android.cpp \
../../cpp/patch_core/state_core.cpp \
../../cpp/patch_core/update_core_android.cpp \
../../cpp/update_flow_core/flow_json.cpp \
../../cpp/update_flow_core/update_flow_core.cpp \
../../cpp/update_flow_core/update_flow_jni.cpp \
$(Hdp_Files)

include $(BUILD_SHARED_LIBRARY)
Binary file modified android/lib/arm64-v8a/librnupdate.so
Binary file not shown.
Binary file modified android/lib/armeabi-v7a/librnupdate.so
Binary file not shown.
Binary file modified android/lib/x86/librnupdate.so
Binary file not shown.
Binary file modified android/lib/x86_64/librnupdate.so
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,29 @@ private void downloadFile() throws IOException {
this.hash = params.hash;
String url = params.url;
File writePath = params.targetFile;
UpdateFileUtils.ensureParentDirectory(writePath);
Request request = new Request.Builder().url(url).build();

OkHttpClient requestClient = HTTP_CLIENT;
if (params.deadlineNanos > 0) {
long remainingNanos = params.deadlineNanos - System.nanoTime();
if (remainingNanos <= 0) {
throw new IOException("Download deadline expired before start");
}
long remainingMillis = Math.max(
1L,
java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(remainingNanos)
);
requestClient = HTTP_CLIENT.newBuilder()
.callTimeout(remainingMillis, java.util.concurrent.TimeUnit.MILLISECONDS)
.build();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

UpdateFileUtils.ensureParentDirectory(writePath);
if (writePath.exists() && !writePath.delete()) {
throw new IOException("Failed to replace existing file: " + writePath);
}

try (Response response = HTTP_CLIENT.newCall(request).execute()) {
try (Response response = requestClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Server error: " + response.code() + " " + response.message());
}
Expand Down Expand Up @@ -418,32 +433,53 @@ private boolean isPatchTask(int taskType) {
|| taskType == DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
}

private boolean hasCompletedPatchDirectory() {
return params.unzipDirectory != null
&& new File(params.unzipDirectory, "index.bundlejs").isFile()
&& new File(
params.unzipDirectory,
UpdateContext.VERSION_COMPLETE_FILE
).isFile();
}

@Override
public void run() {
int taskType = params.type;
final boolean alreadyCompleted = isPatchTask(taskType)
&& hasCompletedPatchDirectory();
try {
switch (taskType) {
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
doFullPatch();
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
doPatchFromApk();
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
doPatchFromPpk();
break;
case DownloadTaskParams.TASK_TYPE_CLEANUP:
doCleanUp();
break;
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
downloadFile();
break;
default:
break;
if (alreadyCompleted) {
Log.i(UpdateContext.TAG,
"download task: version " + params.hash + " already completed");
} else {
switch (taskType) {
case DownloadTaskParams.TASK_TYPE_PATCH_FULL:
doFullPatch();
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK:
doPatchFromApk();
break;
case DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK:
doPatchFromPpk();
break;
case DownloadTaskParams.TASK_TYPE_CLEANUP:
doCleanUp();
break;
case DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD:
downloadFile();
break;
default:
break;
}
}
} catch (Throwable error) {
Log.e(UpdateContext.TAG, "download task failed", error);
cleanUpAfterFailure(taskType);
// A duplicate task must never delete a version completed by an
// earlier queued task. The marker + bundle pair is the ownership
// handoff: once present, this failure did not create that install.
if (!hasCompletedPatchDirectory()) {
cleanUpAfterFailure(taskType);
}

if (params.listener != null) {
// A patch task that failed after its artifact was fully
Expand All @@ -462,6 +498,25 @@ && isPatchTask(taskType)
return;
}

if (isPatchTask(taskType) && !alreadyCompleted) {
try {
File marker = new File(
params.unzipDirectory,
UpdateContext.VERSION_COMPLETE_FILE
);
if (!marker.createNewFile() && !marker.isFile()) {
throw new IOException("Failed to mark completed update: " + marker);
}
} catch (Throwable error) {
Log.e(UpdateContext.TAG, "failed to mark completed update", error);
cleanUpAfterFailure(taskType);
if (params.listener != null) {
params.listener.onDownloadFailed(error);
}
return;
}
}

// The task itself succeeded. Run the completion callback outside the
// try/catch above so an exception thrown by the callback (e.g. a
// FileProvider misconfiguration during installApk) is not mistaken for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class DownloadTaskParams {
String originHash;
// TASK_TYPE_CLEANUP only: entries younger than this survive; 0 = delete all
int maxAgeDays = 3;
// Absolute System.nanoTime deadline for orchestrated cold-start downloads;
// 0 keeps the normal public API's 10-minute per-call timeout.
long deadlineNanos;
File targetFile;
File unzipDirectory;
File originDirectory;
Expand Down
Loading
Loading