diff --git a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt index 7812a753c..f33827f95 100644 --- a/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt +++ b/app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt @@ -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" - ) } - } - } diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt index 4ae6d3c61..66893e54a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt @@ -28,10 +28,17 @@ abstract class RvItem { abstract class ComparableRvItem : 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)?.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) diff --git a/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt index 6f2de1747..e44360ba8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/RepositoryModule.kt @@ -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()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt index 59c3ed3b5..dd2c4b191 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -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()) } diff --git a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt index dfd0a89f4..7608eee18 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt @@ -65,40 +65,22 @@ val PackageInfo.processes val Array.processNames get() = mapNotNull { it.processName } -val ApplicationInfo.packageInfo: PackageInfo? - get() { - val pm: PackageManager by inject() +val ApplicationInfo.packageInfo: PackageInfo get() { + val pm = get() - 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) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideApp.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideApp.kt new file mode 100644 index 000000000..e55a3db35 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideApp.kt @@ -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 { + + 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( + { 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 +) : Comparable { + override fun compareTo(other: HideAppTarget) = compareValuesBy(this, other) { it.info } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideAppInfo.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideAppInfo.kt deleted file mode 100644 index 425f62900..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideAppInfo.kt +++ /dev/null @@ -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 -) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItems.kt similarity index 56% rename from app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItem.kt rename to app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItems.kt index 3b99406b7..90511ea70 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideRvItems.kt @@ -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() { + app: HideAppTarget +) : ObservableItem(), Comparable { 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( + { it.itemsChecked == 0 }, + { it.info } + ) + } } class HideProcessItem( - val item: StatefulProcess, - val viewModel: HideViewModel + val process: HideProcessInfo ) : ObservableItem() { 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 } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideTarget.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideTarget.kt deleted file mode 100644 index 12309b9f0..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideTarget.kt +++ /dev/null @@ -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 } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt index e0d78337b..159f8be79 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt @@ -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() + 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): 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): 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.sort() = compareByDescending { 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" + ) } + } } diff --git a/app/src/main/res/layout/item_hide_md2.xml b/app/src/main/res/layout/item_hide_md2.xml index bd4cd2863..2cb86b700 100644 --- a/app/src/main/res/layout/item_hide_md2.xml +++ b/app/src/main/res/layout/item_hide_md2.xml @@ -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" /> + android:progress="@{item.checkedPercent}" /> diff --git a/app/src/main/res/layout/item_hide_process_md2.xml b/app/src/main/res/layout/item_hide_process_md2.xml index 70c7423bc..41e4e19b4 100644 --- a/app/src/main/res/layout/item_hide_process_md2.xml +++ b/app/src/main/res/layout/item_hide_process_md2.xml @@ -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"