Skip to content
Merged
Show file tree
Hide file tree
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 Aug 21, 2026
2643454
ADFA-4128: qb 04 review fixes — reload failure attribution + surfacing
fryanpan Aug 22, 2026
926cb0c
ADFA-4128 (4/11): address CodeRabbit review
fryanpan Aug 27, 2026
e2be799
ADFA-4128 (4/11): address Akash's review
fryanpan Aug 29, 2026
357acdf
ADFA-4128 (4/11): drop the unused IQuickBuildHost.disconnect AIDL method
fryanpan Aug 29, 2026
1140a2e
ADFA-4128: qb-04 review fixes - overlay observer capture, BUILD_FAILE…
fryanpan Sep 1, 2026
bef777d
ADFA-4128: 0902 review round on quickbuild:runtime
fryanpan Sep 3, 2026
5abc002
style: spotless reformat of QuickBuildClient, no functional change
fryanpan Sep 3, 2026
73dae77
ADFA-4128: hold a generation blamable until its first frame has drawn
fryanpan Sep 3, 2026
f13be8b
ADFA-4128: serialize and order the API 28/29 resource swap
fryanpan Sep 3, 2026
9a975d8
ADFA-4128: a stale handshake failure must not tear down a live binding
fryanpan Sep 3, 2026
274358c
ADFA-4128: delete a temp file on every failure, not just a failed rename
fryanpan Sep 3, 2026
dd996ce
ADFA-4128: drop the no-op test-heap override
fryanpan Sep 3, 2026
77d7b6c
ADFA-4128: style: spotless reformat, no functional change
fryanpan Sep 3, 2026
153fb54
ADFA-4128: hold a backgrounded deploy's ack until its resource swaps …
fryanpan Sep 3, 2026
e1ea19a
ADFA-4128: name the unreported-relaunch-crash gap by its ticket, not …
fryanpan Sep 3, 2026
4a6fe90
ADFA-4128: pin the first-frame call site, and refuse an abandoned gen…
fryanpan Sep 4, 2026
4bd15a1
ADFA-4128: say why the banner copy is inline English
fryanpan Sep 4, 2026
cab4fe1
ADFA-4128: report a mixed state when the failed swap had already comm…
fryanpan Sep 4, 2026
69ada65
ADFA-4128: run the boot resource restore off the main thread and repo…
fryanpan Sep 4, 2026
f5ec090
ADFA-4128: remove the first-frame draw listener from the captured obs…
fryanpan Sep 4, 2026
f1ab589
ADFA-4128: write the client's host proxy under the monitor everywhere
fryanpan Sep 4, 2026
9e7ee0e
ADFA-4128: document the recreate-into-stopped case FirstFrameGate doe…
fryanpan Sep 4, 2026
40beeb8
ADFA-4128: refuse asset payloads below API 30 instead of acking a mer…
fryanpan Sep 4, 2026
408aebd
style: spotless reformat, no functional change
fryanpan Sep 4, 2026
2698d1e
ADFA-4128: decide what a frame proves on its draw pass, not when its …
fryanpan Sep 4, 2026
f22832a
ADFA-4128: say what a failed boot restore actually leaves behind
fryanpan Sep 5, 2026
709854a
ADFA-4128: serialize the cumulative asset merge against a second bind…
fryanpan Sep 6, 2026
ee2cf45
ADFA-4128: stop two runtime tests passing for a reason they do not test
fryanpan Sep 6, 2026
21b40f6
ADFA-4128: refuse an abandoned generation's swaps by generation, not …
fryanpan Sep 7, 2026
3b92f17
ADFA-4128: sweep the API 28/29 apk cache from the first write, off th…
fryanpan Sep 7, 2026
6a60473
ADFA-4128: stash a persisted generation for boot restore only when it…
fryanpan Sep 7, 2026
d8101cf
ADFA-4128: give the boot restore's failure listener the deploy path's…
fryanpan Sep 7, 2026
481ae62
ADFA-4128: report a payload that fails before acceptance instead of r…
fryanpan Sep 8, 2026
8b11c27
ADFA-4128: keep unbind and rebind outside the client monitor, and dro…
fryanpan Sep 8, 2026
b9be64e
ADFA-4128: make the status banner a polite live region
fryanpan Sep 8, 2026
2caa5a5
ADFA-4128: clear a persisted payload the stamped baseline rejects
fryanpan Sep 8, 2026
210e4a4
ADFA-4128: pass the stamp read failure to the logger
fryanpan Sep 8, 2026
6fc911d
ADFA-4128: round 5 doc and comment fixes on quickbuild/runtime
fryanpan Sep 8, 2026
22fe2ca
style: spotless reformat, no functional change
fryanpan Sep 8, 2026
0fb00bd
ADFA-4128: sample the drawn generation on the draw pass, not at compl…
fryanpan Sep 9, 2026
a55c34f
ADFA-4128: keep the boot restore's payload files out of the orphan sweep
fryanpan Sep 9, 2026
b67843a
ADFA-4128: guard the two apply catches on the swap gate too
fryanpan Sep 9, 2026
4cf2725
ADFA-4128: put both apply catches through one tested report-once seam
fryanpan Sep 10, 2026
8eb348f
ADFA-4128: say what PersistedSelection gates instead of naming a test…
fryanpan Sep 19, 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
4 changes: 2 additions & 2 deletions quickbuild/docs/reliability-gaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ relink-crash recovery gap.
| Gap | What the user sees | Frequency | Blocks v1? |
| --- | --- | --- | --- |
| #89 | Red-alert icon; tapping Quick Build does nothing until "Restart session" | No device repro `[inferred]` | TBD |
| #91 | Their own app crash is never surfaced; CoGo blames deploy infra | `[unmeasured]` | TBD |
| #91 | Their own app crash is never surfaced; CoGo blames deploy infra | `[unmeasured]` | ADFA-5466 |
| #87 | A one-line edit in a Room/KSP project runs a full ~200s rebuild + reinstall | 3/3 when attempted | TBD |
| Relink crash | A reload that crashes the app repeats the crash at every process boot | Trigger fixed; net still absent | TBD |
| Relink stuck | A failed relink re-fails on every later save until a gradle-file touch | `[unmeasured]` | No - fixed below |
Expand Down Expand Up @@ -52,7 +52,7 @@ flowchart LR
## #91 - an organic proxy-app crash never reaches the crash surface

- **Root cause:** the runtime only reports a crash while a reload is in flight
(`QuickBuildRuntime.java:306` gates on `pendingReloadGeneration >= 0`, which is -1 between
(the crash guard gates on `FirstFrameGate.pending() >= 0`, which is -1 between
builds). The disconnect *is* detected - `ProxyAppConnections.onDisconnected()` emits
`TargetReport.Disconnected` - but the session manager's collector only tests for `Crashed`
(`QuickBuildSessionManager.kt:281`). Today's entire user-visible consequence of a crash is an
Expand Down
105 changes: 105 additions & 0 deletions quickbuild/runtime/build.gradle.kts
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*")
Comment thread
fryanpan marked this conversation as resolved.
// 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)
}
35 changes: 35 additions & 0 deletions quickbuild/runtime/src/main/AndroidManifest.xml
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" />
Comment thread
fryanpan marked this conversation as resolved.
</application>

</manifest>
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);
}
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);
}
Loading
Loading