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
6 changes: 6 additions & 0 deletions .github/workflows/analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ jobs:
# the unit-test compile path.
FIREBASE_CONSOLE_URL: ${{ secrets.FIREBASE_CONSOLE_URL }}
GLITCHTIP_DSN: ${{ secrets.GLITCHTIP_DSN }}
# The aapt2/d8/Compose regression tests (ADFA-4128 bugs 5/6/8) are
# assumption-guarded, so on a runner without an Android SDK they would skip
# green and take that coverage with them. This turns an absent toolchain
# into a hard failure instead. The runner does have an SDK - Assemble V8
# Debug above could not run otherwise.
REQUIRE_BUILD_TOOLCHAIN: "1"
run: flox activate -d flox/base -- ./gradlew :testing:tooling:assemble :testing:common:assemble sonarqube --info --no-build-cache -x lint --continue

- name: Upload JaCoCo report
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ sentry.properties
/composite-builds/build-deps/build/

/app/google-services.json
/app/keystore-debug.jks


# Written by the gradle-plugin tests, not by a Gradle task: writeInitScript() in
# gradle-plugin/src/test/.../utils.kt resolves FileProvider.testHomeDir() and creates
Expand Down
19 changes: 11 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil
|---|---|---|
| Application | `app` | The IDE itself — activities, fragments, services, DI, agent, web server. Wires everything together. |
| Build engine | `subprojects:tooling-api*`, `gradle-plugin*`, `subprojects:projects`, `subprojects:builder-model-impl` | Runs a real Gradle build of the user's project out-of-process and streams events back. |
| Quick Build (experimental, ADFA-4128) | `quickbuild:core`, `quickbuild:daemon`, `quickbuild:protocol`, `quickbuild:runtime` | Live-reloads the user's app on every save in seconds, by running it as a generated proxy app instead of doing a full Gradle rebuild. |
| Language tooling | `lsp:{api,java,kotlin,xml,indexing,refactor-core,ui,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. `lsp:refactor-core` holds the language-agnostic half of the refactorings (offset spans, block geometry, rewrite composition, name primitives) so `lsp:java` and `lsp:kotlin` share one copy; `lsp:ui` holds the Compose sheets they share. Neither depends on a language server. |
| UI design tooling | `layouteditor`, `uidesigner`, `xml-inflater`, `vectormaster`, `compose-preview` | Visual/XML design surfaces for the *user's* app. |
| Shell | `termux:{termux-app,termux-shared,termux-view,termux-emulator}` | Embedded Termux shell and terminal. |
Expand All @@ -80,6 +81,7 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil
| Testing | `testing:{android,unit,lsp,tooling,common}` | Shared test harnesses, split by what's under test. |

**Dependency rules (enforced):**

- **`app` depends inward; libraries never depend on `app`.** Subsystems are consumed by `app`, not vice versa.
- **Vendored forks are substituted, not imported ad hoc.** `composite-builds/build-deps` and `build-deps-common` provide forked `javac`/`jdt`/`layoutlib`/etc.; `settings.gradle.kts` substitutes them in for `com.itsaky.androidide.build:*`. Don't add a Maven coordinate for something already substituted.
- **All module config flows through `composite-builds/build-logic`.** Every Android module gets the `v7`/`v8` ABI flavors centrally (`AndroidModuleConf.kt`) — there is no flavorless `assembleDebug`. `:plugin-api` is intentionally excluded from flavors.
Expand All @@ -106,9 +108,9 @@ These structural facts shape every module. Day-to-day build *commands* live in `
| Asynchronous work | **Kotlin Coroutines + Flow** (`StateFlow`/`SharedFlow`, `viewModelScope`, app-scoped `CoroutineScope(SupervisorJob() + Dispatchers.IO)`); **GreenRobot EventBus** for cross-subsystem events. |
| Networking | Offline-first; no general REST layer. External I/O is **Google GenAI SDK** (Gemini), **on-device llama.cpp**, and **JGit** (git). Retrofit is in the catalog but effectively unused in app code. |
| Database / Persistence | **Room** is the default for relational/queryable data; **filesystem + preferences (DataStore)** for non-relational settings. **Raw SQLite** (`SQLiteDatabase` / `SupportSQLiteOpenHelper`) only for justified exceptions (see policy below). |
| Serialization | `kotlinx.serialization` and Gson. |
| Parceling | Kotlin **`@Parcelize`** (`kotlin-parcelize` plugin) for `Parcelable` data classes — never hand-implement `Parcelable`. Do it manually only if `@Parcelize` genuinely can't express it (custom serialization logic, unsupported member types). |
| AI agent | Google GenAI (cloud) + llama (local), behind `GeminiRepository` / `SwitchableGeminiRepository`, with planner/critic/executor agents in `agent/repository`. |
| Serialization | `kotlinx.serialization` and Gson. |
| Parceling | Kotlin **`@Parcelize`** (`kotlin-parcelize` plugin) for `Parcelable` data classes — never hand-implement `Parcelable`. Do it manually only if `@Parcelize` genuinely can't express it (custom serialization logic, unsupported member types). |
| AI agent | Google GenAI (cloud) + llama (local), behind `GeminiRepository` / `SwitchableGeminiRepository`, with planner/critic/executor agents in `agent/repository`. |

