-
-
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
Open
fryanpan
wants to merge
3
commits into
feature/ADFA-4128-qb-03-protocol
from
feature/ADFA-4128-qb-04-runtime
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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
| 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 256 MB payload cap through the default readFully | ||
| // overload; a capped reader legitimately buffers up to the cap before throwing, | ||
| // which overflows 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) | ||
| } |
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" /> | ||
| </application> | ||
|
|
||
| </manifest> | ||
27 changes: 27 additions & 0 deletions
27
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,27 @@ | ||
| 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); | ||
|
|
||
| /** Drop the registration for {@code packageName}, so CoGo stops sending it payloads. */ | ||
| void disconnect(String packageName); | ||
| } |
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.
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.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: appdevforall/CodeOnTheGo
Length of output: 1240
🏁 Script executed:
Repository: appdevforall/CodeOnTheGo
Length of output: 30105
🏁 Script executed:
Repository: appdevforall/CodeOnTheGo
Length of output: 50381
Restrict access to
QuickBuildKeepAliveService.onBind()returns its binder to every caller, and the manifest declares no permission. Any installed app can bind to the service and keep the proxy process out of the cached-app freezer. Authorize only CoGo with a permission or caller check.🤖 Prompt for AI Agents
Uh oh!
There was an error while loading. Please reload this page.
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.
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.