diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ffa7ea7260 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +Code On The Go is an Android IDE — it lets users edit, build, and deploy their own Android apps on-device, like Eclipse or VSCode. + +There is at least one Android emulator available. Find it with `adb devices -l | grep -v offline`, then use the `ANDROID_SERIAL` env var. + +For new persistence, use SQLite or the filesystem — never add Room. (See ARCHITECTURE.md for the full model and the one legacy exception.) + +Avoid adding dependencies — we almost certainly already have what you need. Check `build.gradle.kts`. + +Plan before building, and size the change (files + LOC). If it will exceed 500 LOC or 10 files, split it into 2+ change sets so the user can land them as separate PRs. + +When you change code, update the docs that describe it in the same change — a module's `README.md`, `ARCHITECTURE.md`, or an ADR — so a doc never outlives the API it documents. See REVIEW.md (Code quality). If the doc fix is out of scope, file a ticket rather than let it drift. + +Keep docs, tickets, commit messages, and PR descriptions crisp — say it once, lead with the point, cut hedging and restated context. Brevity is the soul of wit; a reader's attention is the scarce resource. + +Never draw our UI over the two Android system bars: the top status bar (clock, notifications, status icons) and the bottom navigation bar (home, back, recents). + +## Official/Public Actions Run in CI, Not Locally + +Anything official or public-facing runs only through version-controlled GitHub Actions (`.github/workflows/*.yml`), never locally — SonarQube/SonarCloud uploads, releases, artifact publishing, deploys, pushes to external services. Tokens like `SONAR_TOKEN` are GitHub secrets scoped to those workflows; don't hunt for them locally. Asked to run e.g. the sonar task locally, treat it as verification only (build/test to confirm a fix) and let the official analysis happen in CI. + +## Reading Jira Tickets + +Read tickets with the local authenticated `jira` CLI (e.g. `jira issue view ADFA-1234`), configured via `JIRA_API_TOKEN`, `JIRA_HOST`, and `JIRA_USER`. Don't start the Atlassian MCP OAuth flow for reads — it's unnecessary when the CLI works. + +## SonarQube MCP Server + +The sonarqube MCP server runs in Docker, so Docker must be up before launching Claude Code. Its first launch pulls a ~225MB image (`mcp/sonarqube:latest`) that exceeds Claude Code's 30s MCP handshake timeout — so the first connect reports a timeout though nothing is broken. Pre-pull the image (or let one launch finish) so later `/mcp` reconnects succeed. `docker system prune` removes it and brings back the slow first launch. + +## Resolving CI Job Names + +When the user names a CI/CD job ("the sonar job", "the analyze workflow"), read `.github/workflows/*.yml` — the YAML is the authoritative gradle/shell invocation. Don't reverse-engineer it from gradle tasks or build files. + +## Multi-line git/gh Messages + +Default to writing the body to a tempfile via the Write tool, then `git commit -F /tmp/msg.txt` or `gh pr create --body-file /tmp/body.md`. Use heredoc/`--body "$(cat < Audience: engineers working in this repository. This describes how the code is *actually* organized today, including where the codebase is mid-migration. Patterns that are the agreed target for new code are called out explicitly. + +## Overview + +Code On The Go (CoGo) is a full Android IDE that runs **on the device** — it edits, builds, and deploys real Android apps offline, embedding a Termux toolchain and running an actual Gradle build in a separate process via the `tooling-api`. It is the maintained successor to AndroidIDE, so the codebase namespace is still `com.itsaky.androidide`. + +There is **no single architectural philosophy** across the whole app. This large, layered application is still **predominantly View-based**: newer feature surfaces (plugin manager, AI agent, git, project list) follow a deliberate **Unidirectional Data Flow (UDF)** with Koin DI, `ViewModel` + `StateFlow`, sealed UI-state/effect types, and repositories, while older surfaces still use `LiveData` and talk to GreenRobot EventBus directly. New work follows the UDF pattern documented below, and new UI is built in **Jetpack Compose** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)) — Compose replaces the view layer only; the UDF stack (ViewModel + `StateFlow`, Koin, repositories) is unchanged. Existing XML/View screens remain until substantially reworked. + +## Core Architecture & Data Flow + +Feature code layers as **UI → ViewModel → Repository → data source**, with state flowing up and events/intents flowing down. Koin provides dependencies (`coreModule`, `pluginModule`), constructor-injected into ViewModels. + +- **Data sources** — Room (`RecentProjectRoomDatabase` + DAO, `suspend` functions), raw SQLite (`SQLiteOpenHelper`, e.g. `localWebServer/WebServer`), the filesystem/preferences, the embedded `tooling-api` (on-device Gradle), and external clients (Gemini via the Google GenAI SDK, on-device llama.cpp, JGit). Most are exposed through `suspend` functions. +- **Repositories** — e.g. `agent/repository/GeminiRepository`, `repositories/PluginRepository`, `repositories/BreakpointRepository`. They wrap data sources and hide threading/IO from the ViewModel. +- **ViewModels** — run work in `viewModelScope` on `Dispatchers.IO`, hold a private `MutableStateFlow`/`MutableSharedFlow`, and expose read-only `StateFlow`/`SharedFlow`. One-shot effects (toasts, navigation, dialogs) go through a separate `SharedFlow` of a sealed `*UiEffect` type. +- **UI (Fragments / Activities / Views)** — collect state in a lifecycle-aware coroutine and render it; user actions return to the ViewModel as method calls or sealed `*UiEvent` intents. The existing UI is **Android Views + Fragments + RecyclerView adapters**; new UI is Jetpack Compose ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)). (`compose-preview` previews the *user's* Compose code, not CoGo's own.) + +``` + ┌─────────────────────────────────────────────┐ + intents ↓ │ UI LAYER │ ↑ state + (UiEvent / │ Activity / Fragment / RecyclerView.Adapter │ (StateFlow) + method call) │ collects StateFlow, renders; emits intents │ ↑ effects + └───────────────┬─────────────────────────────┘ (UiEffect SharedFlow) + │ + ┌───────────────▼─────────────────────────────┐ + │ VIEWMODEL LAYER │ + │ viewModelScope + Dispatchers.IO │ + │ private MutableStateFlow ──► StateFlow │ + │ private MutableSharedFlow ─► UiEffect flow │ + │ sealed UiState / UiEvent / UiEffect / State │ + └───────────────┬─────────────────────────────┘ + │ suspend calls + ┌───────────────▼─────────────────────────────┐ + │ REPOSITORY LAYER │ + │ GeminiRepository / PluginRepository / ... │ + └───────────────┬─────────────────────────────┘ + │ + ┌───────────────────────┼────────────────────────────────────┐ + ▼ ▼ ▼ + ┌───────────┐ ┌───────────────┐ ┌──────────────────┐ + │ Room / │ │ tooling-api │ │ External clients │ + │ SQLite / │ │ (on-device │ │ Gemini · llama │ + │ FS / prefs│ │ Gradle build)│ │ · JGit │ + └───────────┘ └───────────────┘ └──────────────────┘ + + Cross-cutting: GreenRobot EventBus carries decoupled, app-wide events + (build progress, install results, editor signals) outside the UDF spine. +``` + +**EventBus is a deliberate side-channel.** Long-running, cross-module signals (build/install lifecycle, editor events) are broadcast via GreenRobot EventBus (`@Subscribe(threadMode = ThreadMode.MAIN)`) and the `eventbus-events` module's shared event types. Treat it as the integration bus *between* subsystems; don't use it to replace a ViewModel's own state inside a single screen. + +## Module Structure + +Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle build has ~80 modules (`settings.gradle.kts`) plus three included composite builds. `app` is the integration point; the rest are libraries it composes. + +| Group | Modules | Responsibility | +|---|---|---| +| 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. | +| Language tooling | `lsp:{api,java,kotlin,xml,indexing,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. | +| 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. | +| Plugin system | `plugin-api`, `plugin-api:plugin-builder`, `plugin-manager` | In-app plugin SDK + manager (`plugin.json` manifest, permissions, extensions). | +| On-device AI | `llama-api`, `llama-impl` | llama.cpp integration, shipped as a per-flavor native AAR. | +| Cross-cutting | `eventbus`, `eventbus-android`, `eventbus-events`, `common`, `common-ui`, `logger`, `resources`, `preferences`, `shared` | Shared infra and the event bus. | +| 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. +- **`RepositoriesMode.FAIL_ON_PROJECT_REPOS`** — repositories are declared once in `settings.gradle.kts`; modules must not declare their own. +- **Avoid new dependencies.** Check `gradle/libs.versions.toml` first; the build almost certainly already has it. + +## Build & Module Configuration + +These structural facts shape every module. Day-to-day build *commands* live in `CLAUDE.md`; the rules that produce them live here. + +- **Centralized convention logic.** All Android module setup flows through `composite-builds/build-logic` (`conf/AndroidModuleConf.kt`). Modules stay thin and share configuration, so understanding any module's setup starts here. +- **ABI product flavors.** Every Android module *except* `:plugin-api` gets two flavors on the `abi` dimension — `v7` (`armeabi-v7a`) and `v8` (`arm64-v8a`) — defined centrally. There is no flavorless variant; tasks are `assembleV8Debug`, `assembleV7Release`, etc. +- **SDK levels** (`build-logic/.../build/config/BuildConfig.kt`): `COMPILE_SDK=36`, `MIN_SDK=28`, `TARGET_SDK=28`. `MIN_SDK_FOR_APPS_BUILT_WITH_COGO=16` is the floor for the apps a *user* builds with CoGo — distinct from CoGo's own `MIN_SDK`. +- **Native asset bundling.** The on-device LLM (`llama-impl`) ships as a per-flavor native AAR, wired through the root `build.gradle.kts` (`bundleLlamaV8Assets` / `assembleV8Assets`, …); prebuilt per-flavor assets live under `assets/release/v7/` and `assets/release/v8/`. +- **`app` package layout is by concern, not feature:** `activities`, `fragments`, `services`, `di`, `agent`, `viewmodel(s)`, `repositories`, `roomData`, `localWebServer`, `preferences`, `ui`, `utils`, …. + +## Technology Stack + +| Concern | Library / Approach | +|---|---| +| UI | **Jetpack Compose for all new UI** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)). The existing majority is still Android Views + Fragments + `RecyclerView` (Material Components); those legacy screens stay until reworked, but new IDE UI is Compose-only. | +| Dependency Injection | **Koin** (`org.koin`) — `coreModule`/`pluginModule`, `startKoin` in `IDEApplication`, plus a `ServiceLocator : KoinComponent` for lazy post-startup access. No Hilt/Dagger. | +| 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 | **Raw SQLite** (`SQLiteOpenHelper` / `SQLiteDatabase`) and **filesystem + preferences** for almost everything. **Room** survives in exactly one place — the Recent Projects feature (see policy below). | +| Serialization | `kotlinx.serialization` and Gson. | +| AI agent | Google GenAI (cloud) + llama (local), behind `GeminiRepository` / `SwitchableGeminiRepository`, with planner/critic/executor agents in `agent/repository`. | + +> **Persistence policy (authoritative):** new persistence must use **raw SQLite or the filesystem/preferences — not Room.** Do not add Room entities or extend it to new tables. +> +> Room is used in exactly **one** place: the **Recent Projects** feature in the `app` module — `app/src/main/java/com/itsaky/androidide/roomData/recentproject/` (`RecentProjectRoomDatabase`, `@Database version = 4` with migrations 1→4; `RecentProjectDao`; the `RecentProject` `@Entity` → table `recent_project_table`). It's provided via Koin in `di/AppModule.kt` and consumed by `MainViewModel`, `RecentProjectsViewModel`, `MainActivity`, `ProjectInfoBottomSheet`, and `ProjectCreationManager`. +> +> Two things look like Room but aren't: `idetooltips` declares the Room Gradle deps but doesn't use them (its tooltip store is raw `SQLiteDatabase` in `ToolTipManager`), and the Room coordinates in `editor`'s `GroovyAutoComplete` are autocomplete suggestions for the *user's* code, not CoGo's own usage. + +## State Management + +- **UI state is a single immutable `data class`** exposed as a `StateFlow<…UiState>`; the ViewModel mutates a private `MutableStateFlow` via `update { it.copy(...) }`. Derived booleans live as computed properties on the state class (so the UI stays dumb). +- **One-shot effects** (errors, navigation, dialogs, restart prompts) are modeled as a **sealed `…UiEffect`** emitted through a separate `SharedFlow` — never folded into the persistent state, so they don't replay on rotation. +- **Inbound intents** are a sealed `…UiEvent` (or direct ViewModel method calls on older screens). +- **Process/long-task state** uses dedicated sealed hierarchies — e.g. `BuildState`, `TaskState`, `InstallationState`, `ApkInstallationViewModel.SessionState`, `agent/AgentState`. +- **Legacy screens** still expose `LiveData` (~8 ViewModels) instead of `StateFlow` (~20). When touching one substantially, prefer migrating it to `StateFlow`. + +Real example from `ui/models/PluginManagerUiState.kt` — copy this pattern for new screens: + +```kotlin +// Persistent, immutable UI state — exposed as StateFlow +data class PluginManagerUiState( + val isLoading: Boolean = false, + val plugins: List = emptyList(), + val isPluginManagerAvailable: Boolean = false, + val isInstalling: Boolean = false, +) { + val isEmpty: Boolean get() = plugins.isEmpty() && !isLoading // derived in state + val showEmptyState: Boolean get() = isEmpty && isPluginManagerAvailable +} + +// Inbound intents from the UI +sealed class PluginManagerUiEvent { + object LoadPlugins : PluginManagerUiEvent() + data class EnablePlugin(val pluginId: String) : PluginManagerUiEvent() + data class InstallPlugin(val uri: Uri, val deleteSourceAfterInstall: Boolean) : PluginManagerUiEvent() + // ... +} + +// One-shot effects — emitted via a SharedFlow, not stored in UiState +sealed class PluginManagerUiEffect { + data class ShowError(@StringRes val messageResId: Int, val formatArgs: List = emptyList()) : PluginManagerUiEffect() + object ShowRestartPrompt : PluginManagerUiEffect() + // ... +} +``` + +Typical ViewModel wiring: + +```kotlin +private val _uiState = MutableStateFlow(PluginManagerUiState()) +val uiState: StateFlow = _uiState.asStateFlow() + +private val _effects = MutableSharedFlow() +val effects: SharedFlow = _effects.asSharedFlow() + +fun onEvent(event: PluginManagerUiEvent) = viewModelScope.launch(Dispatchers.IO) { + when (event) { + PluginManagerUiEvent.LoadPlugins -> { + _uiState.update { it.copy(isLoading = true) } + val plugins = repository.loadPlugins() + _uiState.update { it.copy(isLoading = false, plugins = plugins) } + } + // ... + } +} +``` + +## Testing Guidelines + +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). | + +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`. +- On the M2 emulator, prefer short `flakySafely` timeouts (2–3s) and `AccessibilityNodeInfo.ACTION_CLICK` over raw coordinate taps — the bottom system-bar region swallows coordinate clicks, and tests must never obstruct the two system bars (top status, bottom nav). +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..4deab70079 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md + +Guidance for Claude Code (claude.ai/code) in this repository. + +## What this is + +Code On The Go (CoGo) is an Android IDE that runs *on the device* — edit, build, and deploy real Android apps from a phone, offline. It's the maintained successor to AndroidIDE, so the Gradle/AGP namespace stays `com.itsaky.androidide` even though the product is "Code On The Go". A bundled Termux provides the shell/toolchain; a `tooling-api` runs a real Gradle build inside the app. + +`AGENTS.md` holds the operational rules (branch naming, Jira CLI, SonarQube MCP, session/push protocol) — read it; this file doesn't duplicate it. + +## Build & test + +Wrap every Gradle invocation in `flox` for the right toolchain: + +```bash +flox activate -d flox/local -- ./gradlew +``` + +- **Debug APK (arm64, the usual target):** `flox activate -d flox/local -- ./gradlew :app:assembleV8Debug --parallel --max-workers=6` +- **Single unit test:** `flox activate -d flox/local -- ./gradlew :module:test --tests "com.itsaky.androidide.SomeTest"` +- **Module unit tests:** `flox activate -d flox/local -- ./gradlew :testing:unit:test` + +For a CI/CD job named by the user ("the sonar job", "the analyze workflow"), `.github/workflows/*.yml` is the authoritative command — don't reverse-engineer it from Gradle tasks. Official/public actions (Sonar upload, releases, deploys) run only in CI; local runs are verification only. + +### ABI product flavors (v7 / v8) + +Every module carries `v7` (`armeabi-v7a`) and `v8` (`arm64-v8a`) flavors, so build tasks are `assembleV8Debug`, `assembleV7Release`, etc. — there is **no** flavorless `assembleDebug`. See [ARCHITECTURE.md](ARCHITECTURE.md) (Build & Module Configuration) for flavors, native asset bundling, and SDK levels. + +## Architecture + +See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for the module map, layering/data flow, dependency rules, tech stack (DI, async, persistence, networking), state management, and testing strategy. Don't re-document those here; update ARCHITECTURE.md. + +## Project-specific constraints + +- **Avoid new dependencies** — the build almost certainly already has what's needed. Check `gradle/libs.versions.toml` and `build.gradle.kts` first. +- **Protect the two Android system bars** in any UI work: the top status bar (clock, notifications, status icons) and the bottom navigation bar (home, back, recents). Don't draw over or intercept them. +- **Plan and size before building.** Estimated >500 LOC or >10 files → split into 2+ PRs. +- `.androidide_root` is a sentinel file tests use to locate the project root — don't delete it. + +## Code style + +2-space indents everywhere. Java: Google style (`google-java-format`); Kotlin: `ktfmt` Google-internal style; XML: Android Studio formatter. Branch names must match `.../ADFA-#####` (3–5 digits) — see CONTRIBUTING.md; a pre-commit hook enforces it (`sh ./scripts/install-git-hooks.sh`). diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000000..aa24a62247 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,174 @@ +# Code Review Guide + +How to give a good review on Code On The Go. This is a coaching doc, not a gate — use judgment, explain the *why*, and prefer a concrete suggestion (or a diff) over "this is wrong." It complements, and does not replace, [ARCHITECTURE.md](ARCHITECTURE.md) (the source of truth for patterns) and the operational rules in `AGENTS.md` / `CLAUDE.md`. + +## How to use this + +- **Author:** self-review against this list *before* requesting review. Most of it you can check in five minutes. +- **Reviewer:** you own correctness, leaks, security, and tests. Don't rubber-stamp; don't bikeshed style the formatter already enforces (`ktfmt` / `google-java-format`, 2-space indents). +- Tie every blocking comment to a concrete risk (a crash, a leak, a CVE class, an untested branch). Tag non-blocking polish as **nit:** so the author can triage. + +## The 60-second checklist + +- [ ] **Exceptions** are handled locally — nothing unexpected reaches the global Sentry crash handler. +- [ ] **No leaks** LeakCanary would catch later: every register/open/subscribe has a matching unregister/close in the right lifecycle callback. +- [ ] **No main-thread disk/network I/O** — no new StrictMode violations, and no whitelisting of *our own* code. +- [ ] **Security:** untrusted input (zip entries, URLs, file paths, web-server requests) is validated; no secrets in code, logs, or analytics. +- [ ] **Tests:** non-UI logic has unit coverage. Where coverage is thin, there's logging to diagnose it in the field. +- [ ] **No duplication:** the change reuses existing helpers instead of copy-pasting. +- [ ] **Docs:** public classes/functions have KDoc/Javadoc explaining *why*, not *what*; any module `README`/`ARCHITECTURE.md`/ADR the change affects is updated in the same PR. +- [ ] **Strings** are in `strings.xml`, not inline literals. +- [ ] **Accessibility:** every actionable view has a `contentDescription` (XML *or* programmatic); decorative views are marked `importantForAccessibility="no"`. +- [ ] **Contextual help:** new interactive elements (and any new screen/panel) have long-press help wired to the 3-tier tooltip system. +- [ ] **Analytics:** meaningful user/build actions emit an event (see below). +- [ ] **Scope/size:** PR is focused and within the ~500 LOC / 10-file guideline (`AGENTS.md`). + +--- + +## 1. Exception handling — stay out of the Sentry crash wrapper + +`IDEApplication` installs a global uncaught-exception handler (`handleUncaughtException`) that reports to **Sentry** and then runs the device/credential-protected loaders' handlers. An exception that escapes your code lands there and is recorded as a **crash**. That handler is a safety net, not a control-flow tool. + +- **Catch where you can recover.** Wrap I/O, parsing, IPC to the `tooling-api`, git, and plugin calls. Convert failures into a sealed error state (`…UiEffect.ShowError`, `Result`, `BuildState.Failed`) the UI can render. +- **Never swallow silently.** A bare `catch (e: Exception) {}` hides bugs. At minimum log it; if it's notable-but-handled, report it explicitly with the established idiom: + ```kotlin + } catch (e: IOException) { + log.error("Failed to install APK for {}", projectName, e) + Sentry.captureException(e) // handled, but we want visibility + _uiState.update { it.copy(error = e.toUserMessage()) } + } + ``` +- **Catch narrowly.** Prefer specific types over `Throwable`/`Exception`. Don't catch `CancellationException` in coroutines — rethrow it so structured cancellation works. +- **Coroutines:** an uncaught exception in a `launch` propagates to the scope's handler. Handle it inside the coroutine; don't rely on the crash wrapper to mop up. + +## 2. Memory & resource leaks — fix them before LeakCanary does + +LeakCanary runs in debug builds (`debugImplementation`), so leaks *will* surface — catch them in review first. Common offenders here: + +- **EventBus:** every `EventBus.getDefault().register(this)` needs an `unregister(this)` in the symmetric lifecycle callback (`onStart`/`onStop`, `onResume`/`onPause`). Missing unregister = leak **and** stale-event bugs. +- **Context:** don't hold an `Activity`/`Fragment`/`View` context in a `single` (Koin), a long-lived object, or a companion. Use `applicationContext` for anything outliving a screen. +- **Listeners/receivers/observers:** `BroadcastReceiver`, `ServiceConnection`, `ContentObserver`, editor/terminal callbacks — unregister them. We have real examples (`AppLogsCoordinator`, the log service connection); follow that lifecycle. +- **Closeables:** files, cursors, streams, the `tooling-api` connection — use `use {}` or close in `finally`. +- **Coroutines:** launch in `viewModelScope` / `lifecycleScope`, never `GlobalScope`. A scope tied to a destroyed component must be cancelled. +- **Bitmaps/large buffers** (APK viewer, image-to-XML, previews): release them; don't cache unbounded. + +## 3. Threading & StrictMode — don't whitelist our own sins + +The app runs a real StrictMode policy via `StrictModeManager` with a **whitelist engine**. Project rule (see learnings/memory): **the whitelist is only for vendor/framework code we can't change — never for app-owned violations.** If your code trips StrictMode, fix the code. + +- Move disk and network I/O off the main thread (`Dispatchers.IO`, `withContext`). No DB reads, file reads, or `SharedPreferences` first-access on the UI thread. +- Don't reach for `allowThreadDiskReads()` / `permitAll()` to silence a violation in our code. +- Touch UI only on the main thread; post results back via `StateFlow`/`withContext(Dispatchers.Main)`. + +## 4. Security — OWASP Top Ten for a mobile IDE + +This app extracts archives, runs a local web server, stores git credentials and signing keys, and executes builds — so the attack surface is real. Review accordingly: + +- **Injection / path traversal (Zip Slip):** template/project extraction (`ZipRecipeExecutor`) and any unzip must reject entries that resolve outside the target dir (`canonicalPath.startsWith(targetDir)`). Validate file paths built from user/project input. +- **SQL:** use parameterized queries (`rawQuery(sql, args)` with `?` placeholders), never string-concatenated SQL. (Existing `WebServer`/tooltip queries already do this — match them.) +- **Secrets & credential storage:** git tokens, keystore/signing passwords → `EncryptedSharedPreferences` / the Android Keystore, never plaintext files, never committed, **never logged or sent to analytics/Sentry**. Scrub secrets from breadcrumbs and exception messages. +- **Local web server (`WebServer`):** bind to loopback, scope what it serves, and don't reflect unsanitized input into responses. Treat every request as untrusted. +- **Network:** HTTPS only; no disabled TLS/hostname verification; verify git remotes. +- **Untrusted code/plugins:** respect the `plugin.json` permission model; don't widen plugin capabilities or load classes from untrusted sources without the manager's checks. +- **Deserialization & intents:** validate parsed JSON (Gson/kotlinx) and incoming `Intent`/deep-link extras; don't trust their shape. +- **Logging leaks:** PII, file contents, tokens, and full request bodies don't belong in logs. + +## 5. Tests & coverage + +**If the code is not purely UI, expect unit tests in the same PR.** ViewModels, repositories, parsers, builder/tooling logic, and security-sensitive helpers are all testable off-device. + +- Test the *behavior*: drive `onEvent(...)` / methods against a fake or MockK'd repository and assert the emitted `UiState` sequence (and effects via the effect `SharedFlow`). See ARCHITECTURE.md → Testing. +- Tools: **JUnit (Jupiter)**, **Truth** assertions, **MockK** (new) / Mockito-Kotlin (legacy), **Robolectric** for framework-dependent JVM tests. Live in the module or the shared `testing:*` harnesses. +- Cover the **error and edge paths**, not just the happy path — those are exactly what the crash wrapper would otherwise catch in production. +- Pure-UI changes (layout, view wiring) may legitimately have no unit test; say so in the PR description so the reviewer isn't guessing. + +### When coverage is unavoidably low, add logging +Some code is genuinely hard to unit-test (IPC with the `tooling-api`, native/llama boundaries, terminal/PTY, real device I/O). When you can't test a non-trivial branch, **instrument it** so it's diagnosable in the field — log decision points and failure modes via SLF4J: +```kotlin +private val log = LoggerFactory.getLogger(MyClass::class.java) +... +log.info("Selected build strategy {} for project {}", strategy, projectName) +log.warn("Falling back to {} after {}", fallback, reason) +``` +Logging is a **stopgap that buys observability, not a substitute for a test.** Prefer a test where one is feasible. + +## 6. Observability — logging & analytics + +**Logging:** use SLF4J (`LoggerFactory.getLogger(Class::class.java)`), not `android.util.Log`. Right level (`debug` for flow, `info` for milestones, `warn`/`error` for problems), structured `{}` placeholders, and pass the throwable as the last arg (don't `"$e"`). No secrets/PII (see §4). + +**Firebase Analytics:** events go through the injected `IAnalyticsManager` (Koin `single`), not `FirebaseAnalytics` directly. When a PR adds a meaningful user or build action, ask whether it deserves an event. Good candidate locations: + +| Where | Call | +|---|---| +| Screen/fragment shown | `trackScreenView(name)` | +| Project created / opened / cloned | `trackProjectOpened(path)` | +| A feature is actually used (editor action, layout designer, plugin install, AI agent run) | `trackFeatureUsed(name)` | +| Build started / completed / strategy chosen | `trackBuildRun(...)`, `trackBuildCompleted(...)`, `trackGradleStrategySelected(...)` | +| Recoverable, notable error | `trackError(type, message)` | + +Keep event names/params stable and low-cardinality; **no PII, file paths with usernames, or secrets** in event payloads. + +## 7. Code quality + +- **No duplication.** If you copy-pasted a block, extract a function/extension into the right `common`/`utils` module. Before adding a helper, grep — we likely already have it. Repeated literals/magic numbers → named constants. +- **Docstrings.** Public classes, functions, and non-obvious logic get KDoc/Javadoc. Document the *contract and the why* (threading expectations, nullability, side effects, units), not a restatement of the signature. +- **Keep docs in sync with the code.** If a change alters a module's public API, commands, or behavior that's described in its `README.md` (or `ARCHITECTURE.md` / an ADR), update that doc in the *same* PR. A doc that documents a non-existent API is worse than no doc. If the doc-fix is genuinely out of scope, leave a tracked note (a ticket) rather than silently letting it drift. +- **Strings in `strings.xml`.** User-facing text must be a string resource, never an inline literal — lint flags `HardcodedText`, and externalized strings feed our Crowdin translation flow. Use plurals/`getQuantityString` and positional args for formatting. Log messages and analytics keys are *not* user-facing and stay in code. +- **Dependencies:** don't add one without checking `gradle/libs.versions.toml` first — we probably already have it (`AGENTS.md`). + +## 8. Accessibility — every actionable view speaks + +CoGo serves visually-impaired developers, so TalkBack support is a correctness requirement, not a nice-to-have (pattern set by ADFA-2667). New UI is Compose ([§10](#10-architecture-alignment) / [ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)), so each rule gives the View and Compose form — the requirement is the same in either. + +- **Label every actionable view.** Buttons, `ImageButton`s, and icon-only controls need a `contentDescription`. + - *View:* `android:contentDescription="@string/cd_…"`; an unlabeled icon button reads as "unlabeled button". + - *Compose:* pass `contentDescription` on `Icon`/`Image`; for a custom clickable or `IconButton` (whose inner `Icon` is usually `null`), label the control via `Modifier.semantics { contentDescription = … }`. +- **Don't forget elements built in code.** + - *View:* the miss is anything *not* in XML — toolbar actions, RecyclerView rows, inflated buttons. Set `contentDescription` when you create them (see `EditorHandlerActivity.getToolbarContentDescription()`, `MainActionsListAdapter`, `DiagnosticItemAdapter`). + - *Compose:* `Icon`/`Image` force a `contentDescription`, so the miss is a vague label — or a lazy `null` on something actionable. +- **Silence decoration.** Separators, background images, an icon beside a label that already says the same thing — skip them, don't read them as noise. + - *View:* `android:importantForAccessibility="no"` (or `View.IMPORTANT_FOR_ACCESSIBILITY_NO`). + - *Compose:* `contentDescription = null` on the decorative `Icon`/`Image`, or `Modifier.clearAndSetSemantics { }` to drop a subtree. +- **Describe the action, not the picture.** `cd_sync_project`, not "circular arrows icon". For toggles, make it state-aware — `cd_drawer_open` vs. `cd_drawer_close` — not one ambiguous label. (In Compose, pull text with `stringResource(R.string.cd_…)`.) +- **Externalize, with the `cd_` convention.** Content descriptions live in `strings.xml` as `cd_*` — greppable, translatable, reusable; check for an existing one first. `HardcodedText` lint does **not** catch Compose literals, so reviewers must. +- **Bonus — it stabilizes tests.** Screen-reader semantics are what UI tests match on (`ACTION_CLICK` for Views, `onNodeWithContentDescription(…)` for Compose), so a11y and reliable instrumentation tests are the same work. + +## 9. Contextual help — long-press works everywhere + +Help in CoGo is reached by **long-press**, anywhere: a progressive three-tier experience — **Tiers 1 & 2 are tooltips** (anchored popups from `idetooltips`), **Tier 3 is a full help web page** via the tooltip's "See More" link. A long-press should never be met with silence. + +- **Wire up help on new interactive elements.** Anything tappable — buttons, icon controls, menu items, list rows, toolbar actions — gets long-press help. A new actionable view with no tooltip is as incomplete as a missing `contentDescription`. +- **Cover new screens and panels too.** Even where pixels aren't interactive, a new screen/panel/dialog needs a top-level help entry so help is always reachable. +- **The affordance is the requirement, not finished copy.** Tooltip content may still be in authoring — fine — but the long-press must be wired and routed into the tier system. Don't ship UI that can never surface help. +- **Reuse the system.** Wire help through `idetooltips` — today the `View.displayTooltipOnLongPress(context, anchorView, category, tag)` extension (`setOnLongClickListener` → `TooltipManager.showTooltip`) — not a one-off popup. +- **Compose has no native entry point yet** (tracked by **ADFA-4381**). The helper is View-based (needs an `anchorView`), so until `idetooltips` grows a Compose API, a composable wires help via `AndroidView` interop. Flag it in review rather than skipping help, and build the reusable `Modifier`/wrapper once instead of copy-pasting interop. + +## 10. Architecture alignment + +Hold the change to the patterns in [ARCHITECTURE.md](ARCHITECTURE.md): +- **New UI is Jetpack Compose** ([ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)) — a new XML layout / `Fragment`-rendered screen for the IDE's own UI should be sent back. Compose changes only the view layer; the rest of this list still applies. (Existing XML screens are fine until reworked.) +- New screens follow **UDF**: `ViewModel` + `StateFlow`, sealed `UiEvent`/`UiEffect`, repository for data. Composables collect state via `collectAsState()`; keep I/O and business logic out of composables/Activities. +- DI via **Koin** (constructor injection); register new singletons/viewModels in the module. +- **Persistence:** raw SQLite or filesystem — **not Room** (Recent Projects is the lone legacy exception). +- **UI safety:** never place our UI over the two Android system bars — the top status bar and the bottom navigation bar (`AGENTS.md`). + +## 11. Offline-first + +CoGo is meant to work **without a network** — editing, building, and running an app on-device must not depend on connectivity. Hold new work to that: + +- **Degrade gracefully offline.** A feature that needs the network must still launch, explain itself, and leave the rest of the app usable when there's no connection — never block a core flow (edit/build/run) on a request. +- **Network calls are non-blocking and failure-tolerant.** Analytics, Sentry, and Gemini calls run off the main thread and must tolerate timeouts/failures silently (no crash, no hang, no lost user action). A dropped analytics event is acceptable; a dropped keystroke is not. +- **No network on the critical path.** Don't introduce a connectivity dependency into startup, the editor, or the build pipeline. + +## 12. Experimental features — gate behind the early-access flag + +Features that aren't fully stabilized ship behind the **experimental feature flag** — a user-facing opt-in for early access — so the default experience stays stable. + +- **Gate not-yet-stable work behind the flag, off by default.** A user who hasn't opted in never sees in-progress behavior. +- **It's an early-access opt-in, not a kill switch.** The flag is for users who want features early — not a lever for us to disable things in the field. + +## 13. PR hygiene + +- Focused and reviewable: aim for the **~500 LOC / 10-file** ceiling; split larger work into stacked PRs. +- Title/branch follow `ADFA-####`; description says *what changed, why, how it was verified*, and flags anything intentionally out of scope (e.g. "UI-only, no unit test"). +- No stray debug logs, commented-out code, `dummy.apk`-style artifacts, or unrelated reformatting that buries the real diff. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..09479ff4d2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,100 @@ +# Security Guide + +This is a developer guide for **not introducing new security findings** that our scanners — **SonarQube/SonarCloud**, **Snyk**, and **Semgrep** — flag. It explains the vulnerability classes those tools catch in a Kotlin/Java Android app like ours, with concrete do/don't guidance. For the per-PR checklist, see [REVIEW.md](REVIEW.md) §4; for vulnerability disclosure, see [Reporting](#reporting-a-vulnerability) at the bottom. + +## The policy: the baseline is frozen, don't grow it + +We currently carry a large baseline of **blocker-severity** findings that have been suppressed / marked "won't fix" so the build is workable. That baseline is **accepted technical debt — it is closed.** + +- **Your change must not add a new blocker (or high) finding in SonarCloud, Snyk, or Semgrep.** "New code" is what's measured; clean new code is the bar. +- **Do not extend the whitelist/suppression to cover your own code.** Suppression is reserved for **vendored / third-party code we don't own** (the same philosophy as our StrictMode whitelist: fix what we own, suppress only what we can't change). A suppression on app-owned code in a PR is a red flag, not a fix. +- If a tool flags something you believe is a false positive, get explicit reviewer sign-off and annotate *why* — don't silently suppress. + +Most of our existing blockers live in vendored subtrees (the `javac`/`jdt` forks, `termux`, native libs). The goal of this doc is to keep **our** code — `app`, plugins, feature modules — clean. + +--- + +## What the three tools catch (and where they differ) + +All three overlap heavily on code-level vulnerabilities; each has a distinct strength: + +| Tool | Strongest at | In our repo | +|---|---|---| +| **SonarQube / SonarCloud** | Broad Java/Kotlin **quality + security**, Android-aware rules, security hotspots. Most of our blocker count. | Runs in CI (`analyze.yml`) against SonarCloud project `appdevforall_CodeOnTheGo`. | +| **Snyk** | **Vulnerable dependencies (SCA)** — known CVEs in transitive libraries — plus Snyk Code SAST. | Watches our Gradle dependency graph; this is the one that fires on a bad library version. | +| **Semgrep** | **Pattern + taint-flow SAST** — tracks untrusted data from source to dangerous sink; highly customizable rules. | OWASP / Android / secrets rulesets. | + +If you only remember one distinction: **Snyk is mostly about the libraries you pull in; Sonar and Semgrep are mostly about the code you write.** + +--- + +## The vulnerability classes to avoid + +These are the categories that produce blocker/high findings in apps like ours. Representative SonarQube rule IDs are given where well-known (IDs and severities can shift between versions — treat them as pointers). + +### 1. Hardcoded secrets & credentials +*(Sonar S2068/S6418; Snyk Code; Semgrep `secrets`)* +- **Don't** put API keys, tokens, passwords, signing-key passwords, or `google-services.json` values in source, resources, or committed config. All three tools scan for high-entropy strings and known key formats. +- **Do** read secrets from `BuildConfig` fields injected at build time (CI secrets), the Android Keystore, or `EncryptedSharedPreferences`. Git tokens and keystore passwords (`GitCredentialsManager`, the keystore generator) must never be plaintext or logged. + +### 2. Injection +- **SQL injection** *(Sonar S3649)* — never concatenate user/project input into SQL. Use parameterized queries: `db.rawQuery(sql, arrayOf(arg))` with `?` placeholders (our `WebServer`/tooltip queries already do this — match them). +- **OS command injection** *(Sonar S2076; Semgrep taint)* — building shell strings from untrusted input is the classic finding. We run a real shell (Termux) and Gradle; pass arguments as a list/array, never an interpolated command string, and validate anything derived from a project path or filename. +- **Path traversal / Zip Slip** *(Sonar S2083 / S6096)* — extracting archives is a top hit for us. Every zip/tar entry (templates via `ZipRecipeExecutor`, imported projects, plugin packages) must be resolved and checked to stay inside the target dir before writing: + ```kotlin + val outFile = File(targetDir, entry.name) + if (!outFile.canonicalPath.startsWith(targetDir.canonicalPath + File.separator)) { + throw SecurityException("Zip Slip blocked: ${entry.name}") + } + ``` + Same idea for any `File(base, userControlledName)`. + +### 3. Weak cryptography & randomness +- **Weak hashes** *(Sonar S4790)* — MD5/SHA-1 for anything security-relevant. Use SHA-256+. (MD5 for a non-security cache key is fine but will still get flagged — prefer a non-crypto hash there.) +- **Weak ciphers / modes** *(Sonar S5542/S5547)* — no DES/3DES/RC4, no ECB mode, no hardcoded IVs *(S3329)*. Use AES-GCM with a random IV. +- **Insecure randomness** *(Sonar S2245)* — `java.util.Random` / `Math.random()` for tokens, keys, nonces, or filenames-as-secrets. Use `java.security.SecureRandom`. +- Prefer Jetpack Security / Keystore primitives over rolling your own crypto. + +### 4. Insecure network & transport +- **Cleartext traffic** *(Sonar S5332)* — no `http://`, `ftp://`, `ws://` for anything but localhost. Use HTTPS/WSS. +- **Disabled TLS validation** *(Sonar S4830 / S5527)* — never install a trust-all `TrustManager`, an `ALLOW_ALL` `HostnameVerifier`, or override `onReceivedSslError` to proceed. These are guaranteed blockers and a real MITM risk for git/clone and update flows. +- Don't widen `network_security_config` to permit cleartext or user CAs broadly. + +### 5. WebView & the local web server +We render content in WebViews (tooltips, markdown preview, APK viewer) and run a local `WebServer` — both are scanner magnets. +- *(Sonar S6362)* `setJavaScriptEnabled(true)` only when required, and never on a WebView loading **remote/untrusted** content. +- **`addJavascriptInterface`** with untrusted content is a remote-code-execution class finding — avoid it; if unavoidable, gate to `@JavascriptInterface` methods and trusted local content only. +- *(Sonar S6363)* don't enable `setAllowFileAccess` / `setAllowUniversalAccessFromFileURLs` unless strictly needed. +- The local `WebServer`: bind to **loopback only**, serve a scoped directory, don't reflect unsanitized request input into responses (XSS/SSRF), and treat every request as untrusted. + +### 6. Android component & data exposure +- **Exported components** — `activity`/`service`/`receiver`/`provider` with `android:exported="true"` and no permission is a standard finding. Export only what must be, and protect it with a signature-level permission. Validate all incoming `Intent` extras (intent-redirection / spoofing). +- **PendingIntent** — must be `FLAG_IMMUTABLE` unless mutability is genuinely required. +- **Insecure storage** — no `MODE_WORLD_READABLE/WRITEABLE`; don't put sensitive data on external/shared storage; don't log file contents, tokens, or PII (scanners flag `Log`/print of tainted data, and it also leaks into Sentry/analytics). +- **Manifest hygiene** — `android:allowBackup` and `android:debuggable` are flagged for sensitive apps; set deliberately. +- Request the minimum permissions; over-requesting is flagged. + +### 7. Insecure deserialization & XML +- **XXE** *(Sonar S2755)* — when parsing XML (layouts, manifests, project files) disable external entities/DTDs on the factory (`setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)`). +- **Untrusted deserialization** — validate JSON (Gson/kotlinx) shape before use; never deserialize untrusted data into arbitrary types. + +### 8. Vulnerable & risky dependencies — Snyk's specialty +- **Before adding or bumping a dependency**, check it has no known high/critical CVEs and is maintained. Snyk fires on the *transitive* graph too, so a new direct dep can drag in a flagged one. +- Reuse what's already in `gradle/libs.versions.toml` (our standing rule is to avoid new deps anyway). Pin versions in the catalog; don't introduce dynamic/`+` versions. +- When Snyk flags an existing dep, prefer the smallest safe upgrade; if no fix exists, that's a conversation (and a tracked exception), not a silent ignore. +- Watch licenses too — Snyk/legal flags incompatible licenses for a GPLv3 project. + +### 9. Reliability "blockers" (not strictly security) +A chunk of Sonar **blocker** findings are reliability bugs, not vulnerabilities: guaranteed NPEs, resource leaks (unclosed streams/cursors/connections), and dead/always-true conditions. These overlap our leak guidance — see [REVIEW.md](REVIEW.md) §1–2. Use `use {}`, null-safety, and exhaustive `when` to keep them out. + +--- + +## Before you push + +- **Self-review against §1–9** for the surfaces your change touches (input handling, crypto, network, WebView, manifest, new deps). +- If you have the SonarQube MCP / `sonar` CLI handy, scan your branch locally and confirm **no new** blocker/high on changed files. Remember: official analysis runs in CI — local is verification only (`AGENTS.md`). +- If a finding is genuinely a false positive, document the reasoning and get reviewer sign-off **before** suppressing — and never suppress on code we own without that. + +## Reporting a vulnerability + +Found a security issue in Code On The Go? **Do not open a public issue.** Email **feedback@appdevforall.org** with details and reproduction steps, and allow time for a fix before public disclosure. diff --git a/docs/adr/0001-persistence-without-room.md b/docs/adr/0001-persistence-without-room.md new file mode 100644 index 0000000000..534f657aa3 --- /dev/null +++ b/docs/adr/0001-persistence-without-room.md @@ -0,0 +1,42 @@ +# 0001. Persistence uses SQLite/filesystem, not Room + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +Code On The Go stores modest local state — recent projects, tooltips, breakpoints, preferences, and assorted IDE settings — on resource-constrained Android devices, entirely offline. We care about APK size, method count, build time, and full control over storage, and we already run a large multi-module build where every annotation processor adds real cost. + +Room, Android's default persistence library, generates code via `kapt`/KSP, ships a runtime, and manages a schema. It saves boilerplate but adds footprint and build overhead, and was applied inconsistently across the codebase. + +## Decision + +New persistence uses **raw SQLite** (`SQLiteOpenHelper` / `SQLiteDatabase`) or the **filesystem / preferences**. Room is **not** used for new code. + +The one exception is the **Recent Projects** feature (`app/src/main/java/com/itsaky/androidide/roomData/recentproject/`, `@Database version = 4`), which predates this decision and is grandfathered in. Do not extend it with new entities or tables. (`idetooltips` still declares unused Room Gradle deps; its tooltip store is raw SQLite — remove those deps.) + +## Consequences + +**Positive** +- Smaller footprint; no annotation processing for persistence. +- Full control over SQL, migrations, and threading. +- One fewer library to track for CVEs/updates. + +**Negative / costs** +- Hand-written SQL, migrations, and row→object mapping. +- No compile-time query verification (a Room benefit we forgo). + +**Follow-ups** +- Provide shared SQLite helper utilities to keep raw-SQLite boilerplate consistent and safe (parameterized queries — see SECURITY.md). +- Remove the unused Room dependencies from `idetooltips`. + +## Alternatives considered + +- **Room everywhere** — rejected: adds `kapt`/runtime/footprint and build cost not justified on constrained devices, and it had crept in unevenly. +- **DataStore / SharedPreferences only** — insufficient for relational/queryable data (e.g. recent projects). +- **A third-party ORM** — more dependencies and surface for little gain over raw SQLite. + +## Related + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — persistence policy (authoritative model). diff --git a/docs/adr/0002-on-device-builds-via-gradle-tooling-api.md b/docs/adr/0002-on-device-builds-via-gradle-tooling-api.md new file mode 100644 index 0000000000..0cd4059956 --- /dev/null +++ b/docs/adr/0002-on-device-builds-via-gradle-tooling-api.md @@ -0,0 +1,44 @@ +# 0002. On-device builds run real Gradle out-of-process via the Tooling API + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +The product's core promise is to build and deploy *real* Android apps on-device, offline — producing the same results as a desktop Gradle build (correct dependency resolution, AGP behavior, manifest merging, R8/D8), not an approximation. + +Gradle is memory-hungry and can OOM or crash, especially on a phone. Running it inside the IDE process would couple the build's lifecycle and memory to the UI: a build OOM would take down the editor, and a stuck build couldn't be cleanly killed. + +## Decision + +Run builds with the **Gradle Tooling API in a separate JVM process**, and have the app drive it over IPC. + +- `:subprojects:tooling-api` — the client-facing API and `ToolingApiLauncher`. +- `:subprojects:tooling-api-impl` — the forked process (`tooling.impl.Main`) that hosts the Tooling API connection and runs the build. +- `:subprojects:tooling-api-events` / `:subprojects:tooling-api-model` — the serializable event/model types exchanged across the process boundary. + +The app streams progress/events back from this process and renders them (e.g. `BuildState`, build output). This depends on the vendored Gradle/toolchain from [ADR 0003](0003-vendored-forked-desktop-toolchain.md). + +## Consequences + +**Positive** +- Build crashes and OOMs are isolated from the IDE — the UI survives a failed build. +- Real Gradle semantics, so on-device builds match desktop builds. +- Builds can be cancelled/killed and the process restarted independently. + +**Negative / costs** +- IPC complexity: every model and event crossing the boundary must be serializable and versioned (`tooling-api-events`/`-model`). +- Process startup time and additional memory for a second JVM. +- The Tooling API client/impl must stay in lockstep with the vendored Gradle version. + +## Alternatives considered + +- **Run Gradle in-process** — rejected: an OOM or crash would kill the IDE, and the build lifecycle would be entangled with the UI. +- **A custom/cut-down build engine** — rejected: enormous effort and a permanent correctness gap versus real Gradle/AGP. +- **Cloud/remote builds** — rejected: violates the offline-first requirement. + +## Related + +- [ADR 0003](0003-vendored-forked-desktop-toolchain.md) — the forked toolchain the build process runs. +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — build engine module group. diff --git a/docs/adr/0003-vendored-forked-desktop-toolchain.md b/docs/adr/0003-vendored-forked-desktop-toolchain.md new file mode 100644 index 0000000000..4fc2f87840 --- /dev/null +++ b/docs/adr/0003-vendored-forked-desktop-toolchain.md @@ -0,0 +1,39 @@ +# 0003. Vendor & fork the desktop toolchain via composite-build substitution + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +Compiling Java/Kotlin/Android projects requires desktop developer tools — `javac`, the JDK compiler/`jdeps`, the Eclipse JDT compiler, `layoutlib`, AAPT, JAXP, etc. These assume a desktop JDK environment and aren't published in forms that run on Android's ART runtime. On-device builds (see [ADR 0002](0002-on-device-builds-via-gradle-tooling-api.md)) require making them work there. + +## Decision + +**Fork and vendor** these tools into the repository as composite builds under `composite-builds/build-deps` and `composite-builds/build-deps-common`, and **substitute** them for their `com.itsaky.androidide.build:*` Maven coordinates via `dependencySubstitution` in `settings.gradle.kts`. + +Substituted modules include `javac`, `jdk-compiler`, `jdk-jdeps`, `jdt`, `layoutlib-api`, `java-compiler`, `jaxp`, `javapoet`, and `google-java-format`, among others. Any consuming module gets the forked, Android-compatible version transparently. + +## Consequences + +**Positive** +- The desktop toolchain runs on-device — the thing that makes the whole product possible. +- We can patch compiler/tooling internals for Android compatibility and bug fixes. +- Consumers reference normal coordinates; the substitution is centralized and invisible to them. + +**Negative / costs** +- A large vendored source subtree and ongoing maintenance: tracking upstream changes and JDK/AGP/Gradle updates. +- The **majority of suppressed scanner findings live in these subtrees** — treated as vendored code (suppress-only, never "fixed" to our standards) per [SECURITY.md](../../SECURITY.md). +- Onboarding cost: contributors must understand the substitution model before touching builds. + +## Alternatives considered + +- **Depend on upstream artifacts** — rejected: they don't run on Android. +- **Require a companion desktop** — rejected: defeats the product's reason to exist. +- **Bundle a full JDK** — rejected: not feasible/licensable for on-device use in this form. + +## Related + +- [ADR 0002](0002-on-device-builds-via-gradle-tooling-api.md) — the build process that runs this toolchain. +- [SECURITY.md](../../SECURITY.md) — vendored-code suppression policy. +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — dependency-substitution rules. diff --git a/docs/adr/0004-embedded-termux-runtime.md b/docs/adr/0004-embedded-termux-runtime.md new file mode 100644 index 0000000000..d227d2e23d --- /dev/null +++ b/docs/adr/0004-embedded-termux-runtime.md @@ -0,0 +1,42 @@ +# 0004. Embed Termux as the shell & toolchain runtime + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +A self-contained on-device IDE needs a real POSIX shell, a package ecosystem (git, clang, coreutils, and the binaries the build depends on), and an interactive terminal. Building any of that from scratch — shell, package manager, terminal emulator/view — is a multi-year effort and a distraction from the IDE itself. + +Termux is a mature, GPL-licensed Android terminal and package environment that already solves this. + +## Decision + +**Embed Termux** as the shell and toolchain runtime, vendored as modules: + +- `:termux:termux-app` — the Termux application/bootstrap layer. +- `:termux:termux-shared` — shared runtime, file/PTY, and package plumbing. +- `:termux:termux-view` / `:termux:termux-emulator` — the terminal view and terminal emulator used in the IDE's terminal UI. + +The IDE's build and shell flows run through this environment. + +## Consequences + +**Positive** +- A proven shell + package ecosystem and a battle-tested terminal emulator, for free. +- License-compatible with our GPLv3 project. + +**Negative / costs** +- A large native/runtime surface and a real **security consideration**: Termux executes arbitrary commands, so command-execution sinks must be handled carefully (see [SECURITY.md](../../SECURITY.md)). +- Maintenance: tracking Termux upstream and integrating it with our build orchestration. +- Adds to APK size and the set of native components per ABI ([ADR 0005](0005-per-abi-product-flavors.md)). + +## Alternatives considered + +- **A custom shell / NDK-only toolchain** — rejected: enormous scope, and we'd reinvent a package ecosystem. +- **Remote/SSH execution** — rejected: violates offline-first. + +## Related + +- [SECURITY.md](../../SECURITY.md) — command-execution surface. +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — shell module group. diff --git a/docs/adr/0005-per-abi-product-flavors.md b/docs/adr/0005-per-abi-product-flavors.md new file mode 100644 index 0000000000..dea7c6f12d --- /dev/null +++ b/docs/adr/0005-per-abi-product-flavors.md @@ -0,0 +1,38 @@ +# 0005. Ship per-ABI product flavors (v7/v8), not a universal APK + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +The app bundles large native components per ABI — llama.cpp, plus the on-device toolchain/Termux binaries. A universal APK carrying both `armeabi-v7a` and `arm64-v8a` copies of everything would be very large. Code On The Go is distributed primarily as a **direct APK download** from the App Dev for All website, not exclusively through Google Play, so we can't rely on Play's automatic per-ABI splitting to slim downloads. + +## Decision + +Define two **product flavors** on an `abi` dimension — `v7` (`armeabi-v7a`) and `v8` (`arm64-v8a`) — **centrally** in `composite-builds/build-logic` (`conf/AndroidModuleConf.kt`), applied to every Android module except `:plugin-api`. + +Consequences for the build: +- Build tasks are flavor-qualified: `assembleV8Debug`, `assembleV7Release`, etc. **There is no flavorless `assembleDebug`.** +- Native assets (e.g. the llama AAR) are bundled per flavor via the root `build.gradle.kts`; prebuilt assets live under `assets/release/v7/` and `assets/release/v8/`. + +## Consequences + +**Positive** +- Each APK ships only one ABI's native libraries → much smaller downloads. +- Explicit, centralized control over per-ABI native bundling. + +**Negative / costs** +- No flavorless variant; every build/test/release task is doubled into V7/V8. +- Larger build matrix and more release artifacts to produce and track. +- A recurring onboarding gotcha: contributors must use `assembleV8Debug` (etc.), not `assembleDebug`. + +## Alternatives considered + +- **Universal (fat) APK** — rejected: size, given the large per-ABI native payload. +- **Android App Bundle ABI splits** — rejected: our primary distribution is direct APK download, so we can't depend on Play-side splitting; flavors give us split artifacts on every channel. + +## Related + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — Build & Module Configuration. +- [CLAUDE.md](../../CLAUDE.md) — build task commands. diff --git a/docs/adr/0006-koin-dependency-injection.md b/docs/adr/0006-koin-dependency-injection.md new file mode 100644 index 0000000000..ebeaf8dab2 --- /dev/null +++ b/docs/adr/0006-koin-dependency-injection.md @@ -0,0 +1,36 @@ +# 0006. Use Koin for dependency injection, not Hilt/Dagger + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +The app needs dependency injection to wire ViewModels, repositories, and managers across many modules. On a build this large (~80 modules), annotation-processing DI (Dagger/Hilt via `kapt`/KSP) adds noticeable compile time and couples DI to the Android Gradle plugin and the build graph. The team values fast iteration and simple, testable wiring. + +## Decision + +Use **Koin** for dependency injection. + +- Modules `coreModule` and `pluginModule` declare dependencies via Koin's DSL (`single { }`, `viewModel { }`); `startKoin { }` runs in `IDEApplication`. +- Prefer **constructor injection** into ViewModels/classes. +- A small `ServiceLocator` (a `KoinComponent`) provides lazy access for components that must resolve a dependency *after* Koin starts but *before* their screen exists. + +## Consequences + +**Positive** +- No annotation processing → faster builds, no `kapt`/KSP for DI. +- Simple, readable DSL; trivial to override dependencies in tests. + +**Negative / costs** +- DI errors surface at **runtime**, not compile time — a missing binding fails when resolved, so DI paths need test coverage. +- `ServiceLocator` is a deliberate escape hatch; over-use reintroduces hidden global state. Keep it limited and prefer constructor injection. + +## Alternatives considered + +- **Hilt / Dagger** — rejected: compile-time safety is real, but the `kapt`/KSP build cost and AGP coupling are significant at this module scale, and the team prioritized iteration speed. +- **Manual DI / by-hand wiring** — rejected: boilerplate and poor ergonomics across this many modules. + +## Related + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — technology stack (DI). diff --git a/docs/adr/0007-strictmode-whitelist-engine.md b/docs/adr/0007-strictmode-whitelist-engine.md new file mode 100644 index 0000000000..e3e2d6a24f --- /dev/null +++ b/docs/adr/0007-strictmode-whitelist-engine.md @@ -0,0 +1,40 @@ +# 0007. Enforce StrictMode via a custom whitelist engine + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +StrictMode catches main-thread disk/network I/O and leaked resources early. But Code On The Go is a large app built on inherited and vendored code (the AndroidIDE lineage, the forked toolchain in [ADR 0003](0003-vendored-forked-desktop-toolchain.md), Termux) that trips many StrictMode violations we can't reasonably fix. + +Both blunt options fail us: disabling StrictMode hides regressions in **our** code, while a death/penalty policy crashes or spams on **third-party** violations we don't control. + +## Decision + +Build a **custom StrictMode whitelist engine** that suppresses specific, known violations while still surfacing everything else. + +Components (in `app/.../app/strictmode/`): `StrictModeManager`, `WhitelistBuilder`, `WhitelistEngine`, `ViolationDispatcher`, `ViolationListener`, `ViolationHandler`, `StrictModeConfig`, with tests (`WhitelistEngineTest`, `WhitelistRulesTest`). + +**Policy:** the whitelist is **only** for vendor/framework violations we can't change. App-owned violations must be **fixed**, not whitelisted. (This is the same suppress-only-what-we-don't-own philosophy applied to security scanners in [SECURITY.md](../../SECURITY.md).) + +## Consequences + +**Positive** +- StrictMode stays useful in a large inherited codebase — our regressions still surface. +- Suppressions are explicit, rule-based, reviewable, and unit-tested. + +**Negative / costs** +- Custom infrastructure to maintain and understand. +- Risk of over-whitelisting if the policy isn't enforced — so an app-owned entry added to the whitelist is a review red flag, not a fix. + +## Alternatives considered + +- **StrictMode off** — rejected: loses regression detection on our own code. +- **StrictMode with `penaltyDeath`/`penaltyLog` globally** — rejected: crashes/spams on vendored violations we can't fix. +- **Per-call `allowThreadDiskReads()` suppressions** — rejected: scatters suppressions through the code, easy to abuse, and impossible to audit centrally. + +## Related + +- [REVIEW.md](../../REVIEW.md) — §3 threading & StrictMode review guidance. +- [SECURITY.md](../../SECURITY.md) — the parallel suppress-only-vendor policy for scanners. diff --git a/docs/adr/0008-retain-androidide-namespace.md b/docs/adr/0008-retain-androidide-namespace.md new file mode 100644 index 0000000000..cd8ab5f316 --- /dev/null +++ b/docs/adr/0008-retain-androidide-namespace.md @@ -0,0 +1,35 @@ +# 0008. Retain the `com.itsaky.androidide` namespace after rebrand + +- **Status:** Accepted +- **Date:** 2026-06-18 +- **Deciders:** Code On The Go team + +## Context + +Code On The Go is the rebranded successor to **AndroidIDE**. The product name, branding, and assets changed, but the inherited codebase carries the original identity deeply: the application id and Gradle namespace are `com.itsaky.androidide` (`BuildConfig.PACKAGE_NAME`), `rootProject.name` is `AndroidIDE`, plus many thousands of references, the generated `R` class, the manifest, package-qualified vendored substitutions, signing identity, and existing installs in the field. + +Changing an Android **application id** breaks the update path for installed users (a different app id is a different app) and disrupts signing/identity continuity. A rename of this size also ripples through the vendored `com.itsaky.androidide.build:*` substitutions ([ADR 0003](0003-vendored-forked-desktop-toolchain.md)). + +## Decision + +**Keep** the `com.itsaky.androidide` application id / namespace and the internal Gradle project names. Rebranding (`INTERNAL_NAME = "CodeOnTheGo"`, the app label, icons, and user-facing assets) is applied at the **presentation layer only**, not the package identity. + +## Consequences + +**Positive** +- Preserves the update path and signing continuity for existing installs. +- Avoids a massive, high-risk refactor and the churn it would cause across vendored substitutions and the `R` class. + +**Negative / costs** +- The codebase namespace doesn't match the product name, confusing to newcomers. [CLAUDE.md](../../CLAUDE.md) and [ARCHITECTURE.md](../../ARCHITECTURE.md) call this out so the mismatch is expected, not surprising. +- Care is needed to ensure user-facing strings/branding are overridden and no "AndroidIDE" naming leaks to users. + +## Alternatives considered + +- **Full rename to `org.appdevforall.*`** — rejected: breaks updates for existing users, requires enormous error-prone churn, and risks destabilizing the vendored substitutions for little gain. +- **Partial rename** (some modules) — rejected: produces an inconsistent namespace with the same risks and no clean payoff. + +## Related + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — overview note on the namespace/product-name mismatch. +- [ADR 0003](0003-vendored-forked-desktop-toolchain.md) — vendored coordinates that a rename would disrupt. diff --git a/docs/adr/0009-jetpack-compose-for-new-ui.md b/docs/adr/0009-jetpack-compose-for-new-ui.md new file mode 100644 index 0000000000..bc20fd7d8b --- /dev/null +++ b/docs/adr/0009-jetpack-compose-for-new-ui.md @@ -0,0 +1,43 @@ +# 0009. Build new UI in Jetpack Compose, not XML Views + +- **Status:** Accepted +- **Date:** 2026-06-19 +- **Deciders:** Code On The Go team + +## Context + +Historically the IDE's own UI is **View-based** — XML layouts, `Fragment`s, and `RecyclerView` with Material Components (see [ADR 0006](0006-koin-dependency-injection.md) context and ARCHITECTURE.md). Newer surfaces already moved to a Unidirectional Data Flow (UDF) architecture — `ViewModel` + `StateFlow`, sealed UI-state/effect types, repositories, Koin DI — but still render through XML and `findViewById`/binding. + +That split has a cost: two ways to build a screen, manual view-state wiring, boilerplate binding code, and UI logic that's awkward to unit-test. Jetpack Compose collapses the view layer into Kotlin, binds naturally to `StateFlow` via `collectAsState()`, and fits the UDF pattern the team already follows. Unlike most ADRs here, this one is **forward-looking** — it sets direction for new work rather than documenting an existing decision. + +## Decision + +**All new UI for the IDE is built in Jetpack Compose. New XML View-based screens are not permitted.** + +- New screens, panels, dialogs, and reusable UI components are composables. Reviewers should reject a new XML layout / `Fragment`-rendered screen for the IDE's own UI. +- **The rest of the stack is unchanged.** Compose replaces only the view layer. UDF still holds: `ViewModel` + `StateFlow`, sealed `UiEvent`/`UiEffect`, and repositories for data. Composables collect state with `collectAsState()` and send events up to the ViewModel. +- **DI is still Koin** ([ADR 0006](0006-koin-dependency-injection.md)) — ViewModels are resolved through Koin, not a new mechanism. +- **Existing XML/View screens stay as-is.** This is not a migration mandate; legacy surfaces are rewritten in Compose only when they're being substantially reworked anyway. +- This does **not** apply to the *user's* app or the visual design tooling (`layouteditor`, `uidesigner`, `xml-inflater`), which manipulate XML by definition. + +## Consequences + +**Positive** +- One way to build new screens; less boilerplate (no binding/`findViewById`), state-driven rendering that maps cleanly onto the existing `StateFlow` UDF. +- UI logic is easier to test and preview (`compose-preview` already exists in the tree). + +**Negative / costs** +- A **mixed codebase** for the foreseeable future — Compose and Views coexist; contributors must know both, and interop (`ComposeView` / `AndroidView`) is needed at the seams. +- New Compose dependencies and compiler plugin; some learning curve and added build surface. +- **Cross-cutting UI rules written for XML need Compose equivalents** — notably accessibility (`contentDescription` → `Modifier.semantics`/`contentDescription`, decorative views → `null` semantics) and the long-press 3-tier help affordance (see REVIEW.md §8–§9). The help affordance has no Compose entry point yet — building the bridge is tracked as a prerequisite in **ADFA-4381**; sequence it before the first Compose feature screen. + +## Alternatives considered + +- **Stay View-based** — rejected: perpetuates two ways to build a screen and the binding boilerplate; doesn't leverage the `StateFlow` UDF the team already standardized on. +- **Compose, and mandate migrating existing screens** — rejected: a forced migration of all legacy UI is high-risk churn with little user-facing value. Migrate opportunistically instead. + +## Related + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) — technology stack (UI), state management & UDF. +- [ADR 0006](0006-koin-dependency-injection.md) — Koin DI (unchanged by this decision). +- [REVIEW.md](../../REVIEW.md) — §8 Accessibility, §9 Contextual help (rules that need Compose equivalents). diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000000..36c86c5437 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,25 @@ +# Architecture Decision Records + +This directory holds **Architecture Decision Records (ADRs)** — short documents capturing a significant architectural decision, its context, and its consequences. They explain *why* the codebase is the way it is, so a decision isn't silently undone later. + +Format is lightweight **MADR / Nygard**: Context → Decision → Consequences → Alternatives. Most records here are *retroactive*, documenting decisions already embedded in the code. + +## Conventions + +- One decision per file, named `NNNN-kebab-title.md` with a zero-padded sequence number. +- **Status** lifecycle: `Proposed` → `Accepted` → `Superseded by NNNN` / `Deprecated`. Don't edit a decision after it's accepted; write a new ADR that supersedes it. +- Keep it short and concrete. Link related ADRs and relevant code paths. + +## Index + +| # | Decision | Status | +|---|---|---| +| [0001](0001-persistence-without-room.md) | Persistence uses SQLite/filesystem, not Room | Accepted | +| [0002](0002-on-device-builds-via-gradle-tooling-api.md) | On-device builds run real Gradle out-of-process via the Tooling API | Accepted | +| [0003](0003-vendored-forked-desktop-toolchain.md) | Vendor & fork the desktop toolchain via composite-build substitution | Accepted | +| [0004](0004-embedded-termux-runtime.md) | Embed Termux as the shell & toolchain runtime | Accepted | +| [0005](0005-per-abi-product-flavors.md) | Ship per-ABI product flavors (v7/v8), not a universal APK | Accepted | +| [0006](0006-koin-dependency-injection.md) | Use Koin for dependency injection, not Hilt/Dagger | Accepted | +| [0007](0007-strictmode-whitelist-engine.md) | Enforce StrictMode via a custom whitelist engine | Accepted | +| [0008](0008-retain-androidide-namespace.md) | Retain the `com.itsaky.androidide` namespace after rebrand | Accepted | +| [0009](0009-jetpack-compose-for-new-ui.md) | Build new UI in Jetpack Compose, not XML Views | Accepted |