From ffdb5cb5114017d3d5fa6fd466a0ddb0bc79ac72 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Tue, 3 Mar 2026 12:15:10 +0800 Subject: [PATCH] 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 --- .../magisk/ui/deny/DenyListFragment.kt | 124 ++++----- .../magisk/ui/deny/DenyListRvItem.kt | 130 ---------- .../magisk/ui/deny/DenyListScreen.kt | 245 ++++++++++++++++++ .../magisk/ui/deny/DenyListViewModel.kt | 152 +++++++---- .../src/main/res/layout/fragment_deny_md2.xml | 57 ---- app/apk/src/main/res/layout/item_hide_md2.xml | 121 --------- .../main/res/layout/item_hide_process_md2.xml | 53 ---- 7 files changed, 389 insertions(+), 493 deletions(-) delete mode 100644 app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListRvItem.kt create mode 100644 app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt delete mode 100644 app/apk/src/main/res/layout/fragment_deny_md2.xml delete mode 100644 app/apk/src/main/res/layout/item_hide_md2.xml delete mode 100644 app/apk/src/main/res/layout/item_hide_process_md2.xml diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListFragment.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListFragment.kt index 5f8bbcacc..28e51d1ef 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListFragment.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListFragment.kt @@ -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(), MenuProvider { +class DenyListFragment : Fragment(), ViewModelHolder { - override val layoutRes = R.layout.fragment_deny_md2 - override val viewModel by viewModel() + 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) } + } } } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListRvItem.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListRvItem.kt deleted file mode 100644 index b301991d1..000000000 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListRvItem.kt +++ /dev/null @@ -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, Comparable { - - 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( - { it.itemsChecked == 0 }, - { it.info } - ) - } - -} - -class ProcessRvItem( - val process: ProcessInfo -) : ObservableRvItem(), DiffItem { - - 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 -} diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt new file mode 100644 index 000000000..0473ced50 --- /dev/null +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt @@ -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()) + } +} diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt index dfca9852d..ce31d8fba 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt @@ -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 = _loading.asStateFlow() + + private val _allApps = MutableStateFlow>(emptyList()) + + private val _query = MutableStateFlow("") + val query: StateFlow = _query.asStateFlow() + + private val _showSystem = MutableStateFlow(false) + val showSystem: StateFlow = _showSystem.asStateFlow() + + private val _showOS = MutableStateFlow(false) + val showOS: StateFlow = _showOS.asStateFlow() + + val filteredApps: StateFlow> = 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(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 { + 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( + { 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() } } diff --git a/app/apk/src/main/res/layout/fragment_deny_md2.xml b/app/apk/src/main/res/layout/fragment_deny_md2.xml deleted file mode 100644 index 89a202a08..000000000 --- a/app/apk/src/main/res/layout/fragment_deny_md2.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/apk/src/main/res/layout/item_hide_md2.xml b/app/apk/src/main/res/layout/item_hide_md2.xml deleted file mode 100644 index 280ee1982..000000000 --- a/app/apk/src/main/res/layout/item_hide_md2.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/apk/src/main/res/layout/item_hide_process_md2.xml b/app/apk/src/main/res/layout/item_hide_process_md2.xml deleted file mode 100644 index 5317e80bb..000000000 --- a/app/apk/src/main/res/layout/item_hide_process_md2.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - -