diff --git a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt index 58bcf62bd..18d5247da 100644 --- a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt +++ b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt @@ -1,12 +1,9 @@ package com.topjohnwu.magisk.ui.home import android.content.Intent -import android.net.Uri import android.os.Build import android.os.PowerManager import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,8 +19,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -36,7 +31,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -45,7 +39,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -70,12 +63,9 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.getSystemService -import androidx.core.net.toUri import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.BuildConfig import com.topjohnwu.magisk.core.Config @@ -88,14 +78,11 @@ import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.tasks.AppMigration import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.ui.MainActivity -import com.topjohnwu.magisk.ui.component.ConfirmResult import com.topjohnwu.magisk.ui.component.LoadingDialogHandle -import com.topjohnwu.magisk.ui.component.MarkdownText import com.topjohnwu.magisk.ui.component.MarkdownTextAsync -import com.topjohnwu.magisk.ui.component.SettingsArrow -import com.topjohnwu.magisk.ui.component.rememberConfirmDialog import com.topjohnwu.magisk.ui.component.rememberLoadingDialog import com.topjohnwu.magisk.ui.flash.FlashUtils +import com.topjohnwu.magisk.ui.install.InstallBottomSheet import com.topjohnwu.magisk.ui.install.InstallViewModel import kotlinx.coroutines.launch import com.topjohnwu.magisk.core.R as CoreR @@ -104,7 +91,6 @@ import com.topjohnwu.magisk.core.R as CoreR @Composable fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { val uiState by viewModel.uiState.collectAsState() - val installUiState by installVm.uiState.collectAsState() val context = LocalContext.current val activity = context as MainActivity val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() @@ -117,41 +103,8 @@ fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { var showHideDialog by rememberSaveable { mutableStateOf(false) } var showRestoreDialog by rememberSaveable { mutableStateOf(false) } val showInstallSheet = rememberSaveable { mutableStateOf(false) } - val showDownloadDialog = rememberSaveable { mutableStateOf(false) } var envFixCode by remember { mutableIntStateOf(0) } - val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { installVm.onPatchFileSelected(it) } - } - - val secondSlotDialog = rememberConfirmDialog() - val secondSlotTitle = stringResource(android.R.string.dialog_alert_title) - val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg) - - LaunchedEffect(installUiState.requestFilePicker) { - if (installUiState.requestFilePicker) { - filePicker.launch("*/*") - installVm.onFilePickerConsumed() - } - } - - LaunchedEffect(installUiState.showSecondSlotWarning) { - if (installUiState.showSecondSlotWarning) { - val result = secondSlotDialog.awaitConfirm(title = secondSlotTitle, content = secondSlotMsg) - installVm.onSecondSlotWarningConsumed() - if (result == ConfirmResult.Confirmed) { - installVm.install() - } - } - } - - LaunchedEffect(installUiState.showDownloadDialog) { - if (installUiState.showDownloadDialog) { - showDownloadDialog.value = true - installVm.onDownloadDialogConsumed() - } - } - LaunchedEffect(uiState.showUninstall) { if (uiState.showUninstall) { showUninstallDialog.value = true @@ -232,13 +185,6 @@ fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { ) } - if (showDownloadDialog.value) { - DownloadComposableDialog( - showDialog = showDownloadDialog, - onConfirm = { url -> installVm.onDownloadUrlSelected(url) } - ) - } - Scaffold( topBar = { TopAppBar( @@ -310,7 +256,6 @@ fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { InstallBottomSheet( show = showInstallSheet, installVm = installVm, - installUiState = installUiState, ) } @@ -774,230 +719,6 @@ private fun DevelopersCard(onLinkClicked: (String) -> Unit) { } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun InstallBottomSheet( - show: MutableState, - installVm: InstallViewModel, - installUiState: InstallViewModel.UiState, -) { - if (show.value) { - ModalBottomSheet( - onDismissRequest = { show.value = false }, - ) { - Text( - text = stringResource(CoreR.string.install), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(16.dp) - ) - Column(modifier = Modifier.padding(bottom = 16.dp)) { - if (installUiState.notes.isNotEmpty()) { - Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - MarkdownText(installUiState.notes) - } - HorizontalDivider(thickness = 0.75.dp) - } - - if (!installVm.skipOptions) { - InstallOptionsSection(installUiState, installVm) - } - - SettingsArrow( - title = stringResource(CoreR.string.select_patch_file), - summary = stringResource(CoreR.string.select_patch_file_summary), - onClick = { - show.value = false - installVm.selectMethod(InstallViewModel.Method.PATCH) - }, - ) - - SettingsArrow( - title = stringResource(CoreR.string.download_patch_file), - onClick = { - show.value = false - installVm.selectMethod(InstallViewModel.Method.DOWNLOAD) - }, - ) - - if (installVm.isRooted) { - SettingsArrow( - title = stringResource(CoreR.string.direct_install), - summary = stringResource(CoreR.string.direct_install_summary), - onClick = { - show.value = false - installVm.selectMethod(InstallViewModel.Method.DIRECT) - installVm.install() - }, - ) - } - - if (!installVm.noSecondSlot) { - SettingsArrow( - title = stringResource(CoreR.string.install_inactive_slot), - summary = stringResource(CoreR.string.install_inactive_slot_summary), - onClick = { - show.value = false - installVm.selectMethod(InstallViewModel.Method.INACTIVE_SLOT) - }, - ) - } - } - } - } -} - -@Composable -private fun InstallOptionsSection( - uiState: InstallViewModel.UiState, - viewModel: InstallViewModel -) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(CoreR.string.install_options_title), - style = MaterialTheme.typography.titleLarge, - ) - if (uiState.step == 0) { - TextButton( - onClick = { viewModel.nextStep() } - ) { - Text(stringResource(CoreR.string.install_next)) - } - } - } - - if (uiState.step == 0) { - Spacer(Modifier.height(8.dp)) - if (!Info.isSAR) { - CheckboxRow( - label = stringResource(CoreR.string.keep_dm_verity), - checked = Config.keepVerity, - onCheckedChange = { Config.keepVerity = it } - ) - } - if (Info.isFDE) { - CheckboxRow( - label = stringResource(CoreR.string.keep_force_encryption), - checked = Config.keepEnc, - onCheckedChange = { Config.keepEnc = it } - ) - } - if (!Info.ramdisk) { - CheckboxRow( - label = stringResource(CoreR.string.recovery_mode), - checked = Config.recovery, - onCheckedChange = { Config.recovery = it } - ) - } - } - } -} - -@Composable -private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Checkbox( - checked = checked, - onCheckedChange = { onCheckedChange(it) } - ) - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - ) - } -} - -@Composable -private fun DownloadComposableDialog( - showDialog: MutableState, - onConfirm: (Uri) -> Unit -) { - if (!showDialog.value) return - - var url by rememberSaveable { mutableStateOf("") } - var isError by rememberSaveable { mutableStateOf(false) } - - fun isValidUrl(url: String): Uri? { - if (url.isEmpty()) return null - val uri = url.toUri() - if (!uri.scheme.equals("https", ignoreCase = true)) return null - if (uri.host.isNullOrEmpty()) return null - if (uri.path.isNullOrEmpty()) return null - return uri - } - - AlertDialog( - onDismissRequest = { showDialog.value = false }, - title = { Text(stringResource(CoreR.string.download_dialog_title)) }, - text = { - Column(modifier = Modifier.padding(top = 8.dp)) { - OutlinedTextField( - value = url, - onValueChange = { - url = it - isError = false - }, - modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(CoreR.string.download_dialog_msg)) }, - isError = isError, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - isValidUrl(url.trim())?.let { - showDialog.value = false - onConfirm(it) - } ?: run { - isError = true - } - } - ) - ) - if (isError) { - Text( - text = stringResource(CoreR.string.download_dialog_title), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 16.dp, top = 4.dp) - ) - } - } - }, - confirmButton = { - TextButton( - onClick = { - isValidUrl(url.trim())?.let { - showDialog.value = false - onConfirm(it) - } ?: run { - isError = true - } - } - ) { - Text(stringResource(android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = { showDialog.value = false }) { - Text(stringResource(android.R.string.cancel)) - } - } - ) -} - @Composable private fun UninstallComposableDialog( showDialog: MutableState, diff --git a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallBottomSheet.kt b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallBottomSheet.kt new file mode 100644 index 000000000..2acf460bc --- /dev/null +++ b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallBottomSheet.kt @@ -0,0 +1,310 @@ +package com.topjohnwu.magisk.ui.install + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import com.topjohnwu.magisk.core.Config +import com.topjohnwu.magisk.core.Info +import com.topjohnwu.magisk.ui.component.ConfirmResult +import com.topjohnwu.magisk.ui.component.MarkdownText +import com.topjohnwu.magisk.ui.component.SettingsArrow +import com.topjohnwu.magisk.ui.component.rememberConfirmDialog +import com.topjohnwu.magisk.core.R as CoreR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InstallBottomSheet( + show: MutableState, + installVm: InstallViewModel, +) { + val installUiState by installVm.uiState.collectAsState() + val showDownloadDialog = rememberSaveable { mutableStateOf(false) } + val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { installVm.onPatchFileSelected(it) } + } + + val secondSlotDialog = rememberConfirmDialog() + val secondSlotTitle = stringResource(android.R.string.dialog_alert_title) + val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg) + + LaunchedEffect(installUiState.requestFilePicker) { + if (installUiState.requestFilePicker) { + filePicker.launch("*/*") + installVm.onFilePickerConsumed() + } + } + + LaunchedEffect(installUiState.showSecondSlotWarning) { + if (installUiState.showSecondSlotWarning) { + val result = secondSlotDialog.awaitConfirm(title = secondSlotTitle, content = secondSlotMsg) + installVm.onSecondSlotWarningConsumed() + if (result == ConfirmResult.Confirmed) { + installVm.install() + } + } + } + + LaunchedEffect(installUiState.showDownloadDialog) { + if (installUiState.showDownloadDialog) { + showDownloadDialog.value = true + installVm.onDownloadDialogConsumed() + } + } + + if (showDownloadDialog.value) { + DownloadComposableDialog( + showDialog = showDownloadDialog, + onConfirm = { url -> installVm.onDownloadUrlSelected(url) } + ) + } + + if (show.value) { + ModalBottomSheet( + onDismissRequest = { show.value = false }, + ) { + Text( + text = stringResource(CoreR.string.install), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(16.dp) + ) + Column(modifier = Modifier.padding(bottom = 16.dp)) { + if (installUiState.notes.isNotEmpty()) { + Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + MarkdownText(installUiState.notes) + } + HorizontalDivider(thickness = 0.75.dp) + } + + if (!installVm.skipOptions) { + InstallOptionsSection(installUiState, installVm) + } + + SettingsArrow( + title = stringResource(CoreR.string.select_patch_file), + summary = stringResource(CoreR.string.select_patch_file_summary), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.PATCH) + }, + ) + + SettingsArrow( + title = stringResource(CoreR.string.download_patch_file), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.DOWNLOAD) + }, + ) + + if (installVm.isRooted) { + SettingsArrow( + title = stringResource(CoreR.string.direct_install), + summary = stringResource(CoreR.string.direct_install_summary), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.DIRECT) + installVm.install() + }, + ) + } + + if (!installVm.noSecondSlot) { + SettingsArrow( + title = stringResource(CoreR.string.install_inactive_slot), + summary = stringResource(CoreR.string.install_inactive_slot_summary), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.INACTIVE_SLOT) + }, + ) + } + } + } + } +} + +@Composable +private fun InstallOptionsSection( + uiState: InstallViewModel.UiState, + viewModel: InstallViewModel +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(CoreR.string.install_options_title), + style = MaterialTheme.typography.titleLarge, + ) + if (uiState.step == 0) { + TextButton( + onClick = { viewModel.nextStep() } + ) { + Text(stringResource(CoreR.string.install_next)) + } + } + } + + if (uiState.step == 0) { + Spacer(Modifier.height(8.dp)) + if (!Info.isSAR) { + CheckboxRow( + label = stringResource(CoreR.string.keep_dm_verity), + checked = Config.keepVerity, + onCheckedChange = { Config.keepVerity = it } + ) + } + if (Info.isFDE) { + CheckboxRow( + label = stringResource(CoreR.string.keep_force_encryption), + checked = Config.keepEnc, + onCheckedChange = { Config.keepEnc = it } + ) + } + if (!Info.ramdisk) { + CheckboxRow( + label = stringResource(CoreR.string.recovery_mode), + checked = Config.recovery, + onCheckedChange = { Config.recovery = it } + ) + } + } + } +} + +@Composable +private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Checkbox( + checked = checked, + onCheckedChange = { onCheckedChange(it) } + ) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + ) + } +} + +@Composable +fun DownloadComposableDialog( + showDialog: MutableState, + onConfirm: (Uri) -> Unit +) { + if (!showDialog.value) return + + var url by rememberSaveable { mutableStateOf("") } + var isError by rememberSaveable { mutableStateOf(false) } + + fun isValidUrl(url: String): Uri? { + if (url.isEmpty()) return null + val uri = url.toUri() + if (!uri.scheme.equals("https", ignoreCase = true)) return null + if (uri.host.isNullOrEmpty()) return null + if (uri.path.isNullOrEmpty()) return null + return uri + } + + AlertDialog( + onDismissRequest = { showDialog.value = false }, + title = { Text(stringResource(CoreR.string.download_dialog_title)) }, + text = { + Column(modifier = Modifier.padding(top = 8.dp)) { + OutlinedTextField( + value = url, + onValueChange = { + url = it + isError = false + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(CoreR.string.download_dialog_msg)) }, + isError = isError, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + isValidUrl(url.trim())?.let { + showDialog.value = false + onConfirm(it) + } ?: run { + isError = true + } + } + ) + ) + if (isError) { + Text( + text = stringResource(CoreR.string.download_dialog_title), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { + isValidUrl(url.trim())?.let { + showDialog.value = false + onConfirm(it) + } ?: run { + isError = true + } + } + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = { showDialog.value = false }) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +}