Repository navigation
ADFA-4128 (4/11): quickbuild:runtime — swapping code in the running app #1716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
fryanpan
merged 45 commits into
feature/ADFA-4128-qb-03-protocol
from
feature/ADFA-4128-qb-04-runtime
Oct 2, 2026
Merged
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
590fee3
ADFA-4128: qb 04/12 runtime — Inside the proxy app: swaps code, resou…
fryanpan 2643454
ADFA-4128: qb 04 review fixes — reload failure attribution + surfacing
fryanpan 926cb0c
ADFA-4128 (4/11): address CodeRabbit review
fryanpan e2be799
ADFA-4128 (4/11): address Akash's review
fryanpan 357acdf
ADFA-4128 (4/11): drop the unused IQuickBuildHost.disconnect AIDL method
fryanpan 1140a2e
ADFA-4128: qb-04 review fixes - overlay observer capture, BUILD_FAILE…
fryanpan bef777d
ADFA-4128: 0902 review round on quickbuild:runtime
fryanpan 5abc002
style: spotless reformat of QuickBuildClient, no functional change
fryanpan 73dae77
ADFA-4128: hold a generation blamable until its first frame has drawn
fryanpan f13be8b
ADFA-4128: serialize and order the API 28/29 resource swap
fryanpan 9a975d8
ADFA-4128: a stale handshake failure must not tear down a live binding
fryanpan 274358c
ADFA-4128: delete a temp file on every failure, not just a failed rename
fryanpan dd996ce
ADFA-4128: drop the no-op test-heap override
fryanpan 77d7b6c
ADFA-4128: style: spotless reformat, no functional change
fryanpan 153fb54
ADFA-4128: hold a backgrounded deploy's ack until its resource swaps …
fryanpan e1ea19a
ADFA-4128: name the unreported-relaunch-crash gap by its ticket, not …
fryanpan 4a6fe90
ADFA-4128: pin the first-frame call site, and refuse an abandoned gen…
fryanpan 4bd15a1
ADFA-4128: say why the banner copy is inline English
fryanpan cab4fe1
ADFA-4128: report a mixed state when the failed swap had already comm…
fryanpan 69ada65
ADFA-4128: run the boot resource restore off the main thread and repo…
fryanpan f5ec090
ADFA-4128: remove the first-frame draw listener from the captured obs…
fryanpan f1ab589
ADFA-4128: write the client's host proxy under the monitor everywhere
fryanpan 9e7ee0e
ADFA-4128: document the recreate-into-stopped case FirstFrameGate doe…
fryanpan 40beeb8
ADFA-4128: refuse asset payloads below API 30 instead of acking a mer…
fryanpan 408aebd
style: spotless reformat, no functional change
fryanpan 2698d1e
ADFA-4128: decide what a frame proves on its draw pass, not when its …
fryanpan f22832a
ADFA-4128: say what a failed boot restore actually leaves behind
fryanpan 709854a
ADFA-4128: serialize the cumulative asset merge against a second bind…
fryanpan ee2cf45
ADFA-4128: stop two runtime tests passing for a reason they do not test
fryanpan 21b40f6
ADFA-4128: refuse an abandoned generation's swaps by generation, not …
fryanpan 3b92f17
ADFA-4128: sweep the API 28/29 apk cache from the first write, off th…
fryanpan 6a60473
ADFA-4128: stash a persisted generation for boot restore only when it…
fryanpan d8101cf
ADFA-4128: give the boot restore's failure listener the deploy path's…
fryanpan 481ae62
ADFA-4128: report a payload that fails before acceptance instead of r…
fryanpan 8b11c27
ADFA-4128: keep unbind and rebind outside the client monitor, and dro…
fryanpan b9be64e
ADFA-4128: make the status banner a polite live region
fryanpan 2caa5a5
ADFA-4128: clear a persisted payload the stamped baseline rejects
fryanpan 210e4a4
ADFA-4128: pass the stamp read failure to the logger
fryanpan 6fc911d
ADFA-4128: round 5 doc and comment fixes on quickbuild/runtime
fryanpan 22fe2ca
style: spotless reformat, no functional change
fryanpan 0fb00bd
ADFA-4128: sample the drawn generation on the draw pass, not at compl…
fryanpan a55c34f
ADFA-4128: keep the boot restore's payload files out of the orphan sweep
fryanpan b67843a
ADFA-4128: guard the two apply catches on the swap gate too
fryanpan 4cf2725
ADFA-4128: put both apply catches through one tested report-once seam
fryanpan 8eb348f
ADFA-4128: say what PersistedSelection gates instead of naming a test…
fryanpan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import com.itsaky.androidide.build.config.BuildConfig | ||
|
|
||
| plugins { | ||
| id("com.android.library") | ||
| } | ||
|
|
||
| description = | ||
| "Quick Build runtime embedded in generated proxy apps: binds to CoGo, receives payload fds, hot-reloads (ADFA-4128)" | ||
|
|
||
| // CoGo stages this AAR into its assets and the device reads it by name, so pin the archive | ||
| // name instead of inheriting the module name. | ||
| base.archivesName.set("quickbuild-runtime") | ||
|
|
||
| android { | ||
| namespace = "${BuildConfig.PACKAGE_NAME}.quickbuild.runtime" | ||
|
|
||
| defaultConfig { | ||
| // Runs inside apps BUILT WITH CoGo, not inside the IDE. | ||
| minSdk = BuildConfig.MIN_SDK_FOR_APPS_BUILT_WITH_COGO | ||
| } | ||
|
|
||
| compileOptions { | ||
| // Java-only and Java 8, like :logsender - the AAR is injected into user | ||
| // projects and must not drag kotlin-stdlib or any other dependency in. | ||
| sourceCompatibility = JavaVersion.VERSION_1_8 | ||
| targetCompatibility = JavaVersion.VERSION_1_8 | ||
| } | ||
|
|
||
| buildFeatures.apply { | ||
| aidl = true | ||
| viewBinding = false | ||
| buildConfig = false | ||
| } | ||
| } | ||
|
|
||
| // JVM unit tests for the plain-Java payload logic (generation gate, metadata/component | ||
| // map parsing, asset extraction). Mirrors :quick-build's jupiter setup. | ||
| tasks.withType<Test> { | ||
| useJUnitPlatform() | ||
| } | ||
|
|
||
| // DoD coverage gate: >=90% line+branch on non-UI (domain/data) code. | ||
| // Same shape as :quick-build's report: the root build attaches the jacoco agent to | ||
| // every Test task, and for Android modules the exec lands at | ||
| // build/outputs/unit_test_code_coverage/<variant>UnitTest/, NOT build/jacoco/ -- a | ||
| // JacocoReport pointed at build/jacoco/ silently SKIPs and the gate is never | ||
| // measured (ADFA-3834 learnings). | ||
| tasks.register<JacocoReport>("jacocoTestReport") { | ||
| group = "verification" | ||
| description = "JaCoCo line+branch coverage for the v8Debug unit tests." | ||
| dependsOn("testV8DebugUnitTest") | ||
|
|
||
| reports { | ||
| xml.required.set(true) | ||
| html.required.set(true) | ||
| } | ||
|
|
||
| // Java-only module: the hand-written surface is the javac output. The AIDL stubs | ||
| // (IQuickBuildHost/IQuickBuildTarget + nested Stub/Proxy/Default) are generated | ||
| // code, so they are excluded from the measured set. | ||
| // | ||
| // Device-only Android/binder glue is EXEMPT from the JVM coverage bar (DoD: >=90% | ||
| // line+branch on non-UI code; these classes only execute meaningfully on a device | ||
| // and are covered by the android-qa device walks instead). Anything JVM-testable | ||
| // stays in the measured set - notably LegacyResourceSwap's file half and all | ||
| // parsing/persistence code. | ||
| classDirectories.setFrom( | ||
| fileTree( | ||
| layout.buildDirectory.dir("intermediates/javac/v8Debug/compileV8DebugJavaWithJavac/classes"), | ||
| ) { | ||
| exclude("com/itsaky/androidide/quickbuild/IQuickBuild*") | ||
| // Binder host service: payload fds, Handler/Looper, activity relaunch orchestration. | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/QuickBuildRuntime*") | ||
| // ServiceConnection bind/reconnect to CoGo; binder death + rebind only happen on-device. | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/QuickBuildClient*") | ||
| // Framework-instantiated AppComponentFactory (Activity/Service/Provider hooks). | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/QuickBuildAppComponentFactory*") | ||
| // InMemoryDexClassLoader (ART-only) + /proc + android.os.Process boot path; not | ||
| // splittable without moving prod code around - the generation-gate logic it defers | ||
| // to (Generations, PayloadPersistence, PersistedSelection) is JVM-tested. | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/PayloadStore*") | ||
| // API 30+ ResourcesLoader/ResourcesProvider attach; framework Resources objects only. | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/ResourceStore*") | ||
| // Overlay banner View/TextView UI (UI is DoD-exempt; OverlayState text model is JVM-tested). | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/StatusOverlay*") | ||
| // Application.ActivityLifecycleCallbacks census over real Activity instances. | ||
| exclude("com/itsaky/androidide/quickbuild/runtime/ActivityTracker*") | ||
| }, | ||
| ) | ||
| sourceDirectories.setFrom(files("src/main/java")) | ||
| executionData.setFrom( | ||
| layout.buildDirectory.file( | ||
| "outputs/unit_test_code_coverage/v8DebugUnitTest/testV8DebugUnitTest.exec", | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| dependencies { | ||
| testImplementation(libs.tests.junit.jupiter) | ||
| testImplementation(libs.tests.google.truth) | ||
| // Shared offline-guard scanner (OfflineNetworkGuardTest). Test-only: this never | ||
| // reaches the AAR, so the module's no-kotlin-stdlib rule still holds. | ||
| testImplementation(testFixtures(projects.quickbuild.protocol)) | ||
| testRuntimeOnly(libs.tests.junit.platformLauncher) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <!-- | ||
| Package visibility (API 30+ filtering): the proxy app binds CoGo's deploy service. | ||
| Declared here so every generated proxy app inherits it via manifest merge. | ||
| --> | ||
| <queries> | ||
| <package android:name="com.itsaky.androidide" /> | ||
| </queries> | ||
|
|
||
| <!-- | ||
| Do NOT declare android:appComponentFactory here: a debuggable app that also pulls | ||
| androidx.core (which declares androidx.core.app.CoreComponentFactory) then fails manifest | ||
| merge, and that happens BEFORE the proxy app build's merged-manifest transform runs. The | ||
| proxy app build owns the attribute instead - QuickBuildManifestTransformer sets it on the | ||
| MERGED manifest, adding it when absent and replacing a library-injected one. An | ||
| <application> ELEMENT is fine, and is what the keep-alive service below needs; only the | ||
| attribute is forbidden. | ||
| --> | ||
| <application> | ||
| <!-- | ||
| Lets CoGo bind into this app so the cached-app freezer leaves it alone for the life of a | ||
| Quick Build session; without it the app is frozen ~1 min after it loses the foreground | ||
| and stops answering the reload handshake - see QuickBuildKeepAliveService. Exported | ||
| because CoGo is a different uid, with no intent-filter so it is reachable by explicit | ||
| component only, and named in the Gradle plugin's UNPROXIABLE_BY_NAME so the proxy-app | ||
| manifest transform leaves this name intact. | ||
| --> | ||
| <service | ||
| android:name="com.itsaky.androidide.quickbuild.runtime.QuickBuildKeepAliveService" | ||
| android:exported="true" /> | ||
|
fryanpan marked this conversation as resolved.
|
||
| </application> | ||
|
|
||
| </manifest> | ||
24 changes: 24 additions & 0 deletions
24
quickbuild/runtime/src/main/aidl/com/itsaky/androidide/quickbuild/IQuickBuildHost.aidl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.itsaky.androidide.quickbuild; | ||
|
|
||
| import com.itsaky.androidide.quickbuild.IQuickBuildTarget; | ||
|
|
||
| /** | ||
| * CoGo side of the deploy channel (bound service, LogSender bind pattern). The proxy app | ||
| * binds on launch and registers its callback. CoGo verifies Binder.getCallingUid() | ||
| * against the proxy app's installed uid on every call. | ||
| */ | ||
| interface IQuickBuildHost { | ||
|
|
||
| /** | ||
| * Register the proxy app. CoGo replies (possibly immediately) with an | ||
| * {@link IQuickBuildTarget#onPayload} carrying the current generation when the | ||
| * app's running generation is stale. | ||
| */ | ||
| void connect(IQuickBuildTarget target, String packageName, long runningGeneration); | ||
|
|
||
| /** The payload for {@code generation} was loaded and rendered in {@code reloadMillis}. */ | ||
| oneway void reportReloaded(long generation, long reloadMillis); | ||
|
|
||
| /** The payload for {@code generation} crashed in render/lifecycle. */ | ||
| oneway void reportCrash(long generation, String stackSummary); | ||
| } |
46 changes: 46 additions & 0 deletions
46
quickbuild/runtime/src/main/aidl/com/itsaky/androidide/quickbuild/IQuickBuildTarget.aidl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.itsaky.androidide.quickbuild; | ||
|
|
||
| /** | ||
| * Proxy app side of the deploy channel. CoGo calls this after a successful | ||
| * quick build. Payloads travel as ParcelFileDescriptors; nothing touches shared storage. | ||
| * The target accepts a payload only when {@code generation} is strictly newer than the | ||
| * generation it currently runs. | ||
| * | ||
| * Versioning: CoGo and an installed proxy app can run DIFFERENT revisions of this | ||
| * interface (the runtime AAR is baked into the proxy app at proxy app build time). Only ever | ||
| * APPEND methods at the end - never reorder or remove. An older proxy app's stub answers | ||
| * an unknown transaction code with "not handled", and because the interface is oneway | ||
| * the caller never notices; the message is simply ignored. | ||
| */ | ||
| oneway interface IQuickBuildTarget { | ||
|
|
||
| /** | ||
| * Deliver generation {@code generation}. | ||
| * | ||
| * @param dexPayload classes.dex containing ALL user classes + generated proxies, | ||
| * or null for a resources/assets-only deploy. | ||
| * @param resourcesPayload fd to the full relinked resource apk (resources.arsc plus | ||
| * every compiled resource file, not a bare table - see | ||
| * Aapt2Link's KDoc) for | ||
| * ResourcesProvider.loadFromApk, or null when resources did | ||
| * not change. | ||
| * @param assetsPayload a zip of changed asset files, or null. | ||
| * @param metadataJson JSON: entry activity class, changed-asset paths, flags. | ||
| * Schema in quickbuild/protocol/README.md. | ||
| */ | ||
| void onPayload(long generation, in @nullable ParcelFileDescriptor dexPayload, | ||
| in @nullable ParcelFileDescriptor resourcesPayload, | ||
| in @nullable ParcelFileDescriptor assetsPayload, String metadataJson); | ||
|
|
||
| /** | ||
| * Build-status message: tells the running proxy app that a quick build | ||
| * FAILED CoGo-side (a compile error never produces a payload, so without this the | ||
| * app would silently keep running old code with no user-visible signal), or that a | ||
| * build succeeded (clears a previously shown failure). | ||
| * | ||
| * @param statusJson JSON with string-only values; schema in quickbuild/protocol/README.md. | ||
| * Unknown kinds and unknown fields are ignored by the runtime, so | ||
| * the schema can grow without breaking installed proxy apps. | ||
| */ | ||
| void onBuildStatus(String statusJson); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.