Migrate DenyList screen to Jetpack Compose with miuix

Replace the DenyList screen's data-binding XML layouts and RecyclerView
system with a Compose UI using LazyColumn, Card, Checkbox, and Switch.

DenyListViewModel now uses StateFlow with combine for reactive filtering
instead of the custom filterList/ObservableHost pattern. App and process
state is tracked via Compose mutableStateOf for efficient recomposition.

Remove DenyListRvItem.kt and all associated XML layouts (fragment_deny_md2,
item_hide_md2, item_hide_process_md2).

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 12:15:10 +08:00
committed by topjohnwu
parent 47f1a33585
commit ffdb5cb511
7 changed files with 389 additions and 493 deletions

View File

@@ -1,99 +1,63 @@
package com.topjohnwu.magisk.ui.deny
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.hideKeyboard
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
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.ui.theme.MagiskTheme
import com.topjohnwu.magisk.core.R as CoreR
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>(), MenuProvider {
class DenyListFragment : Fragment(), ViewModelHolder {
override val layoutRes = R.layout.fragment_deny_md2
override val viewModel by viewModel<DenyListViewModel>()
override val viewModel by lazy {
ViewModelProvider(this, VMFactory)[DenyListViewModel::class.java]
}
private lateinit var searchView: SearchView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.denylist)
(activity as? NavigationActivity<*>)?.setTitle(CoreR.string.denylist)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) activity?.hideKeyboard()
}
})
binding.appList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onPreBind(binding: FragmentDenyMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (searchView.isIconfiedByDefault && !searchView.isIconified) {
searchView.isIconified = true
return true
}
return super.onBackPressed()
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_deny_md2, menu)
searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.queryHint = searchView.context.getString(CoreR.string.hide_filter_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query = query ?: ""
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.query = newText ?: ""
return true
}
})
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_show_system -> {
val check = !item.isChecked
viewModel.isShowSystem = check
item.isChecked = check
return true
}
R.id.action_show_OS -> {
val check = !item.isChecked
viewModel.isShowOS = check
item.isChecked = check
return true
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MagiskTheme {
DenyListScreen(viewModel = viewModel as DenyListViewModel)
}
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareMenu(menu: Menu) {
val showSystem = menu.findItem(R.id.action_show_system)
val showOS = menu.findItem(R.id.action_show_OS)
showOS.isEnabled = showSystem.isChecked
override fun onResume() {
super.onResume()
(viewModel as DenyListViewModel).startLoading()
}
override fun onEventDispatched(event: ViewEvent) {
when (event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) }
}
}
}

View File

