Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,18 @@ object AssetsInstallationHelper {
val freeStorage = getAvailableStorage(File(DEFAULT_ROOT))

val snapshot =
buildString {
entryStatusMap.forEach { (entry, status) ->
appendLine("$entry ${if (status == STATUS_FINISHED) "✓" else ""}")
if (percent >= 99.99) {
"Post install processing in progress...."
} else {
buildString {
entryStatusMap.forEach { (entry, status) ->
appendLine("$entry ${if (status == STATUS_FINISHED) "✓" else ""}")
}
appendLine("--------------------")
appendLine("Progress: ${formatPercent(percent)}")
appendLine("Installed: ${formatBytes(installedSize)} / ${formatBytes(totalSize)}")
appendLine("Remaining storage: ${formatBytes(freeStorage)}")
}
appendLine("--------------------")
appendLine("Progress: ${formatPercent(percent)}")
appendLine("Installed: ${formatBytes(installedSize)} / ${formatBytes(totalSize)}")
appendLine("Remaining storage: ${formatBytes(freeStorage)}")
}
Comment on lines 178 to 191
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Gate the “post install” message on completion, not percent.

If any expectedSize is approximate/zero, percent can reach 99.99 before all entries are finished, which hides the per‑entry status prematurely. Consider switching the condition to “all entries finished” for a more accurate status.

🔧 Suggested adjustment
-                    val snapshot =
-                        if (percent >= 99.99) {
+                    val allFinished =
+                        entryStatusMap.size == expectedEntries.size &&
+                            entryStatusMap.values.all { it == STATUS_FINISHED }
+
+                    val snapshot =
+                        if (allFinished) {
                             "Post install processing in progress...."
                         } else {
                             buildString {
                                 entryStatusMap.forEach { (entry, status) ->
                                     appendLine("$entry ${if (status == STATUS_FINISHED) "✓" else ""}")
                                 }
                                 appendLine("--------------------")
                                 appendLine("Progress: ${formatPercent(percent)}")
                                 appendLine("Installed: ${formatBytes(installedSize)} / ${formatBytes(totalSize)}")
                                 appendLine("Remaining storage: ${formatBytes(freeStorage)}")
                             }
                         }
🤖 Prompt for AI Agents
In `@app/src/main/java/com/itsaky/androidide/assets/AssetsInstallationHelper.kt`
around lines 178 - 191, The snapshot logic currently uses percent >= 99.99 to
show the "Post install processing..." message; instead, change it to check that
all entries are finished by inspecting entryStatusMap (e.g.
entryStatusMap.values.all { it == STATUS_FINISHED }) so the per-entry status
list is only hidden when every entry is STATUS_FINISHED; update the condition
around the snapshot variable (and any related branches that produce the "Post
install processing in progress...." text) to use this all-finished check rather
than the percent threshold so approximate/zero expectedSize won’t prematurely
hide entry statuses.


if (snapshot != previousSnapshot) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
package com.itsaky.androidide.assets

import android.content.Context
import com.itsaky.androidide.utils.Environment
import org.slf4j.LoggerFactory
import java.io.File
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import com.termux.shared.termux.TermuxConstants
import com.itsaky.androidide.utils.Environment
import kotlin.system.measureTimeMillis


abstract class BaseAssetsInstaller : AssetsInstaller {
private val logger = LoggerFactory.getLogger(BaseAssetsInstaller::class.java)

override suspend fun postInstall(
context: Context,
stagingDir: Path
) {
Environment.AAPT2.setExecutable(true)

installNdk(
File(Environment.ANDROID_HOME, Environment.NDK_TAR_XZ),
Environment.ANDROID_HOME
)
}

private fun installNdk(archiveFile: File, outputDir: File): Boolean {
Copy link
Collaborator

@jatezzz jatezzz Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jomen-adfa I have a question about this process: does it run on the main thread or is it called from a coroutine? Since it processes files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jatezzz It's called from assetsinstallationhelper after the doinstalls are done, either for debug or release. This needs to be done in postInstall since it needs both android-sdk and bootstrap packages to be already extracted for tar and xz to run without potential issues for the moment so we can release and get feedback. This would be a bit temporary since in the near future we could be putting the ndk package in a plugin and additionally if we do decide to adapt tar.xz packaging then we'd do the extraction in our code using apache commons packages and not delegated to tar and xz utilities in termux.

if (!archiveFile.exists()) {
logger.debug("NDK installable package not found: ${archiveFile.absolutePath}")
return false
}

logger.debug("Starting installation of ${archiveFile.absolutePath}")

var exitCode: Int
var result: String
val elapsed = measureTimeMillis {
val processBuilder = ProcessBuilder(
"${TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH}/bash",
"-c",
"tar -xJf ${archiveFile.absolutePath} -C ${outputDir.absolutePath} --no-same-owner"
)
.redirectErrorStream(true)

val env = processBuilder.environment()
env["PATH"] = "${TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH}:${env["PATH"]}"

val process = processBuilder.start()

result = process.inputStream.bufferedReader().use { it.readText() }
val completed = process.waitFor(2, TimeUnit.MINUTES)
exitCode = if (completed) process.exitValue() else {
process.destroyForcibly()
-1
}
Comment on lines +49 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Timeout is ineffective because output is read before waitFor.

readText() blocks until the process exits, so a hung tar extraction can still block indefinitely (and can deadlock if output buffers fill). Drain output concurrently, then apply the timeout.

🔧 Suggested fix (drain output concurrently)
 import java.util.concurrent.TimeUnit
+import kotlin.concurrent.thread
 ...
             val process = processBuilder.start()
 
-            result = process.inputStream.bufferedReader().use { it.readText() }
-            val completed = process.waitFor(2, TimeUnit.MINUTES)
-            exitCode = if (completed) process.exitValue() else {
-                process.destroyForcibly()
-                -1
-            }
+            val output = StringBuilder()
+            val reader = thread(start = true, name = "ndk-extract-output") {
+                process.inputStream.bufferedReader().useLines { lines ->
+                    lines.forEach { output.appendLine(it) }
+                }
+            }
+
+            val completed = process.waitFor(2, TimeUnit.MINUTES)
+            if (!completed) {
+                process.destroyForcibly()
+            }
+            reader.join()
+            result = output.toString()
+            exitCode = if (completed) process.exitValue() else -1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val process = processBuilder.start()
result = process.inputStream.bufferedReader().use { it.readText() }
val completed = process.waitFor(2, TimeUnit.MINUTES)
exitCode = if (completed) process.exitValue() else {
process.destroyForcibly()
-1
}
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
val process = processBuilder.start()
val output = StringBuilder()
val reader = thread(start = true, name = "ndk-extract-output") {
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { output.appendLine(it) }
}
}
val completed = process.waitFor(2, TimeUnit.MINUTES)
if (!completed) {
process.destroyForcibly()
}
reader.join()
result = output.toString()
exitCode = if (completed) process.exitValue() else -1
🤖 Prompt for AI Agents
In `@app/src/main/java/com/itsaky/androidide/assets/BaseAssetsInstaller.kt` around
lines 49 - 56, The current code reads process.inputStream with
process.inputStream.bufferedReader().use { it.readText() } before calling
process.waitFor, which can block indefinitely; instead start draining both
stdout and stderr concurrently (e.g., spawn threads or use Executors to read
process.inputStream and process.errorStream into strings or StringBuilders)
immediately after processBuilder.start(), then call process.waitFor(2,
TimeUnit.MINUTES) and set exitCode based on the completed flag (and call
process.destroyForcibly() on timeout); finally collect the drained output into
result and include stderr if needed. Ensure you keep references to the started
threads/futures so you can await their completion (or cancel) after waitFor, and
update variables result and exitCode accordingly.

}

return if (exitCode == 0) {
logger.debug("Extraction of ${archiveFile.absolutePath} successful took ${elapsed}ms : $result")

if (archiveFile.exists()) {
val deleted = archiveFile.delete()
if (deleted) {
logger.debug("${archiveFile.absolutePath} deleted successfully.")
} else {
logger.debug("Failed to delete ${archiveFile.absolutePath}.")
}
deleted
} else {
logger.debug("Archive file not found for deletion.")
false
}
} else {
logger.error("Extraction failed with code $exitCode: $result")
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() {

override fun expectedSize(entryName: String): Long = when (entryName) {
GRADLE_DISTRIBUTION_ARCHIVE_NAME -> 63399283L
ANDROID_SDK_ZIP -> 53226785L
ANDROID_SDK_ZIP -> 254814511L
DOCUMENTATION_DB -> 297763377L
LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME -> 97485855L
AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> 124120151L
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {
logger.debug("Completed extracting '{}' to dir: {}", entry.name, destDir)
}

AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> {
AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> {
logger.debug("Extracting 'bootstrap.zip' to dir: {}", stagingDir)

val result = retryOnceOnNoSuchFile(
Expand Down Expand Up @@ -173,7 +173,7 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {

override fun expectedSize(entryName: String): Long = when (entryName) {
GRADLE_DISTRIBUTION_ARCHIVE_NAME -> 137260932L
ANDROID_SDK_ZIP -> 85024182L
ANDROID_SDK_ZIP -> 286625871L
DOCUMENTATION_DB -> 224296960L
LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME -> 215389106L
AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> 456462823L
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ public final class Environment {
"PL", "PT", "RO", "SK", "SI", "ES", "SE"
};

public static final String NDK_TAR_XZ = "ndk-cmake.tar.xz";
public static File NDK_DIR;

public static String getArchitecture() {
return IDEBuildConfigProvider.getInstance().getCpuAbiName();
}
Expand Down Expand Up @@ -176,6 +179,8 @@ public static void init(Context context) {
KEYSTORE_RELEASE = new File(KEYSTORE_DIR, KEYSTORE_RELEASE_NAME);
KEYSTORE_PROPERTIES = new File(KEYSTORE_DIR, KEYSTORE_PROPERTIES_NAME);

NDK_DIR = new File(ANDROID_HOME,"ndk");

isInitialized.set(true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ object TooltipTag {
const val TEMPLATE_BASIC_ACTIVITY = "template.basic.activity"
const val TEMPLATE_NO_ACTIVITY = "template.no.activity"
const val TEMPLATE_NAV_DRAWER_ACTIVITY = "template.navdrawer.activity"
const val TEMPLATE_NDK_ACTIVITY = "template.ndk.activity"

// Editor screen
const val EDITOR_PROJECT_OVERVIEW = "project.overview"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
<string name="template_no_activity">No Activity</string>
<string name="template_no_AndroidX">Legacy Project (No AndroidX)</string>
<string name="template_plugin">Code on the Go Plugin</string>
<string name="template_ndk">NDK Activity</string>

<!-- Plugin Template Wizard -->
<string name="wizard_plugin_name">Plugin Name</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import com.itsaky.androidide.templates.base.util.stringRes
import com.squareup.javapoet.TypeSpec
import java.io.File

class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() {
open class AndroidModuleTemplateBuilder : ModuleTemplateBuilder() {

/**
* Set whether this Android module is a Jetpack Compose module or not.
Expand Down
Loading