Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -384,6 +385,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 @@ -436,6 +438,69 @@ dependencies {
implementation("io.pebbletemplates:pebble:4.1.1")
}

// 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 ->
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,9 @@ import org.junit.runners.Suite

@RunWith(Suite::class)
@Suite.SuiteClasses(
CleanupTest::class,
EndToEndTest::class,
CleanupTest::class,
EndToEndTest::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