Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
10617dc
ADFA-4128: style: spotless reformat of the build-status layout, no fu…
fryanpan Sep 4, 2026
4f676ea
style: spotless reformat of SaveFileAction.kt, no functional change
fryanpan Sep 4, 2026
06a5c9d
ADFA-4128: qb 11/11 app + bench — wires Quick Build into the IDE and …
fryanpan Aug 21, 2026
b2b04d6
ADFA-4128: qb 11 review fixes — install rotation window, flag-off int…
fryanpan Aug 22, 2026
9bd17e6
ADFA-4128: state the daemon-timeout rationale without a benchmark mul…
fryanpan Aug 24, 2026
49761f7
ADFA-4128 (11/11): address CodeRabbit review
fryanpan Aug 27, 2026
afbbd92
ADFA-4128: manual-QA script fixes from the 22-test device walk
fryanpan Aug 24, 2026
85db996
ADFA-4128: doc corrections from the PR review sweep
fryanpan Aug 24, 2026
067ec88
ADFA-4128: three small UX fixes found by the manual QA walk
fryanpan Aug 24, 2026
c52099e
ADFA-4128: record the v1 blocking decision instead of proposing it
fryanpan Aug 25, 2026
4ab6989
ADFA-4128: qb 12 review fixes - two doc corrections held on the trail…
fryanpan Aug 27, 2026
f39a6cd
ADFA-4128: two review fixes - a repeated clobber prompt and a hard-co…
fryanpan Aug 28, 2026
44e3c3a
ADFA-4128: the CoGo reload-crashed notice carries the same text as th…
fryanpan Aug 29, 2026
dae9a35
ADFA-4128: resolve ProvisioningFailedUnexpectedly to a user-facing st…
fryanpan Sep 1, 2026
7f090ab
ADFA-4128: reliability-gaps #89 is fixed in-stack, not open
fryanpan Sep 1, 2026
cbc3501
ADFA-4128: 0902 review round on the app wiring
fryanpan Sep 3, 2026
6bbef3c
ADFA-4128: stop a re-subscribe re-stomping the bar with the start-fai…
fryanpan Sep 3, 2026
3367c86
ADFA-4128: correct the Koin module's "nothing spawns" claim
fryanpan Sep 3, 2026
77d84ae
ADFA-4128: the status line cannot be read by swiping the sheet up
fryanpan Sep 3, 2026
9c3d7e7
ADFA-4128: style: spotless reformat of QuickBuildStatusBar, no functi…
fryanpan Sep 3, 2026
74672e5
ADFA-4128: delete the requestDowngrade parameter and its plumbing
fryanpan Sep 3, 2026
0671447
ADFA-4128: give DaemonStartFailed the user-facing string it was missing
fryanpan Sep 4, 2026
0587bb9
ADFA-4128: build the Quick Build graph off the main thread
fryanpan Sep 4, 2026
4f5c450
ADFA-4128: pin the failed-start tone to the session's lifetime
fryanpan Sep 4, 2026
1caa3ac
ADFA-4128: size the collapsed sheet header to the status text, not to…
fryanpan Sep 4, 2026
a932769
ADFA-4128: give the swipe hint room inside the collapsed sheet
fryanpan Sep 4, 2026
4cb2fb5
ADFA-4128: keep the collapsed sheet at its earlier height
fryanpan Sep 4, 2026
e8ac84a
ADFA-4128: request a header layout only when the status height changed
fryanpan Sep 4, 2026
83e7611
ADFA-4128: a manifest save runs generateSources again
fryanpan Sep 4, 2026
a69fcaf
ADFA-4128: spell the product "Code on the Go" in the unknown-app dialog
fryanpan Sep 4, 2026
26354f1
ADFA-4128: hand status-line ownership to every writer, the debugger i…
fryanpan Sep 4, 2026
f79118d
ADFA-4128: arm the bench standard-build latch only once the build hol…
fryanpan Sep 4, 2026
81cf76f
ADFA-4128: never fire a bench autostart against another project's wor…
fryanpan Sep 4, 2026
b469770
ADFA-4128: build the Quick Build graph once, off the main thread, and…
fryanpan Sep 4, 2026
96c5207
ADFA-4128: never wipe the daemon directory under a live compile daemon
fryanpan Sep 4, 2026
4755d0b
ADFA-4128: cover the analytics sink's fallbacks, route and outcome names
fryanpan Sep 4, 2026
585bf23
ADFA-4128: adopt the suspend contracts and surface compile diagnostic…
fryanpan Sep 4, 2026
ff58db6
ADFA-4128: lay the collapsed header out inside the window so the swip…
fryanpan Sep 5, 2026
883f71d
ADFA-4128: tidy the stager KDoc and the no-op autostart branch
fryanpan Sep 5, 2026
0bef5ed
ADFA-4128: keep the deferral's own generateSources build out of the h…
fryanpan Sep 8, 2026
4936bda
ADFA-4128: size the header for the status block only while it is the …
fryanpan Sep 8, 2026
28f89c2
ADFA-4128: let an unknown-id consent at tap time cover the id the ins…
fryanpan Sep 8, 2026
41057d1
ADFA-4128: await the clobber confirmations on the tap path
fryanpan Sep 8, 2026
9e5bda4
ADFA-4128: post one Quick Build save event per save operation
fryanpan Sep 8, 2026
68e295e
ADFA-4128: abort a Run whose pre-build save found the editor gone
fryanpan Sep 8, 2026
4065b6c
ADFA-4128: do not count a Quick Build stop tap as a feature use
fryanpan Sep 8, 2026
c07a6b4
ADFA-4128: ASCII ellipsis in the truncated Gradle cause
fryanpan Sep 8, 2026
b5d0ecd
ADFA-4128: pass the rejection to the repeated-connect debug log
fryanpan Sep 8, 2026
8e028fd
ADFA-4128: carry the reinstalled app's uid on a rebuild success
fryanpan Sep 8, 2026
5dc1044
ADFA-4128: manual QA case for Run during the eager proxy-app build
fryanpan Sep 8, 2026
e476d20
ADFA-4128: Run Tasks must not inherit a Run tap's clobber answer
fryanpan Sep 8, 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
65 changes: 65 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.itsaky.androidide.build.config.BuildConfig
import com.itsaky.androidide.desugaring.utils.JavaIOReplacements.applyJavaIOReplacements
import com.itsaky.androidide.plugins.AndroidIDEAssetsPlugin
import com.itsaky.androidide.plugins.tasks.AddFileToAssetsTask
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import org.json.JSONObject
import java.io.BufferedOutputStream
Expand Down Expand Up @@ -387,6 +388,7 @@ dependencies {
implementation(projects.floatingWindow)
implementation(projects.gitCore)
implementation(projects.profiler)
implementation(projects.quickbuild.core)

// This is to build the tooling-api-impl project before the app is built
// So we always copy the latest JAR file to assets
Expand Down Expand Up @@ -440,6 +442,69 @@ dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs.v215)
}

