diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c0ff665..c8df26640 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(project(":app:signing")) implementation("com.github.topjohnwu:jtar:1.0.0") + implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.6") implementation("com.jakewharton.timber:timber:4.7.1") implementation(kotlin("stdlib")) diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt index f14fea6af..6ecbb70f3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/HideAppInfo.kt @@ -5,14 +5,12 @@ import android.graphics.drawable.Drawable import com.topjohnwu.magisk.ktx.packageInfo import com.topjohnwu.magisk.ktx.processes -class HideAppInfo( +data class HideAppInfo( val info: ApplicationInfo, val name: String, val icon: Drawable ) { - val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName) - } data class StatefulProcess( @@ -21,7 +19,7 @@ data class StatefulProcess( val isHidden: Boolean ) -class ProcessHideApp( +data class HideAppTarget( val info: HideAppInfo, val processes: List ) diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt index cc5189fe1..a1402405f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/HideRvItem.kt @@ -7,60 +7,64 @@ import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.R import com.topjohnwu.magisk.databinding.ObservableItem import com.topjohnwu.magisk.ktx.startAnimations -import com.topjohnwu.magisk.model.entity.ProcessHideApp +import com.topjohnwu.magisk.model.entity.HideAppTarget import com.topjohnwu.magisk.model.entity.StatefulProcess import com.topjohnwu.magisk.ui.hide.HideViewModel import com.topjohnwu.magisk.utils.addOnPropertyChangedCallback import com.topjohnwu.magisk.utils.set import kotlin.math.roundToInt -class HideItem(val item: ProcessHideApp) : ObservableItem() { +class HideItem( + val item: HideAppTarget, + viewModel: HideViewModel +) : ObservableItem() { override val layoutRes = R.layout.item_hide_md2 val packageName = item.info.info.packageName.orEmpty() - val items = item.processes.map { HideProcessItem(it) } + val items = item.processes.map { HideProcessItem(it, viewModel) } @get:Bindable var isExpanded = false set(value) = set(value, field, { field = it }, BR.expanded) - @get:Bindable var itemsChecked = 0 - set(value) = set(value, field, { field = it }, BR.itemsChecked, BR.itemsCheckedPercent) + set(value) = set(value, field, { field = it }, BR.itemsCheckedPercent) @get:Bindable val itemsCheckedPercent get() = (itemsChecked.toFloat() / items.size * 100).roundToInt() - private val isHidden get() = itemsChecked == items.size + private var state: Boolean? = false + set(value) = set(value, field, { field = it }, BR.hiddenState) + + @get:Bindable + var hiddenState: Boolean? + get() = state + set(value) = set(value, state, { state = it }, BR.hiddenState) { + if (value == true) { + items.filterNot { it.isHidden } + } else { + items + }.forEach { it.toggle() } + } init { items.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } } recalculateChecked() } - fun collapse(v: View) { - (v.parent.parent as? ViewGroup)?.startAnimations() - isExpanded = false - } - - fun toggle(v: View) { + fun toggleExpand(v: View) { (v.parent as? ViewGroup)?.startAnimations() isExpanded = !isExpanded } - fun toggle(viewModel: HideViewModel): Boolean { - // contract implies that isHidden == all checked - if (!isHidden) { - items.filterNot { it.isHidden } - } else { - items - }.forEach { it.toggle(viewModel) } - return true - } - private fun recalculateChecked() { itemsChecked = items.count { it.isHidden } + state = when (itemsChecked) { + 0 -> false + items.size -> true + else -> null + } } override fun contentSameAs(other: HideItem): Boolean = item == other.item @@ -68,18 +72,21 @@ class HideItem(val item: ProcessHideApp) : ObservableItem() { } -class HideProcessItem(val item: StatefulProcess) : ObservableItem() { +class HideProcessItem( + val item: StatefulProcess, + val viewModel: HideViewModel +) : ObservableItem() { override val layoutRes = R.layout.item_hide_process_md2 @get:Bindable var isHidden = item.isHidden - set(value) = set(value, field, { field = it }, BR.hidden) + set(value) = set(value, field, { field = it }, BR.hidden) { + viewModel.toggleItem(this) + } - - fun toggle(viewModel: HideViewModel) { + fun toggle() { isHidden = !isHidden - viewModel.toggleItem(this) } override fun contentSameAs(other: HideProcessItem) = item == other.item 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 7922ce556..293a373fe 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 @@ -8,8 +8,8 @@ import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.data.repository.MagiskRepository import com.topjohnwu.magisk.model.entity.HideAppInfo +import com.topjohnwu.magisk.model.entity.HideAppTarget import com.topjohnwu.magisk.model.entity.HideTarget -import com.topjohnwu.magisk.model.entity.ProcessHideApp import com.topjohnwu.magisk.model.entity.StatefulProcess import com.topjohnwu.magisk.model.entity.recycler.HideItem import com.topjohnwu.magisk.model.entity.recycler.HideProcessItem @@ -54,7 +54,10 @@ class HideViewModel( val apps = magiskRepo.fetchApps() val hides = magiskRepo.fetchHideTargets() val (appList, diff) = withContext(Dispatchers.Default) { - val list = apps.map { mergeAppTargets(it, hides) }.map { HideItem(it) }.sort() + val list = apps + .map { createTarget(it, hides) } + .map { HideItem(it, this@HideViewModel) } + .sort() list to items.calculateDiff(list) } items.update(appList, diff) @@ -64,12 +67,13 @@ class HideViewModel( // --- - private fun mergeAppTargets(a: HideAppInfo, ts: List): ProcessHideApp { - val relevantTargets = ts.filter { it.packageName == a.info.packageName } - val packageName = a.info.packageName - val processes = a.processes - .map { StatefulProcess(it, packageName, relevantTargets.any { i -> it == i.process }) } - return ProcessHideApp(a, processes) + 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 } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt index 970240da1..d10686fad 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -14,6 +14,8 @@ import androidx.annotation.DrawableRes import androidx.appcompat.widget.Toolbar import androidx.core.view.updateLayoutParams import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import androidx.databinding.InverseBindingListener import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.recyclerview.widget.* import com.google.android.material.button.MaterialButton @@ -23,6 +25,7 @@ import com.google.android.material.textfield.TextInputLayout import com.topjohnwu.magisk.R import com.topjohnwu.magisk.ktx.replaceRandomWithSpecial import com.topjohnwu.superuser.internal.UiThreadHandler +import com.topjohnwu.widget.IndeterminateCheckBox import kotlinx.coroutines.* import kotlin.math.roundToInt @@ -246,3 +249,23 @@ fun RecyclerView.setSpanCount(count: Int) { is StaggeredGridLayoutManager -> lama.spanCount = count } } + +@BindingAdapter("state") +fun setState(view: IndeterminateCheckBox, state: Boolean?) { + if (view.state != state) + view.state = state +} + +@InverseBindingAdapter(attribute = "state") +fun getState(view: IndeterminateCheckBox) = view.state + + +@BindingAdapter("stateAttrChanged") +fun setListeners( + view: IndeterminateCheckBox, + attrChange: InverseBindingListener +) { + view.setOnStateChangedListener { _, _ -> + attrChange.onChange() + } +} diff --git a/app/src/main/res/layout/fragment_hide_md2.xml b/app/src/main/res/layout/fragment_hide_md2.xml index 3d8116f32..ee92ebc13 100644 --- a/app/src/main/res/layout/fragment_hide_md2.xml +++ b/app/src/main/res/layout/fragment_hide_md2.xml @@ -17,7 +17,7 @@ + android:orientation="vertical" + tools:visibility="gone"> - + android:onClick="@{item::toggleExpand}"> - - - - - - + state="@={item.hiddenState}"/> 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 63a2892b0..feb9afbf8 100644 --- a/app/src/main/res/layout/item_hide_process_md2.xml +++ b/app/src/main/res/layout/item_hide_process_md2.xml @@ -19,11 +19,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" - android:alpha="@{item.isHidden() ? 1f : .7f}" - android:background="?selectableItemBackground" - android:onClick="@{() -> item.toggle(viewModel)}"> + android:alpha="@{item.hidden ? 1f : .7f}"> - - + app:layout_constraintTop_toTopOf="parent" />