Updated Hide screen to be fully functioning

...although still misses search :(
This commit is contained in:
Viktor De Pasquale 2019-10-31 20:34:07 +01:00
parent f76c020dd7
commit 6aa22267f4
12 changed files with 202 additions and 69 deletions

View File

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

View File

@ -17,6 +17,7 @@ class HideAppInfo(
data class StatefulProcess(
val name: String,
val packageName: String,
val isHidden: Boolean
)

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R
@ -14,6 +15,7 @@ import com.topjohnwu.magisk.model.entity.ProcessHideApp
import com.topjohnwu.magisk.model.entity.StatefulProcess
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.redesign.hide.HideViewModel
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
@ -22,14 +24,18 @@ class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
override val layoutRes = R.layout.item_hide_md2
val packageName = item.info.info.packageName.orEmpty()
val items = item.processes.map { HideProcessItem(it) }
val isExpanded = KObservableField(false)
val itemsChecked = KObservableField(0)
val isHidden get() = itemsChecked.value == items.size
/** [toggle] depends on this functionality */
private val isHidden get() = itemsChecked.value == items.size
init {
items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } }
recalculateChecked()
}
fun collapse(v: View) {
@ -42,6 +48,17 @@ class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
isExpanded.value = true
}
fun toggle(menuItem: MenuItem, viewModel: HideViewModel): Boolean {
if (menuItem.itemId != R.id.action_toggle) return false
// contract implies that isHidden == all checked
if (!isHidden) {
items.filterNot { it.isHidden.value }
} else {
items
}.forEach { it.toggle(viewModel) }
return true
}
private fun recalculateChecked() {
itemsChecked.value = items.count { it.isHidden.value }
}
@ -57,7 +74,10 @@ class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessI
val isHidden = KObservableField(item.isHidden)
fun toggle() = isHidden.toggle()
fun toggle(viewModel: HideViewModel) {
isHidden.toggle()
viewModel.toggleItem(this)
}
override fun contentSameAs(other: HideProcessItem) = item == other.item
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name

View File

@ -2,6 +2,9 @@ package com.topjohnwu.magisk.redesign.hide
import android.content.Context
import android.graphics.Insets
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
import com.topjohnwu.magisk.redesign.compat.CompatFragment
@ -16,8 +19,17 @@ class HideFragment : CompatFragment<HideViewModel, FragmentHideMd2Binding>() {
override fun onAttach(context: Context) {
super.onAttach(context)
activity.setTitle(R.string.magiskhide)
setHasOptionsMenu(true)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_hide_md2, menu)
menu.findItem(R.id.action_show_system)?.isChecked = viewModel.isShowSystem
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return viewModel.menuItemPressed(item)
}
}

View File

@ -1,46 +1,37 @@
package com.topjohnwu.magisk.redesign.hide
import android.content.pm.ApplicationInfo
import android.view.MenuItem
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.model.entity.HideAppInfo
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
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
import com.topjohnwu.magisk.redesign.home.itemBindingOf
import com.topjohnwu.magisk.redesign.superuser.diffListOf
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.utils.currentLocale
import io.reactivex.disposables.Disposable
import java.util.*
class HideViewModel(
private val magiskRepo: MagiskRepository,
rxBus: RxBus
private val magiskRepo: MagiskRepository
) : CompatViewModel() {
@Volatile
private var cache = listOf<HideItem>()
var isShowSystem = false
set(value) {
field = Collections.synchronizedList(value)
}
private var queryJob: Disposable? = null
set(value) {
field?.dispose()
field = value
query()
}
val query = KObservableField("")
val isShowSystem = KObservableField(true)
val items = diffListOf<HideItem>()
val items = filterableListOf<HideItem>()
val itemBinding = itemBindingOf<HideItem> {
it.bindExtra(BR.viewModel, this)
}
@ -48,12 +39,6 @@ class HideViewModel(
it.bindExtra(BR.viewModel, this)
}
init {
rxBus.register<HideProcessEvent>()
.subscribeK { toggleItem(it.item) }
.add()
}
override fun refresh() = magiskRepo.fetchApps()
.map { it to magiskRepo.fetchHideTargets().blockingGet() }
.map { pair -> pair.first.map { mergeAppTargets(it, pair.second) } }
@ -61,64 +46,66 @@ class HideViewModel(
.map { HideItem(it) }
.toList()
.map { it.sort() }
.map { it to items.calculateDiff(it) }
.subscribeK {
cache = it
queryIfNecessary()
items.update(it.first, it.second)
query()
}
override fun onCleared() {
queryJob?.dispose()
super.onCleared()
}
// ---
private fun mergeAppTargets(a: HideAppInfo, ts: List<HideTarget>): ProcessHideApp {
val relevantTargets = ts.filter { it.packageName == a.info.packageName }
val packageName = a.info.packageName
val processes = a.processes
.map { StatefulProcess(it, relevantTargets.any { i -> it == i.process }) }
.map { StatefulProcess(it, packageName, relevantTargets.any { i -> it == i.process }) }
return ProcessHideApp(a, processes)
}
private fun List<HideItem>.sort() = sortedWith(compareBy(
{ it.isHidden },
{ it.item.info.name.toLowerCase(currentLocale) },
{ it.item.info.info.packageName }
))
private fun List<HideItem>.sort() = compareByDescending<HideItem> { it.itemsChecked.value }
.thenBy { it.item.info.name.toLowerCase(currentLocale) }
.thenBy { it.item.info.info.packageName }
.let { sortedWith(it) }
// ---
/** We don't need to re-query when the app count matches. */
private fun queryIfNecessary() {
if (items.size != cache.size) {
query()
}
}
private fun query(
query: String = this.query.value,
showSystem: Boolean = isShowSystem.value
) = cache.toSingle()
.flattenAsFlowable { it }
.parallel()
.filter { showSystem || it.item.info.info.flags and ApplicationInfo.FLAG_SYSTEM == 0 }
.filter {
showSystem: Boolean = isShowSystem
) = items.filter {
fun filterSystem(): Boolean {
return showSystem || it.item.info.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
}
fun filterQuery(): Boolean {
val inName = it.item.info.name.contains(query, true)
val inPackage = it.item.info.info.packageName.contains(query, true)
val inProcesses = it.item.processes.any { it.name.contains(query, true) }
inName || inPackage || inProcesses
return inName || inPackage || inProcesses
}
.sequential()
.toList()
.map { it to items.calculateDiff(it) }
.subscribeK { items.update(it.first, it.second) }
.let { queryJob = it }
filterSystem() && filterQuery()
}
// ---
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
.toggleHide(item.isHidden.value, item.packageName, item.process)
fun menuItemPressed(menuItem: MenuItem) = when (menuItem.itemId) {
R.id.action_show_system -> isShowSystem = (!menuItem.isChecked)
.also { menuItem.isChecked = it }
else -> null
}?.let { true } ?: false
fun toggleItem(item: HideProcessItem) = magiskRepo
.toggleHide(item.isHidden.value, item.item.packageName, item.item.name)
// might wanna reorder the list to display the item at the top
.subscribeK()
.add()
}
inline fun <T : ComparableRvItem<T>> filterableListOf(
vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems.toList()) }

