diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6956c9b8b1..32f401d28b 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,14 +16,41 @@ import java.net.URL import java.nio.file.Files import java.nio.file.StandardCopyOption import java.nio.file.attribute.FileTime +import java.util.Locale import java.util.Properties +import java.util.zip.CRC32 import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import java.util.zip.CRC32 import kotlin.reflect.jvm.javaMethod +fun TaskContainer.registerD8Task( + taskName: String, + inputJar: File, + outputDex: File +): org.gradle.api.tasks.TaskProvider { + val androidSdkDir = android.sdkDirectory.absolutePath + val buildToolsVersion = android.buildToolsVersion // Gets the version from your project + val d8Executable = File("$androidSdkDir/build-tools/$buildToolsVersion/d8") + + if (!d8Executable.exists()) { + throw FileNotFoundException("D8 executable not found at: ${d8Executable.absolutePath}") + } + + return register(taskName) { + inputs.file(inputJar) + outputs.file(outputDex) + + commandLine( + d8Executable.absolutePath, + "--release", // Enables optimizations + "--output", outputDex.parent, // D8 outputs to a directory + inputJar.absolutePath + ) + } +} + plugins { id("com.android.application") id("kotlin-android") @@ -120,6 +147,11 @@ android { excludes.add("META-INF/gradle/incremental.annotation.processors") } } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true + } } kapt { arguments { arg("eventBusIndex", "${BuildConfig.PACKAGE_NAME}.events.AppEventsIndex") } } @@ -270,10 +302,9 @@ dependencies { implementation(libs.common.markwon.linkify) implementation(libs.commons.text.v1140) - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation(libs.kotlinx.serialization.json) // Koin for Dependency Injection - implementation("io.insert-koin:koin-android:3.5.3") + implementation(libs.koin.android) implementation(libs.androidx.security.crypto) // Sentry Android SDK (core + replay for quality configuration) @@ -289,8 +320,8 @@ dependencies { implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.google.genai) - "v7Implementation"(files("libs/v7/llama-v7-release.aar")) - "v8Implementation"(files("libs/v8/llama-v8-release.aar")) + implementation(project(":llama-api")) + coreLibraryDesugaring(libs.desugar.jdk.libs.v215) } tasks.register("downloadDocDb") { @@ -340,30 +371,118 @@ tasks.register("downloadDocDb") { } fun createAssetsZip(arch: String) { - val outputDir = - project.layout.buildDirectory - .dir("outputs/assets") - .get() - .asFile - if (!outputDir.exists()) { - outputDir.mkdirs() - println("Creating output directory: ${outputDir.absolutePath}") - } + val outputDir = + project.layout.buildDirectory + .dir("outputs/assets") + .get() + .asFile + if (!outputDir.exists()) { + outputDir.mkdirs() + } + val zipFile = outputDir.resolve("assets-$arch.zip") + + // --- Part 1: Get the classes.jar from our llama-impl AAR --- + val llamaAarName = when (arch) { + "arm64-v8a" -> "llama-impl-v8-release.aar" + "armeabi-v7a" -> "llama-impl-v7-release.aar" + else -> throw IllegalArgumentException("Unsupported architecture for Llama AAR: $arch") + } + val originalLlamaAarFile = project.rootDir.resolve("llama-impl/build/outputs/aar/$llamaAarName") + + val tempDir = project.layout.buildDirectory.dir("tmp/d8/$arch").get().asFile + tempDir.deleteRecursively() + tempDir.mkdirs() + val tempClassesJar = File(tempDir, "classes.jar") + + // Extract just the classes.jar from our target AAR + ZipInputStream(originalLlamaAarFile.inputStream()).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + if (entry.name == "classes.jar") { + tempClassesJar.outputStream().use { fos -> zis.copyTo(fos) } + break + } + entry = zis.nextEntry + } + } + if (!tempClassesJar.exists()) { + throw GradleException("classes.jar not found inside ${originalLlamaAarFile.name}") + } - val zipFile = outputDir.resolve("assets-$arch.zip") - val sourceDir = project.rootDir.resolve("assets") - val bootstrapName = "bootstrap-$arch.zip" - val androidSdkName = "android-sdk-$arch.zip" - - ZipOutputStream(zipFile.outputStream()).use { zipOut -> - arrayOf( - androidSdkName, - "localMvnRepository.zip", - "gradle-8.14.3-bin.zip", - "gradle-api-8.14.3.jar.zip", - "documentation.db", - bootstrapName, - ).forEach { fileName -> + val llamaImplProject = project.project(":llama-impl") + val flavorName = if (arch == "arm64-v8a") "v8" else "v7" + val configName = "${flavorName}ReleaseRuntimeClasspath" + val runtimeClasspathFiles = llamaImplProject.configurations.getByName(configName).files + + val explodedAarsDir = project.layout.buildDirectory.dir("tmp/exploded-aars/$arch").get().asFile + explodedAarsDir.mkdirs() + + val d8Classpath = mutableListOf() + runtimeClasspathFiles.forEach { file -> + if (file.name.endsWith(".jar")) { + d8Classpath.add(file) + } else if (file.name.endsWith(".aar")) { + // It's an AAR, extract its classes.jar + project.copy { + from(project.zipTree(file)) { + include("classes.jar") + } + into(explodedAarsDir) + // Rename to avoid collisions + rename { "${file.nameWithoutExtension}-classes.jar" } + } + d8Classpath.add(File(explodedAarsDir, "${file.nameWithoutExtension}-classes.jar")) + } + } + + // --- Part 3: Run D8 with the corrected command-line arguments --- + val dexOutputFile = File(tempDir, "classes.dex") + project.exec { + val androidSdkDir = android.sdkDirectory.absolutePath + val buildToolsVersion = android.buildToolsVersion + val d8Executable = File("$androidSdkDir/build-tools/$buildToolsVersion/d8") + + // 1. Start building the command arguments list + val d8Command = mutableListOf() + d8Command.add(d8Executable.absolutePath) + d8Command.add("--release") + d8Command.add("--min-api") + d8Command.add(android.defaultConfig.minSdk.toString()) // Add minSdk for better desugaring + + // 2. Add the --classpath flag REPEATEDLY for each dependency file + d8Classpath.forEach { file -> + if (file.exists()) { + d8Command.add("--classpath") + d8Command.add(file.absolutePath) + } + } + + // 3. Add the final output and input arguments + d8Command.add("--output") + d8Command.add(tempDir.absolutePath) + d8Command.add(tempClassesJar.absolutePath) + + // 4. Set the full command line + commandLine(d8Command) + }.assertNormalExitValue() + + if (!dexOutputFile.exists()) { + throw GradleException("D8 task failed to produce classes.dex") + } + + // --- Part 4: Repackage everything into the final assets-*.zip (Unchanged) --- + val sourceDir = project.rootDir.resolve("assets") + val bootstrapName = "bootstrap-$arch.zip" + val androidSdkName = "android-sdk-$arch.zip" + ZipOutputStream(zipFile.outputStream()).use { zipOut -> + arrayOf( + androidSdkName, + "localMvnRepository.zip", + "gradle-8.14.3-bin.zip", + "gradle-api-8.14.3.jar.zip", + "documentation.db", + bootstrapName, + ).forEach { fileName -> val filePath = sourceDir.resolve(fileName) if (!filePath.exists()) { throw FileNotFoundException(filePath.absolutePath) @@ -381,18 +500,126 @@ fun createAssetsZip(arch: String) { filePath.inputStream().use { input -> input.copyTo(zipOut) } zipOut.closeEntry() } + project.logger.lifecycle("Repackaging Llama AAR with classes.dex...") + + // Create the entry for our modified AAR inside assets-*.zip + zipOut.putNextEntry(ZipEntry("dynamic_libs/llama.aar")) + + // Use another ZipOutputStream to build the new AAR in memory and stream it + ZipOutputStream(zipOut).use { aarZipOut -> + // Copy all files from the original AAR *except* classes.jar + ZipInputStream(originalLlamaAarFile.inputStream()).use { originalAarStream -> + var entry = originalAarStream.nextEntry + while (entry != null) { + if (entry.name != "classes.jar") { + aarZipOut.putNextEntry(ZipEntry(entry.name)) + originalAarStream.copyTo(aarZipOut) + aarZipOut.closeEntry() + } + entry = originalAarStream.nextEntry + } + } + aarZipOut.putNextEntry(ZipEntry("classes.dex")) + dexOutputFile.inputStream().use { dexInput -> dexInput.copyTo(aarZipOut) } + aarZipOut.closeEntry() + } + println("Created ${zipFile.name} successfully at ${zipFile.parentFile.absolutePath}") + } +} - println("Created ${zipFile.name} successfully at ${zipFile.parentFile.absolutePath}") - } +fun registerBundleLlamaAssetsTask(flavor: String, arch: String): TaskProvider { + val capitalized = + flavor.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } + return tasks.register("bundle${capitalized}LlamaAssets") { + dependsOn("assemble${capitalized}Assets") + + doLast { + val assetsZip = + project.layout.buildDirectory + .file("outputs/assets/assets-$arch.zip") + .get() + .asFile + if (!assetsZip.exists()) { + throw GradleException("Assets zip not found: ${assetsZip.absolutePath}. Run assemble${capitalized}Assets first.") + } + + val tempAar = Files.createTempFile("llama-$flavor", ".aar").toFile() + var found = false + ZipInputStream(assetsZip.inputStream()).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + if (entry.name == "dynamic_libs/llama.aar") { + tempAar.outputStream().use { zis.copyTo(it) } + found = true + break + } + entry = zis.nextEntry + } + } + + if (!found) { + tempAar.delete() + throw GradleException("dynamic_libs/llama.aar not found inside ${assetsZip.name}") + } + + val targetDir = project.rootProject.file("assets/release/$flavor/dynamic_libs") + targetDir.mkdirs() + val destBr = File(targetDir, "llama-$flavor.aar.br") + val destAar = File(targetDir, "llama-$flavor.aar") + + destBr.delete() + destAar.delete() + + val brotliAvailable = + try { + val result = + project.exec { + commandLine("brotli", "--version") + isIgnoreExitValue = true + } + result.exitValue == 0 + } catch (_: Exception) { + false + } + + if (brotliAvailable) { + project.exec { + commandLine("brotli", "-f", "-o", destBr.absolutePath, tempAar.absolutePath) + } + project.logger.lifecycle( + "Bundled llama AAR compressed to ${ + destBr.relativeTo( + project.rootProject.projectDir + ) + }" + ) + destAar.delete() + } else { + project.logger.warn( + "brotli CLI not found; bundling llama AAR uncompressed at ${ + destAar.relativeTo( + project.rootProject.projectDir + ) + }" + ) + tempAar.copyTo(destAar, overwrite = true) + destBr.delete() + } + + tempAar.delete() + } + } } tasks.register("assembleV8Assets") { + dependsOn(":llama-impl:assembleV8Release") doLast { createAssetsZip("arm64-v8a") } } tasks.register("assembleV7Assets") { + dependsOn(":llama-impl:assembleV7Release") doLast { createAssetsZip("armeabi-v7a") } @@ -402,6 +629,9 @@ tasks.register("assembleAssets") { dependsOn("assembleV8Assets", "assembleV7Assets") } +val bundleLlamaV7Assets = registerBundleLlamaAssetsTask(flavor = "v7", arch = "armeabi-v7a") +val bundleLlamaV8Assets = registerBundleLlamaAssetsTask(flavor = "v8", arch = "arm64-v8a") + tasks.register("recompressApk") { doLast { val abi: String = extensions.extraProperties["abi"].toString() @@ -437,6 +667,8 @@ afterEvaluate { extensions.extraProperties["noCompressExtensions"] = noCompress } } + + dependsOn(bundleLlamaV8Assets) } tasks.named("assembleV7Release").configure { @@ -449,6 +681,8 @@ afterEvaluate { extensions.extraProperties["noCompressExtensions"] = noCompress } } + + dependsOn(bundleLlamaV7Assets) } tasks.named("assembleV8Debug").configure { diff --git a/app/src/main/java/com/itsaky/androidide/agent/fragments/AiSettingsFragment.kt b/app/src/main/java/com/itsaky/androidide/agent/fragments/AiSettingsFragment.kt index 513253c139..a56a17da3a 100644 --- a/app/src/main/java/com/itsaky/androidide/agent/fragments/AiSettingsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/agent/fragments/AiSettingsFragment.kt @@ -21,6 +21,7 @@ import com.google.android.material.textfield.TextInputLayout import com.itsaky.androidide.R import com.itsaky.androidide.agent.repository.AiBackend import com.itsaky.androidide.agent.viewmodel.AiSettingsViewModel +import com.itsaky.androidide.agent.viewmodel.EngineState import com.itsaky.androidide.agent.viewmodel.ModelLoadingState import com.itsaky.androidide.databinding.FragmentAiSettingsBinding import com.itsaky.androidide.utils.flashInfo @@ -45,11 +46,7 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) { requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) val uriString = it.toString() - // The fragment's only job is to save the path via the ViewModel. - viewModel.saveLocalModelPath(uriString) viewModel.loadModelFromUri(uriString, requireContext()) - // It also updates its own UI. - updateLocalLlmUi(binding.backendSpecificSettingsContainer) flashInfo("Attempting to load selected model...") } } @@ -84,7 +81,6 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) { binding.backendAutocomplete.setOnItemClickListener { _, _, position, _ -> val selectedBackend = backends[position] - // Its only job is to save the backend selection. viewModel.saveBackend(selectedBackend) updateBackendSpecificUi(selectedBackend) } @@ -100,7 +96,6 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) { .inflate(R.layout.layout_settings_local_llm, container, true) updateLocalLlmUi(localLlmView) } - AiBackend.GEMINI -> { val geminiApiView = LayoutInflater.from(requireContext()) .inflate(R.layout.layout_settings_gemini_api, container, true) @@ -114,8 +109,7 @@ class AiSettingsFragment : Fragment(R.layout.fragment_ai_settings) { val browseButton = view.findViewById