diff --git a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt index 6a17d66d..be5a9922 100644 --- a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt @@ -19,7 +19,6 @@ 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 @@ -118,7 +117,7 @@ private fun SelectDroidBoot(c: CreateBackupDataHolder) { @Composable private fun Flash(c: CreateBackupDataHolder) { - Terminal(logFile = "flash_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(c.vm, logFile = "flash_${System.currentTimeMillis()}.txt") { terminal -> c.vm.logic.extractToolkit(terminal) terminal.add(c.vm.activity.getString(R.string.term_starting)) val p = c.meta!!.dumpKernelPartition(c.pi) @@ -143,13 +142,11 @@ private fun Flash(c: CreateBackupDataHolder) { ).to(terminal).exec() if (!result2.isSuccess) { terminal.add(c.vm.activity.getString(R.string.term_failure)) - return@Terminal + return@WizardTerminalWork } } else { throw IOException(c.vm.activity.getString(R.string.term_invalid_action)) } terminal.add(c.vm.activity.getString(R.string.term_success)) - c.vm.nextText = c.vm.activity.getString(R.string.finish) - c.vm.onNext = { it.finish() } } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt index bd8fed21..8f93b87c 100644 --- a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt @@ -52,7 +52,6 @@ import org.andbootmgr.app.CreatePartDataHolder.Part 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 org.json.JSONObject import org.json.JSONTokener import java.io.File @@ -652,7 +651,7 @@ private fun Os(c: CreatePartDataHolder) { @Composable private fun Flash(c: CreatePartDataHolder) { val vm = c.vm - Terminal(logFile = "install_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(vm, logFile = "install_${System.currentTimeMillis()}.txt") { terminal -> c.vm.logic.extractToolkit(terminal) c.vm.downloadRemainingFiles(terminal) if (c.partitionName == null) { // OS install @@ -695,7 +694,7 @@ private fun Flash(c: CreatePartDataHolder) { terminal.add(vm.activity.getString(R.string.term_created_part)) } else { terminal.add(vm.activity.getString(R.string.term_failure)) - return@Terminal + return@WizardTerminalWork } } terminal.add(vm.activity.getString(R.string.term_created_pt)) @@ -703,7 +702,7 @@ private fun Flash(c: CreatePartDataHolder) { val meta = SDUtils.generateMeta(vm.deviceInfo) if (meta == null) { terminal.add(vm.activity.getString(R.string.term_cant_get_meta)) - return@Terminal + return@WizardTerminalWork } terminal.add(vm.activity.getString(R.string.term_building_cfg)) @@ -722,7 +721,7 @@ private fun Flash(c: CreatePartDataHolder) { entry.exportToFile(File(vm.logic.abmEntries, "$fn.conf")) if (!SuFile.open(File(vm.logic.abmBootset, fn).toURI()).mkdir()) { terminal.add(vm.activity.getString(R.string.term_mkdir_failed)) - return@Terminal + return@WizardTerminalWork } terminal.add(vm.activity.getString(R.string.term_flashing_imgs)) @@ -741,7 +740,7 @@ private fun Flash(c: CreatePartDataHolder) { f.delete() if (!result2.isSuccess) { terminal.add(vm.activity.getString(R.string.term_failure)) - return@Terminal + return@WizardTerminalWork } } else { c.vm.copyPriv(f.openInputStream(c.vm), tp) @@ -760,12 +759,10 @@ private fun Flash(c: CreatePartDataHolder) { val result = vm.logic.runShFileWithArgs(cmd).to(terminal).exec() if (!result.isSuccess) { terminal.add(vm.activity.getString(R.string.term_failure)) - return@Terminal + return@WizardTerminalWork } terminal.add(vm.activity.getString(R.string.term_success)) - vm.nextText = vm.activity.getString(R.string.finish) - vm.onNext = { it.finish() } } else { // Portable partition terminal.add(vm.activity.getString(R.string.term_create_part)) vm.logic.unmountBootset() @@ -779,8 +776,6 @@ private fun Flash(c: CreatePartDataHolder) { terminal.add(vm.activity.getString(R.string.term_reboot_asap)) } if (r.isSuccess) { - vm.nextText = c.vm.activity.getString(R.string.finish) - vm.onNext = { it.finish() } terminal.add(vm.activity.getString(R.string.term_success)) } else { terminal.add(vm.activity.getString(R.string.term_failure)) diff --git a/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt b/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt index cdae2eff..55322d7d 100644 --- a/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt +++ b/app/src/main/java/org/andbootmgr/app/DeviceLogic.kt @@ -14,7 +14,7 @@ class DeviceLogic(private val ctx: Context) { val fileDir = File(rootDir, "files") val cacheDir = File(rootDir, "cache") val toolkitDir = File(toolkit.targetPath, "Toolkit") // will occasionally be pruned by OS, but it's fine - val rootTmpDir = File("/data/local/tmp") + private val rootTmpDir = File("/data/local/tmp") val abmBootset = File(rootTmpDir, ".abm_bootset") val abmDb = File(abmBootset, "db") val abmEntries = File(abmDb, "entries") diff --git a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt index 5fb8943b..f84feba6 100644 --- a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt @@ -28,10 +28,8 @@ import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext 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 @@ -184,24 +182,24 @@ private fun Input(d: DroidBootFlowDataHolder) { @Composable private fun Flash(d: DroidBootFlowDataHolder) { val vm = d.vm - Terminal(logFile = "blflash_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(d.vm, 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)) - return@Terminal + return@WizardTerminalWork } if (!SuFile.open(vm.logic.abmBootset.toURI()).exists()) { if (!SuFile.open(vm.logic.abmBootset.toURI()).mkdir()) { terminal.add(vm.activity.getString(R.string.term_cant_create_mount_point)) - return@Terminal + return@WizardTerminalWork } } if (!SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).exists()) { if (!SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).createNewFile()) { terminal.add(vm.activity.getString(R.string.term_cant_create_placeholder)) - return@Terminal + return@WizardTerminalWork } } @@ -209,7 +207,7 @@ private fun Flash(d: DroidBootFlowDataHolder) { var meta = SDUtils.generateMeta(vm.deviceInfo) if (meta == null) { terminal.add(vm.activity.getString(R.string.term_cant_get_meta)) - return@Terminal + return@WizardTerminalWork } if (!Shell.cmd(SDUtils.umsd(meta)).to(terminal).exec().isSuccess) { terminal.add(vm.activity.getString(R.string.term_failed_umount_drive)) @@ -218,12 +216,12 @@ private fun Flash(d: DroidBootFlowDataHolder) { .exec().isSuccess ) { terminal.add(vm.activity.getString(R.string.term_failed_create_pt)) - return@Terminal + return@WizardTerminalWork } meta = SDUtils.generateMeta(vm.deviceInfo) if (meta == null) { terminal.add(vm.activity.getString(R.string.term_cant_get_meta)) - return@Terminal + return@WizardTerminalWork } val r = vm.logic.create(meta.s[0] as SDUtils.Partition.FreeSpace, 0, @@ -240,7 +238,7 @@ private fun Flash(d: DroidBootFlowDataHolder) { terminal.add(vm.activity.getString(R.string.term_done)) } else { terminal.add(vm.activity.getString(R.string.term_failed_create_meta)) - return@Terminal + return@WizardTerminalWork } } else { // TODO provision for sdless @@ -248,25 +246,25 @@ private fun Flash(d: DroidBootFlowDataHolder) { if (!vm.logic.mountBootset(vm.deviceInfo)) { terminal.add(vm.activity.getString(R.string.term_failed_mount)) - return@Terminal + return@WizardTerminalWork } if (SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).exists()) { terminal.add(vm.activity.getString(R.string.term_mount_failure_inconsist)) - return@Terminal + return@WizardTerminalWork } if (!SuFile.open(vm.logic.abmDb.toURI()).exists()) { if (!SuFile.open(vm.logic.abmDb.toURI()).mkdir()) { terminal.add(vm.activity.getString(R.string.term_failed_create_db_dir)) vm.logic.unmountBootset() - return@Terminal + return@WizardTerminalWork } } if (!SuFile.open(vm.logic.abmEntries.toURI()).exists()) { if (!SuFile.open(vm.logic.abmEntries.toURI()).mkdir()) { terminal.add(vm.activity.getString(R.string.term_failed_create_entries_dir)) vm.logic.unmountBootset() - return@Terminal + return@WizardTerminalWork } } val tmpFile = if (vm.deviceInfo.postInstallScript) { @@ -304,7 +302,7 @@ private fun Flash(d: DroidBootFlowDataHolder) { terminal.add(vm.activity.getString(R.string.term_bl_failed)) terminal.add(e.message ?: "(null)") terminal.add(vm.activity.getString(R.string.term_consult_doc)) - return@Terminal + return@WizardTerminalWork } } if (vm.deviceInfo.postInstallScript) { @@ -317,16 +315,6 @@ private fun Flash(d: DroidBootFlowDataHolder) { } terminal.add(vm.activity.getString(R.string.term_success)) vm.logic.unmountBootset() - withContext(Dispatchers.Main) { - vm.nextText = vm.activity.getString(R.string.finish) - vm.onNext = { - if (vm.deviceInfo.isBooted(vm.logic)) { - it.finish() - } else { - // TODO prompt user to reboot? - it.finish() - } - } - } + // TODO prompt user to reboot? } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt index c8229435..4289d629 100644 --- a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt @@ -13,11 +13,10 @@ import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException -class FixDroidBootFlow(): WizardFlow() { +class FixDroidBootFlow: WizardFlow() { override fun get(vm: WizardState): List { return listOf(WizardPage("start", NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, @@ -58,7 +57,7 @@ private fun Start(vm: WizardState) { @Composable private fun Flash(vm: WizardState) { - Terminal(logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(vm, logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> vm.logic.extractToolkit(terminal) vm.downloadRemainingFiles(terminal) val tmpFile = if (vm.deviceInfo.postInstallScript) { @@ -78,7 +77,7 @@ private fun Flash(vm: WizardState) { terminal.add(vm.activity.getString(R.string.term_bl_failed)) terminal.add(e.message ?: "(null)") terminal.add(vm.activity.getString(R.string.term_consult_doc)) - return@Terminal + return@WizardTerminalWork } if (vm.deviceInfo.postInstallScript) { terminal.add(vm.activity.getString(R.string.term_device_setup)) @@ -89,11 +88,5 @@ private fun Flash(vm: WizardState) { tmpFile.delete() } terminal.add(vm.activity.getString(R.string.term_success)) - withContext(Dispatchers.Main) { - vm.nextText = vm.activity.getString(R.string.finish) - vm.onNext = { - it.finish() - } - } } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/MainActivity.kt b/app/src/main/java/org/andbootmgr/app/MainActivity.kt index 0ac8805e..17fdcfae 100644 --- a/app/src/main/java/org/andbootmgr/app/MainActivity.kt +++ b/app/src/main/java/org/andbootmgr/app/MainActivity.kt @@ -72,6 +72,7 @@ import org.andbootmgr.app.util.AbmTheme import org.andbootmgr.app.util.ConfigFile import org.andbootmgr.app.util.StayAliveService import org.andbootmgr.app.util.Terminal +import org.andbootmgr.app.util.TerminalList import java.util.concurrent.atomic.AtomicBoolean class MainActivityState(val activity: MainActivity?) { @@ -80,7 +81,7 @@ class MainActivityState(val activity: MainActivity?) { var deviceInfo: DeviceInfo? = null val theme = ThemeViewModel(this) var defaultCfg = mutableStateMapOf() - var isOk = false + var isOk by mutableStateOf(false) var logic: DeviceLogic? = null private fun loadDefaultCfg() { @@ -117,17 +118,15 @@ class MainActivityState(val activity: MainActivity?) { // This will be called on startup, and after StayAlive work completes. fun init() { - if (!StayAliveService.isRunning) { - val installed = deviceInfo?.isInstalled(logic!!) - if (installed == true) { - mountBootset() - } else { - Log.i("ABM", "not installed, not trying to mount") - } - if (deviceInfo != null) { - isOk = installed!! && deviceInfo!!.isBooted(logic!!) && - !(!logic!!.mounted || deviceInfo!!.isCorrupt(logic!!)) - } + val installed = deviceInfo?.isInstalled(logic!!) + if (installed == true) { + mountBootset() + } else { + Log.i("ABM", "not installed, not trying to mount") + } + if (deviceInfo != null) { + isOk = installed!! && deviceInfo!!.isBooted(logic!!) && + !(!logic!!.mounted || deviceInfo!!.isCorrupt(logic!!)) } } @@ -247,14 +246,16 @@ class MainActivity : ComponentActivity() { // == temp migration code end == } vm.deviceInfo = di.await() // blocking - vm.init() + if (StayAliveService.instance == null) { + vm.init() + } withContext(Dispatchers.Main) { setContent { - // TODO allow rotating device while viewing logs without loosing logs (will require rememberSavable) AbmTheme { - var showTerminal by remember { mutableStateOf(StayAliveService.isRunning) } - if (showTerminal) { - var canFinish by remember { mutableStateOf(false) } + if (StayAliveService.instance != null && + StayAliveService.instance!!.workExtra is TerminalList) { + val i = StayAliveService.instance!! + val we = i.workExtra as TerminalList DisposableEffect(Unit) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) onDispose { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } @@ -262,16 +263,24 @@ class MainActivity : ComponentActivity() { BackHandler {} Column(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxWidth().weight(1.0f)) { - Terminal(null, { canFinish = true }, null) + Terminal(we) } Box(modifier = Modifier.fillMaxWidth()) { - BasicButtonRow("", {}, if (canFinish) - stringResource(R.string.finish) else "") { - if (canFinish) - CoroutineScope(Dispatchers.IO).launch { - vm.init() - showTerminal = false - } + if (we.isCancelled == false || i.isWorkDone) { + BasicButtonRow( + if (we.isCancelled == false) + stringResource(R.string.cancel) else "", { + if (we.isCancelled == false) + we.cancel?.invoke() + }, if (i.isWorkDone) + stringResource(R.string.finish) else "" + ) { + if (i.isWorkDone) + CoroutineScope(Dispatchers.IO).launch { + vm.init() + StayAliveService.instance!!.stopSelf() + } + } } } } diff --git a/app/src/main/java/org/andbootmgr/app/Start.kt b/app/src/main/java/org/andbootmgr/app/Start.kt index c65ac84a..4941c686 100644 --- a/app/src/main/java/org/andbootmgr/app/Start.kt +++ b/app/src/main/java/org/andbootmgr/app/Start.kt @@ -169,6 +169,7 @@ fun Start(vm: MainActivityState) { PartTool(vm) } else { Text(stringResource(R.string.invalid), textAlign = TextAlign.Center) + Text("metaOnSd:$metaOnSd isOk:${vm.isOk}") } } } diff --git a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt index 19f74d35..df9a162a 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt @@ -11,9 +11,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -58,7 +55,7 @@ private fun Start(vm: WizardState) { @Composable private fun Flash(vm: WizardState) { - Terminal(logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(vm, logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> vm.logic.extractToolkit(terminal) vm.downloadRemainingFiles(terminal) val tmpFile = if (vm.deviceInfo.postInstallScript) { @@ -78,7 +75,7 @@ private fun Flash(vm: WizardState) { terminal.add(vm.activity.getString(R.string.term_bl_failed)) terminal.add(e.message ?: "(null)") terminal.add(vm.activity.getString(R.string.term_consult_doc)) - return@Terminal + return@WizardTerminalWork } if (vm.deviceInfo.postInstallScript) { terminal.add(vm.activity.getString(R.string.term_device_setup)) @@ -89,9 +86,5 @@ private fun Flash(vm: WizardState) { tmpFile.delete() } terminal.add(vm.activity.getString(R.string.term_success)) - withContext(Dispatchers.Main) { - vm.nextText = vm.activity.getString(R.string.finish) - vm.onNext = { it.finish() } - } } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt index 50825c16..1742f312 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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 @@ -207,7 +206,7 @@ private fun Local(u: UpdateFlowDataHolder) { @Composable private fun Flash(u: UpdateFlowDataHolder) { - Terminal(logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> + WizardTerminalWork(u.vm, logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> u.vm.logic.extractToolkit(terminal) u.vm.downloadRemainingFiles(terminal) val sp = u.e!!["xpart"]!!.split(":") @@ -258,7 +257,5 @@ private fun Flash(u: UpdateFlowDataHolder) { u.e!!.exportToFile(u.ef!!) terminal.add(u.vm.activity.getString(R.string.term_success)) tmpFile?.delete() - u.vm.nextText = u.vm.activity.getString(R.string.finish) - u.vm.onNext = { it.finish() } } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/Wizard.kt b/app/src/main/java/org/andbootmgr/app/Wizard.kt index d35b2258..8a8e0c7f 100644 --- a/app/src/main/java/org/andbootmgr/app/Wizard.kt +++ b/app/src/main/java/org/andbootmgr/app/Wizard.kt @@ -1,7 +1,6 @@ package org.andbootmgr.app import android.net.Uri -import android.os.CancellationSignal import android.view.WindowManager import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement @@ -39,6 +38,7 @@ import com.topjohnwu.superuser.io.SuFileOutputStream import org.andbootmgr.app.util.AbmOkHttp import org.andbootmgr.app.util.TerminalCancelException import org.andbootmgr.app.util.TerminalList +import org.andbootmgr.app.util.TerminalWork import java.io.File import java.io.FileInputStream import java.io.IOException @@ -137,7 +137,7 @@ class WizardState(val mvm: MainActivityState) { } } suspend fun downloadRemainingFiles(terminal: TerminalList) { - terminal.isCancelled.value = false + terminal.isCancelled = false for (id in idNeeded.filter { !chosen.containsKey(it) }) { if (!inetAvailable.containsKey(id)) throw IllegalStateException("$id not chosen and not available from inet") @@ -152,24 +152,24 @@ class WizardState(val mvm: MainActivityState) { "${readBytes / (1024 * 1024)} MiB", "${total / (1024 * 1024)} MiB" ) } - terminal.cancel = { terminal.isCancelled.value = true; client.cancel() } + terminal.cancel = { terminal.isCancelled = true; client.cancel() } try { client.run() } catch (e: IOException) { - if (terminal.isCancelled.value == true) { + if (terminal.isCancelled == true) { throw TerminalCancelException() } throw e } - if (terminal.isCancelled.value == true) { + if (terminal.isCancelled == true) { throw TerminalCancelException() } chosen[id] = DownloadedFile(null, f) } - if (terminal.isCancelled.value == true) { + if (terminal.isCancelled == true) { throw TerminalCancelException() } else { - terminal.isCancelled.value = null + terminal.isCancelled = null } } @@ -300,4 +300,13 @@ fun WizardDownloader(vm: WizardState, next: String) { } } } +} + +@Composable +fun WizardTerminalWork(vm: WizardState, logFile: String? = null, + action: suspend (TerminalList) -> Unit) { + TerminalWork(logFile) { + vm.mvm.currentWizardFlow = null + action(it) + } } \ No newline at end of file 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 8a9be37b..feeb60ac 100644 --- a/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt +++ b/app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt @@ -10,36 +10,33 @@ import android.os.IBinder import android.os.PowerManager import android.os.PowerManager.WakeLock import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.LifecycleOwner +import androidx.core.util.Supplier import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.andbootmgr.app.R -interface IStayAlive { - fun startWork(work: suspend (Context) -> Unit, extra: Any) - val workExtra: Any -} - -class StayAliveService : LifecycleService(), IStayAlive { +class StayAliveService : LifecycleService() { + companion object { + private const val TAG = "ABM_StayAlive" + private const val SERVICE_CHANNEL = "service" + private const val FG_SERVICE_ID = 1001 + var instance by mutableStateOf(null) + private set + } private lateinit var wakeLock: WakeLock private var work: (suspend (Context) -> Unit)? = null - var isWorkDone = false - get() { - if (destroyed) { - throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") - } - return field - } + var isWorkDone by mutableStateOf(false) + private set private var extra: Any? = null - override val workExtra: Any + val workExtra: Any get() { if (destroyed) { throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") @@ -52,7 +49,7 @@ class StayAliveService : LifecycleService(), IStayAlive { private var destroyed = false private var onDone: (() -> Unit)? = null @SuppressLint("WakelockTimeout") - override fun startWork(work: suspend (Context) -> Unit, extra: Any) { + fun startWork(work: suspend (Context) -> Unit, extra: Any) { if (destroyed) { throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") } @@ -63,6 +60,10 @@ class StayAliveService : LifecycleService(), IStayAlive { startService(Intent(this, this::class.java)) this.work = work this.extra = extra + if (instance != null) { + throw IllegalStateException("expected instance to be null for non-running service") + } + instance = this lifecycleScope.launch { wakeLock.acquire() try { @@ -76,24 +77,9 @@ class StayAliveService : LifecycleService(), IStayAlive { } } } - fun finish() { - if (!isWorkDone) { - Log.e(TAG, "Warning: finishing StayAliveService before work is done.") - } - if (!destroyed) { - if (!isRunning) throw IllegalStateException("excepted isRunning to be true for non-destroyed service") - isRunning = false - destroyed = true - } - stopSelf() - } override fun onCreate() { super.onCreate() - if (isRunning) { - throw IllegalStateException("expected isRunning=false for new service") - } - isRunning = true NotificationManagerCompat.from(this).createNotificationChannel( NotificationChannelCompat.Builder(SERVICE_CHANNEL, NotificationManagerCompat.IMPORTANCE_HIGH) @@ -104,14 +90,6 @@ class StayAliveService : LifecycleService(), IStayAlive { .setSound(null, null) .build() ) - startForeground(FG_SERVICE_ID, NotificationCompat.Builder(this, SERVICE_CHANNEL) - .setSmallIcon(R.drawable.abm_notif) - .setContentTitle(getString(R.string.abm_processing_title)) - .setContentText(getString(R.string.abm_processing_text)) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setLocalOnly(true) - .build()) wakeLock = getSystemService(PowerManager::class.java) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ABM::StayAlive(user_task)") lifecycleScope.launch { @@ -125,6 +103,14 @@ class StayAliveService : LifecycleService(), IStayAlive { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) + startForeground(FG_SERVICE_ID, NotificationCompat.Builder(this, SERVICE_CHANNEL) + .setSmallIcon(R.drawable.abm_notif) + .setContentTitle(getString(R.string.abm_processing_title)) + .setContentText(getString(R.string.abm_processing_text)) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setLocalOnly(true) + .build()) return START_NOT_STICKY } @@ -133,94 +119,49 @@ class StayAliveService : LifecycleService(), IStayAlive { if (work != null) { throw IllegalStateException("Work was already set on this StayAliveService.") } - return object : Binder(), Provider { - override var service = this@StayAliveService - override var 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() + return object : Binder(), Supplier { + override fun get(): StayAliveService { + return this@StayAliveService } } } override fun onDestroy() { Log.i(TAG, "Goodbye!") + if (!isWorkDone) + throw IllegalStateException("work isn't done but destroying?") super.onDestroy() if (!destroyed) { - if (!isRunning) throw IllegalStateException("excepted isRunning to be true for non-destroyed service") - isRunning = false + if (instance != this) + throw IllegalStateException("excepted instance to be this for non-destroyed service") + instance = null destroyed = true } } - - companion object { - private const val TAG = "ABM_StayAlive" - private const val SERVICE_CHANNEL = "service" - private const val FG_SERVICE_ID = 1001 - var isRunning = false - private set - } -} - -private interface Provider { - val service: IStayAlive - var onDone: (() -> Unit)? - val isWorkDone: Boolean - fun finish() } class StayAliveConnection(inContext: Context, - lifecycleOwner: LifecycleOwner, - private val doWhenDone: (() -> Unit)?, - private val onConnected: (IStayAlive) -> Unit) - : ServiceConnection, DefaultLifecycleObserver { - companion object { - @SuppressLint("StaticFieldLeak") // application context - private var currentConn: StayAliveConnection? = null - } + private val work: suspend (Context) -> Unit, + private val extra: Any) + : ServiceConnection { private val context = inContext.applicationContext - private var provider: Provider? = 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 ) - lifecycleOwner.lifecycle.addObserver(this) } override fun onServiceConnected(name: ComponentName?, inService: IBinder?) { - val provider = inService as Provider - this.provider = provider - val service = provider.service - val onDone = { - onServiceDisconnected(null) - provider.finish() - doWhenDone?.invoke(); Unit - } - onConnected(service) - if (provider.isWorkDone) { - onDone() - } else { - provider.onDone = onDone - } - } - - override fun onServiceDisconnected(name: ComponentName?) { + val provider = inService as Supplier<*> + val service = provider.get() as StayAliveService + service.startWork(work, extra) context.unbindService(this) - provider?.onDone = null - currentConn = null } - override fun onDestroy(owner: LifecycleOwner) { - onServiceDisconnected(null) + 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 fa9539f0..1497b61b 100644 --- a/app/src/main/java/org/andbootmgr/app/util/Terminal.kt +++ b/app/src/main/java/org/andbootmgr/app/util/Terminal.kt @@ -7,21 +7,14 @@ 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 -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable 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 @@ -37,10 +30,12 @@ import java.io.FileOutputStream private class BudgetCallbackList(private val scope: CoroutineScope, private val log: FileOutputStream?) : MutableList, TerminalList { - override val isCancelled = mutableStateOf(null) + override var isCancelled by mutableStateOf(null) override var cancel: (() -> Unit)? = null - val internalList = ArrayList() - var cb: (() -> Unit)? = null + private val internalList = ArrayList() + private var textMinusLastLine = "" + override var text by mutableStateOf("") + private set override val size: Int get() = internalList.size @@ -126,7 +121,7 @@ private class BudgetCallbackList(private val scope: CoroutineScope, override fun set(index: Int, element: String): String { return internalList.set(index, element).also { - cb?.invoke() + text = textMinusLastLine + element + "\n" } } @@ -135,109 +130,64 @@ private class BudgetCallbackList(private val scope: CoroutineScope, } fun onAdd(element: String) { + textMinusLastLine = text + text += element + "\n" scope.launch { log?.write((element + "\n").encodeToByteArray()) } - cb?.invoke() } } interface TerminalList : MutableList { - val isCancelled: MutableState + val text: String + var isCancelled: Boolean? 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 (TerminalList) -> Unit)?) { +fun Terminal(list: TerminalList) { val scrollH = rememberScrollState() val scrollV = rememberScrollState() - 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 - val lo = LocalLifecycleOwner.current - LaunchedEffect(Unit) { - if (action == null && logFile != null) { - throw IllegalArgumentException("logFile must be null if action is null") - } - if (action != null && doWhenDone != null) { - throw IllegalArgumentException("Don't use both action and doWhenDone") - } - if (!didConnectAndFinish) { - StayAliveConnection( - ctx, - lo, - { didConnectAndFinish = true; doWhenDone?.invoke() }) { service -> - if (action != null) { - val logDispatcher = Dispatchers.IO.limitedParallelism(1) - val log = logFile?.let { FileOutputStream(File(ctx.externalCacheDir, it)) } - val s = BudgetCallbackList(CoroutineScope(logDispatcher), log) - isCancelledState = s.isCancelled - doCancelState = { s.cancel!!() } - s.cb = { - val l = s.toList() - scope.launch { - 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) - } - } - service.startWork({ - 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)) - s.add(Log.getStackTraceString(e)) - } - withContext(logDispatcher) { - log?.close() - } - } - }, 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 = { - val l = s.toList() - scope.launch { - 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) - } - } - - } - } - } + LaunchedEffect(list.text) { + delay(200) // Give it time to re-measure + scrollV.animateScrollTo(scrollV.maxValue) + scrollH.animateScrollTo(0) } Column(modifier = Modifier.fillMaxSize()) { - Text(text, modifier = Modifier + Text(list.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)) + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +@Composable +fun TerminalWork(logFile: String? = null, action: suspend (TerminalList) -> Unit) { + val ctx = LocalContext.current.applicationContext + LaunchedEffect(Unit) { + val logDispatcher = Dispatchers.IO.limitedParallelism(1) + val log = logFile?.let { FileOutputStream(File(ctx.externalCacheDir, it)) } + val s = BudgetCallbackList(CoroutineScope(logDispatcher), log) + StayAliveConnection(ctx, { + 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)) + s.add(Log.getStackTraceString(e)) + } + withContext(logDispatcher) { + log?.close() + } } - } + }, s) } } \ No newline at end of file