// Quick Build (ADFA-4128): stage the runtime AAR + daemon (jar + runtime classpath)
// into APK assets, mirroring the LogSender AAR flow in AndroidIDEAssetsPlugin. The
// artifacts are extracted to <ANDROIDIDE_HOME>/quickbuild/ at session start
// (QuickBuildArtifactStager).
evaluationDependsOn(":quickbuild:runtime")
evaluationDependsOn(":quickbuild:daemon")

val quickBuildDaemonZip =
tasks.register<Zip>("quickBuildDaemonZip") {
archiveFileName.set("quickbuild-daemon.zip")
destinationDirectory.set(layout.buildDirectory.dir("intermediates/quickbuild"))
val daemonProject = rootProject.project(":quickbuild:daemon")
dependsOn(daemonProject.tasks.named("daemonJar"))
from(daemonProject.tasks.named("daemonJar"))
// The daemon jar's manifest Class-Path names these by file name; they must sit
// next to the jar after extraction.
from(daemonProject.configurations.named("runtimeClasspath"))
// Compose compiler plugin, version-matched to the daemon's compiler; the stable
// name is the contract EnvironmentQuickBuildPaths.composeCompilerPlugin reads.
from(daemonProject.configurations.named("composeCompilerPlugin")) {
rename { "compose-compiler-plugin.jar" }
}
}

androidComponents.onVariants { variant ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMPORTANT: this stages the ~62 MB Quick Build daemon payload into every variant's assets, release included, with no mention of it anywhere in the PR.

androidComponents.onVariants { variant -> ... } has no selector(), so copy<Variant>QuickBuildDaemonZip and copy<Variant>QuickBuildRuntimeAar run for release as well as debug. The zip is the daemon jar plus its entire runtimeClasspath (kotlin-build-tools-impl, the embeddable Kotlin compiler) plus the Compose compiler plugin - quickbuild/daemon/build.gradle.kts puts it at "~62 MB" in its own size audit - and the feature consuming it is opt-in behind FeatureFlags.isExperimentsEnabled. Every user pays the download and the on-device storage; ADR 0005 exists because this project treats APK size as a first-order constraint.

Either gate the two addGeneratedSourceDirectory calls the way line 714 already gates a release-only block, or state the measured release-APK delta in the PR body so the release owner can make the call deliberately. I did not build an APK, so the exact delta is unverified.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured for the body rather than gated. The zip is a byte copy into assets/data/common/: quickbuild-daemon.zip is 61,963,732 bytes, the runtime AAR 95,371; both are in noCompress for release, so the release APK grows by 62.06 MB (59.2 MiB). Gating the copy to debug would break release: FeatureFlags.isExperimentsEnabled is a runtime preference a release user can turn on, and the daemon is what Quick Build runs. Once the flag graduates the payload is the feature's cost either way. The delta and the reasoning are in the PR body for the release owner.

val variantName = variant.name.replaceFirstChar(Char::uppercaseChar)
val flavorName = variant.flavorName!!

val copyRuntimeAar =
tasks.register<AddFileToAssetsTask>("copy${variantName}QuickBuildRuntimeAar") {
val runtimeProject = rootProject.project(":quickbuild:runtime")
dependsOn(
runtimeProject.tasks.named(
"assemble${flavorName.replaceFirstChar(Char::uppercaseChar)}Release",
),
)
inputFile.set(
runtimeProject.layout.buildDirectory.file(
"outputs/aar/quickbuild-runtime-$flavorName-release.aar",
),
)
baseAssetsPath.set("data/common")
// Flavor-agnostic asset name: the runtime AAR is pure Java, both flavors
// produce identical bits, and the stager doesn't need to care.
fileName.set("quickbuild-runtime.aar")
}
variant.sources.assets?.addGeneratedSourceDirectory(
copyRuntimeAar,
AddFileToAssetsTask::outputDirectory,
)

val copyDaemonZip =
tasks.register<AddFileToAssetsTask>("copy${variantName}QuickBuildDaemonZip") {
dependsOn(quickBuildDaemonZip)
inputFile.set(quickBuildDaemonZip.flatMap { it.archiveFile })
baseAssetsPath.set("data/common")
}
variant.sources.assets?.addGeneratedSourceDirectory(
copyDaemonZip,
AddFileToAssetsTask::outputDirectory,
)
}

