Added implementation of hide screen

Very much wip and doesn't work at all
This commit is contained in:
Viktor De Pasquale 2019-10-30 21:58:42 +01:00
parent 722fba7805
commit f76c020dd7
10 changed files with 426 additions and 26 deletions

View File

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

View File

@ -43,35 +43,35 @@ typealias OnErrorListener = (Throwable) -> Unit
/*=== ALIASES FOR OBSERVABLES ===*/
fun <T> Observable<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun <T> Single<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onNext: OnSuccessListener<T> = {}
onError: OnErrorListener = { it.printStackTrace() },
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError)
fun <T> Maybe<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun <T> Flowable<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun Completable.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {}
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {}
) = applySchedulers()
.subscribe(onComplete, onError)
@ -197,5 +197,8 @@ fun <T> ObservableField<T>.toObservable(): Observable<T> {
fun <T : Any> T.toSingle() = Single.just(this)
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
inline fun <T1, T2, R> zip(
t1: Single<T1>,
t2: Single<T2>,
crossinline zipper: (T1, T2) -> R
) = Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })

View File

@ -14,3 +14,13 @@ class HideAppInfo(
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
}
data class StatefulProcess(
val name: String,
val isHidden: Boolean
)
class ProcessHideApp(
val info: HideAppInfo,
val processes: List<StatefulProcess>
)

View File

