diff --git a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index 2eb6508a6..33fa0983e 100644 --- a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -111,6 +111,13 @@ class FlashViewModel : BaseViewModel() { MagiskInstaller.Patch(uri, outItems, logItems).exec() }) } + Const.Value.DOWNLOAD -> { + uri ?: return@launch + _showReboot.value = false + onResult(withContext(Dispatchers.IO) { + MagiskInstaller.Download(uri.toString(), outItems, logItems).exec() + }) + } } } } 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 2ed5bdfc1..58bcf62bd 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,6 +1,7 @@ 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 @@ -21,6 +22,8 @@ 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 @@ -67,9 +70,12 @@ 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 @@ -111,6 +117,7 @@ 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 -> @@ -138,6 +145,13 @@ fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { } } + LaunchedEffect(installUiState.showDownloadDialog) { + if (installUiState.showDownloadDialog) { + showDownloadDialog.value = true + installVm.onDownloadDialogConsumed() + } + } + LaunchedEffect(uiState.showUninstall) { if (uiState.showUninstall) { showUninstallDialog.value = true @@ -218,6 +232,13 @@ fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) { ) } + if (showDownloadDialog.value) { + DownloadComposableDialog( + showDialog = showDownloadDialog, + onConfirm = { url -> installVm.onDownloadUrlSelected(url) } + ) + } + Scaffold( topBar = { TopAppBar( @@ -788,7 +809,14 @@ private fun InstallBottomSheet( show.value = false installVm.selectMethod(InstallViewModel.Method.PATCH) }, - // enabled = installUiState.step >= 1 || installVm.skipOptions + ) + + SettingsArrow( + title = stringResource(CoreR.string.download_patch_file), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.DOWNLOAD) + }, ) if (installVm.isRooted) { @@ -800,7 +828,6 @@ private fun InstallBottomSheet( installVm.selectMethod(InstallViewModel.Method.DIRECT) installVm.install() }, - // enabled = installUiState.step >= 1 || installVm.skipOptions ) } @@ -812,7 +839,6 @@ private fun InstallBottomSheet( show.value = false installVm.selectMethod(InstallViewModel.Method.INACTIVE_SLOT) }, - // enabled = installUiState.step >= 1 || installVm.skipOptions ) } } @@ -891,6 +917,87 @@ private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boole } } +@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/InstallViewModel.kt b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt index d1aac18fa..0605c90ef 100644 --- a/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt +++ b/app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt @@ -25,7 +25,7 @@ import com.topjohnwu.magisk.core.R as CoreR class InstallViewModel(svc: NetworkService) : BaseViewModel() { - enum class Method { NONE, PATCH, DIRECT, INACTIVE_SLOT } + enum class Method { NONE, PATCH, DIRECT, INACTIVE_SLOT, DOWNLOAD } data class UiState( val step: Int = 0, @@ -34,6 +34,7 @@ class InstallViewModel(svc: NetworkService) : BaseViewModel() { val patchUri: Uri? = null, val requestFilePicker: Boolean = false, val showSecondSlotWarning: Boolean = false, + val showDownloadDialog: Boolean = false, ) val isRooted get() = Info.isRooted @@ -79,6 +80,9 @@ class InstallViewModel(svc: NetworkService) : BaseViewModel() { Method.INACTIVE_SLOT -> { _uiState.update { it.copy(showSecondSlotWarning = true) } } + Method.DOWNLOAD -> { + _uiState.update { it.copy(showDownloadDialog = true) } + } else -> {} } } @@ -91,6 +95,10 @@ class InstallViewModel(svc: NetworkService) : BaseViewModel() { _uiState.update { it.copy(showSecondSlotWarning = false) } } + fun onDownloadDialogConsumed() { + _uiState.update { it.copy(showDownloadDialog = false) } + } + fun onPatchFileSelected(uri: Uri) { _uiState.update { it.copy(patchUri = uri) } if (_uiState.value.method == Method.PATCH) { @@ -98,12 +106,23 @@ class InstallViewModel(svc: NetworkService) : BaseViewModel() { } } + fun onDownloadUrlSelected(uri: Uri) { + _uiState.update { it.copy(patchUri = uri) } + if (_uiState.value.method == Method.DOWNLOAD) { + install() + } + } + fun install() { when (_uiState.value.method) { Method.PATCH -> navigateTo(Route.Flash( action = Const.Value.PATCH_FILE, additionalData = _uiState.value.patchUri!!.toString() )) + Method.DOWNLOAD -> navigateTo(Route.Flash( + action = Const.Value.DOWNLOAD, + additionalData = _uiState.value.patchUri!!.toString() + )) Method.DIRECT -> navigateTo(Route.Flash( action = Const.Value.FLASH_MAGISK ))