Migrate Settings screen to Jetpack Compose with miuix

Replace the Settings screen's data-binding XML layouts and RecyclerView
item system with a declarative Compose UI using miuix components:
SuperSwitch for toggles, SuperDropdown for selectors, SuperArrow for
navigation items, and SuperDialog for text input dialogs.

SettingsViewModel now exposes DenyList state via StateFlow and provides
action methods instead of the old Handler/BaseSettingsItem pattern.

Remove BaseSettingsItem.kt, SettingsItems.kt, and all associated XML
layouts (fragment_settings_md2, item_settings, item_settings_section,
dialog_settings_app_name, dialog_settings_download_path,
dialog_settings_update_channel).

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 11:52:03 +08:00
committed by topjohnwu
parent a9ade79617
commit 397a906905
11 changed files with 686 additions and 920 deletions
@@ -1,145 +0,0 @@
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.view.MagiskDialog
sealed class BaseSettingsItem : ObservableRvItem() {
interface Handler {
fun onItemPressed(view: View, item: BaseSettingsItem, andThen: () -> Unit)
fun onItemAction(view: View, item: BaseSettingsItem)
}
override val layoutRes get() = R.layout.item_settings
open val icon: Int get() = 0
open val title: TextHolder get() = TextHolder.EMPTY
@get:Bindable
open val description: TextHolder get() = TextHolder.EMPTY
@get:Bindable
var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled, BR.description)
open fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
handler.onItemAction(view, this)
}
}
open fun refresh() {}
// Only for toggle
open val showSwitch get() = false
@get:Bindable
open val isChecked get() = false
fun onToggle(view: View, handler: Handler, checked: Boolean) =
set(checked, isChecked, { onPressed(view, handler) })
abstract class Value<T> : BaseSettingsItem() {
/**
* Represents last agreed-upon value by the validation process and the user for current
* child. Be very aware that this shouldn't be **set** unless both sides agreed that _that_
* is the new value.
* */
abstract var value: T
protected set
}
abstract class Toggle : Value<Boolean>() {
override val showSwitch get() = true
override val isChecked get() = value
override fun onPressed(view: View, handler: Handler) {
// Make sure the checked state is synced
notifyPropertyChanged(BR.checked)
handler.onItemPressed(view, this) {
value = !value
notifyPropertyChanged(BR.checked)
handler.onItemAction(view, this)
}
}
}
abstract class Input : Value<String>() {
@get:Bindable
abstract val inputResult: String?
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setView(getView(view.context))
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
inputResult?.let { result ->
doNotDismiss = false
value = result
handler.onItemAction(view, this@Input)
return@onClick
}
doNotDismiss = true
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}.show()
}
}
abstract fun getView(context: Context): View
}
abstract class Selector : Value<Int>() {
open val entryRes get() = -1
open val descriptionRes get() = entryRes
open fun entries(res: Resources) = res.getArrayOrEmpty(entryRes)
open fun descriptions(res: Resources) = res.getArrayOrEmpty(descriptionRes)
override val description = object : TextHolder() {
override fun getText(resources: Resources): CharSequence {
return descriptions(resources).getOrElse(value) { "" }
}
}
private fun Resources.getArrayOrEmpty(id: Int): Array<String> =
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setListItems(entries(view.resources)) {
if (value != it) {
value = it
notifyPropertyChanged(BR.description)
handler.onItemAction(view, this@Selector)
}
}
}.show()
}
}
}
abstract class Blank : BaseSettingsItem()
abstract class Section : BaseSettingsItem() {
override val layoutRes = R.layout.item_settings_section
}
}
@@ -1,40 +1,59 @@
package com.topjohnwu.magisk.ui.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.arch.ViewModelHolder
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class SettingsFragment : BaseFragment<FragmentSettingsMd2Binding>() {
class SettingsFragment : Fragment(), ViewModelHolder {
override val layoutRes = R.layout.fragment_settings_md2
override val viewModel by viewModel<SettingsViewModel>()
override val snackbarView: View get() = binding.snackbarContainer
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[SettingsViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.settings)
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.settings)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.settingsList.apply {
addEdgeSpacing(bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
SettingsScreen(viewModel = viewModel as SettingsViewModel)
}
}
}
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}
@@ -1,333 +0,0 @@
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.activity
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.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import com.topjohnwu.magisk.core.R as CoreR
// --- Customization
object Customization : BaseSettingsItem.Section() {
override val title = CoreR.string.settings_customization.asText()
}
object Language : BaseSettingsItem.Selector() {
private val names: Array<String> get() = LocaleSetting.available.names
private val tags: Array<String> get() = LocaleSetting.available.tags
override var value
get() = tags.indexOf(Config.locale)
set(value) {
Config.locale = tags[value]
}
override val title = CoreR.string.language.asText()
override fun entries(res: Resources) = names
override fun descriptions(res: Resources) = names
}
object LanguageSystem : BaseSettingsItem.Blank() {
override val title = CoreR.string.language.asText()
override val description: TextHolder
get() {
val locale = LocaleSetting.instance.appLocale
return locale?.getDisplayName(locale)?.asText() ?: CoreR.string.system_default.asText()
}
}
object Theme : BaseSettingsItem.Blank() {
override val icon = R.drawable.ic_paint
override val title = CoreR.string.section_theme.asText()
}
// --- App
object AppSettings : BaseSettingsItem.Section() {
override val title = CoreR.string.home_app_title.asText()
}
object Hide : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_hide_app_title.asText()
override val description = CoreR.string.settings_hide_app_summary.asText()
override var value = ""
override val inputResult
get() = if (isError) null else result
@get:Bindable
var result = "Settings"
set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength
get() = AppMigration.MAX_LABEL_LENGTH
@get:Bindable
val isError
get() = result.length > maxLength || result.isBlank()
override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object Restore : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_restore_app_title.asText()
override val description = CoreR.string.settings_restore_app_summary.asText()
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(CoreR.string.settings_restore_app_title)
setMessage(CoreR.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
handler.onItemAction(view, this@Restore)
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
show()
}
}
}
}
object AddShortcut : BaseSettingsItem.Blank() {
override val title = CoreR.string.add_shortcut_title.asText()
override val description = CoreR.string.setting_add_shortcut_summary.asText()
}
object DownloadPath : BaseSettingsItem.Input() {
override var value
get() = Config.downloadDir
set(value) {
Config.downloadDir = value
notifyPropertyChanged(BR.description)
}
override val title = CoreR.string.settings_download_path_title.asText()
override val description get() = MediaStoreUtils.fullPath(value).asText()
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult, BR.path)
@get:Bindable
val path get() = MediaStoreUtils.fullPath(inputResult)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChannel : BaseSettingsItem.Selector() {
override var value
get() = Config.updateChannel
set(value) {
Config.updateChannel = value
Info.resetUpdate()
}
override val title = CoreR.string.settings_update_channel_title.asText()
override val entryRes = CoreR.array.update_channel
}
object UpdateChannelUrl : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_update_custom.asText()
override val description get() = value.asText()
override var value
get() = Config.customChannelUrl
set(value) {
Config.customChannelUrl = value
Info.resetUpdate()
notifyPropertyChanged(BR.description)
}
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult)
override fun refresh() {
isEnabled = UpdateChannel.value == Config.Value.CUSTOM_CHANNEL
}
override fun getView(context: Context) = DialogSettingsUpdateChannelBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChecker : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_check_update_title.asText()
override val description = CoreR.string.settings_check_update_summary.asText()
override var value by Config::checkUpdate
}
object DoHToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_doh_title.asText()
override val description = CoreR.string.settings_doh_description.asText()
override var value by Config::doh
}
object SystemlessHosts : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_hosts_title.asText()
override val description = CoreR.string.settings_hosts_summary.asText()
}
object RandNameToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_random_name_title.asText()
override val description = CoreR.string.settings_random_name_description.asText()
override var value by Config::randName
}
// --- Magisk
object Magisk : BaseSettingsItem.Section() {
override val title = CoreR.string.magisk.asText()
}
object Zygisk : BaseSettingsItem.Toggle() {
override val title = CoreR.string.zygisk.asText()
override val description get() =
if (mismatch) CoreR.string.reboot_apply_change.asText()
else CoreR.string.settings_zygisk_summary.asText()
override var value
get() = Config.zygisk
set(value) {
Config.zygisk = value
notifyPropertyChanged(BR.description)
}
val mismatch get() = value != Info.isZygiskEnabled
}
object DenyList : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_denylist_title.asText()
override val description get() = CoreR.string.settings_denylist_summary.asText()
override var value = Config.denyList
set(value) {
field = value
val cmd = if (value) "enable" else "disable"
Shell.cmd("magisk --denylist $cmd").submit { result ->
if (result.isSuccess) {
Config.denyList = value
} else {
field = !value
notifyPropertyChanged(BR.checked)
}
}
}
}
object DenyListConfig : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_denylist_config_title.asText()
override val description = CoreR.string.settings_denylist_config_summary.asText()
}
// --- Superuser
object Tapjack : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_tapjack_title.asText()
override val description = CoreR.string.settings_su_tapjack_summary.asText()
override var value by Config::suTapjack
}
object Authentication : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_auth_title.asText()
override var description = CoreR.string.settings_su_auth_summary.asText()
override var value by Config::suAuth
override fun refresh() {
isEnabled = Info.isDeviceSecure
if (!isEnabled) {
description = CoreR.string.settings_su_auth_insecure.asText()
}
}
}
object Superuser : BaseSettingsItem.Section() {
override val title = CoreR.string.superuser.asText()
}
object AccessMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_access.asText()
override val entryRes = CoreR.array.su_access
override var value by Config::rootMode
}
object MultiuserMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.multiuser_mode.asText()
override val entryRes = CoreR.array.multiuser_mode
override val descriptionRes = CoreR.array.multiuser_summary
override var value by Config::suMultiuserMode
override fun refresh() {
isEnabled = Const.USER_ID == 0
}
}
object MountNamespaceMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.mount_namespace_mode.asText()
override val entryRes = CoreR.array.namespace
override val descriptionRes = CoreR.array.namespace_summary
override var value by Config::suMntNamespaceMode
}
object AutomaticResponse : BaseSettingsItem.Selector() {
override val title = CoreR.string.auto_response.asText()
override val entryRes = CoreR.array.auto_response
override var value by Config::suAutoResponse
}
object RequestTimeout : BaseSettingsItem.Selector() {
override val title = CoreR.string.request_timeout.asText()
override val entryRes = CoreR.array.request_timeout
private val entryValues = listOf(10, 15, 20, 30, 45, 60)
override var value = entryValues.indexOfFirst { it == Config.suDefaultTimeout }
set(value) {
field = value
Config.suDefaultTimeout = entryValues[value]
}
}
object SUNotification : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_notification.asText()
override val entryRes = CoreR.array.su_notification
override var value by Config::suNotification
}
object Reauthenticate : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_reauth_title.asText()
override val description = CoreR.string.settings_su_reauth_summary.asText()
override var value by Config::suReAuth
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
}
}
object Restrict : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_restrict_title.asText()
override val description = CoreR.string.settings_su_restrict_summary.asText()
override var value by Config::suRestrict
}
@@ -0,0 +1,601 @@
package com.topjohnwu.magisk.ui.settings
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.core.content.pm.ShortcutManagerCompat
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
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 top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTitle
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperDropdown
import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.extra.SuperDialog
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
Scaffold { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(padding)
.padding(horizontal = 12.dp)
.padding(top = 8.dp, bottom = 16.dp)
) {
CustomizationSection(viewModel)
Spacer(Modifier.height(12.dp))
AppSettingsSection(viewModel)
if (Info.env.isActive) {
Spacer(Modifier.height(12.dp))
MagiskSection(viewModel)
}
if (Info.showSuperUser) {
Spacer(Modifier.height(12.dp))
SuperuserSection(viewModel)
}
}
}
}
// --- Customization ---
@Composable
private fun CustomizationSection(viewModel: SettingsViewModel) {
val context = LocalContext.current
SmallTitle(text = stringResource(CoreR.string.settings_customization))
Card(modifier = Modifier.fillMaxWidth()) {
SuperArrow(
title = stringResource(CoreR.string.section_theme),
onClick = { viewModel.navigateToTheme() }
)
if (LocaleSetting.useLocaleManager) {
val locale = LocaleSetting.instance.appLocale
val summary = locale?.getDisplayName(locale) ?: stringResource(CoreR.string.system_default)
SuperArrow(
title = stringResource(CoreR.string.language),
summary = summary,
onClick = {
context.startActivity(LocaleSetting.localeSettingsIntent)
}
)
} else {
val names = remember { LocaleSetting.available.names }
val tags = remember { LocaleSetting.available.tags }
var selectedIndex by remember {
mutableIntStateOf(tags.indexOf(Config.locale).coerceAtLeast(0))
}
SuperDropdown(
title = stringResource(CoreR.string.language),
items = names.toList(),
selectedIndex = selectedIndex,
onSelectedIndexChange = { index ->
selectedIndex = index
Config.locale = tags[index]
}
)
}
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
SuperArrow(
title = stringResource(CoreR.string.add_shortcut_title),
summary = stringResource(CoreR.string.setting_add_shortcut_summary),
onClick = { viewModel.requestAddShortcut() }
)
}
}
}
// --- App Settings ---
@Composable
private fun AppSettingsSection(viewModel: SettingsViewModel) {
val context = LocalContext.current
val resources = context.resources
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
SmallTitle(text = stringResource(CoreR.string.home_app_title))
Card(modifier = Modifier.fillMaxWidth()) {
// Update Channel
val updateChannelEntries = remember {
resources.getStringArray(CoreR.array.update_channel).toList()
}
var updateChannel by remember { mutableIntStateOf(Config.updateChannel) }
var showUrlDialog by remember { mutableStateOf(false) }
SuperDropdown(
title = stringResource(CoreR.string.settings_update_channel_title),
items = updateChannelEntries,
selectedIndex = updateChannel,
onSelectedIndexChange = { index ->
updateChannel = index
Config.updateChannel = index
Info.resetUpdate()
if (index == Config.Value.CUSTOM_CHANNEL && Config.customChannelUrl.isBlank()) {
showUrlDialog = true
}
}
)
// Update Channel URL (for custom channel)
if (updateChannel == Config.Value.CUSTOM_CHANNEL) {
UpdateChannelUrlDialog(
show = showUrlDialog,
onDismiss = { showUrlDialog = false }
)
SuperArrow(
title = stringResource(CoreR.string.settings_update_custom),
summary = Config.customChannelUrl.ifBlank { null },
onClick = { showUrlDialog = true }
)
}
// DoH Toggle
var doh by remember { mutableStateOf(Config.doh) }
SuperSwitch(
title = stringResource(CoreR.string.settings_doh_title),
summary = stringResource(CoreR.string.settings_doh_description),
checked = doh,
onCheckedChange = {
doh = it
Config.doh = it
}
)
// Update Checker
var checkUpdate by remember { mutableStateOf(Config.checkUpdate) }
SuperSwitch(
title = stringResource(CoreR.string.settings_check_update_title),
summary = stringResource(CoreR.string.settings_check_update_summary),
checked = checkUpdate,
onCheckedChange = { newValue ->
viewModel.withNotificationPermission {
checkUpdate = newValue
Config.checkUpdate = newValue
}
}
)
// Download Path
var showDownloadDialog by remember { mutableStateOf(false) }
DownloadPathDialog(
show = showDownloadDialog,
onDismiss = { showDownloadDialog = false }
)
SuperArrow(
title = stringResource(CoreR.string.settings_download_path_title),
summary = MediaStoreUtils.fullPath(Config.downloadDir),
onClick = {
viewModel.withDownloadPathPermission { showDownloadDialog = true }
}
)
// Random Package Name
var randName by remember { mutableStateOf(Config.randName) }
SuperSwitch(
title = stringResource(CoreR.string.settings_random_name_title),
summary = stringResource(CoreR.string.settings_random_name_description),
checked = randName,
onCheckedChange = {
randName = it
Config.randName = it
}
)
// Hide / Restore
if (Info.env.isActive && Const.USER_ID == 0) {
if (hidden) {
var showRestoreDialog by remember { mutableStateOf(false) }
RestoreDialog(
show = showRestoreDialog,
onDismiss = { showRestoreDialog = false },
onConfirm = {
showRestoreDialog = false
viewModel.restoreApp(context as Activity)
}
)
SuperArrow(
title = stringResource(CoreR.string.settings_restore_app_title),
summary = stringResource(CoreR.string.settings_restore_app_summary),
onClick = { showRestoreDialog = true }
)
} else {
var showHideDialog by remember { mutableStateOf(false) }
HideAppDialog(
show = showHideDialog,
onDismiss = { showHideDialog = false },
onConfirm = { name ->
showHideDialog = false
viewModel.hideApp(context as Activity, name)
}
)
SuperArrow(
title = stringResource(CoreR.string.settings_hide_app_title),
summary = stringResource(CoreR.string.settings_hide_app_summary),
onClick = { showHideDialog = true }
)
}
}
}
}
// --- Magisk ---
@Composable
private fun MagiskSection(viewModel: SettingsViewModel) {
SmallTitle(text = stringResource(CoreR.string.magisk))
Card(modifier = Modifier.fillMaxWidth()) {
// Systemless Hosts
SuperArrow(
title = stringResource(CoreR.string.settings_hosts_title),
summary = stringResource(CoreR.string.settings_hosts_summary),
onClick = { viewModel.createHosts() }
)
if (Const.Version.atLeast_24_0()) {
// Zygisk
var zygisk by remember { mutableStateOf(Config.zygisk) }
SuperSwitch(
title = stringResource(CoreR.string.zygisk),
summary = stringResource(
if (zygisk != Info.isZygiskEnabled) CoreR.string.reboot_apply_change
else CoreR.string.settings_zygisk_summary
),
checked = zygisk,
onCheckedChange = {
zygisk = it
Config.zygisk = it
viewModel.notifyZygiskChange()
}
)
// DenyList
val denyListEnabled by viewModel.denyListEnabled.collectAsState()
SuperSwitch(
title = stringResource(CoreR.string.settings_denylist_title),
summary = stringResource(CoreR.string.settings_denylist_summary),
checked = denyListEnabled,
onCheckedChange = { viewModel.toggleDenyList(it) }
)
// DenyList Config
SuperArrow(
title = stringResource(CoreR.string.settings_denylist_config_title),
summary = stringResource(CoreR.string.settings_denylist_config_summary),
onClick = { viewModel.navigateToDenyList() }
)
}
}
}
// --- Superuser ---
@Composable
private fun SuperuserSection(viewModel: SettingsViewModel) {
val context = LocalContext.current
val resources = context.resources
SmallTitle(text = stringResource(CoreR.string.superuser))
Card(modifier = Modifier.fillMaxWidth()) {
// Tapjack (SDK < S)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
var tapjack by remember { mutableStateOf(Config.suTapjack) }
SuperSwitch(
title = stringResource(CoreR.string.settings_su_tapjack_title),
summary = stringResource(CoreR.string.settings_su_tapjack_summary),
checked = tapjack,
onCheckedChange = {
tapjack = it
Config.suTapjack = it
}
)
}
// Authentication
var suAuth by remember { mutableStateOf(Config.suAuth) }
SuperSwitch(
title = stringResource(CoreR.string.settings_su_auth_title),
summary = stringResource(
if (Info.isDeviceSecure) CoreR.string.settings_su_auth_summary
else CoreR.string.settings_su_auth_insecure
),
checked = suAuth,
enabled = Info.isDeviceSecure,
onCheckedChange = { newValue ->
viewModel.withAuth {
suAuth = newValue
Config.suAuth = newValue
}
}
)
// Access Mode
val accessEntries = remember {
resources.getStringArray(CoreR.array.su_access).toList()
}
var accessMode by remember { mutableIntStateOf(Config.rootMode) }
SuperDropdown(
title = stringResource(CoreR.string.superuser_access),
items = accessEntries,
selectedIndex = accessMode,
onSelectedIndexChange = {
accessMode = it
Config.rootMode = it
}
)
// Multiuser Mode
val multiuserEntries = remember {
resources.getStringArray(CoreR.array.multiuser_mode).toList()
}
val multiuserDescriptions = remember {
resources.getStringArray(CoreR.array.multiuser_summary).toList()
}
var multiuserMode by remember { mutableIntStateOf(Config.suMultiuserMode) }
SuperDropdown(
title = stringResource(CoreR.string.multiuser_mode),
summary = multiuserDescriptions.getOrElse(multiuserMode) { "" },
items = multiuserEntries,
selectedIndex = multiuserMode,
enabled = Const.USER_ID == 0,
onSelectedIndexChange = {
multiuserMode = it
Config.suMultiuserMode = it
}
)
// Mount Namespace Mode
val namespaceEntries = remember {
resources.getStringArray(CoreR.array.namespace).toList()
}
val namespaceDescriptions = remember {
resources.getStringArray(CoreR.array.namespace_summary).toList()
}
var mntNamespaceMode by remember { mutableIntStateOf(Config.suMntNamespaceMode) }
SuperDropdown(
title = stringResource(CoreR.string.mount_namespace_mode),
summary = namespaceDescriptions.getOrElse(mntNamespaceMode) { "" },
items = namespaceEntries,
selectedIndex = mntNamespaceMode,
onSelectedIndexChange = {
mntNamespaceMode = it
Config.suMntNamespaceMode = it
}
)
// Automatic Response
val autoResponseEntries = remember {
resources.getStringArray(CoreR.array.auto_response).toList()
}
var autoResponse by remember { mutableIntStateOf(Config.suAutoResponse) }
SuperDropdown(
title = stringResource(CoreR.string.auto_response),
items = autoResponseEntries,
selectedIndex = autoResponse,
onSelectedIndexChange = { newIndex ->
val doIt = {
autoResponse = newIndex
Config.suAutoResponse = newIndex
}
if (Config.suAuth) viewModel.withAuth(doIt) else doIt()
}
)
// Request Timeout
val timeoutEntries = remember {
resources.getStringArray(CoreR.array.request_timeout).toList()
}
val timeoutValues = remember { listOf(10, 15, 20, 30, 45, 60) }
var timeoutIndex by remember {
mutableIntStateOf(timeoutValues.indexOf(Config.suDefaultTimeout).coerceAtLeast(0))
}
SuperDropdown(
title = stringResource(CoreR.string.request_timeout),
items = timeoutEntries,
selectedIndex = timeoutIndex,
onSelectedIndexChange = {
timeoutIndex = it
Config.suDefaultTimeout = timeoutValues[it]
}
)
// SU Notification
val notifEntries = remember {
resources.getStringArray(CoreR.array.su_notification).toList()
}
var suNotification by remember { mutableIntStateOf(Config.suNotification) }
SuperDropdown(
title = stringResource(CoreR.string.superuser_notification),
items = notifEntries,
selectedIndex = suNotification,
onSelectedIndexChange = {
suNotification = it
Config.suNotification = it
}
)
// Reauthenticate (SDK < O)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
var reAuth by remember { mutableStateOf(Config.suReAuth) }
SuperSwitch(
title = stringResource(CoreR.string.settings_su_reauth_title),
summary = stringResource(CoreR.string.settings_su_reauth_summary),
checked = reAuth,
onCheckedChange = {
reAuth = it
Config.suReAuth = it
}
)
}
// Restrict (version >= 30.1)
if (Const.Version.atLeast_30_1()) {
var restrict by remember { mutableStateOf(Config.suRestrict) }
SuperSwitch(
title = stringResource(CoreR.string.settings_su_restrict_title),
summary = stringResource(CoreR.string.settings_su_restrict_summary),
checked = restrict,
onCheckedChange = {
restrict = it
Config.suRestrict = it
}
)
}
}
}
// --- Dialogs ---
@Composable
private fun UpdateChannelUrlDialog(show: Boolean, onDismiss: () -> Unit) {
val showState = rememberSaveable { mutableStateOf(show) }
showState.value = show
var url by rememberSaveable { mutableStateOf(Config.customChannelUrl) }
SuperDialog(
show = showState,
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
TextField(
value = url,
onValueChange = { url = it },
modifier = Modifier.fillMaxWidth(),
label = stringResource(CoreR.string.settings_update_custom_msg)
)
Spacer(Modifier.height(16.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = {
Config.customChannelUrl = url
Info.resetUpdate()
onDismiss()
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun DownloadPathDialog(show: Boolean, onDismiss: () -> Unit) {
val showState = rememberSaveable { mutableStateOf(show) }
showState.value = show
var path by rememberSaveable { mutableStateOf(Config.downloadDir) }
SuperDialog(
show = showState,
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
top.yukonga.miuix.kmp.basic.Text(
text = stringResource(CoreR.string.settings_download_path_message, MediaStoreUtils.fullPath(path)),
modifier = Modifier.padding(bottom = 8.dp)
)
TextField(
value = path,
onValueChange = { path = it },
modifier = Modifier.fillMaxWidth(),
label = stringResource(CoreR.string.settings_download_path_title)
)
Spacer(Modifier.height(16.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = {
Config.downloadDir = path
onDismiss()
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun HideAppDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
val showState = rememberSaveable { mutableStateOf(show) }
showState.value = show
var appName by rememberSaveable { mutableStateOf("Settings") }
val isError = appName.length > AppMigration.MAX_LABEL_LENGTH || appName.isBlank()
SuperDialog(
show = showState,
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
TextField(
value = appName,
onValueChange = { appName = it },
modifier = Modifier.fillMaxWidth(),
label = stringResource(CoreR.string.settings_app_name_hint)
)
Spacer(Modifier.height(16.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = { if (!isError) onConfirm(appName) },
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun RestoreDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit) {
val showState = rememberSaveable { mutableStateOf(show) }
showState.value = show
SuperDialog(
show = showState,
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
top.yukonga.miuix.kmp.basic.Text(
text = stringResource(CoreR.string.restore_app_confirmation)
)
Spacer(Modifier.height(16.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = onConfirm,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@@ -1,132 +1,78 @@
package com.topjohnwu.magisk.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
class SettingsViewModel : BaseViewModel() {
val items = createItems()
val extraBindings = bindExtra {
it.put(BR.handler, this)
private val _denyListEnabled = MutableStateFlow(Config.denyList)
val denyListEnabled: StateFlow<Boolean> = _denyListEnabled.asStateFlow()
val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled
fun navigateToTheme() {
SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
}
private fun createItems(): List<BaseSettingsItem> {
val context = AppContext
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
// Customization
val list = mutableListOf(
Customization,
Theme, if (LocaleSetting.useLocaleManager) LanguageSystem else Language
)
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context))
list.add(AddShortcut)
// Manager
list.addAll(listOf(
AppSettings,
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath, RandNameToggle
))
if (Info.env.isActive && Const.USER_ID == 0) {
if (hidden) list.add(Restore) else list.add(Hide)
}
// Magisk
if (Info.env.isActive) {
list.addAll(listOf(
Magisk,
SystemlessHosts
))
if (Const.Version.atLeast_24_0()) {
list.addAll(listOf(Zygisk, DenyList, DenyListConfig))
}
}
// Superuser
if (Info.showSuperUser) {
list.addAll(listOf(
Superuser,
Tapjack, Authentication, AccessMode, MultiuserMode, MountNamespaceMode,
AutomaticResponse, RequestTimeout, SUNotification
))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Re-authenticate is not feasible on 8.0+
list.add(Reauthenticate)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Can hide overlay windows on 12.0+
list.remove(Tapjack)
}
if (Const.Version.atLeast_30_1()) {
list.add(Restrict)
}
}
return list
fun navigateToDenyList() {
SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
}
override fun onItemPressed(view: View, item: BaseSettingsItem, doAction: () -> Unit) {
when (item) {
DownloadPath -> withExternalRW(doAction)
UpdateChecker -> withPostNotificationPermission(doAction)
Authentication -> AuthEvent(doAction).publish()
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction()
else -> doAction()
}
fun requestAddShortcut() {
AddHomeIconEvent().publish()
}
override fun onItemAction(view: View, item: BaseSettingsItem) {
when (item) {
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
LanguageSystem -> view.activity.startActivity(LocaleSetting.localeSettingsIntent)
AddShortcut -> AddHomeIconEvent().publish()
SystemlessHosts -> createHosts()
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
UpdateChannel -> openUrlIfNecessary(view)
is Hide -> viewModelScope.launch { AppMigration.hide(view.activity, item.value) }
Restore -> viewModelScope.launch { AppMigration.restore(view.activity) }
Zygisk -> if (Zygisk.mismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
else -> Unit
}
fun hideApp(activity: Activity, name: String) {
viewModelScope.launch { AppMigration.hide(activity, name) }
}
private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.refresh()
if (UpdateChannelUrl.isEnabled && UpdateChannelUrl.value.isBlank()) {
UpdateChannelUrl.onPressed(view, this)
}
fun restoreApp(activity: Activity) {
viewModelScope.launch { AppMigration.restore(activity) }
}
private fun createHosts() {
fun createHosts() {
viewModelScope.launch {
RootUtils.addSystemlessHosts()
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
fun toggleDenyList(enabled: Boolean) {
_denyListEnabled.value = enabled
val cmd = if (enabled) "enable" else "disable"
Shell.cmd("magisk --denylist $cmd").submit { result ->
if (result.isSuccess) {
Config.denyList = enabled
} else {
_denyListEnabled.value = !enabled
}
}
}
fun withDownloadPathPermission(action: () -> Unit) = withExternalRW(action)
fun withNotificationPermission(action: () -> Unit) = withPostNotificationPermission(action)
fun withAuth(action: () -> Unit) = AuthEvent(action).publish()
fun notifyZygiskChange() {
if (zygiskMismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
}
}
@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.Hide" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_app_name_hint"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:counterEnabled="true"
app:counterMaxLength="@{data.maxLength}"
app:counterOverflowTextColor="?colorError"
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
app:errorEnabled="true"
app:errorTextColor="?colorError"
app:helperText="@string/settings_app_name_helper"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:text="@={data.result}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.DownloadPath" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<TextView
android:id="@+id/dialog_custom_download_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{@string/settings_download_path_message(data.path)}"
android:textAppearance="@style/AppearanceFoundation.Caption"
tools:text="@string/settings_download_path_message" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_download_path_title"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:errorTextColor="?colorError"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={data.inputResult}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.ui.settings.UpdateChannelUrl" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_update_custom_msg"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:errorTextColor="?colorError"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={data.inputResult}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.settings.SettingsViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
app:items="@{viewModel.items}"
app:extraBindings="@{viewModel.extraBindings}"
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="false"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_marginTop="24dp"
tools:listitem="@layout/item_settings"
tools:paddingTop="@dimen/l1" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/snackbar_container"
app:fitsSystemWindowsInsets="top|bottom"/>
</FrameLayout>
</layout>
@@ -1,90 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.ui.settings.BaseSettingsItem" />
<variable
name="handler"
type="com.topjohnwu.magisk.ui.settings.BaseSettingsItem.Handler" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
isEnabled="@{item.enabled}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:clickable="@{item.enabled}"
android:focusable="@{item.enabled}"
android:onClick="@{(view) -> item.onPressed(view, handler)}"
tools:layout_gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@{item.icon == 0 ? @dimen/l1 : 0}"
android:paddingEnd="@dimen/l1">
<ImageView
android:id="@+id/icon"
style="@style/WidgetFoundation.Icon"
gone="@{item.icon == 0}"
android:background="@null"
app:srcCompat="@{item.icon}"
tools:srcCompat="@drawable/ic_fingerprint" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l1">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="start"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<TextView
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Variant"
tools:lines="2"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/selector_indicator"
goneUnless="@{item.showSwitch}"
isEnabled="@{item.enabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@{item.checked}"
android:focusable="@{item.enabled}"
android:onCheckedChanged="@{(v, c) -> item.onToggle(v, handler, c)}" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.ui.settings.BaseSettingsItem" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:paddingBottom="@dimen/l_50">
<TextView
gone="@{item.title.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Large.Secondary"
android:textStyle="bold"
tools:text="@tools:sample/lorem" />
<TextView
gone="@{item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.description}"
android:textAppearance="@style/AppearanceFoundation.Tiny.Bold"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</layout>