diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 97b83a97..ddc9c642 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,6 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") - id("org.jetbrains.kotlinx.atomicfu") id("com.mikepenz.aboutlibraries.plugin") } @@ -111,11 +110,6 @@ android { } } -atomicfu { - dependenciesVersion = "0.25.0" - jvmVariant = "FU" -} - tasks.register("setAssetTs", Task::class) { doLast { File("$rootDir/app/src/main/assets/cp/_ts").writeText((System.currentTimeMillis() / 1000L).toString()) @@ -123,7 +117,6 @@ tasks.register("setAssetTs", Task::class) { } dependencies { - compileOnly("org.jetbrains.kotlinx:atomicfu:0.25.0") implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.legacy:legacy-support-v4:1.0.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53bc0f85..7f811c99 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,16 +28,11 @@ android:value="Update Android device bootloader / install OS to SD card"/> - + android:theme="@style/SplashTheme"> diff --git a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt index 7fb62111..254a0873 100644 --- a/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt @@ -1,6 +1,5 @@ package org.andbootmgr.app -import android.content.Intent import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -28,7 +27,7 @@ class BackupRestoreWizardPageFactory(private val vm: WizardActivityState) { fun get(): List { val c = CreateBackupDataHolder(vm) return listOf(WizardPage("start", - NavButton(vm.activity.getString(R.string.cancel)) { it.startActivity(Intent(it, MainActivity::class.java)); it.finish() }, + NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, NavButton("") {}) { ChooseAction(c) @@ -46,19 +45,18 @@ class BackupRestoreWizardPageFactory(private val vm: WizardActivityState) { } } -private class CreateBackupDataHolder(val vm: WizardActivityState){ +private class CreateBackupDataHolder(val vm: WizardActivityState) { var pi: Int = -1 var action: Int = 0 var path: Uri? = null var meta: SDUtils.SDPartitionMeta? = null - } @Composable private fun ChooseAction(c: CreateBackupDataHolder) { LaunchedEffect(Unit) { c.meta = SDUtils.generateMeta(c.vm.deviceInfo) - c.pi = c.vm.activity.intent.getIntExtra("partitionid", -1) + c.pi = c.vm.mvm.wizardCompatPid!! } Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -163,6 +161,6 @@ private fun Flash(c: CreateBackupDataHolder) { terminal.add(c.vm.activity.getString(R.string.term_success)) c.vm.btnsOverride = true c.vm.nextText.value = c.vm.activity.getString(R.string.finish) - c.vm.onNext.value = { it.startActivity(Intent(it, MainActivity::class.java)); it.finish() } + c.vm.onNext.value = { 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 4a54804e..a7fcdde5 100644 --- a/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/CreatePartFlow.kt @@ -1,7 +1,6 @@ package org.andbootmgr.app import android.annotation.SuppressLint -import android.content.Intent import android.net.Uri import android.util.Log import android.widget.Toast @@ -20,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile @@ -30,30 +28,25 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.* import okio.* -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 org.json.JSONObject +import org.json.JSONTokener import java.io.File import java.io.FileInputStream +import java.io.FileNotFoundException import java.io.InputStream import java.math.BigDecimal -import java.util.concurrent.TimeUnit -import org.json.JSONObject -import org.json.JSONTokener -import java.io.FileNotFoundException import java.net.URL -import java.nio.charset.Charset +import java.util.concurrent.TimeUnit class CreatePartWizardPageFactory(private val vm: WizardActivityState) { fun get(): List { val c = CreatePartDataHolder(vm) return listOf(WizardPage("start", - NavButton(vm.activity.getString(R.string.cancel)) { - it.startActivity(Intent(it, MainActivity::class.java)) - it.finish() - }, + NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, NavButton("") {} ) { Start(c) @@ -68,10 +61,7 @@ class CreatePartWizardPageFactory(private val vm: WizardActivityState) { ) { Os(c) }, WizardPage("dload", - NavButton(vm.activity.getString(R.string.cancel)) { - it.startActivity(Intent(it, MainActivity::class.java)) - it.finish() - }, + NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, NavButton("") {} ) { Download(c) @@ -996,10 +986,7 @@ private fun Flash(c: CreatePartDataHolder) { terminal.add(vm.activity.getString(R.string.term_success)) vm.btnsOverride = true vm.nextText.value = vm.activity.getString(R.string.finish) - vm.onNext.value = { - it.startActivity(Intent(it, MainActivity::class.java)) - it.finish() - } + vm.onNext.value = { it.finish() } } // Fucking complicated code to fairly and flexibly partition space based on preset percentage & bytes values @@ -1063,7 +1050,6 @@ private fun Flash(c: CreatePartDataHolder) { vm.btnsOverride = true vm.nextText.value = c.vm.activity.getString(R.string.finish) vm.onNext.value = { - it.startActivity(Intent(it, MainActivity::class.java)) it.finish() } terminal.add(vm.activity.getString(R.string.term_success)) @@ -1072,19 +1058,4 @@ private fun Flash(c: CreatePartDataHolder) { } } } -} - -@Composable -@Preview -private fun Preview() { - val vm = WizardActivityState("null") - val c = CreatePartDataHolder(vm) - AbmTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Start(c) - } - } } \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt index 84b6db98..b970e7e9 100644 --- a/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt @@ -1,31 +1,39 @@ package org.andbootmgr.app -import android.content.Intent import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log import android.widget.Toast -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.topjohnwu.superuser.Shell 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 @@ -38,7 +46,7 @@ import java.net.URL class DroidBootWizardPageFactory(private val vm: WizardActivityState) { fun get(): List { return listOf(WizardPage("start", - NavButton(vm.activity.getString(R.string.cancel)) { it.startActivity(Intent(it, MainActivity::class.java)); it.finish() }, + NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, NavButton(vm.activity.getString(R.string.next)) { it.navigate("input") }) { Start(vm) @@ -370,26 +378,12 @@ private fun Flash(vm: WizardActivityState) { vm.nextText.value = vm.activity.getString(R.string.finish) vm.onNext.value = { if (vm.deviceInfo.isBooted(vm.logic)) { - it.startActivity(Intent(it, MainActivity::class.java)) + it.finish() } else { // TODO prompt user to reboot? + it.finish() } - it.finish() } } } -} - -@Composable -@Preview -private fun Preview() { - val vm = WizardActivityState("null") - AbmTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Input(vm) - } - } } \ 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 9233be95..8b665941 100644 --- a/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt @@ -3,19 +3,15 @@ package org.andbootmgr.app import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import com.topjohnwu.superuser.io.SuFile 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.AbmTheme import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -103,18 +99,4 @@ private fun Flash(vm: WizardActivityState) { } } } -} - -@Composable -@Preview -private fun Preview() { - val vm = WizardActivityState("null") - AbmTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - SelectDroidBoot(vm) - } - } } \ 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 f3160258..2464975e 100644 --- a/app/src/main/java/org/andbootmgr/app/MainActivity.kt +++ b/app/src/main/java/org/andbootmgr/app/MainActivity.kt @@ -1,32 +1,53 @@ package org.andbootmgr.app import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Log +import android.view.WindowManager import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -37,7 +58,6 @@ import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell.FLAG_MOUNT_MASTER import com.topjohnwu.superuser.Shell.FLAG_REDIRECT_STDERR import com.topjohnwu.superuser.io.SuFile -import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -50,43 +70,31 @@ import org.andbootmgr.app.util.ConfigFile import org.andbootmgr.app.util.SDUtils import org.andbootmgr.app.util.StayAliveService import org.andbootmgr.app.util.Terminal -import org.andbootmgr.app.util.Toolkit -import java.io.File +import java.util.concurrent.atomic.AtomicBoolean class MainActivityState(val activity: MainActivity?) { + var wizardCompat by mutableStateOf(null) + fun startFlow(flow: String) { - val i = Intent(activity!!, WizardActivity::class.java) - i.putExtra("codename", deviceInfo!!.codename) - i.putExtra("flow", flow) - activity.startActivity(i) - activity.finish() + wizardCompat = flow } + var wizardCompatSid: Long? = null fun startCreateFlow(freeSpace: SDUtils.Partition.FreeSpace) { - val i = Intent(activity!!, WizardActivity::class.java) - i.putExtra("codename", deviceInfo!!.codename) - i.putExtra("flow", "create_part") - i.putExtra("part_sid", freeSpace.startSector) - activity.startActivity(i) - activity.finish() + wizardCompat = "create_part" + wizardCompatSid = freeSpace.startSector } + var wizardCompatE: String? = null fun startUpdateFlow(e: String) { - val i = Intent(activity!!, WizardActivity::class.java) - i.putExtra("codename", deviceInfo!!.codename) - i.putExtra("flow", "update") - i.putExtra("entryFilename", e) - activity.startActivity(i) - activity.finish() + wizardCompat = "update" + wizardCompatE = e } + var wizardCompatPid: Int? = null fun startBackupAndRestoreFlow(partition: SDUtils.Partition) { - val i = Intent(activity!!, WizardActivity::class.java) - i.putExtra("codename", deviceInfo!!.codename) - i.putExtra("flow", "backup_restore") - i.putExtra("partitionid", partition.id) - activity.startActivity(i) - activity.finish() + wizardCompat = "backup_restore" + wizardCompatPid = partition.id } var noobMode by mutableStateOf(false) @@ -146,11 +154,51 @@ class MainActivityState(val activity: MainActivity?) { class MainActivity : ComponentActivity() { + private lateinit var newFile: ActivityResultLauncher + private var onFileCreated: ((Uri) -> Unit)? = null + private lateinit var chooseFile: ActivityResultLauncher + private var onFileChosen: ((Uri) -> Unit)? = null override fun onCreate(savedInstanceState: Bundle?) { - val ready = atomic(false) - installSplashScreen().setKeepOnScreenCondition { !ready.value } + val ready = AtomicBoolean(false) + installSplashScreen().setKeepOnScreenCondition { !ready.get() } super.onCreate(savedInstanceState) + 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 { + throw IllegalStateException("expected onFileChosen to not be null") + } + } + 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 { + throw IllegalStateException("expected onFileCreated to not be null") + } + } val vm = MainActivityState(this) vm.logic = DeviceLogic(this) CoroutineScope(Dispatchers.IO).launch { @@ -198,9 +246,15 @@ class MainActivity : ComponentActivity() { } withContext(Dispatchers.Main) { setContent { - // TODO allow rotating device while viewing logs without loosing logs + // TODO allow rotating device while viewing logs without loosing logs (will require rememberSavable) if (remember { StayAliveService.isRunning }) { + DisposableEffect(Unit) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + onDispose { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } + } Terminal(null, null) + } else if (vm.wizardCompat != null) { + WizardCompat(vm, vm.wizardCompat!!) } else { val navController = rememberNavController() AppContent(vm, navController) { @@ -208,10 +262,26 @@ class MainActivity : ComponentActivity() { } } } - ready.value = true + ready.set(true) } } } + + fun chooseFile(mime: String, callback: (Uri) -> Unit) { + if (onFileChosen != null) { + throw IllegalStateException("expected onFileChosen to be null") + } + onFileChosen = callback + chooseFile.launch(mime) + } + + fun createFile(name: String, callback: (Uri) -> Unit) { + if (onFileCreated != null) { + throw IllegalStateException("expected onFileCreated to be null") + } + onFileCreated = callback + newFile.launch(name) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -332,786 +402,6 @@ private fun NavGraph(vm: MainActivityState, navController: NavHostController, it } } -@Composable -private fun Start(vm: MainActivityState) { - val installed: Boolean - val booted: Boolean - val mounted: Boolean - val sdpresent: Boolean - val corrupt: Boolean - val metaonsd: Boolean - if (vm.deviceInfo != null) { - installed = remember { vm.deviceInfo!!.isInstalled(vm.logic!!) } - booted = remember { vm.deviceInfo!!.isBooted(vm.logic!!) } - corrupt = remember { vm.deviceInfo!!.isCorrupt(vm.logic!!) } - mounted = vm.logic!!.mounted - sdpresent = SuFile.open(vm.deviceInfo!!.bdev).exists() - metaonsd = vm.deviceInfo!!.metaonsd - } else { - installed = false - booted = false - corrupt = true - mounted = false - sdpresent = false - metaonsd = false - } - val notOkColor = CardDefaults.cardColors( - containerColor = Color(0xFFFF0F0F) - ) - val okColor = CardDefaults.cardColors( - containerColor = Color(0xFF0DDF0F) - ) - val notOkIcon = R.drawable.ic_baseline_error_24 - val okIcon = R.drawable.ic_baseline_check_circle_24 - val okText = stringResource(R.string.installed) - val partiallyOkText = stringResource(R.string.installed_deactivated) - val corruptDeactivatedText = stringResource(R.string.deactivated_corrupt) - val corruptText = stringResource(R.string.activated_corrupt) - val notOkText = stringResource(R.string.not_installed) - val ok = installed and booted and mounted and !corrupt - val usedText = if (ok) { - okText - } else if (installed and booted and (!mounted || corrupt)) { - corruptText - } else if (installed and !booted and (!mounted || corrupt)) { - corruptDeactivatedText - } else if (installed and !booted) { - partiallyOkText - } else { - notOkText - } - Column { - Card( - colors = if (ok) okColor else notOkColor, modifier = Modifier - .fillMaxWidth() - .padding(5.dp) - ) { - Box(Modifier.padding(10.dp)) { - Column { - Row( - Modifier - .padding(5.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painterResource(if (ok) okIcon else notOkIcon), - "", - Modifier.size(48.dp) - ) - Text(usedText, fontSize = 32.sp) - } - Text(stringResource(id = R.string.activated, stringResource(if (booted) R.string.yes else R.string.no))) - Text(stringResource(id = R.string.mounted_b, stringResource(if (mounted) R.string.yes else R.string.no))) - if (metaonsd) { - Text(stringResource(id = R.string.sd_inserted, stringResource(if (sdpresent) R.string.yes else R.string.no))) - Text(stringResource(id = R.string.sd_formatted, stringResource(if (installed) R.string.yes else R.string.no))) - } else { - Text(stringResource(id = R.string.installed_status, stringResource(if (installed) R.string.yes else R.string.no))) - } - if (mounted) { - Text(stringResource(id = R.string.corrupt_b, stringResource(if (corrupt) R.string.yes else R.string.no))) - } - Text(stringResource(R.string.device, if (vm.deviceInfo == null) stringResource(id = R.string.unsupported) else vm.deviceInfo!!.codename)) - } - } - } - if (Shell.isAppGrantedRoot() != true) { - Text( - stringResource(R.string.need_root), - textAlign = TextAlign.Center - ) - } else if (metaonsd && !sdpresent) { - Text(stringResource(R.string.need_sd), textAlign = TextAlign.Center) - } else if (!installed && !mounted) { - Button(onClick = { vm.startFlow("droidboot") }) { - Text(stringResource(if (metaonsd) R.string.setup_sd else R.string.install)) - } - } else if (!booted && mounted) { - Text(stringResource(R.string.installed_not_booted), textAlign = TextAlign.Center) - Button(onClick = { - vm.startFlow("fix_droidboot") - }) { - Text(stringResource(R.string.repair_droidboot)) - } - } else if (!mounted) { - Text(stringResource(R.string.cannot_mount), textAlign = TextAlign.Center) - } else if (vm.isOk) { - PartTool(vm) - } else { - Text(stringResource(R.string.invalid), textAlign = TextAlign.Center) - } - } -} - -@Composable -private fun PartTool(vm: MainActivityState) { - var filterUnifiedView by remember { mutableStateOf(true) } - var filterPartView by remember { mutableStateOf(false) } - var filterEntryView by remember { mutableStateOf(false) } - if (!vm.noobMode) - Row { - FilterChip( - selected = filterUnifiedView, - onClick = { - filterUnifiedView = true; filterPartView = false; filterEntryView = false - }, - label = { Text(stringResource(R.string.unified)) }, - Modifier.padding(start = 5.dp), - leadingIcon = if (filterUnifiedView) { - { - Icon( - imageVector = Icons.Filled.Done, - contentDescription = stringResource(id = R.string.enabled_content_desc), - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - } else { - null - } - ) - FilterChip( - selected = filterPartView, - onClick = { - filterPartView = true; filterUnifiedView = false; filterEntryView = false - }, - label = { Text(stringResource(R.string.partitions)) }, - Modifier.padding(start = 5.dp), - leadingIcon = if (filterPartView) { - { - Icon( - imageVector = Icons.Filled.Done, - contentDescription = stringResource(R.string.enabled_content_desc), - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - } else { - null - } - ) - FilterChip( - selected = filterEntryView, - onClick = { - filterPartView = false; filterUnifiedView = false; filterEntryView = true - }, - label = { Text(stringResource(R.string.entries)) }, - Modifier.padding(start = 5.dp), - leadingIcon = if (filterEntryView) { - { - Icon( - imageVector = Icons.Filled.Done, - contentDescription = stringResource(R.string.enabled_content_desc), - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - } else { - null - } - ) - } - - var parts by remember { mutableStateOf(SDUtils.generateMeta(vm.deviceInfo!!)) } - if (parts == null) { - Text(stringResource(R.string.part_wizard_err)) - return - } - val entries = remember { - val outList = mutableMapOf() - val list = SuFile.open(vm.logic!!.abmEntries.absolutePath).listFiles() - for (i in list!!) { - try { - outList[ConfigFile.importFromFile(i)] = i - } catch (e: ActionAbortedCleanlyError) { - Log.e("ABM", Log.getStackTraceString(e)) - } - } - return@remember outList - } - var processing by remember { mutableStateOf(false) } - var rename by remember { mutableStateOf(false) } - var delete by remember { mutableStateOf(false) } - var result: String? by remember { mutableStateOf(null) } - var editPartID: SDUtils.Partition? by remember { mutableStateOf(null) } - var editEntryID: ConfigFile? by remember { mutableStateOf(null) } - Column( - Modifier - .verticalScroll(rememberScrollState()) - .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - if (vm.noobMode) { - Card(modifier = Modifier - .fillMaxWidth() - .padding(5.dp)) { - Row( - Modifier - .fillMaxWidth() - .padding(20.dp) - ) { - Icon(painterResource(id = R.drawable.ic_about), stringResource(id = R.string.icon_content_desc)) - Text(stringResource(R.string.click2inspect)) - } - } - } - if (filterUnifiedView) { - var i = 0 - while (i < parts!!.s.size) { - var found = false - if (parts!!.s[i].type != SDUtils.PartitionType.FREE) { - for (e in entries.keys) { - if (e.has("xpart") && e["xpart"] != null && e["xpart"]!!.isNotBlank()) { - for (j in e["xpart"]!!.split(":")) { - if ("${parts!!.s[i].id}" == j) { - found = true - Row(horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { editEntryID = e }) { - Text( - if (e.has("title")) { - stringResource(R.string.entry_title, e["title"]!!) - } else { - stringResource(R.string.invalid_entry) - } - ) - } - while (e["xpart"]!!.split(":").contains("${parts!!.s[i].id}")) { - if (i + 1 == parts!!.s.size) break - if (!e["xpart"]!!.split(":").contains("${parts!!.s[++i].id}")) { - i--; break - } - } - break - } - } - } - if (found) break - } - } - if (!found) { - val p = parts!!.s[i] - Row(horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { editPartID = p }) { - Text( - if (p.type == SDUtils.PartitionType.FREE) - stringResource(id = R.string.free_space_item, p.sizeFancy) - else - stringResource(id = R.string.part_item, p.id, p.name) - ) - } - } - i++ - } - } - if (filterPartView) { - for (p in parts!!.s) { - Row(horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { editPartID = p }) { - Text( - if (p.type == SDUtils.PartitionType.FREE) - stringResource(id = R.string.free_space_item, p.sizeFancy) - else - stringResource(id = R.string.part_item, p.id, p.name) - ) - } - } - } - if (filterEntryView) { - for (e in entries.keys) { - Row(horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { editEntryID = e }) { - /* format: - entry["title"] = str - entry["linux"] = path(str) - entry["initrd"] = path(str) - entry["dtb"] = path(str) - entry["options"] = str - entry["xtype"] = str - entry["xpart"] = array (str.split(":")) - entry["xupdate"] = uri(str) - */ - Text( - if (e.has("title")) { - stringResource(R.string.entry_title, e["title"]!!) - } else { - stringResource(R.string.invalid_entry) - } - ) - } - } - val e = ConfigFile() - Row(horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { editEntryID = e }) { - Text(stringResource(R.string.new_entry)) - } - } - if (editPartID != null) { - val p = editPartID!! - AlertDialog( - onDismissRequest = { - editPartID = null - }, - title = { - val name = if (p.type == SDUtils.PartitionType.FREE) - stringResource(R.string.free_space) - else if (p.name.isBlank()) - stringResource(R.string.part_title, p.id) - else - p.name.trim() - - Text(text = "\"${name}\"") - }, - icon = { - Icon(painterResource(id = R.drawable.ic_sd), stringResource(id = R.string.icon_content_desc)) - }, - text = { - Column { - val fancyType = stringResource(when (p.type) { - SDUtils.PartitionType.RESERVED -> R.string.reserved - SDUtils.PartitionType.ADOPTED -> R.string.adoptable_meta - SDUtils.PartitionType.PORTABLE -> R.string.portable_part - SDUtils.PartitionType.UNKNOWN -> R.string.unknown - SDUtils.PartitionType.FREE -> R.string.free_space - SDUtils.PartitionType.SYSTEM -> R.string.os_system - SDUtils.PartitionType.DATA -> R.string.os_userdata - }) - if (p.type != SDUtils.PartitionType.FREE && !filterUnifiedView) - Text(stringResource(id = R.string.detail_id, p.id, p.major, p.minor)) - Text(stringResource(id = R.string.detail_type, fancyType, if (p.type != SDUtils.PartitionType.FREE && !filterUnifiedView) stringResource(id = R.string.detail_type_code, p.code) else "")) - Text(stringResource(id = R.string.detail_size, p.sizeFancy, if (!filterUnifiedView) stringResource(id = R.string.detail_size_sectors, p.size) else "")) - if (!filterUnifiedView) - Text(stringResource( - id = R.string.detail_position, - p.startSector, - p.endSector - )) - if (p.type != SDUtils.PartitionType.FREE) { - Row { - Button(onClick = { - rename = true - }, Modifier.padding(end = 5.dp)) { - Text(stringResource(R.string.rename)) - } - Button(onClick = { - delete = true - }) { - Text(stringResource(R.string.delete)) - } - } - if (!filterUnifiedView) { - Row { - Button(onClick = { - processing = true - vm.logic!!.mount(p).submit { - processing = false - result = it.out.joinToString("\n") + it.err.joinToString("\n") - } - }, Modifier.padding(end = 5.dp)) { - Text(stringResource(R.string.mount)) - } - Button(onClick = { - processing = true - vm.logic!!.unmount(p).submit { - processing = false - result = it.out.joinToString("\n") + it.err.joinToString("\n") - } - }) { - Text(stringResource(R.string.umount)) - } - } - } - Button(onClick = { vm.startBackupAndRestoreFlow(p) }) { - Text(stringResource(R.string.backupnrestore)) - } - } else { - Button(onClick = { vm.startCreateFlow(p as SDUtils.Partition.FreeSpace) }) { - Text(stringResource(R.string.create)) - } - } - } - }, - confirmButton = { - Button( - onClick = { - editPartID = null - }) { - Text(stringResource(R.string.cancel)) - } - } - ) - if (rename) { - var e by remember { mutableStateOf(false) } - var t by remember { mutableStateOf(p.name) } - AlertDialog( - onDismissRequest = { - rename = false - }, - title = { - Text(stringResource(R.string.rename)) - }, - text = { - TextField(value = t, onValueChange = { - t = it - e = !t.matches(Regex("\\A\\p{ASCII}*\\z")) - }, isError = e, label = { - Text(stringResource(R.string.part_name)) - }) - }, - dismissButton = { - Button(onClick = { rename = false }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - Button(onClick = { - if (!e) { - processing = true - rename = false - vm.logic!!.rename(p, t).submit { r -> - result = r.out.joinToString("\n") + r.err.joinToString("\n") - parts = SDUtils.generateMeta(vm.deviceInfo!!) - editPartID = parts?.s!!.findLast { it.id == p.id } - processing = false - } - } - }, enabled = !e) { - Text(stringResource(R.string.rename)) - } - } - ) - } else if (delete) { - AlertDialog( - onDismissRequest = { - delete = false - }, - title = { - Text(stringResource(R.string.delete)) - }, - text = { - Text(stringResource(R.string.really_delete_part)) - }, - dismissButton = { - Button(onClick = { delete = false }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - Button(onClick = { - processing = true - delete = false - val wasMounted = vm.logic!!.mounted - vm.unmountBootset() - vm.logic!!.delete(p).submit { - vm.mountBootset() - if (wasMounted != vm.logic!!.mounted) vm.activity!!.finish() - else { - processing = false - editPartID = null - parts = SDUtils.generateMeta(vm.deviceInfo!!) - result = it.out.joinToString("\n") + it.err.joinToString("\n") - } - } - }) { - Text(stringResource(R.string.delete)) - } - } - ) - } - } - if (editEntryID != null && !filterUnifiedView) { - val ctx = LocalContext.current - val fn = Regex("[\\da-zA-Z]+\\.conf") - val ascii = Regex("\\A\\p{ASCII}+\\z") - val xtype = arrayOf("droid", "SFOS", "UT", "") - val xpart = Regex("^$|^real$|^\\d(:\\d+)*$") - val e = editEntryID!! - var f = entries[e] - var newFileName by remember { mutableStateOf(f?.name ?: "NewEntry.conf") } - var newFileNameErr by remember { mutableStateOf(!newFileName.matches(fn)) } - var titleT by remember { mutableStateOf(e["title"] ?: "") } - var titleE by remember { mutableStateOf(!titleT.matches(ascii)) } - var linuxT by remember { mutableStateOf(e["linux"] ?: "") } - var linuxE by remember { mutableStateOf(!linuxT.matches(ascii)) } - var initrdT by remember { mutableStateOf(e["initrd"] ?: "") } - var initrdE by remember { mutableStateOf(!initrdT.matches(ascii)) } - var dtbT by remember { mutableStateOf(e["dtb"] ?: "") } - var dtbE by remember { mutableStateOf(!dtbT.matches(ascii)) } - var optionsT by remember { mutableStateOf(e["options"] ?: "") } - var optionsE by remember { mutableStateOf(!optionsT.matches(ascii)) } - var xtypeT by remember { mutableStateOf(e["xtype"] ?: "") } - var xtypeE by remember { mutableStateOf(!xtype.contains(xtypeT)) } - var xpartT by remember { mutableStateOf(e["xpart"] ?: "") } - var xpartE by remember { mutableStateOf(!xpartT.matches(xpart)) } - var xupdateT by remember { mutableStateOf(e["xupdate"] ?: "") } - val isOk = !(newFileNameErr || titleE || linuxE || initrdE || dtbE || optionsE || xtypeE || xpartE) - AlertDialog( - onDismissRequest = { - editEntryID = null - }, - title = { - Text(text = if (e.has("title")) "\"${e["title"]}\"" else if (f != null) stringResource(id = R.string.invalid_entry2) else stringResource(id = R.string.new_entry2)) - }, - icon = { - Icon(painterResource(id = R.drawable.ic_roms), stringResource(R.string.icon_content_desc)) - }, - text = { - Column(Modifier.verticalScroll(rememberScrollState())) { - TextField(value = newFileName, onValueChange = { - if (f != null) return@TextField - newFileName = it - newFileNameErr = !(newFileName.matches(fn)) - }, isError = newFileNameErr, enabled = f == null, label = { - Text(stringResource(R.string.file_name)) - }) - - TextField(value = titleT, onValueChange = { - titleT = it - titleE = !(titleT.matches(ascii)) - }, isError = titleE, label = { - Text(stringResource(R.string.title)) - }) - - TextField(value = linuxT, onValueChange = { - linuxT = it - linuxE = !(linuxT.matches(ascii)) - }, isError = linuxE, label = { - Text(stringResource(R.string.linux)) - }) - - TextField(value = initrdT, onValueChange = { - initrdT = it - initrdE = !(initrdT.matches(ascii)) - }, isError = initrdE, label = { - Text(stringResource(R.string.initrd)) - }) - - TextField(value = dtbT, onValueChange = { - dtbT = it - dtbE = !(dtbT.matches(ascii)) - }, isError = dtbE, label = { - Text(stringResource(R.string.dtb)) - }) - - TextField(value = optionsT, onValueChange = { - optionsT = it - optionsE = !(optionsT.matches(ascii)) - }, isError = optionsE, label = { - Text(stringResource(R.string.options)) - }) - - TextField(value = xtypeT, onValueChange = { - xtypeT = it - xtypeE = !(xtype.contains(xtypeT)) - }, isError = xtypeE, label = { - Text(stringResource(R.string.rom_type)) - }) - - TextField(value = xpartT, onValueChange = { - xpartT = it - xpartE = !(xpartT.matches(xpart)) - }, isError = xpartE, label = { - Text(stringResource(R.string.assigned_parts)) - }) - - TextField(value = xupdateT, onValueChange = { - xupdateT = it - }, label = { - Text(stringResource(R.string.updater_url)) - }) - } - }, - confirmButton = { - if (f != null && e["xpart"] != "real") { - Button( - onClick = { - f!!.delete() - entries.remove(e) - editEntryID = null - }) { - Text(stringResource(R.string.delete)) - } - } - Button( - onClick = { - if (!isOk) return@Button - if (f == null) { - f = SuFile.open(vm.logic!!.abmEntries, newFileName) - if (f!!.exists()) { - Toast.makeText( - ctx, - vm.activity!!.getString(R.string.file_already_exists), - Toast.LENGTH_LONG - ).show() - f = null - return@Button - } - } - entries[e] = f!! - e["title"] = titleT - e["linux"] = linuxT - e["initrd"] = initrdT - e["dtb"] = dtbT - e["options"] = optionsT - e["xtype"] = xtypeT - e["xpart"] = xpartT - e["xupdate"] = xupdateT - e.exportToFile(f!!) - editEntryID = null - }, enabled = isOk - ) { - Text(stringResource(if (f != null) R.string.update else R.string.create)) - } - Button( - onClick = { - editEntryID = null - }) { - Text(stringResource(R.string.cancel)) - } - } - ) - } else if (editEntryID != null) { - val e = editEntryID!! - AlertDialog( - onDismissRequest = { - editEntryID = null - }, - title = { - Text(text = if (e.has("title")) "\"${e["title"]}\"" else stringResource(R.string.invalid_entry2)) - }, - icon = { - Icon(painterResource(id = R.drawable.ic_roms), stringResource(id = R.string.icon_content_desc)) - }, - text = { - Column(Modifier.verticalScroll(rememberScrollState())) { - Button( - onClick = { - if (e.has("xupdate") && !e["xupdate"].isNullOrBlank()) - vm.startUpdateFlow(entries[e]!!.absolutePath) - }, enabled = e.has("xupdate") && !e["xupdate"].isNullOrBlank()) { - Text(stringResource(R.string.update)) - } - Button( - onClick = { - delete = true - }) { - Text(stringResource(R.string.delete)) - } - } - }, - confirmButton = { - Button( - onClick = { - editEntryID = null - }) { - Text(stringResource(R.string.cancel)) - } - } - ) - - if (delete) { - AlertDialog( - onDismissRequest = { - delete = false - }, - title = { - Text(stringResource(R.string.delete)) - }, - text = { - Text(stringResource(R.string.really_delete_os)) - }, - dismissButton = { - Button(onClick = { delete = false }) { - Text(stringResource(id = R.string.cancel)) - } - }, - confirmButton = { - Button(onClick = { - processing = true - delete = false - CoroutineScope(Dispatchers.Default).launch { - var tresult = "" - if (e.has("xpart") && !e["xpart"].isNullOrBlank()) { - val allp = e["xpart"]!!.split(":") - .map { parts!!.dumpKernelPartition(Integer.valueOf(it)) } - vm.unmountBootset() - 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.joinToString("\n") + r.err.joinToString("\n") + "\n" - } - vm.mountBootset() - } - val f = entries[e]!! - val f2 = SuFile(vm.logic!!.abmBootset, f.nameWithoutExtension) - if (!f2.deleteRecursive()) - tresult += vm.activity!!.getString(R.string.cannot_delete, f2.absolutePath) - entries.remove(e) - if (!f.delete()) - tresult += vm.activity!!.getString(R.string.cannot_delete, f.absolutePath) - editEntryID = null - processing = false - parts = SDUtils.generateMeta(vm.deviceInfo!!) - result = tresult - } - }) { - Text(stringResource(R.string.delete)) - } - } - ) - } - } - if (processing) { - AlertDialog( - onDismissRequest = {}, - title = { - Text(stringResource(R.string.please_wait)) - }, - text = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround - ) { - CircularProgressIndicator(Modifier.padding(end = 20.dp)) - Text(stringResource(R.string.loading)) - } - }, - confirmButton = {} - ) - } else if (result != null) { - AlertDialog( - onDismissRequest = { - result = null - }, - title = { - Text(stringResource(R.string.done)) - }, - text = { - result?.let { - Text(it) - } - }, - confirmButton = { - Button(onClick = { result = null }) { - Text(stringResource(id = R.string.ok)) - } - } - ) - } - } -} - @Preview(showBackground = true) @Composable private fun Preview() { diff --git a/app/src/main/java/org/andbootmgr/app/Settings.kt b/app/src/main/java/org/andbootmgr/app/Settings.kt index e53bd297..9ab6e861 100644 --- a/app/src/main/java/org/andbootmgr/app/Settings.kt +++ b/app/src/main/java/org/andbootmgr/app/Settings.kt @@ -101,8 +101,8 @@ fun Settings(vm: MainActivityState) { modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) { Text(stringResource(R.string.noob_mode)) Switch(checked = vm.noobMode, onCheckedChange = { - vm.noobMode = it ctx.getSharedPreferences("abm", 0).edit().putBoolean("noob_mode", it).apply() + vm.noobMode = it }) } } diff --git a/app/src/main/java/org/andbootmgr/app/Start.kt b/app/src/main/java/org/andbootmgr/app/Start.kt new file mode 100644 index 00000000..f4801169 --- /dev/null +++ b/app/src/main/java/org/andbootmgr/app/Start.kt @@ -0,0 +1,831 @@ +package org.andbootmgr.app + +import android.annotation.SuppressLint +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.clickable +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.io.SuFile +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 java.io.File + +private val configFileNameRegex = Regex("[\\da-zA-Z]+\\.conf") +private val asciiRegex = Regex("\\A\\p{ASCII}+\\z") +private val xtypeValidValues = arrayOf("droid", "SFOS", "UT", "") +private val xpartValidValues = Regex("^$|^real$|^\\d(:\\d+)*$") + +@Composable +fun Start(vm: MainActivityState) { + val installed: Boolean + val booted: Boolean + val mounted: Boolean + val sdPresent: Boolean + val corrupt: Boolean + val metaOnSd: Boolean + if (vm.deviceInfo != null) { + installed = remember { vm.deviceInfo!!.isInstalled(vm.logic!!) } + booted = remember { vm.deviceInfo!!.isBooted(vm.logic!!) } + corrupt = remember { vm.deviceInfo!!.isCorrupt(vm.logic!!) } + mounted = vm.logic!!.mounted + metaOnSd = vm.deviceInfo!!.metaonsd + sdPresent = if (metaOnSd) remember { SuFile.open(vm.deviceInfo!!.bdev).exists() } else false + } else { + installed = false + booted = false + corrupt = true + mounted = false + sdPresent = false + metaOnSd = false + } + val notOkColor = CardDefaults.cardColors( + containerColor = Color(0xFFFF0F0F) + ) + val okColor = CardDefaults.cardColors( + containerColor = Color(0xFF0DDF0F) + ) + val notOkIcon = R.drawable.ic_baseline_error_24 + val okIcon = R.drawable.ic_baseline_check_circle_24 + val okText = stringResource(R.string.installed) + val partiallyOkText = stringResource(R.string.installed_deactivated) + val corruptDeactivatedText = stringResource(R.string.deactivated_corrupt) + val corruptText = stringResource(R.string.activated_corrupt) + val notOkText = stringResource(R.string.not_installed) + val ok = installed and booted and mounted and !corrupt + val usedText = if (ok) { + okText + } else if (installed and booted and (!mounted || corrupt)) { + corruptText + } else if (installed and !booted and (!mounted || corrupt)) { + corruptDeactivatedText + } else if (installed and !booted) { + partiallyOkText + } else { + notOkText + } + Column { + Card( + colors = if (ok) okColor else notOkColor, modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Box(Modifier.padding(10.dp)) { + Column { + Row( + Modifier + .padding(5.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(if (ok) okIcon else notOkIcon), + "", + Modifier.size(48.dp) + ) + Text(usedText, fontSize = 32.sp) + } + Text(stringResource(id = R.string.activated, stringResource(if (booted) R.string.yes else R.string.no))) + Text(stringResource(id = R.string.mounted_b, stringResource(if (mounted) R.string.yes else R.string.no))) + if (metaOnSd) { + Text(stringResource(id = R.string.sd_inserted, stringResource(if (sdPresent) R.string.yes else R.string.no))) + Text(stringResource(id = R.string.sd_formatted, stringResource(if (installed) R.string.yes else R.string.no))) + } else { + Text(stringResource(id = R.string.installed_status, stringResource(if (installed) R.string.yes else R.string.no))) + } + if (mounted) { + Text(stringResource(id = R.string.corrupt_b, stringResource(if (corrupt) R.string.yes else R.string.no))) + } + Text(stringResource(R.string.device, if (vm.deviceInfo == null) stringResource(id = R.string.unsupported) else vm.deviceInfo!!.codename)) + } + } + } + if (Shell.isAppGrantedRoot() != true) { + Text( + stringResource(R.string.need_root), + textAlign = TextAlign.Center + ) + } else if (metaOnSd && !sdPresent) { + Text(stringResource(R.string.need_sd), textAlign = TextAlign.Center) + } else if (!installed && !mounted) { + Button(onClick = { vm.startFlow("droidboot") }) { + Text(stringResource(if (metaOnSd) R.string.setup_sd else R.string.install)) + } + } else if (!booted && mounted) { + Text(stringResource(R.string.installed_not_booted), textAlign = TextAlign.Center) + Button(onClick = { + vm.startFlow("fix_droidboot") + }) { + Text(stringResource(R.string.repair_droidboot)) + } + } else if (!mounted) { + Text(stringResource(R.string.cannot_mount), textAlign = TextAlign.Center) + } else if (vm.isOk) { + PartTool(vm) + } else { + Text(stringResource(R.string.invalid), textAlign = TextAlign.Center) + } + } +} + +@Composable +private fun PartTool(vm: MainActivityState) { + var filterUnifiedView by remember { mutableStateOf(true) } + var filterPartView by remember { mutableStateOf(false) } + var filterEntryView by remember { mutableStateOf(false) } + if (!vm.noobMode) + Row { + FilterChip( + selected = filterUnifiedView, + onClick = { + filterUnifiedView = true; filterPartView = false; filterEntryView = false + }, + label = { Text(stringResource(R.string.unified)) }, + Modifier.padding(start = 5.dp), + leadingIcon = if (filterUnifiedView) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = stringResource(id = R.string.enabled_content_desc), + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + } + ) + FilterChip( + selected = filterPartView, + onClick = { + filterPartView = true; filterUnifiedView = false; filterEntryView = false + }, + label = { Text(stringResource(R.string.partitions)) }, + Modifier.padding(start = 5.dp), + leadingIcon = if (filterPartView) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = stringResource(R.string.enabled_content_desc), + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + } + ) + FilterChip( + selected = filterEntryView, + onClick = { + filterPartView = false; filterUnifiedView = false; filterEntryView = true + }, + label = { Text(stringResource(R.string.entries)) }, + Modifier.padding(start = 5.dp), + leadingIcon = if (filterEntryView) { + { + Icon( + imageVector = Icons.Filled.Done, + contentDescription = stringResource(R.string.enabled_content_desc), + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + } + ) + } + + var parts by remember { mutableStateOf(SDUtils.generateMeta(vm.deviceInfo!!)) } + if (parts == null) { + Text(stringResource(R.string.part_wizard_err)) + return + } + @SuppressLint("MutableCollectionMutableState") // lol + var entries by remember { mutableStateOf?>(null) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + val outList = mutableStateMapOf() + val list = SuFile.open(vm.logic!!.abmEntries.absolutePath).listFiles() + for (i in list!!) { + try { + outList[ConfigFile.importFromFile(i)] = i + } catch (e: ActionAbortedCleanlyError) { + Log.e("ABM", Log.getStackTraceString(e)) + } + } + entries = outList + } + } + var processing by remember { mutableStateOf(false) } + var rename by remember { mutableStateOf(false) } + var delete by remember { mutableStateOf(false) } + var result by remember { mutableStateOf(null) } + var editPartID by remember { mutableStateOf(null) } + var editEntryID by remember { mutableStateOf(null) } + Column( + Modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + if (vm.noobMode) { + Card(modifier = Modifier + .fillMaxWidth() + .padding(5.dp)) { + Row( + Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Icon(painterResource(id = R.drawable.ic_about), stringResource(id = R.string.icon_content_desc)) + Text(stringResource(R.string.click2inspect)) + } + } + } + if (filterUnifiedView && entries != null) { + var i = 0 + while (i < parts!!.s.size) { + var found = false + if (parts!!.s[i].type != SDUtils.PartitionType.FREE) { + for (e in entries!!.keys) { + if (e.has("xpart") && e["xpart"] != null && e["xpart"]!!.isNotBlank()) { + for (j in e["xpart"]!!.split(":")) { + if ("${parts!!.s[i].id}" == j) { + found = true + Row(horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { editEntryID = e }) { + Text( + if (e.has("title")) { + stringResource(R.string.entry_title, e["title"]!!) + } else { + stringResource(R.string.invalid_entry) + } + ) + } + while (e["xpart"]!!.split(":").contains("${parts!!.s[i].id}")) { + if (i + 1 == parts!!.s.size) break + if (!e["xpart"]!!.split(":").contains("${parts!!.s[++i].id}")) { + i--; break + } + } + break + } + } + } + if (found) break + } + } + if (!found) { + val p = parts!!.s[i] + Row(horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { editPartID = p }) { + Text( + if (p.type == SDUtils.PartitionType.FREE) + stringResource(id = R.string.free_space_item, p.sizeFancy) + else + stringResource(id = R.string.part_item, p.id, p.name) + ) + } + } + i++ + } + } else if (filterPartView) { + for (p in parts!!.s) { + Row(horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { editPartID = p }) { + Text( + if (p.type == SDUtils.PartitionType.FREE) + stringResource(id = R.string.free_space_item, p.sizeFancy) + else + stringResource(id = R.string.part_item, p.id, p.name) + ) + } + } + } else if (filterEntryView && entries != null) { + for (e in entries!!.keys) { + Row(horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { editEntryID = e }) { + /* format: + entry["title"] = str + entry["linux"] = path(str) + entry["initrd"] = path(str) + entry["dtb"] = path(str) + entry["options"] = str + entry["xtype"] = str + entry["xpart"] = array (str.split(":")) + entry["xupdate"] = uri(str) + */ + Text( + if (e.has("title")) { + stringResource(R.string.entry_title, e["title"]!!) + } else { + stringResource(R.string.invalid_entry) + } + ) + } + } + val e = ConfigFile() + Row(horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { editEntryID = e }) { + Text(stringResource(R.string.new_entry)) + } + } + if (editPartID != null) { + val p = editPartID!! + AlertDialog( + onDismissRequest = { + editPartID = null + }, + title = { + val name = if (p.type == SDUtils.PartitionType.FREE) + stringResource(R.string.free_space) + else if (p.name.isBlank()) + stringResource(R.string.part_title, p.id) + else + p.name.trim() + + Text(text = "\"${name}\"") + }, + icon = { + Icon(painterResource(id = R.drawable.ic_sd), stringResource(id = R.string.icon_content_desc)) + }, + text = { + Column { + val fancyType = stringResource(when (p.type) { + SDUtils.PartitionType.RESERVED -> R.string.reserved + SDUtils.PartitionType.ADOPTED -> R.string.adoptable_meta + SDUtils.PartitionType.PORTABLE -> R.string.portable_part + SDUtils.PartitionType.UNKNOWN -> R.string.unknown + SDUtils.PartitionType.FREE -> R.string.free_space + SDUtils.PartitionType.SYSTEM -> R.string.os_system + SDUtils.PartitionType.DATA -> R.string.os_userdata + }) + if (p.type != SDUtils.PartitionType.FREE && !filterUnifiedView) + Text(stringResource(id = R.string.detail_id, p.id, p.major, p.minor)) + Text(stringResource(id = R.string.detail_type, fancyType, if (p.type != SDUtils.PartitionType.FREE && !filterUnifiedView) stringResource(id = R.string.detail_type_code, p.code) else "")) + Text(stringResource(id = R.string.detail_size, p.sizeFancy, if (!filterUnifiedView) stringResource(id = R.string.detail_size_sectors, p.size) else "")) + if (!filterUnifiedView) + Text( + stringResource( + id = R.string.detail_position, + p.startSector, + p.endSector + ) + ) + if (p.type != SDUtils.PartitionType.FREE) { + Row { + Button(onClick = { + rename = true + }, Modifier.padding(end = 5.dp)) { + Text(stringResource(R.string.rename)) + } + Button(onClick = { + delete = true + }) { + Text(stringResource(R.string.delete)) + } + } + if (!filterUnifiedView) { + Row { + Button(onClick = { + processing = true + vm.logic!!.mount(p).submit { + processing = false + result = it.out.joinToString("\n") + it.err.joinToString("\n") + } + }, Modifier.padding(end = 5.dp)) { + Text(stringResource(R.string.mount)) + } + Button(onClick = { + processing = true + vm.logic!!.unmount(p).submit { + processing = false + result = it.out.joinToString("\n") + it.err.joinToString("\n") + } + }) { + Text(stringResource(R.string.umount)) + } + } + } + Button(onClick = { vm.startBackupAndRestoreFlow(p) }) { + Text(stringResource(R.string.backupnrestore)) + } + } else { + Button(onClick = { vm.startCreateFlow(p as SDUtils.Partition.FreeSpace) }) { + Text(stringResource(R.string.create)) + } + } + } + }, + confirmButton = { + Button( + onClick = { + editPartID = null + }) { + Text(stringResource(R.string.cancel)) + } + } + ) + if (rename) { + var e by remember { mutableStateOf(false) } + var t by remember { mutableStateOf(p.name) } + AlertDialog( + onDismissRequest = { + rename = false + }, + title = { + Text(stringResource(R.string.rename)) + }, + text = { + TextField(value = t, onValueChange = { + t = it + e = !t.matches(Regex("\\A\\p{ASCII}*\\z")) + }, isError = e, label = { + Text(stringResource(R.string.part_name)) + }) + }, + dismissButton = { + Button(onClick = { rename = false }) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + Button(onClick = { + if (!e) { + processing = true + rename = false + vm.logic!!.rename(p, t).submit { r -> + result = r.out.joinToString("\n") + r.err.joinToString("\n") + parts = SDUtils.generateMeta(vm.deviceInfo!!) + editPartID = parts?.s!!.findLast { it.id == p.id } + processing = false + } + } + }, enabled = !e) { + Text(stringResource(R.string.rename)) + } + } + ) + } else if (delete) { + AlertDialog( + onDismissRequest = { + delete = false + }, + title = { + Text(stringResource(R.string.delete)) + }, + text = { + Text(stringResource(R.string.really_delete_part)) + }, + dismissButton = { + Button(onClick = { delete = false }) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + Button(onClick = { + processing = true + delete = false + val wasMounted = vm.logic!!.mounted + vm.unmountBootset() + vm.logic!!.delete(p).submit { + vm.mountBootset() + if (wasMounted != vm.logic!!.mounted) vm.activity!!.finish() + else { + processing = false + editPartID = null + parts = SDUtils.generateMeta(vm.deviceInfo!!) + result = it.out.joinToString("\n") + it.err.joinToString("\n") + } + } + }) { + Text(stringResource(R.string.delete)) + } + } + ) + } + } + if (editEntryID != null && !filterUnifiedView) { + val ctx = LocalContext.current + val e = editEntryID!! + var f = entries!![e] + var newFileName by remember { mutableStateOf(f?.name ?: "NewEntry.conf") } + val newFileNameErr by remember(newFileName) { derivedStateOf { !newFileName.matches(configFileNameRegex) } } + var titleT by remember { mutableStateOf(e["title"] ?: "") } + val titleE by remember { derivedStateOf { !titleT.matches(asciiRegex) } } + var linuxT by remember { mutableStateOf(e["linux"] ?: "") } + val linuxE by remember { derivedStateOf { !linuxT.matches(asciiRegex) } } + var initrdT by remember { mutableStateOf(e["initrd"] ?: "") } + val initrdE by remember { derivedStateOf { !initrdT.matches(asciiRegex) } } + var dtbT by remember { mutableStateOf(e["dtb"] ?: "") } + val dtbE by remember { derivedStateOf { dtbT.matches(asciiRegex) } } + var optionsT by remember { mutableStateOf(e["options"] ?: "") } + val optionsE by remember { derivedStateOf { !optionsT.matches(asciiRegex) } } + var xtypeT by remember { mutableStateOf(e["xtype"] ?: "") } + val xtypeE by remember { derivedStateOf { !xtypeValidValues.contains(xtypeT) } } + var xpartT by remember { mutableStateOf(e["xpart"] ?: "") } + val xpartE by remember { derivedStateOf { !xpartT.matches(xpartValidValues) } } + var xupdateT by remember { mutableStateOf(e["xupdate"] ?: "") } + val isOk = !(newFileNameErr || titleE || linuxE || initrdE || dtbE || optionsE || xtypeE || xpartE) + AlertDialog( + onDismissRequest = { + editEntryID = null + }, + title = { + Text(text = if (e.has("title")) "\"${e["title"]}\"" else if (f != null) stringResource(id = R.string.invalid_entry2) else stringResource(id = R.string.new_entry2)) + }, + icon = { + Icon(painterResource(id = R.drawable.ic_roms), stringResource(R.string.icon_content_desc)) + }, + text = { + Column(Modifier.verticalScroll(rememberScrollState())) { + TextField(value = newFileName, onValueChange = { + if (f != null) return@TextField + newFileName = it + }, isError = newFileNameErr, enabled = f == null, label = { + Text(stringResource(R.string.file_name)) + }) + + TextField(value = titleT, onValueChange = { + titleT = it + }, isError = titleE, label = { + Text(stringResource(R.string.title)) + }) + + TextField(value = linuxT, onValueChange = { + linuxT = it + }, isError = linuxE, label = { + Text(stringResource(R.string.linux)) + }) + + TextField(value = initrdT, onValueChange = { + initrdT = it + }, isError = initrdE, label = { + Text(stringResource(R.string.initrd)) + }) + + TextField(value = dtbT, onValueChange = { + dtbT = it + }, isError = dtbE, label = { + Text(stringResource(R.string.dtb)) + }) + + TextField(value = optionsT, onValueChange = { + optionsT = it + }, isError = optionsE, label = { + Text(stringResource(R.string.options)) + }) + + TextField(value = xtypeT, onValueChange = { + xtypeT = it + }, isError = xtypeE, label = { + Text(stringResource(R.string.rom_type)) + }) + + TextField(value = xpartT, onValueChange = { + xpartT = it + }, isError = xpartE, label = { + Text(stringResource(R.string.assigned_parts)) + }) + + TextField(value = xupdateT, onValueChange = { + xupdateT = it + }, label = { + Text(stringResource(R.string.updater_url)) + }) + } + }, + confirmButton = { + if (f != null && e["xpart"] != "real") { + Button( + onClick = { + f!!.delete() + entries!!.remove(e) + editEntryID = null + }) { + Text(stringResource(R.string.delete)) + } + } + Button( + onClick = { + if (!isOk) return@Button + if (f == null) { + f = SuFile.open(vm.logic!!.abmEntries, newFileName) + if (f!!.exists()) { + Toast.makeText( + ctx, + vm.activity!!.getString(R.string.file_already_exists), + Toast.LENGTH_LONG + ).show() + f = null + return@Button + } + } + entries!![e] = f!! + e["title"] = titleT + e["linux"] = linuxT + e["initrd"] = initrdT + e["dtb"] = dtbT + e["options"] = optionsT + e["xtype"] = xtypeT + e["xpart"] = xpartT + e["xupdate"] = xupdateT + e.exportToFile(f!!) + editEntryID = null + }, enabled = isOk + ) { + Text(stringResource(if (f != null) R.string.update else R.string.create)) + } + Button( + onClick = { + editEntryID = null + }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } else if (editEntryID != null) { + val e = editEntryID!! + AlertDialog( + onDismissRequest = { + editEntryID = null + }, + title = { + Text(text = if (e.has("title")) "\"${e["title"]}\"" else stringResource(R.string.invalid_entry2)) + }, + icon = { + Icon(painterResource(id = R.drawable.ic_roms), stringResource(id = R.string.icon_content_desc)) + }, + text = { + Column(Modifier.verticalScroll(rememberScrollState())) { + Button( + onClick = { + if (e.has("xupdate") && !e["xupdate"].isNullOrBlank()) + vm.startUpdateFlow(entries!![e]!!.absolutePath) + }, enabled = e.has("xupdate") && !e["xupdate"].isNullOrBlank()) { + Text(stringResource(R.string.update)) + } + Button( + onClick = { + delete = true + }) { + Text(stringResource(R.string.delete)) + } + } + }, + confirmButton = { + Button( + onClick = { + editEntryID = null + }) { + Text(stringResource(R.string.cancel)) + } + } + ) + + if (delete) { + AlertDialog( + onDismissRequest = { + delete = false + }, + title = { + Text(stringResource(R.string.delete)) + }, + text = { + Text(stringResource(R.string.really_delete_os)) + }, + dismissButton = { + Button(onClick = { delete = false }) { + Text(stringResource(id = R.string.cancel)) + } + }, + confirmButton = { + Button(onClick = { + processing = true + delete = false + CoroutineScope(Dispatchers.Default).launch { + var tresult = "" + if (e.has("xpart") && !e["xpart"].isNullOrBlank()) { + val allp = e["xpart"]!!.split(":") + .map { parts!!.dumpKernelPartition(Integer.valueOf(it)) } + vm.unmountBootset() + for (p in allp) { // Do not chain, but regenerate meta and unmount every time. Thanks vold + val r = vm.logic!!.delete(p).exec() + parts = SDUtils.generateMeta(vm.deviceInfo!!) + tresult += r.out.joinToString("\n") + r.err.joinToString("\n") + "\n" + } + vm.mountBootset() + } + val f = entries!![e]!! + val f2 = SuFile(vm.logic!!.abmBootset, f.nameWithoutExtension) + if (!f2.deleteRecursive()) + tresult += vm.activity!!.getString(R.string.cannot_delete, f2.absolutePath) + entries!!.remove(e) + if (!f.delete()) + tresult += vm.activity!!.getString(R.string.cannot_delete, f.absolutePath) + editEntryID = null + processing = false + parts = SDUtils.generateMeta(vm.deviceInfo!!) + result = tresult + } + }) { + Text(stringResource(R.string.delete)) + } + } + ) + } + } + if (processing) { + AlertDialog( + onDismissRequest = {}, + title = { + Text(stringResource(R.string.please_wait)) + }, + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + CircularProgressIndicator(Modifier.padding(end = 20.dp)) + Text(stringResource(R.string.loading)) + } + }, + confirmButton = {} + ) + } else if (result != null) { + AlertDialog( + onDismissRequest = { + result = null + }, + title = { + Text(stringResource(R.string.done)) + }, + text = { + result?.let { + Text(it) + } + }, + confirmButton = { + Button(onClick = { result = null }) { + Text(stringResource(id = R.string.ok)) + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt index 041f8688..2c800495 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt @@ -3,17 +3,15 @@ package org.andbootmgr.app import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview 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.AbmTheme import org.andbootmgr.app.util.Terminal import java.io.File import java.io.IOException @@ -101,18 +99,4 @@ private fun Flash(vm: WizardActivityState) { } } } -} - -@Composable -@Preview -private fun Preview() { - val vm = WizardActivityState("null") - AbmTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - SelectDroidBoot(vm) - } - } } \ 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 0eaa9412..dc3a8e3d 100644 --- a/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt +++ b/app/src/main/java/org/andbootmgr/app/UpdateFlow.kt @@ -1,6 +1,5 @@ package org.andbootmgr.app -import android.content.Intent import android.net.Uri import android.util.Log import androidx.compose.foundation.layout.Arrangement @@ -8,7 +7,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.material3.Button import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.io.SuFile @@ -36,17 +40,13 @@ class UpdateFlowWizardPageFactory(private val vm: WizardActivityState) { val noobMode = c.vm.activity.getSharedPreferences("abm", 0).getBoolean("noob_mode", BuildConfig.DEFAULT_NOOB_MODE) return listOf(WizardPage("start", NavButton(vm.activity.getString(R.string.cancel)) { - it.startActivity(Intent(it, MainActivity::class.java)) it.finish() }, if (noobMode) NavButton("") {} else NavButton(vm.activity.getString(R.string.local_update)) { vm.navigate("local") } ) { Start(c) }, WizardPage("local", - NavButton(vm.activity.getString(R.string.cancel)) { - it.startActivity(Intent(it, MainActivity::class.java)) - it.finish() - }, + NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, NavButton(vm.activity.getString(R.string.online_update)) { vm.navigate("start") } ) { Local(c) @@ -372,7 +372,6 @@ private fun Flash(u: UpdateFlowDataHolder) { } u.vm.nextText.value = u.vm.activity.getString(R.string.finish) u.vm.onNext.value = { - it.startActivity(Intent(it, MainActivity::class.java)) it.finish() } } diff --git a/app/src/main/java/org/andbootmgr/app/WizardActivity.kt b/app/src/main/java/org/andbootmgr/app/WizardActivity.kt index b455833f..23a8d6c8 100644 --- a/app/src/main/java/org/andbootmgr/app/WizardActivity.kt +++ b/app/src/main/java/org/andbootmgr/app/WizardActivity.kt @@ -1,26 +1,19 @@ package org.andbootmgr.app -import android.annotation.SuppressLint import android.net.Uri -import android.os.Bundle -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.layout.Arrangement +import android.view.WindowManager 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.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.core.net.toFile import androidx.navigation.NavHostController @@ -28,10 +21,6 @@ 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 org.andbootmgr.app.util.AbmTheme import java.io.File import java.io.FileInputStream import java.io.IOException @@ -57,137 +46,38 @@ class WizardPageFactory(private val vm: WizardActivityState) { } } -class WizardActivity : ComponentActivity() { - private lateinit var vm: WizardActivityState - private lateinit var newFile: ActivityResultLauncher - private var onFileCreated: ((Uri) -> Unit)? = null - private lateinit var chooseFile: ActivityResultLauncher - private var onFileChosen: ((Uri) -> Unit)? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - vm = WizardActivityState(intent.getStringExtra("codename")!!) - vm.activity = 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 - } - if (onFileCreated != null) { - onFileCreated!!(uri) - onFileCreated = null - } else { - Toast.makeText( - this@WizardActivity, - getString(R.string.internal_file_error1), - Toast.LENGTH_LONG - ).show() - } - } - 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 - ) { - Column( - verticalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - 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() - } - } - } - Box(Modifier.fillMaxWidth()) { - BtnsRow(vm) - } - } +@Composable +fun WizardCompat(mvm: MainActivityState, flow: String) { + DisposableEffect(Unit) { + mvm.activity!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + onDispose { mvm.activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } + } + val vm = remember { WizardActivityState(mvm) } + vm.navController = rememberNavController() + val wizardPages = remember(flow) { WizardPageFactory(vm).get(flow) } + Column(modifier = Modifier.fillMaxSize()) { + 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() } } } - } - - @SuppressLint("MissingSuperCall") - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - vm.onPrev.value(this) - } - - fun navigate(next: String) { - vm.navigate(next) - } - fun chooseFile(mime: String, callback: (Uri) -> Unit) { - if (onFileChosen != null) { - Toast.makeText( - this, - getString(R.string.internal_file_error2), - Toast.LENGTH_LONG - ).show() - return - } - onFileChosen = callback - chooseFile.launch(mime) - } - fun createFile(name: String, callback: (Uri) -> Unit) { - if (onFileCreated != null) { - Toast.makeText( - this, - getString(R.string.internal_file_error2), - Toast.LENGTH_LONG - ).show() - return + Box(Modifier.fillMaxWidth()) { + BtnsRow(vm) } - onFileCreated = callback - newFile.launch(name) } } @@ -204,25 +94,32 @@ private class ExpectedDigestInputStream(stream: InputStream?, } class HashMismatchException(message: String) : Exception(message) -class WizardActivityState(val codename: String) { +class WizardActivityState(val mvm: MainActivityState) { + val codename = mvm.deviceInfo!!.codename var btnsOverride = false + val activity = mvm.activity!! lateinit var navController: NavHostController - lateinit var activity: WizardActivity - lateinit var logic: DeviceLogic - lateinit var deviceInfo: DeviceInfo - private var current = mutableStateOf("start") + val logic = mvm.logic!! + val deviceInfo = mvm.deviceInfo!! var prevText = mutableStateOf("") var nextText = mutableStateOf("") - var onPrev: MutableState<(WizardActivity) -> Unit> = mutableStateOf({}) - var onNext: MutableState<(WizardActivity) -> Unit> = mutableStateOf({}) + var onPrev: MutableState<(WizardActivityState) -> Unit> = mutableStateOf({}) + var onNext: MutableState<(WizardActivityState) -> Unit> = mutableStateOf({}) var flashes: HashMap> = HashMap() var texts: HashMap = HashMap() fun navigate(next: String) { btnsOverride = false - current.value = next - navController.navigate(current.value) + navController.navigate(next) { + launchSingleTop = true + } + } + fun finish() { + mvm.wizardCompat = null + mvm.wizardCompatE = null + mvm.wizardCompatPid = null + mvm.wizardCompatSid = null } fun copy(inputStream: InputStream, outputStream: OutputStream): Long { @@ -270,7 +167,7 @@ class WizardActivityState(val codename: String) { } } -class NavButton(val text: String, val onClick: (WizardActivity) -> Unit) +class NavButton(val text: String, val onClick: (WizardActivityState) -> Unit) class WizardPage(override val name: String, override val prev: NavButton, override val next: NavButton, override val run: @Composable () -> Unit ) : IWizardPage @@ -286,12 +183,12 @@ interface IWizardPage { private fun BtnsRow(vm: WizardActivityState) { Row { TextButton(onClick = { - vm.onPrev.value(vm.activity) + vm.onPrev.value(vm) }, modifier = Modifier.weight(1f, true)) { Text(vm.prevText.value) } TextButton(onClick = { - vm.onNext.value(vm.activity) + vm.onNext.value(vm) }, modifier = Modifier.weight(1f, true)) { Text(vm.nextText.value) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34cdccd9..62faca79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,8 +74,6 @@ Please choose an installer shell script, or use the download button to automatically download the latest version from the internet (recommended for most users). This will reinstall DroidBoot File not available, please try again later! - Internal error - no file handler added!! Please cancel install - Internal error - double file choose!! Please cancel install Local update Online update Failed to check for updates! Please try again later. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1a67fae5..39ac7b54 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,6 +19,9 @@ true @color/red + #FE9200 #FE9200 diff --git a/build.gradle.kts b/build.gradle.kts index b9d4444e..10a0edae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,5 @@ plugins { val kotlinVersion = "2.0.0" id("org.jetbrains.kotlin.android") version kotlinVersion apply false id("org.jetbrains.kotlin.plugin.compose") version kotlinVersion apply false - id("org.jetbrains.kotlinx.atomicfu") version "0.25.0" apply false id("com.mikepenz.aboutlibraries.plugin") version "11.2.1" apply false } \ No newline at end of file