mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-01-13 13:28:24 -08:00
Rewrite deny list UI
This commit is contained in:
@@ -9,9 +9,6 @@ import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS
|
||||
import android.content.pm.ServiceInfo.FLAG_USE_APP_ZYGOTE
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
@@ -58,11 +55,6 @@ import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.reflect.Array as JArray
|
||||
|
||||
val ServiceInfo.isIsolated get() = (flags and FLAG_ISOLATED_PROCESS) != 0
|
||||
|
||||
@get:SuppressLint("InlinedApi")
|
||||
val ServiceInfo.useAppZygote get() = (flags and FLAG_USE_APP_ZYGOTE) != 0
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
|
||||
@@ -10,8 +10,6 @@ import android.graphics.drawable.Drawable
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.ktx.isIsolated
|
||||
import com.topjohnwu.magisk.ktx.useAppZygote
|
||||
|
||||
class CmdlineListItem(line: String) {
|
||||
val packageName: String
|
||||
@@ -27,72 +25,63 @@ class CmdlineListItem(line: String) {
|
||||
const val ISOLATED_MAGIC = "isolated"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class AppProcessInfo(info: ApplicationInfo, pm: PackageManager, denyList: List<CmdlineListItem>)
|
||||
: ApplicationInfo(info), Comparable<AppProcessInfo> {
|
||||
class AppProcessInfo(val applicationInfo: ApplicationInfo, pm: PackageManager,
|
||||
denyList: List<CmdlineListItem>) : Comparable<AppProcessInfo> {
|
||||
|
||||
val label = info.getLabel(pm)
|
||||
val iconImage: Drawable = info.loadIcon(pm)
|
||||
val processes = fetchProcesses(pm, denyList)
|
||||
private val denyList = denyList.filter {
|
||||
it.packageName == applicationInfo.packageName || it.packageName == ISOLATED_MAGIC
|
||||
}
|
||||
val label = applicationInfo.getLabel(pm)
|
||||
val iconImage: Drawable = applicationInfo.loadIcon(pm)
|
||||
val processes = fetchProcesses(pm)
|
||||
|
||||
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
|
||||
|
||||
private fun fetchProcesses(
|
||||
pm: PackageManager,
|
||||
denylist: List<CmdlineListItem>
|
||||
): List<ProcessInfo> {
|
||||
// Fetch full PackageInfo
|
||||
val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
|
||||
val packageInfo = try {
|
||||
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, baseFlag or request)
|
||||
} catch (e: NameNotFoundException) {
|
||||
// EdXposed hooked, issue#3276
|
||||
return emptyList()
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, fetch each component type separately
|
||||
pm.getPackageInfo(packageName, baseFlag).apply {
|
||||
runCatching { activities = pm.getPackageInfo(packageName, baseFlag or GET_ACTIVITIES).activities }
|
||||
runCatching { services = pm.getPackageInfo(packageName, baseFlag or GET_SERVICES).services }
|
||||
runCatching { receivers = pm.getPackageInfo(packageName, baseFlag or GET_RECEIVERS).receivers }
|
||||
runCatching { providers = pm.getPackageInfo(packageName, baseFlag or GET_PROVIDERS).providers }
|
||||
}
|
||||
}
|
||||
private fun createProcess(name: String, pkg: String = applicationInfo.packageName) =
|
||||
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
|
||||
|
||||
val enabledList = denylist.filter { it.packageName == packageName || it.packageName == ISOLATED_MAGIC }
|
||||
fun createProcess(name: String, pkg: String = packageName): ProcessInfo {
|
||||
return ProcessInfo(name, pkg, enabledList.any { it.process == name && it.packageName == pkg })
|
||||
}
|
||||
private fun ComponentInfo.getProcName(): String = processName
|
||||
?: applicationInfo.processName
|
||||
?: applicationInfo.packageName
|
||||
|
||||
var haveAppZygote = false
|
||||
fun Array<out ComponentInfo>.processes() = map { createProcess(it.processName) }
|
||||
fun Array<ServiceInfo>.processes() = map {
|
||||
if (it.isIsolated) {
|
||||
if (it.useAppZygote) {
|
||||
haveAppZygote = true
|
||||
// Using app zygote, don't need to track the process
|
||||
null
|
||||
} else {
|
||||
val proc = if (SDK_INT >= 29) "${it.processName}:${it.name}" else it.processName
|
||||
createProcess(proc, ISOLATED_MAGIC)
|
||||
}
|
||||
private fun Array<out ComponentInfo>.processes() = map { createProcess(it.getProcName()) }
|
||||
|
||||
private fun Array<ServiceInfo>.processes() = map {
|
||||
if ((it.flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0) {
|
||||
if ((it.flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) {
|
||||
val proc = applicationInfo.processName ?: applicationInfo.packageName
|
||||
createProcess("${proc}_zygote")
|
||||
} else {
|
||||
createProcess(it.processName)
|
||||
val proc = if (SDK_INT >= 29) "${it.getProcName()}:${it.name}" else it.getProcName()
|
||||
createProcess(proc, ISOLATED_MAGIC)
|
||||
}
|
||||
} else {
|
||||
createProcess(it.getProcName())
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchProcesses(pm: PackageManager): List<ProcessInfo> {
|
||||
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
|
||||
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
val packageInfo = try {
|
||||
pm.getPackageInfo(applicationInfo.packageName, flag)
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, local parsing package
|
||||
pm.getPackageArchiveInfo(applicationInfo.sourceDir, flag) ?: return emptyList()
|
||||
}
|
||||
|
||||
return with(packageInfo) {
|
||||
activities?.processes().orEmpty() +
|
||||
services?.processes().orEmpty() +
|
||||
receivers?.processes().orEmpty() +
|
||||
providers?.processes().orEmpty() +
|
||||
listOf(if (haveAppZygote) createProcess("${processName}_zygote") else null)
|
||||
}.filterNotNull().distinct().sortedBy { it.name }
|
||||
val list = LinkedHashSet<ProcessInfo>()
|
||||
list += packageInfo.activities?.processes().orEmpty()
|
||||
list += packageInfo.services?.processes().orEmpty()
|
||||
list += packageInfo.receivers?.processes().orEmpty()
|
||||
list += packageInfo.providers?.processes().orEmpty()
|
||||
return list.sortedBy { it.name }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<AppProcessInfo>(
|
||||
{ it.label.lowercase(currentLocale) },
|
||||
{ it.packageName }
|
||||
{ it.applicationInfo.packageName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
@@ -17,19 +16,13 @@ import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||
|
||||
class DenyListFragment : BaseUIFragment<DenyListViewModel, FragmentDenyMd2Binding>() {
|
||||
|
||||
override val layoutRes = R.layout.fragment_deny_md2
|
||||
override val viewModel by viewModel<DenyListViewModel>()
|
||||
|
||||
private var isFilterVisible
|
||||
get() = binding.processFilter.isVisible
|
||||
set(value) {
|
||||
if (!value) hideKeyboard()
|
||||
MotionRevealHelper.withViews(binding.processFilter, binding.filterToggle, value)
|
||||
}
|
||||
private lateinit var searchView: SearchView
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -40,12 +33,6 @@ class DenyListFragment : BaseUIFragment<DenyListViewModel, FragmentDenyMd2Bindin
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.filterToggle.setOnClickListener {
|
||||
isFilterVisible = true
|
||||
}
|
||||
binding.appFilterInclude.filterDone.setOnClickListener {
|
||||
isFilterVisible = false
|
||||
}
|
||||
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
|
||||
@@ -66,36 +53,56 @@ class DenyListFragment : BaseUIFragment<DenyListViewModel, FragmentDenyMd2Bindin
|
||||
bottom = l_50,
|
||||
)
|
||||
binding.appList.fixEdgeEffect()
|
||||
|
||||
val lama = binding.appList.layoutManager ?: return
|
||||
lama.isAutoMeasureEnabled = false
|
||||
}
|
||||
|
||||
override fun onPreBind(binding: FragmentDenyMd2Binding) = Unit
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (isFilterVisible) {
|
||||
isFilterVisible = false
|
||||
if (searchView.isIconfiedByDefault && !searchView.isIconified) {
|
||||
searchView.isIconified = true
|
||||
return true
|
||||
}
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_hide_md2, menu)
|
||||
inflater.inflate(R.menu.menu_deny_md2, menu)
|
||||
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
||||
searchView.queryHint = searchView.context.getString(R.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 onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_focus_up -> binding.appList
|
||||
.takeIf { (it.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() ?: 0 > 10 }
|
||||
?.also { it.scrollToPosition(10) }
|
||||
.let { binding.appList }
|
||||
.also { it.post { it.smoothScrollToPosition(0) } }
|
||||
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
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val showSystem = menu.findItem(R.id.action_show_system)
|
||||
val showOS = menu.findItem(R.id.action_show_OS)
|
||||
showOS.isEnabled = showSystem.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,42 +4,39 @@ import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.os.Process
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.Queryable
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.databinding.filterableListOf
|
||||
import com.topjohnwu.magisk.databinding.itemBindingOf
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.stream.Collectors
|
||||
|
||||
class DenyListViewModel : BaseViewModel(), Queryable {
|
||||
|
||||
override val queryDelay = 1000L
|
||||
override val queryDelay = 0L
|
||||
|
||||
@get:Bindable
|
||||
var isShowSystem = Config.showSystemApp
|
||||
set(value) = set(value, field, { field = it }, BR.showSystem) {
|
||||
Config.showSystemApp = it
|
||||
var isShowSystem = false
|
||||
set(value) {
|
||||
field = value
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var isShowOS = false
|
||||
set(value) = set(value, field, { field = it }, BR.showOS) {
|
||||
set(value) {
|
||||
field = value
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var query = ""
|
||||
set(value) = set(value, field, { field = it }, BR.query) {
|
||||
set(value) {
|
||||
field = value
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
@@ -60,64 +57,43 @@ class DenyListViewModel : BaseViewModel(), Queryable {
|
||||
state = State.LOADING
|
||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
||||
val pm = AppContext.packageManager
|
||||
val hideList = Shell.su("magisk --denylist ls").exec().out.map { CmdlineListItem(it) }
|
||||
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES)
|
||||
.asSequence()
|
||||
.filterNot { blacklist.contains(it.packageName) }
|
||||
.map { AppProcessInfo(it, pm, hideList) }
|
||||
val denyList = Shell.su("magisk --denylist ls").exec().out
|
||||
.map { CmdlineListItem(it) }
|
||||
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).parallelStream()
|
||||
.filter { AppContext.packageName != it.packageName }
|
||||
.map { AppProcessInfo(it, pm, denyList) }
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.filter { info -> info.enabled || info.processes.any { it.isEnabled } }
|
||||
.map { DenyListRvItem(it) }
|
||||
.toList()
|
||||
.sorted()
|
||||
.collect(Collectors.toList())
|
||||
apps to items.calculateDiff(apps)
|
||||
}
|
||||
items.update(apps, diff)
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun query() {
|
||||
fun isApp(uid: Int) = run {
|
||||
val appId: Int = uid % 100000
|
||||
appId >= Process.FIRST_APPLICATION_UID && appId <= Process.LAST_APPLICATION_UID
|
||||
}
|
||||
|
||||
fun isSystemApp(flag: Int) = flag and ApplicationInfo.FLAG_SYSTEM != 0
|
||||
|
||||
items.filter {
|
||||
fun showHidden() = it.itemsChecked != 0
|
||||
fun filterSystem() = isShowSystem || !isSystemApp(it.info.applicationInfo.flags)
|
||||
|
||||
fun filterSystem() = isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
|
||||
|
||||
fun isApp(uid: Int) = run {
|
||||
val appId: Int = uid % 100000
|
||||
appId >= Process.FIRST_APPLICATION_UID && appId <= Process.LAST_APPLICATION_UID
|
||||
}
|
||||
|
||||
fun filterOS() = (isShowSystem && isShowOS) || isApp(it.info.uid)
|
||||
fun filterOS() = (isShowSystem && isShowOS) || isApp(it.info.applicationInfo.uid)
|
||||
|
||||
fun filterQuery(): Boolean {
|
||||
fun inName() = it.info.label.contains(query, true)
|
||||
fun inPackage() = it.info.packageName.contains(query, true)
|
||||
fun inPackage() = it.info.applicationInfo.packageName.contains(query, true)
|
||||
fun inProcesses() = it.processes.any { p -> p.process.name.contains(query, true) }
|
||||
return inName() || inPackage() || inProcesses()
|
||||
}
|
||||
|
||||
showHidden() || (filterSystem() && filterOS() && filterQuery())
|
||||
filterSystem() && filterOS() && filterQuery()
|
||||
}
|
||||
state = State.LOADED
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
fun resetQuery() {
|
||||
query = ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val blacklist by lazy { listOf(
|
||||
AppContext.packageName,
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user