> **Persistence policy (authoritative):** new relational/queryable persistence uses **Room** (`@Entity` + DAO + `RoomDatabase` with explicit migrations, provided via Koin). Non-relational settings use the **filesystem/preferences (DataStore)**. **Raw SQLite is the exception, not the default** — see [ADR 0001](docs/adr/0001-prefer-room-for-persistence.md).
>
Expand Down Expand Up @@ -196,13 +198,14 @@ fun onEvent(event: PluginManagerUiEvent) = viewModelScope.launch(Dispatchers.IO)

Test code lives both alongside each module and in the shared `testing:{unit,android,lsp,tooling,common}` harnesses. Run with the flox wrapper, e.g. `flox activate -d flox/local -- ./gradlew :testing:unit:test` or a module's `:module:test --tests "…"`.

| Layer | Runner / Tools | What to test |
|---|---|---|
| Unit (pure JVM) | **JUnit Jupiter (5)**, some legacy **JUnit 4**; assertions via **Google Truth**; mocking via **MockK** (primary) and **Mockito-Kotlin** (legacy) | ViewModels (state transitions over a fake repository), repositories, parsers, builder/tooling logic. Keep these off the device. |
| JVM + Android framework | **Robolectric** | Code needing `Context`/resources/`SQLiteOpenHelper` without an emulator. |
| Instrumented / UI | **Espresso** + **AndroidX Test** + **UiAutomator**, run under **Test Orchestrator**; `mockk-android` for on-device mocks | End-to-end IDE flows (create/build/deploy, editor, terminal). |
| Layer | Runner / Tools | What to test |
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Unit (pure JVM) | **JUnit Jupiter (5)**, some legacy **JUnit 4**; assertions via **Google Truth**; mocking via **MockK** (primary) and **Mockito-Kotlin** (legacy) | ViewModels (state transitions over a fake repository), repositories, parsers, builder/tooling logic. Keep these off the device. |
| JVM + Android framework | **Robolectric** | Code needing `Context`/resources/`SQLiteOpenHelper` without an emulator. |
| Instrumented / UI | **Espresso** + **AndroidX Test** + **UiAutomator**, run under **Test Orchestrator**; `mockk-android` for on-device mocks | End-to-end IDE flows (create/build/deploy, editor, terminal). |

Preferences and conventions:

