Cleanup hide fragment code

This commit is contained in:
topjohnwu 2020-08-26 04:23:43 -07:00
parent b44dcc2da0
commit 84f92bd661
12 changed files with 172 additions and 196 deletions

View File

@ -1,23 +1,13 @@
package com.topjohnwu.magisk.data.repository
import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.await
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.ktx.packageName
import com.topjohnwu.magisk.ui.hide.HideAppInfo
import com.topjohnwu.magisk.ui.hide.HideTarget
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class MagiskRepository(
private val apiRaw: GithubRawServices,
private val packageManager: PackageManager
private val apiRaw: GithubRawServices
) {
suspend fun fetchUpdate() = try {
@ -40,35 +30,4 @@ class MagiskRepository(
null
}
suspend fun fetchApps() = withContext(Dispatchers.Default) {
packageManager.getInstalledApplications(0).filter {
it.enabled && !blacklist.contains(it.packageName)
}.map {
val label = it.getLabel(packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}.filter { it.processes.isNotEmpty() }
}
suspend fun fetchHideTargets() =
Shell.su("magiskhide --ls").await().out.map { HideTarget(it) }
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()
private val Boolean.state get() = if (this) "add" else "rm"
companion object {
private val blacklist by lazy { listOf(
packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
) }
}
}

View File

@ -28,10 +28,17 @@ abstract class RvItem {
abstract class ComparableRvItem<in T> : RvItem() {
abstract fun itemSameAs(other: T): Boolean
abstract fun contentSameAs(other: T): Boolean
// Use Any.equals by default
open fun itemSameAs(other: T) = this == other
// Use compareTo if this is Comparable or assume not same
@Suppress("UNCHECKED_CAST")
open fun contentSameAs(other: T) =
(this as? Comparable<T>)?.run { compareTo(other) == 0 } ?: false
@Suppress("UNCHECKED_CAST")
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T)
@Suppress("UNCHECKED_CAST")
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)

View File

@ -7,7 +7,7 @@ import org.koin.dsl.module
val repositoryModule = module {
single { MagiskRepository(get(), get()) }
single { MagiskRepository(get()) }
single { LogRepository(get()) }
single { StringRepository(get()) }
}

View File

@ -17,7 +17,7 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModules = module {
viewModel { HideViewModel(get()) }
viewModel { HideViewModel() }
viewModel { HomeViewModel(get()) }
viewModel { LogViewModel(get()) }
viewModel { ModuleViewModel(get(), get(), get()) }

View File

@ -65,40 +65,22 @@ val PackageInfo.processes
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
val ApplicationInfo.packageInfo: PackageInfo?
get() {
val pm: PackageManager by inject()
val ApplicationInfo.packageInfo: PackageInfo get() {
val pm = get<PackageManager>()
return try {
val request = GET_ACTIVITIES or
GET_SERVICES or
GET_RECEIVERS or
GET_PROVIDERS
pm.getPackageInfo(packageName, request)
} catch (e1: Exception) {
try {
pm.activities(packageName).apply {
services = pm.services(packageName)
receivers = pm.receivers(packageName)
providers = pm.providers(packageName)
}
} catch (e2: Exception) {
null
}
return try {
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
pm.getPackageInfo(packageName, request)
} catch (e: Exception) {
// Exceed binder data transfer limit, fetch each component type separately
pm.getPackageInfo(packageName, 0).apply {
runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities }
runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services }
runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers }
runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers }
}
}
fun PackageManager.activities(packageName: String) =
getPackageInfo(packageName, GET_ACTIVITIES)
fun PackageManager.services(packageName: String) =
getPackageInfo(packageName, GET_SERVICES).services
fun PackageManager.receivers(packageName: String) =
getPackageInfo(packageName, GET_RECEIVERS).receivers
fun PackageManager.providers(packageName: String) =
getPackageInfo(packageName, GET_PROVIDERS).providers
}
fun Context.rawResource(id: Int) = resources.openRawResource(id)

View File

@ -0,0 +1,47 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.ktx.getLabel
class HideTarget(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
class HideAppInfo(info: ApplicationInfo, pm: PackageManager)
: ApplicationInfo(info), Comparable<HideAppInfo> {
val label = info.getLabel(pm)
val iconImage: Drawable = info.loadIcon(pm)
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<HideAppInfo>(
{ it.label.toLowerCase(currentLocale) },
{ it.packageName }
)
}
}
data class HideProcessInfo(
val name: String,
val packageName: String,
val isHidden: Boolean
)
class HideAppTarget(
val info: HideAppInfo,
val processes: List<HideProcessInfo>
) : Comparable<HideAppTarget> {
override fun compareTo(other: HideAppTarget) = compareValuesBy(this, other) { it.info }
}

View File

@ -1,25 +0,0 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.ktx.packageInfo
import com.topjohnwu.magisk.ktx.processes
data class HideAppInfo(
val info: ApplicationInfo,
val name: String,
val icon: Drawable
) {
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
}
data class StatefulProcess(
val name: String,
val packageName: String,
val isHidden: Boolean
)
data class HideAppTarget(
val info: HideAppInfo,
val processes: List<StatefulProcess>
)

View File

@ -9,27 +9,27 @@ import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.utils.addOnPropertyChangedCallback
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
class HideItem(
val item: HideAppTarget,
viewModel: HideViewModel
) : ObservableItem<HideItem>() {
app: HideAppTarget
) : ObservableItem<HideItem>(), Comparable<HideItem> {
override val layoutRes = R.layout.item_hide_md2
val packageName = item.info.info.packageName.orEmpty()
val items = item.processes.map { HideProcessItem(it, viewModel) }
val info = app.info
val processes = app.processes.map { HideProcessItem(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.itemsCheckedPercent)
set(value) = set(value, field, { field = it }, BR.checkedPercent)
@get:Bindable
val itemsCheckedPercent get() = (itemsChecked.toFloat() / items.size * 100).roundToInt()
val checkedPercent get() = (itemsChecked.toFloat() / processes.size * 100).roundToInt()
private var state: Boolean? = false
set(value) = set(value, field, { field = it }, BR.hiddenState)
@ -39,14 +39,14 @@ class HideItem(
get() = state
set(value) = set(value, state, { state = it }, BR.hiddenState) {
if (value == true) {
items.filterNot { it.isHidden }
processes.filterNot { it.isHidden }
} else {
items
processes
}.forEach { it.toggle() }
}
init {
items.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } }
processes.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } }
recalculateChecked()
}
@ -56,37 +56,44 @@ class HideItem(
}
private fun recalculateChecked() {
itemsChecked = items.count { it.isHidden }
itemsChecked = processes.count { it.isHidden }
state = when (itemsChecked) {
0 -> false
items.size -> true
processes.size -> true
else -> null
}
}
override fun contentSameAs(other: HideItem): Boolean = item == other.item
override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info
override fun compareTo(other: HideItem) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<HideItem>(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class HideProcessItem(
val item: StatefulProcess,
val viewModel: HideViewModel
val process: HideProcessInfo
) : ObservableItem<HideProcessItem>() {
override val layoutRes = R.layout.item_hide_process_md2
@get:Bindable
var isHidden = item.isHidden
var isHidden = process.isHidden
set(value) = set(value, field, { field = it }, BR.hidden) {
viewModel.toggleItem(this)
val arg = if (isHidden) "add" else "rm"
val (name, pkg) = process
Shell.su("magiskhide --$arg $pkg $name").submit()
}
fun toggle() {
isHidden = !isHidden
}
override fun contentSameAs(other: HideProcessItem) = item == other.item
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name
override fun contentSameAs(other: HideProcessItem) = process == other.process
override fun itemSameAs(other: HideProcessItem) = process.name == other.process.name
}

View File

@ -1,14 +0,0 @@
package com.topjohnwu.magisk.ui.hide
class HideTarget(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
@ -9,16 +10,17 @@ import com.topjohnwu.magisk.arch.Queryable
import com.topjohnwu.magisk.arch.filterableListOf
import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.packageInfo
import com.topjohnwu.magisk.ktx.packageName
import com.topjohnwu.magisk.ktx.processes
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HideViewModel(
private val magiskRepo: MagiskRepository
) : BaseViewModel(), Queryable {
class HideViewModel : BaseViewModel(), Queryable {
override val queryDelay = 1000L
@ -45,63 +47,74 @@ class HideViewModel(
override fun refresh() = viewModelScope.launch {
state = State.LOADING
val apps = magiskRepo.fetchApps()
val hides = magiskRepo.fetchHideTargets()
val (appList, diff) = withContext(Dispatchers.Default) {
val list = apps
val (apps, diff) = withContext(Dispatchers.Default) {
val pm = get<PackageManager>()
val hides = Shell.su("magiskhide --ls").exec().out.map { HideTarget(it) }
val apps = pm.getInstalledApplications(0)
.asSequence()
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map { HideAppInfo(it, pm) }
.map { createTarget(it, hides) }
.map { HideItem(it, this@HideViewModel) }
.sort()
list to items.calculateDiff(list)
.filter { it.processes.isNotEmpty() }
.map { HideItem(it) }
.toList()
.sorted()
apps to items.calculateDiff(apps)
}
items.update(appList, diff)
items.update(apps, diff)
submitQuery()
}
// ---
private fun createTarget(info: HideAppInfo, hideList: List<HideTarget>): HideAppTarget {
val pkg = info.packageName
val hidden = hideList.filter { it.packageName == pkg }
val processNames = info.packageInfo.processes.distinct()
val processes = processNames.map { name ->
HideProcessInfo(name, pkg, hidden.any { name == it.process })
}
return HideAppTarget(info, processes)
}
// ---
override fun query() {
items.filter {
fun showHidden() = it.itemsChecked != 0
fun filterSystem() =
isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
fun filterQuery(): Boolean {
fun inName() = it.info.label.contains(query, true)
fun inPackage() = it.info.packageName.contains(query, true)
fun inProcesses() = it.processes.any { p -> p.process.name.contains(query, true) }
return inName() || inPackage() || inProcesses()
}
showHidden() || (filterSystem() && filterQuery())
}
state = State.LOADED
}
// ---
private fun createTarget(app: HideAppInfo, hideList: List<HideTarget>): HideAppTarget {
val hidden = hideList.filter { it.packageName == app.info.packageName }
val packageName = app.info.packageName
val processes = app.processes.map { name ->
StatefulProcess(name, packageName, hidden.any { name == it.process })
}
return HideAppTarget(app, processes)
}
private fun List<HideItem>.sort() = compareByDescending<HideItem> { it.itemsChecked != 0 }
.thenBy { it.item.info.name.toLowerCase(currentLocale) }
.thenBy { it.item.info.info.packageName }
.let { sortedWith(it) }
// ---
override fun query() = items.filter {
fun showHidden()= it.itemsChecked != 0
fun filterSystem(): Boolean {
return isShowSystem || it.item.info.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
}
fun filterQuery(): Boolean {
fun inName() = it.item.info.name.contains(query, true)
fun inPackage() = it.item.info.info.packageName.contains(query, true)
fun inProcesses() = it.item.processes.any { it.name.contains(query, true) }
return inName() || inPackage() || inProcesses()
}
showHidden() || (filterSystem() && filterQuery())
}
// ---
fun toggleItem(item: HideProcessItem) {
magiskRepo.toggleHide(item.isHidden, item.item.packageName, item.item.name)
}
fun resetQuery() {
query = ""
}
companion object {
private val blacklist by lazy { listOf(
packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
) }
}
}

View File

@ -39,12 +39,12 @@
android:id="@+id/hide_icon"
style="@style/WidgetFoundation.Image"
android:layout_margin="@dimen/l1"
android:src="@{item.item.info.icon}"
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_magisk" />
tools:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/hide_name"
@ -53,7 +53,7 @@
android:layout_marginStart="@dimen/l1"
android:ellipsize="middle"
android:singleLine="true"
android:text="@{item.item.info.name}"
android:text="@{item.info.label}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/hide_package"
@ -65,10 +65,9 @@
<TextView
android:id="@+id/hide_package"
gone="@{item.item.info.info.packageName.empty}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.item.info.info.packageName}"
android:text="@{item.info.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/hide_name"
@ -90,7 +89,7 @@
<androidx.recyclerview.widget.RecyclerView
goneUnless="@{item.isExpanded}"
itemBinding="@{viewModel.itemInternalBinding}"
items="@{item.items}"
items="@{item.processes}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurfaceVariant"
@ -105,7 +104,7 @@
style="@style/WidgetFoundation.ProgressBar"
android:layout_width="match_parent"
android:layout_gravity="top"
android:progress="@{item.itemsCheckedPercent}" />
android:progress="@{item.checkedPercent}" />
</com.google.android.material.card.MaterialCardView>

View File

@ -29,7 +29,8 @@
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_75"
android:singleLine="true"
android:text="@{item.item.name}"
android:ellipsize="middle"
android:text="@{item.process.name}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_process_checkbox"