diff --git a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt index 53289b47..6a17d66d 100644 --- a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt @@ -93,7 +93,6 @@ private fun SelectDroidBoot(c: CreateBackupDataHolder) { else -> "" } ) - val next = Button(onClick = { if (c.action != 1) { c.vm.activity.chooseFile("*/*") { diff --git a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt index c2cb2c33..bd8fed21 100644 --- a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt @@ -654,6 +654,7 @@ private fun Flash(c: CreatePartDataHolder) { val vm = c.vm Terminal(logFile = "install_${System.currentTimeMillis()}.txt") { terminal -> c.vm.logic.extractToolkit(terminal) + c.vm.downloadRemainingFiles(terminal) if (c.partitionName == null) { // OS install val createdParts = mutableListOf>() // order is important val fn = c.romFolderName diff --git a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt index 7b846305..5fb8943b 100644 --- a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt @@ -186,6 +186,7 @@ private fun Flash(d: DroidBootFlowDataHolder) { val vm = d.vm Terminal(logFile = "blflash_${System.currentTimeMillis()}.txt") { terminal -> vm.logic.extractToolkit(terminal) + vm.downloadRemainingFiles(terminal) terminal.add(vm.activity.getString(R.string.term_preparing_fs)) if (vm.logic.checkMounted()) { terminal.add(vm.activity.getString(R.string.term_mount_state_bad)) diff --git a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt index 2f6a4e26..c8229435 100644 --- a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt @@ -60,6 +60,7 @@ private fun Start(vm: WizardState) { private fun Flash(vm: WizardState) { Terminal(logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> vm.logic.extractToolkit(terminal) + vm.downloadRemainingFiles(terminal) val tmpFile = if (vm.deviceInfo.postInstallScript) { vm.chosen["_install.sh_"]!!.toFile(vm).also { it.setExecutable(true) diff --git a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt index 91b2ee1f..19f74d35 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt @@ -60,6 +60,7 @@ private fun Start(vm: WizardState) { private fun Flash(vm: WizardState) { Terminal(logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> vm.logic.extractToolkit(terminal) + vm.downloadRemainingFiles(terminal) val tmpFile = if (vm.deviceInfo.postInstallScript) { vm.chosen["_install.sh_"]!!.toFile(vm).also { it.setExecutable(true) diff --git a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt index 6d3baf97..50825c16 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt @@ -209,6 +209,7 @@ private fun Local(u: UpdateFlowDataHolder) { private fun Flash(u: UpdateFlowDataHolder) { Terminal(logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> u.vm.logic.extractToolkit(terminal) + u.vm.downloadRemainingFiles(terminal) val sp = u.e!!["xpart"]!!.split(":") val meta = SDUtils.generateMeta(u.vm.deviceInfo)!! Shell.cmd(SDUtils.umsd(meta)).exec() diff --git a/app/src/main/java/org/andbootmgr/app/Wizard.kt b/app/src/main/java/org/andbootmgr/app/Wizard.kt index 72600d81..044d1b45 100644 --- a/app/src/main/java/org/andbootmgr/app/Wizard.kt +++ b/app/src/main/java/org/andbootmgr/app/Wizard.kt @@ -1,9 +1,8 @@ package org.andbootmgr.app import android.net.Uri -import android.util.Log +import android.os.CancellationSignal import android.view.WindowManager -import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -12,7 +11,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Icon @@ -38,18 +36,18 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.topjohnwu.superuser.io.SuFileOutputStream -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.andbootmgr.app.util.AbmOkHttp -import org.andbootmgr.app.util.SOUtils +import org.andbootmgr.app.util.TerminalCancelException +import org.andbootmgr.app.util.TerminalList import java.io.File import java.io.FileInputStream +import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.nio.file.Files import java.nio.file.StandardCopyOption +import java.util.concurrent.CancellationException abstract class WizardFlow { abstract fun get(vm: WizardState): List @@ -140,7 +138,42 @@ class WizardState(val mvm: MainActivityState) { throw IllegalStateException("invalid DledFile OR safFile failure") } } - //suspend fun downloadRemainingFiles + suspend fun downloadRemainingFiles(terminal: TerminalList) { + terminal.isCancelled.value = false + for (id in idNeeded.filter { !chosen.containsKey(it) }) { + if (!inetAvailable.containsKey(id)) + throw IllegalStateException("$id not chosen and not available from inet") + terminal.add(activity.getString(R.string.downloading_s, id)) + val inet = inetAvailable[id]!! + val f = File(logic.cacheDir, System.currentTimeMillis().toString()) + terminal.add(activity.getString(R.string.connecting_text)) + val client = AbmOkHttp(inet.url, f, inet.hash) { readBytes, total, done -> + terminal[terminal.size - 1] = if (done) activity.getString(R.string.done) else + activity.getString( + R.string.download_progress, + "${readBytes / (1024 * 1024)} MiB", "${total / (1024 * 1024)} MiB" + ) + } + terminal.cancel = { terminal.isCancelled.value = true; client.cancel() } + try { + client.run() + } catch (e: IOException) { + if (terminal.isCancelled.value == true) { + throw TerminalCancelException() + } + throw e + } + if (terminal.isCancelled.value == true) { + throw TerminalCancelException() + } + chosen[id] = DownloadedFile(null, f) + } + if (terminal.isCancelled.value == true) { + throw TerminalCancelException() + } else { + terminal.isCancelled.value = null + } + } fun navigate(next: String) { prevText = null @@ -224,21 +257,6 @@ fun WizardDownloader(vm: WizardState, next: String) { Text(stringResource(id = R.string.provide_images)) } } - var cancelDownload by remember { mutableStateOf<(() -> Unit)?>(null) } - var progressText by remember { mutableStateOf(vm.activity.getString(R.string.connecting_text)) } - if (cancelDownload != null) { - AlertDialog( - onDismissRequest = {}, - confirmButton = { - Button(onClick = { cancelDownload!!() }) { - Text(stringResource(id = R.string.cancel)) - } - }, - title = { Text(stringResource(R.string.downloading)) }, - text = { - LoadingCircle(progressText, paddingBetween = 10.dp) - }) - } for (i in vm.idNeeded) { Row( verticalAlignment = Alignment.CenterVertically, @@ -261,41 +279,6 @@ fun WizardDownloader(vm: WizardState, next: String) { Text(stringResource(R.string.undo)) } } else { - /*if (vm.inetAvailable.containsKey(i)) { - Button(onClick = { - CoroutineScope(Dispatchers.Main).launch { - val url = vm.inetAvailable[i]!!.url - val downloadedFile = File(vm.logic.cacheDir, i) - val h = vm.inetAvailable[i]!!.hash - val client = AbmOkHttp(url, downloadedFile, h) { bytesRead, contentLength, _ -> - progressText = vm.activity.getString(R.string.download_progress, - SOUtils.humanReadableByteCountBin(bytesRead), SOUtils.humanReadableByteCountBin(contentLength)) - } - try { - progressText = vm.activity.getString(R.string.connecting_text) - cancelDownload = { - client.cancel() - cancelDownload = null - } - if (client.run()) { - vm.chosen[i] = WizardState.DownloadedFile(null, downloadedFile) - } - } catch (e: Exception) { - Log.e("ABM", Log.getStackTraceString(e)) - withContext(Dispatchers.Main) { - Toast.makeText( - vm.activity, - vm.activity.getString(R.string.dl_error), - Toast.LENGTH_LONG - ).show() - } - } - cancelDownload = null - } - }) { - Text(stringResource(R.string.download)) - } - }*/ Button(onClick = { vm.activity.chooseFile("*/*") { vm.chosen[i] = WizardState.DownloadedFile(it, null) diff --git a/app/src/main/java/org/andbootmgr/app/util/Terminal.kt b/app/src/main/java/org/andbootmgr/app/util/Terminal.kt index 2cd60e30..fa9539f0 100644 --- a/app/src/main/java/org/andbootmgr/app/util/Terminal.kt +++ b/app/src/main/java/org/andbootmgr/app/util/Terminal.kt @@ -2,13 +2,16 @@ package org.andbootmgr.app.util import android.util.Log import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -18,6 +21,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope @@ -31,9 +35,12 @@ import java.io.File import java.io.FileOutputStream private class BudgetCallbackList(private val scope: CoroutineScope, - private val log: FileOutputStream?) : MutableList { + private val log: FileOutputStream?) + : MutableList, TerminalList { + override val isCancelled = mutableStateOf(null) + override var cancel: (() -> Unit)? = null val internalList = ArrayList() - var cb: ((String) -> Unit)? = null + var cb: (() -> Unit)? = null override val size: Int get() = internalList.size @@ -118,7 +125,9 @@ private class BudgetCallbackList(private val scope: CoroutineScope, } override fun set(index: Int, element: String): String { - return internalList.set(index, element) + return internalList.set(index, element).also { + cb?.invoke() + } } override fun subList(fromIndex: Int, toIndex: Int): MutableList { @@ -129,18 +138,26 @@ private class BudgetCallbackList(private val scope: CoroutineScope, scope.launch { log?.write((element + "\n").encodeToByteArray()) } - cb?.invoke(element) + cb?.invoke() } } +interface TerminalList : MutableList { + val isCancelled: MutableState + var cancel: (() -> Unit)? +} +class TerminalCancelException : RuntimeException() + /* Monospace auto-scrolling text view, fed using MutableList, catching exceptions and running logic on a different thread */ @OptIn(ExperimentalCoroutinesApi::class) @Composable fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null, - action: (suspend (MutableList) -> Unit)?) { + action: (suspend (TerminalList) -> Unit)?) { val scrollH = rememberScrollState() val scrollV = rememberScrollState() - val scope = rememberCoroutineScope() + val scope = rememberCoroutineScope { Dispatchers.Main } + var isCancelledState by remember { mutableStateOf(mutableStateOf(null)) } + var doCancelState by remember { mutableStateOf<(() -> Unit)?>(null) } var didConnectAndFinish by rememberSaveable { mutableStateOf(false) } var text by rememberSaveable { mutableStateOf("") } val ctx = LocalContext.current.applicationContext @@ -161,9 +178,12 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null, val logDispatcher = Dispatchers.IO.limitedParallelism(1) val log = logFile?.let { FileOutputStream(File(ctx.externalCacheDir, it)) } val s = BudgetCallbackList(CoroutineScope(logDispatcher), log) - s.cb = { element -> + isCancelledState = s.isCancelled + doCancelState = { s.cancel!!() } + s.cb = { + val l = s.toList() scope.launch { - text += element + "\n" + text = l.joinToString("\n").let { if (s.isNotEmpty()) it + "\n" else it } delay(200) // Give it time to re-measure scrollV.animateScrollTo(scrollV.maxValue) scrollH.animateScrollTo(0) @@ -173,6 +193,8 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null, withContext(Dispatchers.Default) { try { action(s) + } catch (e: TerminalCancelException) { + s.add(ctx.getString(R.string.install_canceled)) } catch (e: Throwable) { s.add(ctx.getString(R.string.term_failure)) s.add(ctx.getString(R.string.dev_details)) @@ -185,22 +207,37 @@ fun Terminal(logFile: String? = null, doWhenDone: (() -> Unit)? = null, }, s) } else { val s = service.workExtra as BudgetCallbackList + isCancelledState = s.isCancelled + doCancelState = { s.cancel!!() } text = s.joinToString("\n").let { if (s.isNotEmpty()) it + "\n" else it } - s.cb = { element -> + s.cb = { + val l = s.toList() scope.launch { - text += element + "\n" + text = l.joinToString("\n").let { if (s.isNotEmpty()) it + "\n" else it } delay(200) // Give it time to re-measure scrollV.animateScrollTo(scrollV.maxValue) scrollH.animateScrollTo(0) } } + } } } } - Text(text, modifier = Modifier - .fillMaxSize() - .horizontalScroll(scrollH) - .verticalScroll(scrollV) - .padding(10.dp), fontFamily = FontFamily.Monospace) + Column(modifier = Modifier.fillMaxSize()) { + Text(text, modifier = Modifier + .fillMaxSize() + .weight(1f) + .horizontalScroll(scrollH) + .verticalScroll(scrollV) + .padding(10.dp), fontFamily = FontFamily.Monospace + ) + if (isCancelledState.value == false) { + Button({ + doCancelState?.invoke() + }) { + Text(stringResource(R.string.cancel)) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e41346f..a509f457 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,6 +54,7 @@ Install (Connecting…) Downloading… + Downloading %s… Please now provide images for all required IDs. You can use the recommended ones using the \"Download\" button! User-selected Undo