diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt index c712c8242..79bcfcab8 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt @@ -12,11 +12,16 @@ abstract class AsyncLoadViewModel : BaseViewModel() { @MainThread fun startLoading() { if (loadingJob?.isActive == true) { - // Prevent multiple jobs from running at the same time return } loadingJob = viewModelScope.launch { doLoadWork() } } + @MainThread + fun reload() { + loadingJob?.cancel() + loadingJob = viewModelScope.launch { doLoadWork() } + } + protected abstract suspend fun doLoadWork() } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt index d65cccad7..e803ebfe3 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -45,8 +45,6 @@ import com.topjohnwu.magisk.ui.deny.DenyListViewModel import com.topjohnwu.magisk.ui.flash.FlashScreen import com.topjohnwu.magisk.ui.flash.FlashUtils import com.topjohnwu.magisk.ui.flash.FlashViewModel -import com.topjohnwu.magisk.ui.install.InstallScreen -import com.topjohnwu.magisk.ui.install.InstallViewModel import com.topjohnwu.magisk.ui.module.ActionScreen import com.topjohnwu.magisk.ui.module.ActionViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserDetailScreen @@ -171,11 +169,6 @@ class MainActivity : AppCompatActivity(), SplashScreenHost { entry { MainScreen(initialTab = initialTab) } - entry { _ -> - val vm: InstallViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory) - CollectNavEvents(vm, navigator) - InstallScreen(vm, onBack = { navigator.pop() }) - } entry { _ -> val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory) LaunchedEffect(Unit) { vm.startLoading() } @@ -271,7 +264,9 @@ class MainActivity : AppCompatActivity(), SplashScreenHost { showInvalidState.value = true } else { lifecycleScope.launch { - AppMigration.restore(this@MainActivity) + if (!AppMigration.restoreApp(this@MainActivity)) { + toast(CoreR.string.failure, Toast.LENGTH_LONG) + } } } } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt index 6ac604556..58c32b0f7 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt @@ -46,6 +46,7 @@ import com.topjohnwu.magisk.core.base.SplashScreenHost import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.ui.home.HomeScreen import com.topjohnwu.magisk.ui.home.HomeViewModel +import com.topjohnwu.magisk.ui.install.InstallViewModel import com.topjohnwu.magisk.ui.log.LogScreen import com.topjohnwu.magisk.ui.log.LogViewModel import com.topjohnwu.magisk.ui.module.ModuleScreen @@ -85,9 +86,11 @@ fun MainScreen(initialTab: Int = Tab.HOME.ordinal) { when (Tab.entries[page]) { Tab.HOME -> { val vm: HomeViewModel = viewModel(factory = VMFactory) + val installVm: InstallViewModel = viewModel(factory = VMFactory) LaunchedEffect(Unit) { vm.startLoading() } CollectNavEvents(vm, navigator) - HomeScreen(vm) + CollectNavEvents(installVm, navigator) + HomeScreen(vm, installVm) } Tab.SUPERUSER -> { val activity = LocalContext.current as MainActivity diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt index d3d27b062..6096538b9 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt @@ -1,7 +1,6 @@ package com.topjohnwu.magisk.ui.flash import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,20 +12,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import top.yukonga.miuix.kmp.basic.FloatingActionButton +import com.topjohnwu.magisk.R import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.SmallTopAppBar import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.theme.MiuixTheme @@ -38,6 +36,7 @@ fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) { val showReboot by viewModel.showReboot.collectAsState() val items = viewModel.consoleItems val listState = rememberLazyListState() + val finished = flashState != FlashViewModel.State.FLASHING LaunchedEffect(items.size) { if (items.isNotEmpty()) { @@ -68,54 +67,55 @@ fun FlashScreen(viewModel: FlashViewModel, onBack: () -> Unit) { ) } }, + actions = { + if (finished) { + IconButton( + modifier = Modifier.padding(end = 4.dp), + onClick = { viewModel.saveLog() } + ) { + Icon( + painter = painterResource(R.drawable.ic_save_md2), + contentDescription = stringResource(CoreR.string.menuSaveLog), + tint = MiuixTheme.colorScheme.onBackground + ) + } + } + if (flashState == FlashViewModel.State.SUCCESS && showReboot) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { viewModel.restartPressed() } + ) { + Icon( + painter = painterResource(R.drawable.ic_restart), + contentDescription = stringResource(CoreR.string.reboot), + tint = MiuixTheme.colorScheme.onBackground + ) + } + } + }, scrollBehavior = scrollBehavior ) }, popupHost = { } ) { padding -> - Box(modifier = Modifier.fillMaxSize().padding(padding)) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - itemsIndexed(items) { _, line -> - Text( - text = line, - fontFamily = FontFamily.Monospace, - fontSize = 12.sp, - lineHeight = 16.sp, - color = MiuixTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth() - ) - } - } - - if (flashState != FlashViewModel.State.FLASHING) { - TextButton( - text = stringResource(CoreR.string.menuSaveLog), - onClick = { viewModel.saveLog() }, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(16.dp) + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + itemsIndexed(items) { _, line -> + Text( + text = line, + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + lineHeight = 16.sp, + color = MiuixTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) } - - if (flashState == FlashViewModel.State.SUCCESS && showReboot) { - FloatingActionButton( - onClick = { viewModel.restartPressed() }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp) - ) { - Text( - text = stringResource(CoreR.string.reboot), - modifier = Modifier.padding(horizontal = 16.dp) - ) - } - } } } } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt index 682c95a3f..fded924c5 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt @@ -5,6 +5,8 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.PowerManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -65,10 +67,13 @@ import com.topjohnwu.magisk.ui.component.rememberConfirmDialog import com.topjohnwu.magisk.ui.component.rememberLoadingDialog import com.topjohnwu.magisk.ui.component.ListPopupDefaults.MenuPositionProvider import com.topjohnwu.magisk.ui.flash.FlashUtils +import com.topjohnwu.magisk.ui.install.InstallViewModel import com.topjohnwu.magisk.ui.navigation.Route import kotlinx.coroutines.launch import com.topjohnwu.magisk.core.R as CoreR +import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Checkbox import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton @@ -80,12 +85,15 @@ import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.extra.SuperArrow +import top.yukonga.miuix.kmp.extra.SuperBottomSheet import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable -fun HomeScreen(viewModel: HomeViewModel) { +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 = MiuixScrollBehavior() @@ -96,8 +104,34 @@ fun HomeScreen(viewModel: HomeViewModel) { val showUninstallDialog = rememberSaveable { mutableStateOf(false) } val showManagerDialog = rememberSaveable { mutableStateOf(false) } val showEnvFixDialog = rememberSaveable { mutableStateOf(false) } + val showInstallSheet = 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(uiState.showUninstall) { if (uiState.showUninstall) { showUninstallDialog.value = true @@ -139,7 +173,7 @@ fun HomeScreen(viewModel: HomeViewModel) { code = envFixCode, activity = activity, loadingDialog = loadingDialog, - onNavigateInstall = { viewModel.onMagiskPressed() }, + onNavigateInstall = { showInstallSheet.value = true }, ) } @@ -170,7 +204,10 @@ fun HomeScreen(viewModel: HomeViewModel) { NoticeCard(onHide = viewModel::hideNotice) } - MagiskCard(viewModel = viewModel) + MagiskCard( + viewModel = viewModel, + onInstallClicked = { showInstallSheet.value = true } + ) ManagerCard(viewModel = viewModel, uiState = uiState) @@ -187,6 +224,12 @@ fun HomeScreen(viewModel: HomeViewModel) { DevelopersCard(onLinkClicked = { openLink(context, it) }) } } + + InstallBottomSheet( + show = showInstallSheet, + installVm = installVm, + installUiState = installUiState, + ) } @Composable @@ -276,7 +319,7 @@ private fun NoticeCard(onHide: () -> Unit) { } @Composable -private fun MagiskCard(viewModel: HomeViewModel) { +private fun MagiskCard(viewModel: HomeViewModel, onInstallClicked: () -> Unit) { Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -296,11 +339,11 @@ private fun MagiskCard(viewModel: HomeViewModel) { when (viewModel.magiskState) { HomeViewModel.State.OUTDATED -> TextButton( text = stringResource(CoreR.string.update), - onClick = { viewModel.onMagiskPressed() } + onClick = onInstallClicked ) else -> TextButton( text = stringResource(CoreR.string.install), - onClick = { viewModel.onMagiskPressed() } + onClick = onInstallClicked ) } } @@ -535,6 +578,139 @@ private fun openLink(context: Context, url: String) { } catch (_: ActivityNotFoundException) { } } +@Composable +private fun InstallBottomSheet( + show: MutableState, + installVm: InstallViewModel, + installUiState: InstallViewModel.UiState, +) { + SuperBottomSheet( + show = show, + onDismissRequest = { show.value = false }, + title = stringResource(CoreR.string.install), + ) { + Column(modifier = Modifier.padding(bottom = 16.dp)) { + if (installUiState.notes.isNotEmpty()) { + Text( + text = installUiState.notes, + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + if (!installVm.skipOptions) { + InstallOptionsSection(installUiState, installVm) + } + + SuperArrow( + title = stringResource(CoreR.string.select_patch_file), + summary = stringResource(CoreR.string.select_patch_file_summary), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.PATCH) + }, + enabled = installUiState.step >= 1 || installVm.skipOptions + ) + + if (installVm.isRooted) { + SuperArrow( + title = stringResource(CoreR.string.direct_install), + summary = stringResource(CoreR.string.direct_install_summary), + onClick = { + show.value = false + installVm.selectMethod(InstallViewModel.Method.DIRECT) + installVm.install() + }, + enabled = installUiState.step >= 1 || installVm.skipOptions + ) + } + + if (!installVm.noSecondSlot) { + SuperArrow( + 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) + }, + enabled = installUiState.step >= 1 || installVm.skipOptions + ) + } + } + } +} + +@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 = MiuixTheme.textStyles.headline2, + ) + if (uiState.step == 0) { + TextButton( + text = stringResource(CoreR.string.install_next), + onClick = { viewModel.nextStep() } + ) + } + } + + 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 = MiuixTheme.textStyles.body1, + ) + } +} + @Composable private fun UninstallComposableDialog( showDialog: MutableState, @@ -553,10 +729,7 @@ private fun UninstallComposableDialog( color = MiuixTheme.colorScheme.onSurface, ) Spacer(Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { + Row(modifier = Modifier.fillMaxWidth()) { TextButton( text = stringResource(CoreR.string.restore_img), onClick = { @@ -570,9 +743,10 @@ private fun UninstallComposableDialog( Toast.LENGTH_SHORT ) } - } + }, + modifier = Modifier.weight(1f) ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(20.dp)) TextButton( text = stringResource(CoreR.string.complete_uninstall), onClick = { @@ -583,7 +757,9 @@ private fun UninstallComposableDialog( flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } activity.startActivity(intent) - } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() ) } } @@ -605,21 +781,21 @@ private fun ManagerInstallComposableDialog( text } Spacer(Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { + Row(modifier = Modifier.fillMaxWidth()) { TextButton( text = stringResource(android.R.string.cancel), - onClick = { showDialog.value = false } + onClick = { showDialog.value = false }, + modifier = Modifier.weight(1f) ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(20.dp)) TextButton( text = stringResource(CoreR.string.install), onClick = { showDialog.value = false DownloadEngine.startWithActivity(activity, activity.extension, Subject.App()) - } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() ) } } @@ -651,15 +827,13 @@ private fun EnvFixComposableDialog( color = MiuixTheme.colorScheme.onSurface, ) Spacer(Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End - ) { + Row(modifier = Modifier.fillMaxWidth()) { TextButton( text = stringResource(android.R.string.cancel), - onClick = { showDialog.value = false } + onClick = { showDialog.value = false }, + modifier = Modifier.weight(1f) ) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(20.dp)) TextButton( text = stringResource(android.R.string.ok), onClick = { @@ -682,7 +856,9 @@ private fun EnvFixComposableDialog( } } } - } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.textButtonColorsPrimary() ) } } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt index cda12a00a..0bf637c78 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -14,7 +14,6 @@ import com.topjohnwu.magisk.core.download.Subject.App import com.topjohnwu.magisk.core.ktx.await import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.repository.NetworkService -import com.topjohnwu.magisk.ui.navigation.Route import com.topjohnwu.superuser.Shell import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -138,10 +137,6 @@ class HomeViewModel( _uiState.update { it.copy(envFixCode = 0) } } - fun onMagiskPressed() { - navigateTo(Route.Install) - } - fun hideNotice() { Config.safetyNotice = false _uiState.update { it.copy(isNoticeVisible = false) } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallScreen.kt deleted file mode 100644 index 84d43d4cb..000000000 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallScreen.kt +++ /dev/null @@ -1,261 +0,0 @@ -package com.topjohnwu.magisk.ui.install - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.topjohnwu.magisk.core.Config -import com.topjohnwu.magisk.core.Info -import com.topjohnwu.magisk.ui.component.rememberConfirmDialog -import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.Checkbox -import top.yukonga.miuix.kmp.basic.Icon -import top.yukonga.miuix.kmp.basic.IconButton -import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior -import top.yukonga.miuix.kmp.basic.Scaffold -import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.basic.TextButton -import top.yukonga.miuix.kmp.basic.TopAppBar -import top.yukonga.miuix.kmp.icon.MiuixIcons -import top.yukonga.miuix.kmp.icon.extended.Back -import top.yukonga.miuix.kmp.theme.MiuixTheme -import com.topjohnwu.magisk.core.R as CoreR - -@Composable -fun InstallScreen(viewModel: InstallViewModel, onBack: () -> Unit) { - val uiState by viewModel.uiState.collectAsState() - - val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { viewModel.onPatchFileSelected(it) } - } - - val secondSlotDialog = rememberConfirmDialog() - val secondSlotTitle = stringResource(android.R.string.dialog_alert_title) - val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg) - - LaunchedEffect(uiState.requestFilePicker) { - if (uiState.requestFilePicker) { - filePicker.launch("*/*") - viewModel.onFilePickerConsumed() - } - } - - LaunchedEffect(uiState.showSecondSlotWarning) { - if (uiState.showSecondSlotWarning) { - secondSlotDialog.showConfirm(title = secondSlotTitle, content = secondSlotMsg) - viewModel.onSecondSlotWarningConsumed() - } - } - - val scrollBehavior = MiuixScrollBehavior() - Scaffold( - topBar = { - TopAppBar( - title = stringResource(CoreR.string.install), - navigationIcon = { - IconButton( - modifier = Modifier.padding(start = 16.dp), - onClick = onBack - ) { - Icon( - imageVector = MiuixIcons.Back, - contentDescription = null, - tint = MiuixTheme.colorScheme.onBackground - ) - } - }, - scrollBehavior = scrollBehavior - ) - }, - popupHost = { } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) - .padding(padding) - .padding(horizontal = 12.dp) - .padding(top = 8.dp, bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (!viewModel.skipOptions) { - OptionsCard(uiState = uiState, viewModel = viewModel) - } - - MethodCard(uiState = uiState, viewModel = viewModel) - - if (uiState.notes.isNotEmpty()) { - NotesCard(notes = uiState.notes) - } - } - } -} - -@Composable -private fun OptionsCard(uiState: InstallViewModel.UiState, viewModel: InstallViewModel) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(CoreR.string.install_options_title), - style = MiuixTheme.textStyles.headline2, - ) - if (uiState.step == 0) { - TextButton( - text = stringResource(CoreR.string.install_next), - onClick = { viewModel.nextStep() } - ) - } - } - - 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 MethodCard(uiState: InstallViewModel.UiState, viewModel: InstallViewModel) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(CoreR.string.install_method_title), - style = MiuixTheme.textStyles.headline2, - ) - if (uiState.step == 1) { - TextButton( - text = stringResource(CoreR.string.install_start), - onClick = { viewModel.install() }, - enabled = viewModel.canInstall - ) - } - } - - if (uiState.step == 1) { - Spacer(Modifier.height(8.dp)) - - MethodRadioRow( - label = stringResource(CoreR.string.select_patch_file), - selected = uiState.method == InstallViewModel.Method.PATCH, - onClick = { viewModel.selectMethod(InstallViewModel.Method.PATCH) } - ) - if (viewModel.isRooted) { - MethodRadioRow( - label = stringResource(CoreR.string.direct_install), - selected = uiState.method == InstallViewModel.Method.DIRECT, - onClick = { viewModel.selectMethod(InstallViewModel.Method.DIRECT) } - ) - } - if (!viewModel.noSecondSlot) { - MethodRadioRow( - label = stringResource(CoreR.string.install_inactive_slot), - selected = uiState.method == InstallViewModel.Method.INACTIVE_SLOT, - onClick = { viewModel.selectMethod(InstallViewModel.Method.INACTIVE_SLOT) } - ) - } - } - } - } -} - -@Composable -private fun NotesCard(notes: String) { - Card(modifier = Modifier.fillMaxWidth()) { - Text( - text = notes, - style = MiuixTheme.textStyles.body2, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - modifier = Modifier.padding(16.dp) - ) - } -} - -@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 = MiuixTheme.textStyles.body1, - ) - } -} - -@Composable -private fun MethodRadioRow(label: String, selected: Boolean, onClick: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Checkbox( - checked = selected, - onCheckedChange = { onClick() } - ) - Text( - text = label, - style = MiuixTheme.textStyles.body1, - ) - } -} diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt index 4eb71d8ff..fa141a943 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt @@ -96,6 +96,9 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel() fun onPatchFileSelected(uri: Uri) { _uiState.update { it.copy(patchUri = uri) } + if (_uiState.value.method == Method.PATCH) { + install() + } } fun install() { diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt index 3513851a2..96eaf3411 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt @@ -10,12 +10,14 @@ import com.topjohnwu.magisk.core.ktx.timeFormatStandard import com.topjohnwu.magisk.core.ktx.toTime import com.topjohnwu.magisk.core.model.su.SuLog import com.topjohnwu.magisk.core.repository.LogRepository +import com.topjohnwu.magisk.core.su.SuEvents import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -25,6 +27,13 @@ class LogViewModel( private val repo: LogRepository ) : AsyncLoadViewModel() { + init { + @OptIn(kotlinx.coroutines.FlowPreview::class) + viewModelScope.launch { + SuEvents.logUpdated.debounce(500).collect { reload() } + } + } + data class UiState( val loading: Boolean = true, val magiskLog: String = "", diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionScreen.kt index 2e2c74ff9..b2a8d7b37 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionScreen.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionScreen.kt @@ -1,7 +1,6 @@ package com.topjohnwu.magisk.ui.module import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -13,20 +12,19 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import top.yukonga.miuix.kmp.basic.FloatingActionButton +import com.topjohnwu.magisk.R import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.SmallTopAppBar import top.yukonga.miuix.kmp.basic.Text -import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.theme.MiuixTheme @@ -37,6 +35,7 @@ fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> U val actionState by viewModel.actionState.collectAsState() val items = viewModel.consoleItems val listState = rememberLazyListState() + val finished = actionState != ActionViewModel.State.RUNNING LaunchedEffect(items.size) { if (items.isNotEmpty()) { @@ -61,51 +60,42 @@ fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> U ) } }, + actions = { + if (finished) { + IconButton( + modifier = Modifier.padding(end = 16.dp), + onClick = { viewModel.saveLog() } + ) { + Icon( + painter = painterResource(R.drawable.ic_save_md2), + contentDescription = stringResource(CoreR.string.menuSaveLog), + tint = MiuixTheme.colorScheme.onBackground + ) + } + } + }, scrollBehavior = scrollBehavior ) }, popupHost = { } ) { padding -> - Box(modifier = Modifier.fillMaxSize().padding(padding)) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .horizontalScroll(rememberScrollState()) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - itemsIndexed(items) { _, line -> - Text( - text = line, - fontFamily = FontFamily.Monospace, - fontSize = 12.sp, - lineHeight = 16.sp, - color = MiuixTheme.colorScheme.onSurface, - modifier = Modifier.fillMaxWidth() - ) - } - } - - if (actionState != ActionViewModel.State.RUNNING) { - TextButton( - text = stringResource(CoreR.string.menuSaveLog), - onClick = { viewModel.saveLog() }, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(16.dp) + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + itemsIndexed(items) { _, line -> + Text( + text = line, + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + lineHeight = 16.sp, + color = MiuixTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() ) - - FloatingActionButton( - onClick = onBack, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp) - ) { - Text( - text = stringResource(CoreR.string.close), - modifier = Modifier.padding(horizontal = 16.dp) - ) - } } } } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/navigation/Routes.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/navigation/Routes.kt index ac2a4cbc8..0c8269995 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/navigation/Routes.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/navigation/Routes.kt @@ -12,10 +12,6 @@ sealed interface Route : NavKey, Parcelable { @Serializable data object Main : Route - @Parcelize - @Serializable - data object Install : Route - @Parcelize @Serializable data object DenyList : Route diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsScreen.kt index 2ed440d4b..772f4909e 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsScreen.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsScreen.kt @@ -1,6 +1,5 @@ package com.topjohnwu.magisk.ui.settings -import android.app.Activity import android.os.Build import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,6 +19,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -37,6 +37,8 @@ import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.tasks.AppMigration import com.topjohnwu.magisk.core.utils.LocaleSetting import com.topjohnwu.magisk.core.utils.MediaStoreUtils +import com.topjohnwu.magisk.ui.component.rememberLoadingDialog +import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior @@ -45,6 +47,7 @@ import top.yukonga.miuix.kmp.basic.SmallTitle import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.basic.TextButton +import com.topjohnwu.magisk.ui.theme.ThemeState import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch @@ -123,6 +126,23 @@ private fun CustomizationSection(viewModel: SettingsViewModel) { ) } + // Color Mode + val resources = context.resources + val colorModeEntries = remember { + resources.getStringArray(CoreR.array.color_mode).toList() + } + var colorMode by remember { mutableIntStateOf(Config.colorMode) } + SuperDropdown( + title = stringResource(CoreR.string.settings_color_mode), + items = colorModeEntries, + selectedIndex = colorMode, + onSelectedIndexChange = { index -> + colorMode = index + Config.colorMode = index + ThemeState.colorMode = index + } + ) + if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { SuperArrow( title = stringResource(CoreR.string.add_shortcut_title), @@ -231,6 +251,8 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) { // Hide / Restore if (Info.env.isActive && Const.USER_ID == 0) { + val loadingDialog = rememberLoadingDialog() + val scope = rememberCoroutineScope() if (hidden) { var showRestoreDialog by remember { mutableStateOf(false) } RestoreDialog( @@ -238,7 +260,9 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) { onDismiss = { showRestoreDialog = false }, onConfirm = { showRestoreDialog = false - viewModel.restoreApp(context as Activity) + scope.launch { + loadingDialog.withLoading { viewModel.restoreApp(context) } + } } ) SuperArrow( @@ -253,7 +277,9 @@ private fun AppSettingsSection(viewModel: SettingsViewModel) { onDismiss = { showHideDialog = false }, onConfirm = { name -> showHideDialog = false - viewModel.hideApp(context as Activity, name) + scope.launch { + loadingDialog.withLoading { viewModel.hideApp(context, name) } + } } ) SuperArrow( diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt index d56ae5cbb..5f089d420 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt @@ -1,6 +1,6 @@ package com.topjohnwu.magisk.ui.settings -import android.app.Activity +import android.content.Context import android.widget.Toast import androidx.lifecycle.viewModelScope import com.topjohnwu.magisk.arch.BaseViewModel @@ -14,10 +14,12 @@ import com.topjohnwu.magisk.core.utils.RootUtils import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.ui.navigation.Route import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class SettingsViewModel : BaseViewModel() { @@ -36,12 +38,22 @@ class SettingsViewModel : BaseViewModel() { Shortcuts.addHomeIcon(AppContext) } - fun hideApp(activity: Activity, name: String) { - viewModelScope.launch { AppMigration.hide(activity, name) } + suspend fun hideApp(context: Context, name: String): Boolean { + val success = withContext(Dispatchers.IO) { + AppMigration.patchAndHide(context, name) + } + if (!success) { + context.toast(R.string.failure, Toast.LENGTH_LONG) + } + return success } - fun restoreApp(activity: Activity) { - viewModelScope.launch { AppMigration.restore(activity) } + suspend fun restoreApp(context: Context): Boolean { + val success = AppMigration.restoreApp(context) + if (!success) { + context.toast(R.string.failure, Toast.LENGTH_LONG) + } + return success } fun createHosts() { diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt index a4cb7236a..718901077 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt @@ -18,10 +18,12 @@ import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao import com.topjohnwu.magisk.core.ktx.getLabel import com.topjohnwu.magisk.core.model.su.SuPolicy +import com.topjohnwu.magisk.core.su.SuEvents import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -52,6 +54,13 @@ class SuperuserViewModel( var authenticate: (onSuccess: () -> Unit) -> Unit = { it() } + init { + @OptIn(kotlinx.coroutines.FlowPreview::class) + viewModelScope.launch { + SuEvents.policyChanged.debounce(500).collect { reload() } + } + } + data class UiState( val loading: Boolean = true, val policies: List = emptyList(), diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt index a6fd2e9b7..08a37d8b5 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/MagiskTheme.kt @@ -1,26 +1,36 @@ package com.topjohnwu.magisk.ui.theme -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue import com.topjohnwu.magisk.core.Config +import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.LocalContentColor import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.theme.darkColorScheme -import top.yukonga.miuix.kmp.theme.lightColorScheme +import top.yukonga.miuix.kmp.theme.ThemeController + +object ThemeState { + var colorMode by mutableIntStateOf(Config.colorMode) +} @Composable fun MagiskTheme( content: @Composable () -> Unit ) { - val darkTheme = when (Config.darkTheme) { - AppCompatDelegate.MODE_NIGHT_YES -> true - AppCompatDelegate.MODE_NIGHT_NO -> false - else -> isSystemInDarkTheme() + val isDark = isSystemInDarkTheme() + val mode = ThemeState.colorMode + val controller = when (mode) { + 1 -> ThemeController(ColorSchemeMode.Light) + 2 -> ThemeController(ColorSchemeMode.Dark) + 3 -> ThemeController(ColorSchemeMode.MonetSystem, isDark = isDark) + 4 -> ThemeController(ColorSchemeMode.MonetLight) + 5 -> ThemeController(ColorSchemeMode.MonetDark) + else -> ThemeController(ColorSchemeMode.System) } - val colors = if (darkTheme) darkColorScheme() else lightColorScheme() - MiuixTheme(colors = colors) { + MiuixTheme(controller = controller) { CompositionLocalProvider( LocalContentColor provides MiuixTheme.colorScheme.onBackground, content = content diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt index 7fd4f8598..e2dd5a928 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/Config.kt @@ -38,6 +38,7 @@ object Config : PreferenceConfig, DBConfig { const val CUSTOM_CHANNEL = "custom_channel" const val LOCALE = "locale" const val DARK_THEME = "dark_theme_extended" + const val COLOR_MODE = "color_mode" const val DOWNLOAD_DIR = "download_dir" const val SAFETY = "safety_notice" const val THEME_ORDINAL = "theme_ordinal" @@ -86,6 +87,7 @@ object Config : PreferenceConfig, DBConfig { // su notification const val NO_NOTIFICATION = 0 const val NOTIFICATION_TOAST = 1 + const val NOTIFICATION_STATUS_BAR = 2 // su auto response const val SU_PROMPT = 0 @@ -107,6 +109,7 @@ object Config : PreferenceConfig, DBConfig { var safetyNotice by preference(Key.SAFETY, true) var darkTheme by preference(Key.DARK_THEME, -1) var themeOrdinal by preference(Key.THEME_ORDINAL, 0) + var colorMode by preference(Key.COLOR_MODE, 0) private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true) private var localePrefs by preference(Key.LOCALE, "") diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt index 7c0d53172..9d6c8a757 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuCallbackHandler.kt @@ -12,6 +12,7 @@ import com.topjohnwu.magisk.core.ktx.getPackageInfo import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.createSuLog +import com.topjohnwu.magisk.view.Notifications import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -69,10 +70,12 @@ object SuCallbackHandler { } }.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids) - if (notify) - notify(context, log.action >= SuPolicy.ALLOW, log.appName) - runBlocking { ServiceLocator.logRepo.insert(log) } + + if (notify || Config.suNotification == Config.Value.NOTIFICATION_STATUS_BAR) + notify(context, log.action >= SuPolicy.ALLOW, log.appName) + SuEvents.notifyLogUpdated() + SuEvents.notifyPolicyChanged() } private fun handleNotify(context: Context, data: Bundle) { @@ -87,16 +90,18 @@ object SuCallbackHandler { }.getOrNull() ?: "[UID] $uid" notify(context, policy >= SuPolicy.ALLOW, appName) + SuEvents.notifyPolicyChanged() } private fun notify(context: Context, granted: Boolean, appName: String) { - if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) { - val resId = if (granted) - R.string.su_allow_toast - else - R.string.su_deny_toast - - context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT) + when (Config.suNotification) { + Config.Value.NOTIFICATION_TOAST -> { + val resId = if (granted) R.string.su_allow_toast else R.string.su_deny_toast + context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT) + } + Config.Value.NOTIFICATION_STATUS_BAR -> { + Notifications.suNotification(granted, appName) + } } } } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuEvents.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuEvents.kt new file mode 100644 index 000000000..e0ffe51a4 --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuEvents.kt @@ -0,0 +1,20 @@ +package com.topjohnwu.magisk.core.su + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +object SuEvents { + private val _policyChanged = MutableSharedFlow(extraBufferCapacity = 64) + val policyChanged = _policyChanged.asSharedFlow() + + private val _logUpdated = MutableSharedFlow(extraBufferCapacity = 64) + val logUpdated = _logUpdated.asSharedFlow() + + fun notifyPolicyChanged() { + _policyChanged.tryEmit(Unit) + } + + fun notifyLogUpdated() { + _logUpdated.tryEmit(Unit) + } +} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt index a624c01ce..3cf96cd22 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt @@ -104,6 +104,7 @@ class SuRequestHandler( } if (time >= 0) { policyDB.update(policy) + SuEvents.notifyPolicyChanged() } } } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt index 8c78f5b12..cc90eda62 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt @@ -6,15 +6,12 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build -import android.widget.Toast import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.core.AppApkPath import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Const -import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.ktx.await -import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.signing.JarMap import com.topjohnwu.magisk.core.signing.SignApk @@ -237,23 +234,6 @@ object AppMigration { } } - @Suppress("DEPRECATION") - suspend fun hide(activity: Activity, label: String) { - val dialog = android.app.ProgressDialog(activity).apply { - setTitle(activity.getString(R.string.hide_app_title)) - isIndeterminate = true - setCancelable(false) - show() - } - val success = withContext(Dispatchers.IO) { - patchAndHide(activity, label) - } - if (!success) { - dialog.dismiss() - activity.toast(R.string.failure, Toast.LENGTH_LONG) - } - } - suspend fun restoreApp(context: Context): Boolean { val apk = StubApk.current(context) val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME" @@ -266,20 +246,6 @@ object AppMigration { return false } - @Suppress("DEPRECATION") - suspend fun restore(activity: Activity) { - val dialog = android.app.ProgressDialog(activity).apply { - setTitle(activity.getString(R.string.restore_img_msg)) - isIndeterminate = true - setCancelable(false) - show() - } - if (!restoreApp(activity)) { - activity.toast(R.string.failure, Toast.LENGTH_LONG) - } - dialog.dismiss() - } - suspend fun upgradeStub(context: Context, apk: File): Intent? { val label = context.applicationInfo.nonLocalizedLabel val pkg = context.packageName diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt index c70fd3fe5..4a28beea0 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/view/Notifications.kt @@ -28,6 +28,7 @@ object Notifications { private const val UPDATE_CHANNEL = "update" private const val PROGRESS_CHANNEL = "progress" private const val UPDATED_CHANNEL = "updated" + private const val SU_CHANNEL = "su_notification" private val nextId = AtomicInteger(APP_UPDATE_AVAILABLE_ID) @@ -40,7 +41,9 @@ object Notifications { getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW) val channel3 = NotificationChannel(UPDATED_CHANNEL, getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH) - mgr.createNotificationChannels(listOf(channel, channel2, channel3)) + val channel4 = NotificationChannel(SU_CHANNEL, + getString(R.string.su_notification_channel), NotificationManager.IMPORTANCE_HIGH) + mgr.createNotificationChannels(listOf(channel, channel2, channel3, channel4)) } } } @@ -101,5 +104,37 @@ object Notifications { return builder } + private const val SU_NOTIFICATION_TIMEOUT_MS = 3_000L + + @SuppressLint("InlinedApi") + fun suNotification(granted: Boolean, appName: String) { + AppContext.apply { + val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + val pending = PendingIntent.getActivity(this, 0, selfLaunchIntent(), flag) + val title = getString( + if (granted) R.string.su_notification_granted_title + else R.string.su_notification_denied_title + ) + val text = getString( + if (granted) R.string.su_allow_toast + else R.string.su_deny_toast, + appName + ) + val builder = if (SDK_INT >= Build.VERSION_CODES.O) { + Notification.Builder(this, SU_CHANNEL) + .setSmallIcon(getBitmap(R.drawable.ic_magisk_outline).toIcon()) + } else { + Notification.Builder(this).setPriority(Notification.PRIORITY_HIGH) + .setSmallIcon(R.drawable.ic_magisk_outline) + } + .setContentIntent(pending) + .setContentTitle(title) + .setContentText(text) + .setAutoCancel(true) + .setTimeoutAfter(SU_NOTIFICATION_TIMEOUT_MS) + mgr.notify(nextId(), builder.build()) + } + } + fun nextId() = nextId.incrementAndGet() } diff --git a/app/core/src/main/res/values/arrays.xml b/app/core/src/main/res/values/arrays.xml index e64e4970c..3235634c2 100644 --- a/app/core/src/main/res/values/arrays.xml +++ b/app/core/src/main/res/values/arrays.xml @@ -34,6 +34,7 @@ @string/none @string/toast + @string/notification @@ -66,4 +67,13 @@ @string/settings_update_debug @string/settings_update_custom + + + @string/color_mode_system + @string/color_mode_light + @string/color_mode_dark + @string/color_mode_monet_system + @string/color_mode_monet_light + @string/color_mode_monet_dark + diff --git a/app/core/src/main/res/values/strings.xml b/app/core/src/main/res/values/strings.xml index 31fb5e08f..5b1fbb7f1 100644 --- a/app/core/src/main/res/values/strings.xml +++ b/app/core/src/main/res/values/strings.xml @@ -40,7 +40,10 @@ Let\'s go Press to download and install Direct install (Recommended) + Directly install Magisk to the boot partition Install to inactive slot (After OTA) + Install to the inactive slot after an OTA update + Patch a raw image, ODIN tar, or payload.bin file Your device will be FORCED to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? Additional setup Select and patch a file @@ -73,7 +76,11 @@ Revoke? Confirm to revoke %1$s Superuser rights Toast + Notification None + Superuser Notifications + Superuser Granted + Superuser Denied Notifications Revoke No apps have asked for Superuser permission yet. @@ -186,6 +193,13 @@ Restrict root capabilities Will restrict new Superuser apps by default. Warning: this will break most apps. Don\'t enable it unless you know what you\'re doing. Customization + Color mode + System + Light + Dark + Monet (System) + Monet (Light) + Monet (Dark) Add a pretty shortcut to the home screen in case the name and icon are difficult to recognize after hiding the app DNS over HTTPS Workaround DNS poisoning in some nations