-
-
Notifications
You must be signed in to change notification settings - Fork 55
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
Changes from all commits
7b256e0
4290b90
5fa6763
474b432
4a9449b
2db608b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 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() | ||
| // StreamsTest exercises the payload cap through the default readFully overload; a | ||
| // capped reader legitimately buffers up to the cap and then copies it, so the peak is | ||
| // about twice the cap - more headroom than Gradle's default 512 MB test-worker heap. | ||
| maxHeapSize = "1g" | ||
| } | ||
|
|
||
| // 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) | ||
| } | ||
| 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 on lines
+30
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
service_file="$(fd -a '^QuickBuildKeepAliveService\.java$' quickbuild | head -n1)"
test -n "$service_file"
ast-grep outline "$service_file" --items all
rg -n -C3 '`@Override`|onBind|Binder\.getCallingUid|checkCalling|enforceCalling|signature' "$service_file"Repository: appdevforall/CodeOnTheGo Length of output: 1240 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- manifest context ---'
cat -n quickbuild/runtime/src/main/AndroidManifest.xml | sed -n '1,60p'
printf '%s\n' '--- service implementation ---'
cat -n quickbuild/runtime/src/main/java/com/itsaky/androidide/quickbuild/runtime/QuickBuildKeepAliveService.java
printf '%s\n' '--- service and binding references ---'
rg -n -C4 'QuickBuildKeepAliveService|bindService|BIND_' quickbuild --glob '!**/build/**'
printf '%s\n' '--- manifest permissions and package/application context ---'
rg -n -C2 'permission|exported|package=|applicationId|namespace' quickbuild --glob 'AndroidManifest.xml' --glob '*.gradle' --glob '*.gradle.kts'Repository: appdevforall/CodeOnTheGo Length of output: 30105 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- keep-alive binding call sites ---'
rg -n -C8 'QuickBuildKeepAliveService|KEEP.?ALIVE|keep.?alive|setComponent|ComponentName' . \
--glob '!**/build/**' --glob '!**/.git/**'
printf '%s\n' '--- permission declarations and service components ---'
rg -n -C3 'android:permission|<permission|android:exported="true"|extends Service' . \
--glob '!**/build/**' --glob '!**/.git/**' --glob '*.xml' --glob '*.java' --glob '*.kt'
printf '%s\n' '--- proxy-app transform references ---'
rg -n -C5 'UNPROXIABLE_BY_NAME|ComponentProxiabilityResolver|manifest transform|manifest merge' quickbuild \
--glob '!**/build/**'Repository: appdevforall/CodeOnTheGo Length of output: 50381 Restrict access to
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not taking it. The exposure is real but bounded at keeping a developer's own proxy app unfrozen, and the returned object is a bare Binder with no transactions. Both remedies are unavailable: Binder.getCallingUid() inside onBind() returns this app's own uid, and a signature permission cannot work because CoGo is release-signed while the proxy app uses the on-device debug keystore. onUnbind returns false, so handing a caller null would poison the cached binding and break the keep-alive outright. |
||
| </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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MINOR: The PR body's test and coverage evidence no longer matches head, and the coverage figure excludes most of what this round changed.
The body says "33 test files" and "19 of 26 files"; head has 36 test files and 27 sources. It says "220 tests per variant" while the head commit message says 255 green. More importantly,
QuickBuildRuntime*,ResourceStore*andStatusOverlay*are all on this exclusion list, so the quoted 93.2% line / 95.8% branch covers none of thefailReloaddispatch change, theattachedAppResourceslatch, or the observer-capture fix -- four of the five items in the last round. QA reads this section as the evidence ledger REVIEW.md asks for.Refresh the counts and say which of the round's fixes are inside the measured set and which are device-only.