diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b38baaf..53bc0f85 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt index 8a5554d1..b9fd7b2e 100644 --- a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFileInputStream import org.andbootmgr.app.util.SDUtils +import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -121,7 +122,7 @@ private fun SelectDroidBoot(c: CreateBackupDataHolder) { @Composable private fun Flash(c: CreateBackupDataHolder) { - Terminal(c.vm, logFile = "flash_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "flash_${System.currentTimeMillis()}.txt") { terminal -> terminal.add(c.vm.activity.getString(R.string.term_starting)) try { val p = c.meta!!.dumpKernelPartition(c.pi) diff --git a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt index 95da7ccc..d4c983f1 100644 --- a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt @@ -34,6 +34,7 @@ import org.andbootmgr.app.util.AbmTheme import org.andbootmgr.app.util.ConfigFile import org.andbootmgr.app.util.SDUtils import org.andbootmgr.app.util.SOUtils +import org.andbootmgr.app.util.Terminal import java.io.File import java.io.FileInputStream import java.io.InputStream @@ -912,7 +913,7 @@ private fun Download(c: CreatePartDataHolder) { @Composable private fun Flash(c: CreatePartDataHolder) { val vm = c.vm - Terminal(vm, logFile = "install_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "install_${System.currentTimeMillis()}.txt") { terminal -> if (c.t == null) { // OS install val parts = ArrayMap() val fn = c.t2.value @@ -1019,7 +1020,7 @@ private fun Flash(c: CreatePartDataHolder) { vm.logic.unmountBootset() val r = vm.logic.create(c.p, offset, offset + k, code, "").to(terminal).exec() try { - if (r.out.join("\n").contains("kpartx")) { + if (r.out.joinToString("\n").contains("kpartx")) { terminal.add(vm.activity.getString(R.string.term_reboot_asap)) } parts[it] = c.meta!!.nid @@ -1054,7 +1055,7 @@ private fun Flash(c: CreatePartDataHolder) { "0700", c.t!! ).to(terminal).exec() - if (r.out.join("\n").contains("kpartx")) { + if (r.out.joinToString("\n").contains("kpartx")) { terminal.add(vm.activity.getString(R.string.term_reboot_asap)) } if (r.isSuccess) { diff --git a/app/src/main/java/org/andbootmgr/app/DeviceInfo.kt b/app/src/main/java/org/andbootmgr/app/DeviceInfo.kt index 02ea1fc7..f1d9931b 100644 --- a/app/src/main/java/org/andbootmgr/app/DeviceInfo.kt +++ b/app/src/main/java/org/andbootmgr/app/DeviceInfo.kt @@ -5,6 +5,8 @@ import android.content.Context import android.util.Log import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.andbootmgr.app.util.SDUtils import org.json.JSONObject import org.json.JSONTokener @@ -38,7 +40,7 @@ interface DeviceInfo { e.printStackTrace() } val result = Shell.cmd("grep ABM.bootloader=1 /proc/cmdline").exec() - return result.isSuccess && result.out.join("\n").contains("ABM.bootloader=1") + return result.isSuccess && result.out.joinToString("\n").contains("ABM.bootloader=1") } fun isCorrupt(logic: DeviceLogic): Boolean fun getAbmSettings(logic: DeviceLogic): String? @@ -77,42 +79,45 @@ class JsonDeviceInfo( ) : MetaOnSdDeviceInfo() class JsonDeviceInfoFactory(private val ctx: Context) { - fun get(codename: String): DeviceInfo? { + suspend fun get(codename: String): DeviceInfo? { return try { - var fromNet = true - val jsonText = try { - try { - ctx.assets.open("abm.json").readBytes().toString(Charsets.UTF_8) - } catch (e: FileNotFoundException) { - URL("https://raw.githubusercontent.com/Android-Boot-Manager/ABM-json/master/devices/$codename.json").readText() + withContext(Dispatchers.IO) { + var fromNet = true + val jsonText = try { + try { + ctx.assets.open("abm.json").readBytes().toString(Charsets.UTF_8) + } catch (e: FileNotFoundException) { + URL("https://raw.githubusercontent.com/Android-Boot-Manager/ABM-json/master/devices/$codename.json").readText() + } + } catch (e: Exception) { + fromNet = false + Log.e("ABM device info", Log.getStackTraceString(e)) + val f = File(ctx.filesDir, "abm_dd_cache.json") + if (f.exists()) f.readText() else + ctx.assets.open("abm_fallback/$codename.json").readBytes() + .toString(Charsets.UTF_8) } - } catch (e: Exception) { - fromNet = false - Log.e("ABM device info", Log.getStackTraceString(e)) - val f = File(ctx.filesDir, "abm_dd_cache.json") - if (f.exists()) f.readText() else - ctx.assets.open("abm_fallback/$codename.json").readBytes().toString(Charsets.UTF_8) - } - val jsonRoot = JSONTokener(jsonText).nextValue() as JSONObject? ?: return null - val json = jsonRoot.getJSONObject("deviceInfo") - if (BuildConfig.VERSION_CODE < jsonRoot.getInt("minAppVersion")) - throw IllegalStateException("please upgrade app") - if (fromNet) { - val newRoot = JSONObject() - newRoot.put("deviceInfo", json) - newRoot.put("minAppVersion", jsonRoot.getInt("minAppVersion")) - File(ctx.filesDir, "abm_dd_cache.json").writeText(newRoot.toString()) + val jsonRoot = JSONTokener(jsonText).nextValue() as JSONObject? ?: return@withContext null + val json = jsonRoot.getJSONObject("deviceInfo") + if (BuildConfig.VERSION_CODE < jsonRoot.getInt("minAppVersion")) + throw IllegalStateException("please upgrade app") + if (fromNet) { + val newRoot = JSONObject() + newRoot.put("deviceInfo", json) + newRoot.put("minAppVersion", jsonRoot.getInt("minAppVersion")) + File(ctx.filesDir, "abm_dd_cache.json").writeText(newRoot.toString()) + } + if (!json.getBoolean("metaOnSd")) + throw IllegalArgumentException("sd less currently not implemented") + JsonDeviceInfo( + json.getString("codename"), + json.getString("blBlock"), + json.getString("sdBlock"), + json.getString("sdBlockP"), + json.getBoolean("postInstallScript"), + json.getBoolean("haveDtbo") + ) } - if (!json.getBoolean("metaOnSd")) - throw IllegalArgumentException("sd less currently not implemented") - JsonDeviceInfo( - json.getString("codename"), - json.getString("blBlock"), - json.getString("sdBlock"), - json.getString("sdBlockP"), - json.getBoolean("postInstallScript"), - json.getBoolean("haveDtbo") - ) } catch (e: Exception) { Log.e("ABM device info", Log.getStackTraceString(e)) null diff --git a/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt b/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt index 5ffbf217..303cea7d 100644 --- a/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt +++ b/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt @@ -28,7 +28,7 @@ class DeviceLogic(ctx: Context) { .cmd("mount $ast ${abmBootset.absolutePath}") .exec() if (!result.isSuccess) { - val out = result.out.join("\n") + result.err.join("\n") + val out = result.out.joinToString("\n") + result.err.joinToString("\n") if (out.contains("Device or resource busy")) { mounted = false } @@ -45,7 +45,7 @@ class DeviceLogic(ctx: Context) { if (!checkMounted()) return true val result = Shell.cmd("umount ${abmBootset.absolutePath}").exec() if (!result.isSuccess) { - val out = result.out.join("\n") + result.err.join("\n") + val out = result.out.joinToString("\n") + result.err.joinToString("\n") if (out.contains("Device or resource busy")) { mounted = true } diff --git a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt index d65adffd..40a95298 100644 --- a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt @@ -22,11 +22,13 @@ import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.andbootmgr.app.util.AbmTheme import org.andbootmgr.app.util.ConfigFile import org.andbootmgr.app.util.SDUtils +import org.andbootmgr.app.util.Terminal import org.json.JSONObject import org.json.JSONTokener import java.io.File @@ -226,7 +228,7 @@ fun SelectInstallSh(vm: WizardActivityState, update: Boolean = false) { @Composable private fun Flash(vm: WizardActivityState) { val flashType = "DroidBootFlashType" - Terminal(vm, logFile = "blflash_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "blflash_${System.currentTimeMillis()}.txt") { 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)) @@ -271,7 +273,7 @@ private fun Flash(vm: WizardActivityState) { "8301", "abm_settings" ).to(terminal).exec() - if (r.out.join("\n").contains("old")) { + if (r.out.joinToString("\n").contains("old")) { terminal.add(vm.activity.getString(R.string.term_reboot_asap)) } if (r.isSuccess) { diff --git a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt index d740013e..c28cdfdf 100644 --- a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.andbootmgr.app.util.AbmTheme +import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -57,7 +58,7 @@ private fun Start() { @Composable private fun Flash(vm: WizardActivityState) { - Terminal(vm, logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> val tmpFile = if (vm.deviceInfo.postInstallScript) { val tmpFile = createTempFileSu("abm", ".sh", vm.logic.rootTmpDir) vm.copyPriv(vm.flashStream("InstallShFlashType"), tmpFile) diff --git a/app/src/main/java/org/andbootmgr/app/MainActivity.kt b/app/src/main/java/org/andbootmgr/app/MainActivity.kt index d19cc9e9..579e8a76 100644 --- a/app/src/main/java/org/andbootmgr/app/MainActivity.kt +++ b/app/src/main/java/org/andbootmgr/app/MainActivity.kt @@ -220,6 +220,9 @@ class MainActivity : ComponentActivity() { Shell.getShell { shell -> CoroutineScope(Dispatchers.IO).launch { vm.root = shell.isRoot + if (vm.root && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Shell.cmd("pm grant $packageName ${android.Manifest.permission.POST_NOTIFICATIONS}") + } vm.deviceInfo = JsonDeviceInfoFactory(vm.activity!!).get(Build.DEVICE) // == temp migration code start == if (Shell.cmd("mountpoint -q /data/abm/bootset").exec().isSuccess) { @@ -242,6 +245,7 @@ class MainActivity : ComponentActivity() { } withContext(Dispatchers.Main) { setContent { + // TODO support work resumption by instantly opening Terminal with null action if work is going on val navController = rememberNavController() val drawerState = rememberDrawerState(DrawerValue.Closed) val scope = rememberCoroutineScope() @@ -753,7 +757,7 @@ private fun PartTool(vm: MainActivityState) { processing = true vm.logic!!.mount(p).submit { processing = false - result = it.out.join("\n") + it.err.join("\n") + result = it.out.joinToString("\n") + it.err.joinToString("\n") } }, Modifier.padding(end = 5.dp)) { Text(stringResource(R.string.mount)) @@ -762,7 +766,7 @@ private fun PartTool(vm: MainActivityState) { processing = true vm.logic!!.unmount(p).submit { processing = false - result = it.out.join("\n") + it.err.join("\n") + result = it.out.joinToString("\n") + it.err.joinToString("\n") } }) { Text(stringResource(R.string.umount)) @@ -817,7 +821,7 @@ private fun PartTool(vm: MainActivityState) { processing = true rename = false vm.logic!!.rename(p, t).submit { r -> - result = r.out.join("\n") + r.err.join("\n") + result = r.out.joinToString("\n") + r.err.joinToString("\n") parts = SDUtils.generateMeta(vm.deviceInfo!!) editPartID = parts?.s!!.findLast { it.id == p.id } processing = false @@ -857,7 +861,7 @@ private fun PartTool(vm: MainActivityState) { processing = false editPartID = null parts = SDUtils.generateMeta(vm.deviceInfo!!) - result = it.out.join("\n") + it.err.join("\n") + result = it.out.joinToString("\n") + it.err.joinToString("\n") } } }) { @@ -1086,7 +1090,7 @@ private fun PartTool(vm: MainActivityState) { for (p in allp) { // Do not chain, but regenerate meta and unmount every time. Thanks void val r = vm.logic!!.delete(p).exec() parts = SDUtils.generateMeta(vm.deviceInfo!!) - tresult += r.out.join("\n") + r.err.join("\n") + "\n" + tresult += r.out.joinToString("\n") + r.err.joinToString("\n") + "\n" } vm.mountBootset() } diff --git a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt index 9908fe78..2a4628b6 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt @@ -14,6 +14,7 @@ import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.andbootmgr.app.util.AbmTheme +import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -55,7 +56,7 @@ private fun Start() { @Composable private fun Flash(vm: WizardActivityState) { - Terminal(vm, logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> val tmpFile = if (vm.deviceInfo.postInstallScript) { val tmpFile = createTempFileSu("abm", ".sh", vm.logic.rootTmpDir) vm.copyPriv(vm.flashStream("InstallShFlashType"), tmpFile) diff --git a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt index 172dddc0..8272106f 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt @@ -6,12 +6,9 @@ import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Button import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.* -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile @@ -26,12 +23,12 @@ import okio.buffer import okio.sink import org.andbootmgr.app.util.ConfigFile import org.andbootmgr.app.util.SDUtils +import org.andbootmgr.app.util.Terminal import org.json.JSONObject import org.json.JSONTokener import java.io.File import java.net.URL import java.util.concurrent.TimeUnit -import kotlin.coroutines.CoroutineContext class UpdateFlowWizardPageFactory(private val vm: WizardActivityState) { fun get(): List { @@ -250,9 +247,9 @@ private fun dlFile(u: UpdateFlowDataHolder, l: String): File? { @Composable private fun Flash(u: UpdateFlowDataHolder) { - Terminal(u.vm, logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> + Terminal(logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> val sp = u.e!!["xpart"]!!.split(":") - val meta = SDUtils.generateMeta(u.vm.deviceInfo!!)!! + val meta = SDUtils.generateMeta(u.vm.deviceInfo)!! Shell.cmd(SDUtils.umsd(meta)).exec() if (u.hasUpdate) { // online diff --git a/app/src/main/java/org/andbootmgr/app/WizardActivity.kt b/app/src/main/java/org/andbootmgr/app/WizardActivity.kt index bf93e82e..8acc53d6 100644 --- a/app/src/main/java/org/andbootmgr/app/WizardActivity.kt +++ b/app/src/main/java/org/andbootmgr/app/WizardActivity.kt @@ -3,26 +3,26 @@ package org.andbootmgr.app import android.annotation.SuppressLint import android.net.Uri import android.os.Bundle -import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.net.toFile import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -31,14 +31,10 @@ import androidx.navigation.compose.rememberNavController import com.topjohnwu.superuser.io.SuFileOutputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.andbootmgr.app.util.AbmTheme -import org.andbootmgr.app.util.StayAliveService import java.io.File import java.io.FileInputStream -import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream @@ -72,75 +68,88 @@ class WizardActivity : ComponentActivity() { super.onCreate(savedInstanceState) vm = WizardActivityState(intent.getStringExtra("codename")!!) vm.activity = this - vm.deviceInfo = JsonDeviceInfoFactory(this).get(vm.codename)!! - vm.logic = DeviceLogic(this) - chooseFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> - if (uri == null) { - Toast.makeText(this, getString(R.string.file_unavailable), Toast.LENGTH_LONG).show() - onFileChosen = null - return@registerForActivityResult - } - if (onFileChosen != null) { - onFileChosen!!(uri) - onFileChosen = null - } else { - Toast.makeText( - this@WizardActivity, - getString(R.string.internal_file_error1), - Toast.LENGTH_LONG - ).show() - } - } - newFile = registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri: Uri? -> - if (uri == null) { - Toast.makeText(this, getString(R.string.file_unavailable), Toast.LENGTH_LONG).show() - onFileCreated = null - return@registerForActivityResult + chooseFile = + registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + if (uri == null) { + Toast.makeText( + this, + getString(R.string.file_unavailable), + Toast.LENGTH_LONG + ).show() + onFileChosen = null + return@registerForActivityResult + } + if (onFileChosen != null) { + onFileChosen!!(uri) + onFileChosen = null + } else { + Toast.makeText( + this@WizardActivity, + getString(R.string.internal_file_error1), + Toast.LENGTH_LONG + ).show() + } } - if (onFileCreated != null) { - onFileCreated!!(uri) - onFileCreated = null - } else { - Toast.makeText( - this@WizardActivity, - getString(R.string.internal_file_error1), - Toast.LENGTH_LONG - ).show() + newFile = + registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri: Uri? -> + if (uri == null) { + Toast.makeText( + this, + getString(R.string.file_unavailable), + Toast.LENGTH_LONG + ).show() + onFileCreated = null + return@registerForActivityResult + } + if (onFileCreated != null) { + onFileCreated!!(uri) + onFileCreated = null + } else { + Toast.makeText( + this@WizardActivity, + getString(R.string.internal_file_error1), + Toast.LENGTH_LONG + ).show() + } } - } - val wizardPages = WizardPageFactory(vm).get(intent.getStringExtra("flow")!!) - setContent { - vm.navController = rememberNavController() - AbmTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column(verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + CoroutineScope(Dispatchers.Main).launch { + vm.deviceInfo = JsonDeviceInfoFactory(vm.activity).get(vm.codename)!! + vm.logic = DeviceLogic(vm.activity) + val wizardPages = WizardPageFactory(vm).get(intent.getStringExtra("flow")!!) + setContent { + vm.navController = rememberNavController() + AbmTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background ) { - NavHost( - navController = vm.navController, - startDestination = "start", - modifier = Modifier - .fillMaxWidth() - .weight(1.0f) + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() ) { - for (i in wizardPages) { - composable(i.name) { - if (!vm.btnsOverride) { - vm.prevText.value = i.prev.text - vm.nextText.value = i.next.text - vm.onPrev.value = i.prev.onClick - vm.onNext.value = i.next.onClick + NavHost( + navController = vm.navController, + startDestination = "start", + modifier = Modifier + .fillMaxWidth() + .weight(1.0f) + ) { + for (i in wizardPages) { + composable(i.name) { + if (!vm.btnsOverride) { + vm.prevText.value = i.prev.text + vm.nextText.value = i.next.text + vm.onPrev.value = i.prev.onClick + vm.onNext.value = i.next.onClick + } + i.run() } - i.run() } } - } - Box(Modifier.fillMaxWidth()) { - BtnsRow(vm) + Box(Modifier.fillMaxWidth()) { + BtnsRow(vm) + } } } } @@ -191,9 +200,10 @@ private class ExpectedDigestInputStream(stream: InputStream?, fun doAssert() { val hash = digest.digest().toHexString() if (hash != expectedDigest) - throw IllegalArgumentException("digest $hash does not match expected hash $expectedDigest") + throw HashMismatchException("digest $hash does not match expected hash $expectedDigest") } } +class HashMismatchException(message: String) : Exception(message) class WizardActivityState(val codename: String) { var btnsOverride = false @@ -216,6 +226,7 @@ class WizardActivityState(val codename: String) { navController.navigate(current.value) } + // TODO have callers handle HashMismatchException when appropriate fun copy(inputStream: InputStream, outputStream: OutputStream): Long { var nread = 0L val buf = ByteArray(8192) diff --git a/app/src/main/java/org/andbootmgr/app/ext.kt b/app/src/main/java/org/andbootmgr/app/ext.kt index e85ecc17..ea2e5fdf 100644 --- a/app/src/main/java/org/andbootmgr/app/ext.kt +++ b/app/src/main/java/org/andbootmgr/app/ext.kt @@ -6,14 +6,6 @@ import java.io.File import java.io.IOException import kotlin.math.abs -fun String.Companion.join(delimiter: String, list: Iterable): String { - return java.lang.String.join(delimiter, list) -} - -fun Iterable.join(delimiter: String): String { - return String.join(delimiter, this) -} - open class ActionAbortedError(e: Exception?) : Exception(e) class ActionAbortedCleanlyError(e: Exception?) : ActionAbortedError(e) diff --git a/app/src/main/java/org/andbootmgr/app/themes/Simulator.kt b/app/src/main/java/org/andbootmgr/app/themes/Simulator.kt index d5de0502..579ab600 100644 --- a/app/src/main/java/org/andbootmgr/app/themes/Simulator.kt +++ b/app/src/main/java/org/andbootmgr/app/themes/Simulator.kt @@ -19,7 +19,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuRandomAccessFile -import org.andbootmgr.app.join import java.io.File import kotlin.math.min import kotlin.system.exitProcess @@ -95,7 +94,7 @@ class Simulator : AppCompatActivity() { @Suppress("unused") // jni private fun blockCount(): Long { - return Shell.cmd("blockdev --getsz ${f.absolutePath}").exec().out.join("\n").toLong().also { + return Shell.cmd("blockdev --getsz ${f.absolutePath}").exec().out.joinToString("\n").toLong().also { Log.i("Simulator", "block count: $it") } } diff --git a/app/src/main/java/org/andbootmgr/app/util/SDUtils.kt b/app/src/main/java/org/andbootmgr/app/util/SDUtils.kt index 57ab83c0..01a272ee 100644 --- a/app/src/main/java/org/andbootmgr/app/util/SDUtils.kt +++ b/app/src/main/java/org/andbootmgr/app/util/SDUtils.kt @@ -3,7 +3,6 @@ package org.andbootmgr.app.util import android.util.Log import com.topjohnwu.superuser.Shell import org.andbootmgr.app.DeviceInfo -import org.andbootmgr.app.join import java.util.* import java.util.stream.Collectors import kotlin.jvm.optionals.getOrElse @@ -99,7 +98,7 @@ object SDUtils { ocut[2].toLong()/* endSector */, meta.logicalSectorSizeBytes, code, - String.join(" ", ocut.copyOfRange(6, ocut.size).toList()) /* label/name */, + ocut.copyOfRange(6, ocut.size).toList().joinToString(" ") /* label/name */, meta.major, meta.minor + id ) diff --git a/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt b/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt index 3738e085..e300dc95 100644 --- a/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt +++ b/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.ServiceConnection import android.os.Binder import android.os.IBinder +import android.util.Log import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -18,14 +19,12 @@ import org.andbootmgr.app.R interface IStayAlive { fun startWork(work: suspend (Context) -> Unit, extra: Any) - val isWorkDone: Boolean val workExtra: Any - fun finish() } class StayAliveService : LifecycleService(), IStayAlive { private var work: (suspend (Context) -> Unit)? = null - override var isWorkDone = false + var isWorkDone = false get() { if (destroyed) { throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") @@ -52,6 +51,8 @@ class StayAliveService : LifecycleService(), IStayAlive { if (this.work != null) { throw IllegalStateException("Work already set on this StayAliveService.") } + // make sure we get promoted to started service + startService(Intent(this, this::class.java)) this.work = work this.extra = extra lifecycleScope.launch { @@ -60,7 +61,10 @@ class StayAliveService : LifecycleService(), IStayAlive { onDone!!.invoke() } } - override fun finish() { + fun finish() { + if (!isWorkDone) { + Log.e(TAG, "Warning: finishing StayAliveService before work is done.") + } destroyed = true stopSelf() } @@ -104,13 +108,16 @@ class StayAliveService : LifecycleService(), IStayAlive { if (work != null) { throw IllegalStateException("Work was already set on this StayAliveService.") } - // make sure we get promoted to started service - startService(Intent(this, this::class.java)) return object : Binder(), Provider { override var service = this@StayAliveService override var onDone - get() = this@StayAliveService.onDone!! + get() = this@StayAliveService.onDone set(value) { this@StayAliveService.onDone = value } + override val isWorkDone: Boolean + get() = this@StayAliveService.isWorkDone + override fun finish() { + this@StayAliveService.finish() + } } } @@ -119,46 +126,58 @@ class StayAliveService : LifecycleService(), IStayAlive { destroyed = true } - private interface Provider { - val service: IStayAlive - var onDone: () -> Unit - } - companion object { + private const val TAG = "ABM_StayAlive" private const val SERVICE_CHANNEL = "service" private const val FG_SERVICE_ID = 1001 + } +} + +private interface Provider { + val service: IStayAlive + var onDone: (() -> Unit)? + val isWorkDone: Boolean + fun finish() +} + +class StayAliveConnection(inContext: Context, + private val onConnected: (IStayAlive) -> Unit) : ServiceConnection { + companion object { @SuppressLint("StaticFieldLeak") // application context private var currentConn: StayAliveConnection? = null } + private val context = inContext.applicationContext - class StayAliveConnection(inContext: Context, - private val onConnected: (IStayAlive) -> Unit) : ServiceConnection { - private val context = inContext.applicationContext - private var service: IStayAlive? = null - - init { - if (currentConn != null) { - throw IllegalStateException("There should only be one StayAliveConnection at a time.") - } - currentConn = this - context.bindService( - Intent(context, StayAliveService::class.java), - this, - Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE - ) + init { + if (currentConn != null) { + throw IllegalStateException("There should only be one StayAliveConnection at a time.") } + currentConn = this + context.bindService( + Intent(context, StayAliveService::class.java), + this, + Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE + ) + } - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val provider = service as Provider - provider.onDone = { - context.unbindService(this) - currentConn = null - } - onConnected(provider.service) + override fun onServiceConnected(name: ComponentName?, inService: IBinder?) { + val provider = inService as Provider + val service = provider.service + val onDone = { + provider.finish() + provider.onDone = null + context.unbindService(this) + currentConn = null } - - override fun onServiceDisconnected(name: ComponentName?) { - this.service = null + if (provider.isWorkDone) { + onDone() + } else { + provider.onDone = onDone } + onConnected(service) + } + + override fun onServiceDisconnected(name: ComponentName?) { + // Do nothing } } \ No newline at end of file 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 d69f2517..7839340d 100644 --- a/app/src/main/java/org/andbootmgr/app/util/Terminal.kt +++ b/app/src/main/java/org/andbootmgr/app/util/Terminal.kt @@ -16,13 +16,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.andbootmgr.app.R -import org.andbootmgr.app.WizardActivityState import java.io.File import java.io.FileOutputStream @@ -126,41 +124,54 @@ private class BudgetCallbackList(private val log: FileOutputStream?) : MutableLi } } - /* Monospace auto-scrolling text view, fed using MutableList, catching exceptions and running logic on a different thread */ @Composable -fun Terminal(vm: WizardActivityState, logFile: String? = null, action: suspend (MutableList) -> Unit) { +fun Terminal(logFile: String? = null, action: (suspend (MutableList) -> Unit)?) { val scrollH = rememberScrollState() val scrollV = rememberScrollState() val scope = rememberCoroutineScope() val text = remember { mutableStateOf("") } val ctx = LocalContext.current.applicationContext LaunchedEffect(Unit) { - // TODO support resumption - StayAliveService.StayAliveConnection(ctx) { service -> - val log = logFile?.let { FileOutputStream(File(ctx.externalCacheDir, it)) } - // Budget CallbackList - val s = BudgetCallbackList(log) - service.startWork({ - withContext(Dispatchers.Default) { - try { - action(s) - } catch (e: Throwable) { - s.add(ctx.getString(R.string.term_failure)) - s.add(ctx.getString(R.string.dev_details)) - s.add(Log.getStackTraceString(e)) - } - withContext(Dispatchers.IO) { - log?.close() + if (action == null && logFile != null) { + throw IllegalArgumentException("logFile must be null if action is null") + } + StayAliveConnection(ctx) { service -> + if (action != null) { + val log = logFile?.let { FileOutputStream(File(ctx.externalCacheDir, it)) } + val s = BudgetCallbackList(log) + s.cb = { element -> + scope.launch { + text.value += element + "\n" + delay(200) // Give it time to re-measure + scrollV.animateScrollTo(scrollV.maxValue) + scrollH.animateScrollTo(0) } } - }, s) - (service.workExtra as BudgetCallbackList).cb = { element -> - scope.launch { - text.value += element + "\n" - delay(200) // Give it time to re-measure - scrollV.animateScrollTo(scrollV.maxValue) - scrollH.animateScrollTo(0) + service.startWork({ + withContext(Dispatchers.Default) { + try { + action(s) + } catch (e: Throwable) { + s.add(ctx.getString(R.string.term_failure)) + s.add(ctx.getString(R.string.dev_details)) + s.add(Log.getStackTraceString(e)) + } + withContext(Dispatchers.IO) { + log?.close() + } + } + }, s) + } else { + val s = service.workExtra as BudgetCallbackList + text.value = s.joinToString("\n") + s.cb = { element -> + scope.launch { + text.value += element + "\n" + delay(200) // Give it time to re-measure + scrollV.animateScrollTo(scrollV.maxValue) + scrollH.animateScrollTo(0) + } } } } diff --git a/app/src/main/java/org/andbootmgr/app/util/Toolkit.kt b/app/src/main/java/org/andbootmgr/app/util/Toolkit.kt index 4814c836..0e62468b 100644 --- a/app/src/main/java/org/andbootmgr/app/util/Toolkit.kt +++ b/app/src/main/java/org/andbootmgr/app/util/Toolkit.kt @@ -11,6 +11,11 @@ import java.io.* // Manage & extract Toolkit class Toolkit(private val ctx: Context) { + companion object { + private const val TAG = "ABM_AssetCopy" + private const val DEBUG = false + } + private var fail = false private val targetPath = File(ctx.filesDir.parentFile, "assets") @@ -36,7 +41,7 @@ class Toolkit(private val ctx: Context) { } val s2 = String(b).trim() if (s != s2) { - uinf.run() + uinf.invoke() shell.newJob().add("rm -rf " + targetPath.absolutePath).exec() if (!targetPath.exists()) fail = fail or !targetPath.mkdir() if (!File(ctx.filesDir.parentFile, "files").exists()) fail = fail or !File(ctx.filesDir.parentFile, "files").mkdir() @@ -54,7 +59,7 @@ class Toolkit(private val ctx: Context) { try { files = assetManager.list(src) } catch (e: IOException) { - Log.e("ABM_AssetCopy", "Failed to get asset file list.", e) + Log.e(TAG, "Failed to get asset file list.", e) fail = true } assert(files != null) @@ -80,27 +85,23 @@ class Toolkit(private val ctx: Context) { out.flush() out.close() } catch (e: FileNotFoundException) { - Log.d( - "ABM_AssetCopy", - "Result of mkdir #1: " + File(targetPath, outp).mkdir() - ) - Log.d("ABM_AssetCopy", Log.getStackTraceString(e)) + val r = File(targetPath, outp).mkdir() + if (DEBUG) Log.d(TAG, "Result of mkdir #1: $r") + if (DEBUG) Log.d(TAG, Log.getStackTraceString(e)) try { assetManager.open(src + File.separator + filename).close() copyAssets(src, outp, assetManager, filename) } catch (e2: FileNotFoundException) { - Log.d( - "ABM_AssetCopy", - "Result of mkdir #2: " + File(File(targetPath, outp), filename).mkdir() - ) - Log.d("ABM_AssetCopy", Log.getStackTraceString(e2)) + val r2 = File(File(targetPath, outp), filename).mkdir() + if (DEBUG) Log.d(TAG, "Result of mkdir #2: $r2") + if (DEBUG) Log.d(TAG, Log.getStackTraceString(e2)) copyAssets(src + File.separator + filename, outp + File.separator + filename) } catch (ex: IOException) { - Log.e("ABM_AssetCopy", "Failed to copy asset file: $filename", ex) + Log.e(TAG, "Failed to copy asset file: $filename", ex) fail = true } } catch (e: IOException) { - Log.e("ABM_AssetCopy", "Failed to copy asset file: $filename", e) + Log.e(TAG, "Failed to copy asset file: $filename", e) fail = true } }