Add SU live-reload, status bar notifications, Monet theming, and UI improvements

- Add SuEvents shared flow for live-reloading Superuser and Log tabs
  when root decisions are made
- Add status bar notification option for SU grant/deny with auto-dismiss
- Add Monet/Material You theme support with instant theme switching
- Replace install page navigation with bottom sheet from Home screen
- Replace deprecated ProgressDialog with MiUIX loading dialog for
  app hide/restore
- Move save log and reboot to top bar action buttons in flash/action screens
- Fix dialog button layout to use evenly distributed buttons with
  primary color on positive actions

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-04 13:08:03 +08:00
committed by topjohnwu
parent 3b73a0ea2c
commit 73c3cfc7f3
23 changed files with 479 additions and 457 deletions
@@ -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()
}
@@ -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<Route.Main> {
MainScreen(initialTab = initialTab)
}
entry<Route.Install> { _ ->
val vm: InstallViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
CollectNavEvents(vm, navigator)
InstallScreen(vm, onBack = { navigator.pop() })
}
entry<Route.DenyList> { _ ->
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)
}
}
}
}
@@ -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
@@ -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)
)
}
}
}
}
}
@@ -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<Boolean>,
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<Boolean>,
@@ -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()
)
}
}
@@ -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) }
@@ -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,
)
}
}
@@ -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() {
@@ -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 = "",
@@ -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)
)
}
}
}
}
@@ -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
@@ -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(
@@ -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() {
@@ -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<PolicyItem> = emptyList(),
@@ -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
@@ -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, "")
@@ -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)
}
}
}
}
@@ -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<Unit>(extraBufferCapacity = 64)
val policyChanged = _policyChanged.asSharedFlow()
private val _logUpdated = MutableSharedFlow<Unit>(extraBufferCapacity = 64)
val logUpdated = _logUpdated.asSharedFlow()
fun notifyPolicyChanged() {
_policyChanged.tryEmit(Unit)
}
fun notifyLogUpdated() {
_logUpdated.tryEmit(Unit)
}
}
@@ -104,6 +104,7 @@ class SuRequestHandler(
}
if (time >= 0) {
policyDB.update(policy)
SuEvents.notifyPolicyChanged()
}
}
}
@@ -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
@@ -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()
}
+10
View File
@@ -34,6 +34,7 @@
<string-array name="su_notification">
<item>@string/none</item>
<item>@string/toast</item>
<item>@string/notification</item>
</string-array>
<string-array name="multiuser_mode">
@@ -66,4 +67,13 @@
<item>@string/settings_update_debug</item>
<item>@string/settings_update_custom</item>
</string-array>
<string-array name="color_mode">
<item>@string/color_mode_system</item>
<item>@string/color_mode_light</item>
<item>@string/color_mode_dark</item>
<item>@string/color_mode_monet_system</item>
<item>@string/color_mode_monet_light</item>
<item>@string/color_mode_monet_dark</item>
</string-array>
</resources>
+14
View File
@@ -40,7 +40,10 @@
<string name="install_start">Let\'s go</string>
<string name="manager_download_install">Press to download and install</string>
<string name="direct_install">Direct install (Recommended)</string>
<string name="direct_install_summary">Directly install Magisk to the boot partition</string>
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
<string name="install_inactive_slot_summary">Install to the inactive slot after an OTA update</string>
<string name="select_patch_file_summary">Patch a raw image, ODIN tar, or payload.bin file</string>
<string name="install_inactive_slot_msg">Your device will be FORCED to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
<string name="setup_title">Additional setup</string>
<string name="select_patch_file">Select and patch a file</string>
@@ -73,7 +76,11 @@
<string name="su_revoke_title">Revoke?</string>
<string name="su_revoke_msg">Confirm to revoke %1$s Superuser rights</string>
<string name="toast">Toast</string>
<string name="notification">Notification</string>
<string name="none">None</string>
<string name="su_notification_channel">Superuser Notifications</string>
<string name="su_notification_granted_title">Superuser Granted</string>
<string name="su_notification_denied_title">Superuser Denied</string>
<string name="superuser_toggle_notification">Notifications</string>
<string name="superuser_toggle_revoke">Revoke</string>
<string name="superuser_policy_none">No apps have asked for Superuser permission yet.</string>
@@ -186,6 +193,13 @@
<string name="settings_su_restrict_title">Restrict root capabilities</string>
<string name="settings_su_restrict_summary">Will restrict new Superuser apps by default. Warning: this will break most apps. Don\'t enable it unless you know what you\'re doing.</string>
<string name="settings_customization">Customization</string>
<string name="settings_color_mode">Color mode</string>
<string name="color_mode_system">System</string>
<string name="color_mode_light">Light</string>
<string name="color_mode_dark">Dark</string>
<string name="color_mode_monet_system">Monet (System)</string>
<string name="color_mode_monet_light">Monet (Light)</string>
<string name="color_mode_monet_dark">Monet (Dark)</string>
<string name="setting_add_shortcut_summary">Add a pretty shortcut to the home screen in case the name and icon are difficult to recognize after hiding the app</string>
<string name="settings_doh_title">DNS over HTTPS</string>
<string name="settings_doh_description">Workaround DNS poisoning in some nations</string>