View File

@ -404,3 +404,8 @@ fun MaterialCardView.setCardElevationBound(elevation: Float) {
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
strokeWidth = stroke.roundToInt()
}
@BindingAdapter("onMenuClick")
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
setOnMenuItemClickListener(listener)
}

View File

@ -18,9 +18,9 @@ open class DiffObservableList<T>(
) : AbstractList<T>(), ObservableList<T> {
private val LIST_LOCK = Object()
private var list: MutableList<T> = ArrayList()
protected var list: MutableList<T> = ArrayList()
private val listeners = ListChangeRegistry()
private val listCallback = ObservableListUpdateCallback()
protected val listCallback = ObservableListUpdateCallback()
override val size: Int get() = list.size
@ -38,7 +38,7 @@ open class DiffObservableList<T>(
return doCalculateDiff(frozenList, newItems)
}
private fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult {
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size

View File

@ -0,0 +1,85 @@
package com.topjohnwu.magisk.utils
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import java.util.*
class FilterableDiffObservableList<T>(
callback: Callback<T>
) : DiffObservableList<T>(callback) {
var filter: ((T) -> Boolean)? = null
set(value) {
field = value
queueUpdate()
}
@Volatile
private var sublist: MutableList<T> = super.list
// ---
private val ui by lazy { Handler(Looper.getMainLooper()) }
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
private val updater = Runnable {
val filter = filter ?: { true }
val newList = super.list.filter(filter)
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
ui.post {
sublist = Collections.synchronizedList(newList)
diff.dispatchUpdatesTo(listCallback)
}
}
private fun queueUpdate() {
handler.removeCallbacks(updater)
handler.post(updater)
}
fun hasFilter() = filter != null
fun filter(switch: (T) -> Boolean) {
filter = switch
}
fun reset() {
filter = null
}
// ---
override fun get(index: Int): T {
return sublist.get(index)
}
override fun add(element: T): Boolean {
return sublist.add(element)
}
override fun add(index: Int, element: T) {
sublist.add(index, element)
}
override fun addAll(elements: Collection<T>): Boolean {
return sublist.addAll(elements)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean {
return sublist.addAll(index, elements)
}
override fun remove(element: T): Boolean {
return sublist.remove(element)
}
override fun removeAt(index: Int): T {
return sublist.removeAt(index)
}
override fun set(index: Int, element: T): T {
return sublist.set(index, element)
}
override val size: Int
get() = sublist.size
}

View File

@ -59,6 +59,8 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_package"
android:layout_width="0dp"
gone="@{item.item.info.info.packageName.empty}"
android:text="@{item.item.info.info.packageName}"
android:layout_height="wrap_content"
android:textAppearance="?appearanceTextCaptionVariant"
app:layout_constraintBottom_toBottomOf="parent"
@ -90,6 +92,8 @@
style="?styleToolbar"
onNavigationClick="@{(v) -> item.collapse(v)}"
android:layout_width="match_parent"
onMenuClick="@{(it) -> item.toggle(it, viewModel)}"
app:menu="@menu/menu_hide_item"
android:layout_height="48dp"
app:navigationIcon="@drawable/ic_back_md2"
app:title="Processes"
@ -100,6 +104,7 @@
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:itemCount="2"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_process_md2" />

View File

@ -17,6 +17,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="?selectableItemBackground"
android:onClick="@{() -> item.toggle(viewModel)}"
android:layout_height="wrap_content"
android:layout_gravity="center">
@ -41,10 +43,8 @@
style="?styleImageSmall"
isSelected="@{item.isHidden}"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginEnd="@dimen/l_75"
android:layout_marginBottom="@dimen/l_50"
android:background="?selectableItemBackgroundBorderless"
android:onClick="@{() -> item.toggle()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_toggle"
android:icon="@drawable/ic_hide_md2"
android:title=""
app:showAsAction="always" />
</menu>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_show_system"
android:checkable="true"
android:title="@string/show_system_app"
app:showAsAction="never" />
</menu>