mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-05-24 10:04:36 -04:00
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:
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user