- **Assertions: Google Truth** (`assertThat(x).isEqualTo(...)`) over raw JUnit asserts.
- **Mocking: MockK** for new code; relax it deliberately rather than over-stubbing.
- For UDF ViewModels, drive `onEvent(...)`/method calls against a fake or mocked repository and assert the emitted `UiState` sequence (collect the `StateFlow`); assert effects by collecting the effect `SharedFlow`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ class GradleBuildService :
internal fun startToolingServer(listener: OnServerStartListener?) {
if (toolingServerRunner?.isStarted != true) {
val envs = TermuxShellEnvironment().getEnvironment(this, false)
toolingServerRunner = ToolingServerRunner(listener, this).also { it.startAsync(envs) }
toolingServerRunner = ToolingServerRunner(listener, this, this).also { it.startAsync(envs) }
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

package com.itsaky.androidide.services.builder

import android.content.Context
import com.itsaky.androidide.logging.provider.IdeLogRouter
import com.itsaky.androidide.managers.ToolsManager
import com.itsaky.androidide.tasks.cancelIfActive
import com.itsaky.androidide.tasks.ifCancelledOrInterrupted
import com.itsaky.androidide.tooling.api.IToolingApiClient
Expand Down Expand Up @@ -47,6 +49,7 @@ import kotlin.time.Duration.Companion.seconds
internal class ToolingServerRunner(
private var listener: OnServerStartListener?,
private var observer: Observer?,
private val context: Context,
) {
internal var pid: Int? = null
private var job: Job? = null
Expand Down Expand Up @@ -105,6 +108,15 @@ internal class ToolingServerRunner(
var process: Process?
try {
log.info("Starting tooling API server...")
// The bundled jar is extracted asynchronously at app init, and nothing else
// orders that against this launch. On an APK update the PREVIOUS install's jar
// still sits at the final path until that extraction renames over it, so
// launching first would run the prior APK's tooling server for the whole
// session. This is stamp-guarded and idempotent - a no-op once init has done
// it, the extraction itself when it has not - and we are on Dispatchers.IO.
if (!ToolsManager.ensureToolingJar(context)) {
log.error("Tooling API jar is not from this install; the server may misbehave")
}
val command =
listOf(
Environment.JAVA.absolutePath, // The 'java' binary executable
Expand Down
5 changes: 4 additions & 1 deletion build-info/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ tasks.create("generateBuildInfo") {
"AGP_VERSION_LATEST" to
libs.versions.agp.tooling
.get(),
"AGP_VERSION_GRADLE_LATEST" to "8.6", // From SdkConstants.GRADLE_LATEST_VERSION
// The Gradle version AGP_VERSION_LATEST gets exercised against: the
// distribution the IDE bundles. 8.6 was stale - AGP 8.11 refuses to
// configure on anything older than 8.13.
"AGP_VERSION_GRADLE_LATEST" to "8.14.3",

@itsaky-adfa itsaky-adfa Aug 24, 2026

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.

We're in the process of upgrading this to AGP 9+. Given this PR stack of 11 PRs, I guess that change would land first before this stack. Since it would be a major version upgrade, what changes would we need for Quick Build?

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.

Thanks — we're already working on a branch that merges this stack with Daniel's stack. If the AGP 9 upgrade lands first, we can add one more PR to the end of this stack to pick it up.

One data point, offered as a data point rather than a guarantee: Quick Build has executed against AGP 9.3.1 — a full benchmark pass ran from a branch pinned to it. That tells us it runs there; it is not a compatibility audit, and we have not enumerated what AGP 9 changes about the specific APIs the Quick Build Gradle plugin depends on. Happy to do that properly once the upgrade path is settled.

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.

Answered above on Aug 24: if the AGP 9 upgrade lands first, this stack gets one more PR at the end. Nothing changes here. The piece still open is enumerating what AGP 9 changes about the specific APIs the Quick Build Gradle plugin uses - that is owned by #1722 (gradle-plugin/**), and I am tracking it there.

"SNAPSHOTS_REPOSITORY" to VersionUtils.SONATYPE_SNAPSHOTS_REPO,
"PUBLIC_REPOSITORY" to VersionUtils.SONATYPE_PUBLIC_REPO,
),
Expand Down
Loading
Loading