@@ -1,130 +0,0 @@
package com.topjohnwu.magisk.ui.deny
import android.view.View
import android.view.ViewGroup
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
class DenyListRvItem(
val info: AppProcessInfo
) : ObservableRvItem(), DiffItem<DenyListRvItem>, Comparable<DenyListRvItem> {
override val layoutRes get() = R.layout.item_hide_md2
val processes = info.processes.map { ProcessRvItem(it) }
@get:Bindable
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
var itemsChecked = 0
set(value) = set(value, field, { field = it }, BR.checkedPercent)
val isChecked get() = itemsChecked != 0
@get:Bindable
val checkedPercent get() = (itemsChecked.toFloat() / processes.size * 100).roundToInt()
private var _state: Boolean? = false
set(value) = set(value, field, { field = it }, BR.state)
@get:Bindable
var state: Boolean?
get() = _state
set(value) = set(value, _state, { _state = it }, BR.state) {
if (value == true) {
processes
.filterNot { it.isEnabled }
.filter { isExpanded || it.defaultSelection }
.forEach { it.toggle() }
} else {
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
processes.filter { it.isEnabled }.forEach {
if (it.process.isIsolated) {
it.toggle()
} else {
it.isEnabled = !it.isEnabled
notifyPropertyChanged(BR.enabled)
}
}
}
}
init {
processes.forEach { it.addOnPropertyChangedCallback(BR.enabled) { recalculateChecked() } }
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
recalculateChecked()
}
fun toggleExpand(v: View) {
(v.parent as? ViewGroup)?.startAnimations()
isExpanded = !isExpanded
}
private fun recalculateChecked() {
itemsChecked = processes.count { it.isEnabled }
_state = if (isExpanded) {
when (itemsChecked) {
0 -> false
processes.size -> true
else -> null
}
} else {
val defaultProcesses = processes.filter { it.defaultSelection }
when (defaultProcesses.count { it.isEnabled }) {
0 -> false
defaultProcesses.size -> true
else -> null
}
}
}
override fun compareTo(other: DenyListRvItem) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<DenyListRvItem>(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class ProcessRvItem(
val process: ProcessInfo
) : ObservableRvItem(), DiffItem<ProcessRvItem> {
override val layoutRes get() = R.layout.item_hide_process_md2
val displayName = if (process.isIsolated) "(isolated) ${process.name}" else process.name
@get:Bindable
var isEnabled
get() = process.isEnabled
set(value) = set(value, process.isEnabled, { process.isEnabled = it }, BR.enabled) {
val arg = if (it) "add" else "rm"
val (name, pkg) = process
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
}
fun toggle() {
isEnabled = !isEnabled
}
val defaultSelection get() =
process.isIsolated || process.isAppZygote || process.name == process.packageName
override fun itemSameAs(other: ProcessRvItem) =
process.name == other.process.name && process.packageName == other.process.packageName
override fun contentSameAs(other: ProcessRvItem) =
process.isEnabled == other.process.isEnabled
}

View File

@@ -0,0 +1,245 @@
package com.topjohnwu.magisk.ui.deny
import androidx.compose.animation.AnimatedVisibility
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
import top.yukonga.miuix.kmp.basic.Switch
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun DenyListScreen(viewModel: DenyListViewModel) {
val loading by viewModel.loading.collectAsState()
val apps by viewModel.filteredApps.collectAsState()
val query by viewModel.query.collectAsState()
val showSystem by viewModel.showSystem.collectAsState()
val showOS by viewModel.showOS.collectAsState()
Column(modifier = Modifier.fillMaxSize()) {
// Search input
SearchInput(
query = query,
onQueryChange = viewModel::setQuery,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
// Filter chips
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
FilterChip(
label = stringResource(CoreR.string.show_system_app),
checked = showSystem,
onCheckedChange = viewModel::setShowSystem
)
FilterChip(
label = stringResource(CoreR.string.show_os_app),
checked = showOS,
enabled = showSystem,
onCheckedChange = viewModel::setShowOS
)
}
Spacer(Modifier.height(8.dp))
if (loading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(CoreR.string.loading),
style = MiuixTheme.textStyles.headline2
)
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = apps,
key = { it.info.packageName }
) { app ->
DenyAppCard(app)
}
item { Spacer(Modifier.height(8.dp)) }
}
}
}
}
@Composable
private fun SearchInput(query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) {
top.yukonga.miuix.kmp.basic.TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier,
label = stringResource(CoreR.string.hide_filter_hint)
)
}
@Composable
private fun FilterChip(
label: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true
) {
Row(
modifier = Modifier
.clickable(enabled = enabled) { onCheckedChange(!checked) }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Checkbox(
checked = checked,
onCheckedChange = if (enabled) onCheckedChange else null,
enabled = enabled
)
Text(
text = label,
style = MiuixTheme.textStyles.body2,
color = if (enabled) MiuixTheme.colorScheme.onSurface
else MiuixTheme.colorScheme.disabledOnSecondaryVariant
)
}
}
@Composable
private fun DenyAppCard(app: DenyAppState) {
Card(modifier = Modifier.fillMaxWidth()) {
Column {
// Progress bar showing percentage of checked processes
if (app.checkedPercent > 0f) {
LinearProgressIndicator(
progress = app.checkedPercent,
modifier = Modifier.fillMaxWidth()
)
}
// App row
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { app.isExpanded = !app.isExpanded }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberDrawablePainter(app.info.iconImage),
contentDescription = app.info.label,
modifier = Modifier.size(40.dp)
)
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = app.info.label,
style = MiuixTheme.textStyles.body1,
)
Text(
text = app.info.packageName,
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
Checkbox(
checked = app.isChecked,
onCheckedChange = { app.toggleAll() }
)
}
// Expanded process list
AnimatedVisibility(visible = app.isExpanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 52.dp)
) {
app.processes.forEach { proc ->
ProcessRow(proc)
}
}
}
}
}
}
@Composable
private fun ProcessRow(proc: DenyProcessState) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { proc.toggle() }
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = proc.displayName,
style = MiuixTheme.textStyles.body2,
color = if (proc.isEnabled) MiuixTheme.colorScheme.onSurface
else MiuixTheme.colorScheme.onSurfaceVariantSummary,
modifier = Modifier.weight(1f)
)
Switch(
checked = proc.isEnabled,
onCheckedChange = { proc.toggle() }
)
}
}
@Composable
private fun rememberDrawablePainter(drawable: Drawable): Painter {
return remember(drawable) {
val w = drawable.intrinsicWidth.coerceAtLeast(1)
val h = drawable.intrinsicHeight.coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
drawable.draw(canvas)
BitmapPainter(bitmap.asImageBitmap())
}
}

