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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ assets/gradle-*.zip
app/src/release/assets/database/documentation.db
app/src/main/assets/database/documentation
app/src/main/assets/database/documentation.db
app/src/main/assets/data/common/kotlin-compiler-carrier.apk

# AI plugin development artifacts (moved to plugin-examples repo)
*.cgp
Expand Down
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ 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. |
| Language tooling | `lsp:{api,java,kotlin,xml,indexing,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. |
| Language tooling | `lsp:{api,java,kotlin,kotlin-api,kotlin-compiler-impl,xml,indexing,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. `lsp:kotlin` is a thin shell (LSP wiring, `DexClassLoader` loader); `lsp:kotlin-api` holds the interfaces shared across the split; `lsp:kotlin-compiler-impl` carries the actual Kotlin Analysis API dependency and is never resident in the app's main dex — see `subprojects:kotlin-compiler-carrier` below (ADFA-5010). |
| On-device compiler carrier | `subprojects:kotlin-analysis-api`, `subprojects:kotlin-compiler-carrier` | Packages `lsp:kotlin-compiler-impl` + the downloaded Kotlin Analysis API jar into a standalone (never-installed) APK shipped in `app`'s assets; `lsp:kotlin`'s `KotlinCompilerLoader` extracts and `DexClassLoader`-loads it lazily on first Kotlin file interaction, keeping the ~28MB dependency graph out of the main app dex (ADFA-5010). |
| 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 — `AndroidManifest.xml` `<meta-data>` contract, permissions, extensions. See [plugin-api.md](docs/plugin-api.md) for the API surface & compatibility policy. |
Expand Down
168 changes: 167 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ plugins {
alias(libs.plugins.google.services)
}

// Forces :subprojects:kotlin-compiler-carrier to configure eagerly, ahead of app's own
// configuration. Without this, org.gradle.configureondemand=true (gradle.properties) only
// reaches that project lazily via copyKotlinCompilerCarrierToAssets's cross-project dependsOn,
// which trips "DefaultClassLoaderScope must be locked before it can be used to compute a
// classpath" for this (com.android.application) target specifically -- plugin-api, referenced
// the same way by copyPluginApiJarToAssets below, doesn't hit this because app already
// configures it naturally as a real compile dependency.
evaluationDependsOn(":subprojects:kotlin-compiler-carrier")

fun propOrEnv(name: String): String =
project.findProperty(name) as String?
?: System.getenv(name)
Expand Down Expand Up @@ -289,7 +298,6 @@ dependencies {
implementation(projects.gradlePluginConfig)
implementation(projects.subprojects.aaptcompiler)
implementation(projects.subprojects.javacServices)
implementation(projects.subprojects.kotlinAnalysisApi)
implementation(projects.subprojects.shizukuApi)
implementation(projects.subprojects.shizukuManager)
implementation(projects.subprojects.shizukuProvider)
Expand Down Expand Up @@ -409,6 +417,164 @@ tasks.register("downloadDocDb") {
}
}

// Copies the Kotlin Analysis API "carrier" APK -- built by :subprojects:kotlin-compiler-carrier,
// which bundles the isolated lsp:kotlin-compiler-impl module + the downloaded analysis-api jar --
// straight into app's own assets, so it ships in the base APK and D8 never merges it into app's own
// classes*.dex. KotlinCompilerLoader (lsp:kotlin) extracts and DexClassLoader-loads it lazily on
// first Kotlin file interaction (ADFA-5010). Unlike plugin-api.jar/the big installer zips below, this
// doesn't need the root assets/ + assets-<arch>.zip pipeline -- it's a few MB, not hundreds, and
// belongs in the base APK unconditionally rather than an optional first-run download.
tasks.register("copyKotlinCompilerCarrierToAssets") {
// The source directory below is AGP's own Provider<Directory> for the release variant's real
// APK output (see releaseApkOutputDir in kotlin-compiler-carrier's build.gradle.kts) --
// wiring inputs.dir() to it, rather than a hardcoded path guessing the output filename
// ("-unsigned", versioned, etc.), ties Gradle's dependency tracking to the actual producing
// task (packageV8Release) instead of just dependsOn ordering, which intermittently raced the
// file's own write-to-disk on some CI runs (same defect as ADFA-5053's
// copyJavaCompilerCarrierToAssets, fixed there the same way). See
// evaluationDependsOn(":subprojects:kotlin-compiler-carrier") above for why reading its
// extraProperty here, rather than project(...).layout... directly, avoids the "classloader
// scope must be locked" failure under org.gradle.configureondemand=true.
dependsOn(":subprojects:kotlin-compiler-carrier:assembleV8Release")
@Suppress("UNCHECKED_CAST")
val sourceDir =
project(":subprojects:kotlin-compiler-carrier").extensions.extraProperties["releaseApkOutputDir"]
as Provider<Directory>
val destFile = layout.projectDirectory.file("src/main/assets/data/common/kotlin-compiler-carrier.apk")
inputs.dir(sourceDir)
outputs.file(destFile)
doLast {
val apkFile =
sourceDir
.get()
.asFileTree
.matching { include("*.apk") }
.singleFile
apkFile.copyTo(destFile.asFile, overwrite = true)
optimizeCarrierApkPngs(destFile.asFile)
}
}

// The carrier APK's res/ entries (pulled in transitively, e.g. androidx.core's notification
// backgrounds) are dead weight -- this APK is never installed or its resources loaded; it's only
// ever opened as a raw dex source via DexClassLoader (ADFA-5010). Shrinks them with pngquant
// (a lossy, palette-based recompression) purely for size; correctness of the images themselves
// is moot since nothing ever renders them. Some are AAPT2-compiled nine-patches (npTc/npOl
// chunks) that pngquant's re-encode will strip -- fine here, would not be elsewhere.
// Skips quietly if pngquant isn't installed, since this runs unconditionally via preBuild.
fun optimizeCarrierApkPngs(apkFile: File) {
if (!isPngquantAvailable()) {
project.logger.info("pngquant not found on PATH; skipping carrier APK PNG optimization")
return
}

val rawEntries = readRawZipEntries(apkFile)
val sizeByName =
ZipFile(apkFile).use { zip ->
zip
.entries()
.asSequence()
.filter { !it.isDirectory }
.associate { it.name to it.size }
}

val pngNames = rawEntries.keys.filter { it.startsWith("res/") && it.endsWith(".png", ignoreCase = true) }
if (pngNames.isEmpty()) {
return
}

// Under the project tree, not the system temp dir: pngquant is snap-confined here and
// cannot read files under /tmp.
val tempDir =
layout.buildDirectory
.dir("tmp/carrierPngquant")
.get()
.asFile
tempDir.deleteRecursively()
tempDir.mkdirs()
var savedBytes = 0L
var shrunkCount = 0
try {
val quantized = mutableMapOf<String, ByteArray>()
ZipFile(apkFile).use { zip ->
for (name in pngNames) {
val original = zip.getInputStream(zip.getEntry(name)).use { it.readBytes() }
val workFile = File(tempDir, name.substringAfterLast('/'))
workFile.writeBytes(original)
@Suppress("DEPRECATION")
project.exec {
commandLine(
"pngquant",
"--force",
"--ext",
".png",
"--skip-if-larger",
"--quality=65-100",
workFile.absolutePath,
)
isIgnoreExitValue = true
}
if (workFile.exists() && workFile.length() in 1 until original.size.toLong()) {
val newBytes = workFile.readBytes()
quantized[name] = newBytes
savedBytes += original.size - newBytes.size
shrunkCount++
}
}
}

if (quantized.isEmpty()) {
project.logger.info("pngquant found nothing to shrink in ${apkFile.name}'s res/ entries")
return
}

val tempZip = File(apkFile.parentFile, "${apkFile.name}.pngquant.tmp")
RawZipWriter(tempZip).use { writer ->
for ((name, raw) in rawEntries) {
val newBytes = quantized[name]
if (newBytes != null) {
val crc = CRC32().apply { update(newBytes) }.value
writer.addEntry(
name,
ZipEntry.STORED,
crc,
newBytes.size.toLong(),
newBytes.size.toLong(),
ByteArrayInputStream(newBytes),
)
} else {
val size = sizeByName.getValue(name)
writer.addEntry(
name,
raw.method,
raw.crc,
size,
raw.data.size.toLong(),
ByteArrayInputStream(raw.data),
)
}
}
}
Files.move(tempZip.toPath(), apkFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
project.logger.lifecycle(
"pngquant shrank $shrunkCount/${pngNames.size} PNG(s) in ${apkFile.name}'s res/ (saved ${savedBytes / 1024}KB)",
)
} finally {
tempDir.deleteRecursively()
}
}

fun isPngquantAvailable(): Boolean =
try {
ProcessBuilder("pngquant", "--version").start().waitFor() == 0
} catch (_: Exception) {
false
}

tasks.named("preBuild") {
dependsOn("copyKotlinCompilerCarrierToAssets")
}

tasks.register("copyPluginApiJarToAssets") {
dependsOn(":plugin-api:createPluginApiJar")
val sourceFile = project(":plugin-api").layout.buildDirectory.file("libs/plugin-api-1.0.0.jar")
Expand Down
36 changes: 0 additions & 36 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,6 @@
# Builder model implementations
-keep class com.itsaky.androidide.builder.model.** { *; }

# lsp/kotlin registers its own IntelliJ project/application services -- some
# by class name in lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml,
# the rest via ::class literals in
# lsp/kotlin/.../registrar/AnalysisApiServiceProviders.kt, which PicoContainer
# then instantiates reflectively via each class's no-arg constructor. A
# ::class literal doesn't count as an actual `new` call to R8, so it kept
# stripping "unused" no-arg constructors one class at a time as each was
# discovered on-device (ClassNotFoundException on DirectInheritorsProvider,
# then a PicoInitializationException on ModuleDependentsProvider's missing
# constructor). Keep the whole package rather than list every implementation
# class in AnalysisApiServiceProviders.kt individually.
-keep class com.itsaky.androidide.lsp.kotlin.compiler.services.** { *; }

# Kotlin Analysis API (bundled in subprojects/kotlin-analysis-api, used by the
# Kotlin LSP). subprojects/kotlin-analysis-api/consumer-rules.pro keeps every
# class this jar's own IntelliJ plugin XML descriptors and ServiceLoader
# entries reference by name, but on-device testing kept surfacing distinct
# reflection paths that narrow list didn't cover -- not just inside this jar,
# but in other lsp/kotlin runtime dependencies too (Caffeine picks a cache
# implementation from dozens of codegenned variant classes at runtime; a
# protobuf-lite message field is resolved by name string; lsp/kotlin's own
# kt-lsp.xml above; and a NullPointerException deep in IntelliJ's own
# JavaCoreApplicationEnvironment bootstrap, verified absent on an unshrunk
# debug build). Each fix was quick but the next gap kept appearing elsewhere
# in the same dependency graph, so rather than keep discovering them one
# on-device crash at a time, keep every runtime dependency lsp/kotlin pulls in
# whole. This gives up the dex-size reduction for this whole dependency graph;
# see ADFA-3604 for the size trade-off.
-keep class org.jetbrains.kotlin.** { *; }
-keep class com.github.benmanes.caffeine.** { *; }
-keep class kotlin.reflect.** { *; }
-keep class kotlin.script.** { *; }
-keep class kotlinx.coroutines.internal.** { *; }
-keep class one.util.streamex.** { *; }
-keep class gnu.trove.** { *; }

# Eclipse
-keep class org.eclipse.** { *; }

Expand Down
Binary file modified app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxhdpi-v4/leak_canary_info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxhdpi-v4/leak_canary_leak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxhdpi-v4/leak_canary_tv_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_dump.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_leak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_tv_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/mipmap-xxhdpi-v4/leak_canary_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/src/debug/res/mipmap-xxxhdpi-v4/leak_canary_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading