Skip to content
Merged
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 @@ -73,9 +73,11 @@ object AssetsInstallationHelper {
}

if (result.isFailure) {
logger.error("Failed to install assets", result.exceptionOrNull())
onProgress(Progress("Failed to install assets"))
return@withContext Result.Failure(result.exceptionOrNull())
val e = result.exceptionOrNull()
val msg = e?.message ?: "Failed to install assets"
logger.error("Failed to install assets", e)
onProgress(Progress(msg))
return@withContext Result.Failure(e, errorMessage = msg)
}

return@withContext Result.Success
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import androidx.annotation.WorkerThread
import com.aayushatharva.brotli4j.decoder.BrotliInputStream
import com.itsaky.androidide.app.configuration.CpuArch
import com.itsaky.androidide.managers.ToolsManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.TerminalInstaller
import com.itsaky.androidide.utils.retryOnceOnNoSuchFile
import com.itsaky.androidide.utils.withTempZipChannel
import com.itsaky.androidide.utils.writeBrotliAssetToPath
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.adfa.constants.ANDROID_SDK_ZIP
Expand All @@ -19,6 +23,7 @@ import org.adfa.constants.LOCAL_MAVEN_REPO_ARCHIVE_ZIP_NAME
import org.slf4j.LoggerFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream
Expand Down Expand Up @@ -69,22 +74,36 @@ data object BundledAssetsInstaller : BaseAssetsInstaller() {
AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> {
val assetPath =
ToolsManager.getCommonAsset("${AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME}.br")
context.assets.open(assetPath).use { assetStream ->
BrotliInputStream(assetStream).use { brotliInputStream ->
val tempZipPath = Files.createTempFile(stagingDir, "bootstrap", ".zip")
try {
Files.newOutputStream(tempZipPath).use { output ->
brotliInputStream.copyTo(output)
}
Files.newByteChannel(tempZipPath).use { channel ->
val result = TerminalInstaller.installIfNeeded(context, channel)
if (result !is TerminalInstaller.InstallResult.Success) {
throw IllegalStateException("Failed to install terminal: $result")
}
}
} finally {
Files.deleteIfExists(tempZipPath)
}

val result = retryOnceOnNoSuchFile (
onFirstFailure = { Files.createDirectories(stagingDir) },
onSecondFailure = { e2 ->
throw IOException(
context.getString(R.string.terminal_installation_failed_low_storage),
e2
)
}
) {
withTempZipChannel(
stagingDir = stagingDir,
prefix = "bootstrap",
writeTo = { path -> writeBrotliAssetToPath(context, assetPath, path) },
useChannel = { ch -> TerminalInstaller.installIfNeeded(context, ch) }
)
}

when (result) {
is TerminalInstaller.InstallResult.Success -> {}
is TerminalInstaller.InstallResult.Error.Interactive -> {
throw IOException("${result.title}: ${result.message}")
}
is TerminalInstaller.InstallResult.Error.IsSecondaryUser -> {
throw IOException(
context.getString(R.string.terminal_installation_failed_secondary_user)
)
}
is TerminalInstaller.InstallResult.NotInstalled -> {
throw IllegalStateException("Terminal installation failed: NotInstalled state")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.itsaky.androidide.app.configuration.CpuArch
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.TerminalInstaller
import com.itsaky.androidide.utils.retryOnceOnNoSuchFile
import com.itsaky.androidide.utils.withTempZipChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.adfa.constants.ANDROID_SDK_ZIP
Expand Down Expand Up @@ -66,22 +68,33 @@ data object SplitAssetsInstaller : BaseAssetsInstaller() {
AssetsInstallationHelper.BOOTSTRAP_ENTRY_NAME -> {
logger.debug("Extracting 'bootstrap.zip' to dir: {}", stagingDir)

// We need a SeekableByteChannel for the TerminalInstaller, but the ZipInputStream is not seekable.
// The only way is to write it to a temporary file first.
val tempBootstrap = Files.createTempFile(stagingDir, "bootstrap", ".zip")
try {
Files.newOutputStream(tempBootstrap).use { out ->
zipInput.copyTo(out)
val result = retryOnceOnNoSuchFile(
onFirstFailure = { Files.createDirectories(stagingDir) },
onSecondFailure = { e2 ->
logger.error("Failed to open temporary bootstrap zip after retry", e2)
return@withContext
}
val channel = Files.newByteChannel(tempBootstrap)
val result = TerminalInstaller.installIfNeeded(context, channel)
if (result !is TerminalInstaller.InstallResult.Success) {
// Log the error and continue with other assets.
logger.error("Failed to install terminal: $result")
}
} finally {
Files.deleteIfExists(tempBootstrap)
) {
withTempZipChannel(
stagingDir = stagingDir,
prefix = "bootstrap",
writeTo = { path ->
zipFile.getInputStream(entry).use { freshZipInput ->
Files.newOutputStream(path).use { out ->
freshZipInput.copyTo(out)
}
}
},
useChannel = { ch ->
TerminalInstaller.installIfNeeded(context, ch)
}
)
}

if (result !is TerminalInstaller.InstallResult.Success) {
logger.error("Failed to install terminal: {}", result)
}

logger.debug("Completed extracting 'bootstrap.zip' to dir: {}", stagingDir)
}

Expand Down
56 changes: 56 additions & 0 deletions app/src/main/java/com/itsaky/androidide/utils/InstallerIoUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.itsaky.androidide.utils

import android.content.Context
import com.aayushatharva.brotli4j.decoder.BrotliInputStream
import java.nio.channels.SeekableByteChannel
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path

internal inline fun <T> withTempZipChannel(
stagingDir: Path,
prefix: String,
writeTo: (Path) -> Unit,
useChannel: (SeekableByteChannel) -> T,
): T {
val tempZipPath = Files.createTempFile(stagingDir, prefix, ".zip")
try {
writeTo(tempZipPath)
Files.newByteChannel(tempZipPath).use { ch ->
return useChannel(ch)
}
} finally {
Files.deleteIfExists(tempZipPath)
}
}

internal fun writeBrotliAssetToPath(
context: Context,
assetPath: String,
destPath: Path,
) {
context.assets.open(assetPath).use { assetStream ->
BrotliInputStream(assetStream).use { brotli ->
Files.newOutputStream(destPath).use { output ->
brotli.copyTo(output)
}
}
}
}

internal inline fun <T> retryOnceOnNoSuchFile(
onFirstFailure: () -> Unit = {},
onSecondFailure: (NoSuchFileException) -> Nothing,
block: () -> T,
): T {
return try {
block()
} catch (_: NoSuchFileException) {
onFirstFailure()
try {
block()
} catch (e2: NoSuchFileException) {
onSecondFailure(e2)
}
}
}
2 changes: 2 additions & 0 deletions resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
<string name="msg_picked_isnt_dir">The picked file is not a directory.</string>
<string name="please_wait">Please wait for a moment.</string>
<string name="not_enough_storage">Not enough storage available for installation. An additional %1$.1fGB is required on the internal storage partition. You currently have %2$.1fGB available.</string>
<string name="terminal_installation_failed_low_storage">Terminal setup failed. Storage space is insufficient, or files were removed during installation. Please free up some space and try again.</string>
<string name="terminal_installation_failed_secondary_user">Terminal installation is only supported for the primary user.</string>

<!-- Project Builder -->
<string name="title_unsupported_device">This device is not supported</string>
Expand Down