View File

@@ -2,54 +2,64 @@ package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import androidx.databinding.Bindable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.concurrentMap
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.filterList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.withContext
class DenyListViewModel : AsyncLoadViewModel() {
var isShowSystem = false
set(value) {
field = value
doQuery(query)
private val _loading = MutableStateFlow(true)
val loading: StateFlow<Boolean> = _loading.asStateFlow()
private val _allApps = MutableStateFlow<List<DenyAppState>>(emptyList())
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
private val _showSystem = MutableStateFlow(false)
val showSystem: StateFlow<Boolean> = _showSystem.asStateFlow()
private val _showOS = MutableStateFlow(false)
val showOS: StateFlow<Boolean> = _showOS.asStateFlow()
val filteredApps: StateFlow<List<DenyAppState>> = combine(
_allApps, _query, _showSystem, _showOS
) { apps, q, showSys, showOS ->
apps.filter { app ->
val passFilter = app.isChecked ||
((showSys || !app.info.isSystemApp()) &&
((showSys && showOS) || app.info.isApp()))
val passQuery = q.isBlank() ||
app.info.label.contains(q, true) ||
app.info.packageName.contains(q, true) ||
app.processes.any { it.process.name.contains(q, true) }
passFilter && passQuery
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
var isShowOS = false
set(value) {
field = value
doQuery(query)
}
var query = ""
set(value) {
field = value
doQuery(value)
}
val items = filterList<DenyListRvItem>(viewModelScope)
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
fun setQuery(q: String) { _query.value = q }
fun setShowSystem(v: Boolean) { _showSystem.value = v }
fun setShowOS(v: Boolean) { _showOS.value = v }
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
loading = true
_loading.value = true
val apps = withContext(Dispatchers.Default) {
val pm = AppContext.packageManager
val denyList = Shell.cmd("magisk --denylist ls").exec().out
@@ -59,31 +69,69 @@ class DenyListViewModel : AsyncLoadViewModel() {
.filter { AppContext.packageName != it.packageName }
.concurrentMap { AppProcessInfo(it, pm, denyList) }
.filter { it.processes.isNotEmpty() }
.concurrentMap { DenyListRvItem(it) }
.concurrentMap { DenyAppState(it) }
.toCollection(ArrayList(size))
}
apps.sort()
apps.sortWith(compareBy(
{ it.processes.count { p -> p.isEnabled } == 0 },
{ it.info }
))
apps
}
items.set(apps)
doQuery(query)
}
private fun doQuery(s: String) {
items.filter {
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
fun filterQuery(): Boolean {
fun inName() = it.info.label.contains(s, true)
fun inPackage() = it.info.packageName.contains(s, true)
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
return inName() || inPackage() || inProcesses()
}
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
}
loading = false
_allApps.value = apps
_loading.value = false
}
}
class DenyAppState(val info: AppProcessInfo) : Comparable<DenyAppState> {
val processes = info.processes.map { DenyProcessState(it) }
var isExpanded by mutableStateOf(false)
val itemsChecked: Int get() = processes.count { it.isEnabled }
val isChecked: Boolean get() = itemsChecked > 0
val checkedPercent: Float get() = if (processes.isEmpty()) 0f else itemsChecked.toFloat() / processes.size
fun toggleAll() {
if (isChecked) {
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
processes.filter { it.isEnabled }.forEach { proc ->
if (proc.process.isIsolated) {
proc.toggle()
} else {
proc.isEnabled = false
}
}
} else {
processes
.filterNot { it.isEnabled }
.filter { if (isExpanded) true else it.defaultSelection }
.forEach { it.toggle() }
}
}
override fun compareTo(other: DenyAppState) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<DenyAppState>(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class DenyProcessState(val process: ProcessInfo) {
var isEnabled by mutableStateOf(process.isEnabled)
val defaultSelection get() =
process.isIsolated || process.isAppZygote || process.name == process.packageName
val displayName: String =
if (process.isIsolated) "(isolated) ${process.name}" else process.name
fun toggle() {
isEnabled = !isEnabled
val arg = if (isEnabled) "add" else "rm"
val (name, pkg) = process
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
}
}

View File

@@ -1,57 +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.deny.DenyListViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:invisible="@{viewModel.loading}"
app:items="@{viewModel.items}"
app:extraBindings="@{viewModel.extraBindings}"
tools:listitem="@layout/item_hide_md2"
tools:paddingTop="40dp" />
<LinearLayout
goneUnless="@{viewModel.loading}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
android:layout_marginTop="@dimen/l1" />
</LinearLayout>
</FrameLayout>
</layout>

View File

@@ -1,121 +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>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.ui.deny.DenyListRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.deny.DenyListViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/hide_expand"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:nextFocusRight="@id/hide_expand_icon"
android:onClick="@{item::toggleExpand}">
<ImageView
android:id="@+id/hide_icon"
style="@style/WidgetFoundation.Image"
android:layout_margin="@dimen/l1"
android:src="@{item.info.iconImage}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/hide_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:ellipsize="middle"
android:singleLine="true"
android:text="@{item.info.label}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/hide_package"
app:layout_constraintEnd_toStartOf="@+id/hide_expand_icon"
app:layout_constraintStart_toEndOf="@+id/hide_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/magisk" />
<TextView
android:id="@+id/hide_package"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.info.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/hide_name"
app:layout_constraintStart_toStartOf="@+id/hide_name"
app:layout_constraintTop_toBottomOf="@+id/hide_name"
tools:text="com.topjohnwu.magisk" />
<com.topjohnwu.widget.IndeterminateCheckBox
android:id="@+id/hide_expand_icon"
state="@={item.state}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l1"
android:minWidth="0dp"
android:minHeight="0dp"
android:nextFocusLeft="@id/hide_expand"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
goneUnless="@{item.isExpanded}"
app:items="@{item.processes}"
app:extraBindings="@{viewModel.extraBindings}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurfaceVariant"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/item_hide_process_md2" />
</LinearLayout>
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
gone="@{item.checkedPercent == 0}"
android:layout_width="match_parent"
android:layout_gravity="top"
android:progress="@{item.checkedPercent}" />
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@@ -1,53 +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.deny.ProcessRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.deny.DenyListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:alpha="@{item.enabled ? 1f : .7f}">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_75"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_75"
android:singleLine="true"
android:ellipsize="middle"
android:text="@{item.displayName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_process_checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="com.topjohnwu.magisk" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_process_checkbox"
android:checked="@={item.enabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>