@ -1,18 +1,69 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.startAnimations
import com.topjohnwu.magisk.extensions.toggle
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.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class HideItem(val item: ProcessHideApp) : ComparableRvItem<HideItem>() {
override val layoutRes = R.layout.item_hide_md2
val items = item.processes.map { HideProcessItem(it) }
val isExpanded = KObservableField(false)
val itemsChecked = KObservableField(0)
val isHidden get() = itemsChecked.value == items.size
init {
items.forEach { it.isHidden.addOnPropertyChangedCallback { recalculateChecked() } }
}
fun collapse(v: View) {
(v.parent.parent as? ViewGroup)?.startAnimations()
isExpanded.value = false
}
fun expand(v: View) {
(v.parent as? ViewGroup)?.startAnimations()
isExpanded.value = true
}
private fun recalculateChecked() {
itemsChecked.value = items.count { it.isHidden.value }
}
override fun contentSameAs(other: HideItem): Boolean = item == other.item
override fun itemSameAs(other: HideItem): Boolean = item.info == other.item.info
}
class HideProcessItem(val item: StatefulProcess) : ComparableRvItem<HideProcessItem>() {
override val layoutRes = R.layout.item_hide_process_md2
val isHidden = KObservableField(item.isHidden)
fun toggle() = isHidden.toggle()
override fun contentSameAs(other: HideProcessItem) = item == other.item
override fun itemSameAs(other: HideProcessItem) = item.name == other.item.name
}
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() {

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.redesign.hide
import android.content.Context
import android.graphics.Insets
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
import com.topjohnwu.magisk.redesign.compat.CompatFragment
@ -11,6 +12,8 @@ class HideFragment : CompatFragment<HideViewModel, FragmentHideMd2Binding>() {
override val layoutRes = R.layout.fragment_hide_md2
override val viewModel by viewModel<HideViewModel>()
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onAttach(context: Context) {
super.onAttach(context)

View File

@ -1,5 +1,124 @@
package com.topjohnwu.magisk.redesign.hide
import android.content.pm.ApplicationInfo
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.data.repository.MagiskRepository
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.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.utils.currentLocale
import io.reactivex.disposables.Disposable
import java.util.*
class HideViewModel : CompatViewModel()
class HideViewModel(
private val magiskRepo: MagiskRepository,
rxBus: RxBus
) : CompatViewModel() {
@Volatile
private var cache = listOf<HideItem>()
set(value) {
field = Collections.synchronizedList(value)
}
private var queryJob: Disposable? = null
set(value) {
field?.dispose()
field = value
}
val query = KObservableField("")
val isShowSystem = KObservableField(true)
val items = diffListOf<HideItem>()
val itemBinding = itemBindingOf<HideItem> {
it.bindExtra(BR.viewModel, this)
}
val itemInternalBinding = itemBindingOf<HideProcessItem> {
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) } }
.flattenAsFlowable { it }
.map { HideItem(it) }
.toList()
.map { it.sort() }
.subscribeK {
cache = it
queryIfNecessary()
}
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 processes = a.processes
.map { StatefulProcess(it, 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 }
))
// ---
/** 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 {
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
}
.sequential()
.toList()
.map { it to items.calculateDiff(it) }
.subscribeK { items.update(it.first, it.second) }
.let { queryJob = it }
// ---
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
.toggleHide(item.isHidden.value, item.packageName, item.process)
.subscribeK()
.add()
}

View File

@ -22,6 +22,27 @@
</vector>
</item>
<item
android:id="@+id/collapsed_selected"
android:state_selected="true">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="group"
android:pivotX="12"
android:pivotY="12">
<path
android:name="path"
android:fillColor="#000"
android:pathData="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 Z" />
</group>
</vector>
</item>
<item android:id="@+id/expanded">
<vector
android:name="vector"
@ -52,4 +73,14 @@
android:fromId="@+id/collapsed"
android:toId="@id/expanded" />
<transition
android:drawable="@drawable/avd_circle_from_filled"
android:fromId="@+id/expanded"
android:toId="@+id/collapsed_selected" />
<transition
android:drawable="@drawable/avd_circle_to_filled"
android:fromId="@+id/collapsed_selected"
android:toId="@id/expanded" />
</animated-selector>

View File

@ -1,23 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.redesign.hide.HideViewModel" />
</data>
<androidx.core.widget.NestedScrollView
<androidx.recyclerview.widget.RecyclerView
dividerVertical="@{R.drawable.divider_l1}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_md2"
tools:paddingTop="40dp" />
</layout>

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.redesign.hide.HideViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="?styleCardNormal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1">
<androidx.constraintlayout.widget.ConstraintLayout
invisible="@{item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:onClick="@{(v) -> item.expand(v)}">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_icon"
style="?styleImageNormal"
android:layout_margin="@dimen/l1"
android:src="@{item.item.info.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_magisk" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:ellipsize="middle"
android:singleLine="true"
android:text="@{item.item.info.name}"
android:textAppearance="?appearanceTextBodyNormal"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/hide_package"
app:layout_constraintEnd_toStartOf="@+id/hide_expand_icon"
app:layout_constraintStart_toEndOf="@+id/hide_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/hide_package"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?appearanceTextCaptionVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/hide_name"
app:layout_constraintStart_toStartOf="@+id/hide_name"
app:layout_constraintTop_toBottomOf="@+id/hide_name"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_expand_icon"
style="?styleIconNormal"
invisible="@{item.item.processes.empty}"
android:background="@null"
android:rotation="180"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_back_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
goneUnless="@{item.isExpanded}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
style="?styleToolbar"
onNavigationClick="@{(v) -> item.collapse(v)}"
android:layout_width="match_parent"
android:layout_height="48dp"
app:navigationIcon="@drawable/ic_back_md2"
app:title="Processes"
app:titleTextAppearance="?appearanceTextBodyNormal" />
<androidx.recyclerview.widget.RecyclerView
itemBinding="@{viewModel.itemInternalBinding}"
items="@{item.items}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_process_md2" />
</LinearLayout>
<ProgressBar
style="?styleProgressDeterminate"
android:layout_width="match_parent"
android:layout_gravity="top"
android:max="@{item.items.size()}"
android:progress="@{item.itemsChecked}" />
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.HideProcessItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.redesign.hide.HideViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:singleLine="true"
android:text="@{item.item.name}"
android:textAppearance="?appearanceTextCaptionVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/hide_process_checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="com.topjohnwu.magisk" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hide_process_checkbox"
style="?styleImageSmall"
isSelected="@{item.isHidden}"
android:layout_marginTop="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
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"
app:srcCompat="@drawable/ic_radio_check_button"
app:tint="?colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>