tasks.register("downloadDocDb") {
doLast {
val githubRepo = "appdevforall/OfflineDocumentationTools"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import org.junit.runners.Suite

@RunWith(Suite::class)
@Suite.SuiteClasses(
CleanupTest::class,
EndToEndTest::class,
CleanupTest::class,
EndToEndTest::class,
QuickBuildPipelineTest::class,
QuickBuildSmokeTest::class,
QuickBuildFlagOffTest::class,
)
class OrderedTestSuite
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.itsaky.androidide

import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.itsaky.androidide.activities.SplashActivity
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import com.itsaky.androidide.app.configuration.IJdkDistributionProvider
import com.itsaky.androidide.helper.isExperimentsFlagSet
import com.itsaky.androidide.helper.setExperimentsFlagForTest
import com.itsaky.androidide.helper.waitForMainHomeOrEditorUi
import com.itsaky.androidide.screens.QuickBuildScreen.assertQuickBuildButtonAbsent
import com.itsaky.androidide.screens.QuickBuildScreen.assertQuickBuildButtonShown
import com.itsaky.androidide.utils.EditorActivityActions
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import org.junit.Test
import org.junit.runner.RunWith

private const val TOOLBAR_TIMEOUT_MS = 15_000L

/**
* The shipping-state gate for Quick Build (ADFA-4128, manual test T13): with no
* `CodeOnTheGo.exp` flag file on the device, the feature must be invisible.
*
* The gate is a single read of [com.itsaky.androidide.utils.FeatureFlags.isExperimentsEnabled]
* at [EditorActivityActions.register], so this drives that decision directly instead of
* restarting the process: flip the flag, re-register, rebuild the toolbar, look. That also
* makes the test honest about what it covers - the registration site, not the process-start
* caching around it.
*
* Both directions run in one test on purpose. An absence assertion alone passes when the
* accessibility selector rots or the toolbar simply never rendered, so the flag-on step
* ahead of it is load-bearing, not decoration.
*
* Runs after [QuickBuildSmokeTest] in [OrderedTestSuite], which leaves the editor open on a
* synced project - this test needs a populated editor toolbar and creates no project of its
* own.
*/
@RunWith(AndroidJUnit4::class)
class QuickBuildFlagOffTest : TestCase() {
private var hadExperimentsFlag = false

@Test
fun test_noExperimentsFlagHidesQuickBuild() =
before {
// A dev device may legitimately have experiments enabled; restore whatever
// state this test found.
hadExperimentsFlag = isExperimentsFlagSet()
IJdkDistributionProvider.getInstance().loadDistributions()
}.after {
setExperimentsFlagForTest(hadExperimentsFlag)
// Leave the toolbar matching the restored flag so a later test does not
// inherit this one's registry.
runCatching { rebuildEditorToolbar() }
}.run {
step("Launch app") {
ActivityScenario.launch(SplashActivity::class.java)
waitForMainHomeOrEditorUi(device.uiDevice)
}

step("Experiments on: the toolbar carries Quick Build") {
setExperimentsFlagForTest(true)
rebuildEditorToolbar()
assertQuickBuildButtonShown(TOOLBAR_TIMEOUT_MS)
}

step("Experiments off: the toolbar drops Quick Build") {
setExperimentsFlagForTest(false)
rebuildEditorToolbar()
assertQuickBuildButtonAbsent(TOOLBAR_TIMEOUT_MS)
}
}

/**
* Re-runs action registration and repopulates the toolbar, which is what an editor
* launch does. On the main thread: both touch the actions registry and the toolbar
* views.
*/
private fun rebuildEditorToolbar() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val activity = resumedEditorActivity()
instrumentation.runOnMainSync {
EditorActivityActions.register(activity)
activity.prepareOptionsMenu()
}
instrumentation.waitForIdleSync()
}

private fun resumedEditorActivity(): EditorHandlerActivity =
device.activities.getResumed() as? EditorHandlerActivity
?: error("Resumed activity is not the editor; this test needs an open project")
}
Loading
Loading