diff --git a/app/src/main/java/com/topjohnwu/magisk/di/RedesignModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/RedesignModule.kt index b4f3bac25..36effd49a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/RedesignModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/RedesignModule.kt @@ -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() } diff --git a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt index a4eefb0e9..69c74c9f2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt @@ -43,35 +43,35 @@ typealias OnErrorListener = (Throwable) -> Unit /*=== ALIASES FOR OBSERVABLES ===*/ fun Observable.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {}, - onNext: OnSuccessListener = {} + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} ) = applySchedulers() .subscribe(onNext, onError, onComplete) fun Single.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onNext: OnSuccessListener = {} + onError: OnErrorListener = { it.printStackTrace() }, + onNext: OnSuccessListener = {} ) = applySchedulers() .subscribe(onNext, onError) fun Maybe.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {}, - onNext: OnSuccessListener = {} + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} ) = applySchedulers() .subscribe(onNext, onError, onComplete) fun Flowable.subscribeK( - onError: OnErrorListener = { it.printStackTrace() }, - onComplete: OnCompleteListener = {}, - onNext: OnSuccessListener = {} + onError: OnErrorListener = { it.printStackTrace() }, + onComplete: OnCompleteListener = {}, + onNext: OnSuccessListener = {} ) = 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 ObservableField.toObservable(): Observable { fun T.toSingle() = Single.just(this) -fun zip(t1: Single, t2: Single, zipper: (T1, T2) -> R) = - Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) +inline fun zip( + t1: Single, + t2: Single, + crossinline zipper: (T1, T2) -> R +) = Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) \ No newline at end of file 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 5d6b3f7dd..6cbc24c5c 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 @@ -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 +) \ No newline at end of file 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 bf7a0d2a3..ceb0390d1 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 @@ -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() { + + 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() { + + 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) : ComparableRvItem() { diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideFragment.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideFragment.kt index d6d9459b4..55af466ff 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideFragment.kt @@ -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() { override val layoutRes = R.layout.fragment_hide_md2 override val viewModel by viewModel() + override fun consumeSystemWindowInsets(insets: Insets) = insets + override fun onAttach(context: Context) { super.onAttach(context) diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt index 683d5007d..a9be0ade7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/hide/HideViewModel.kt @@ -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() \ No newline at end of file +class HideViewModel( + private val magiskRepo: MagiskRepository, + rxBus: RxBus +) : CompatViewModel() { + + @Volatile + private var cache = listOf() + 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() + val itemBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } + val itemInternalBinding = itemBindingOf { + it.bindExtra(BR.viewModel, this) + } + + init { + rxBus.register() + .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): 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.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() + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_radio_check_button.xml b/app/src/main/res/drawable/ic_radio_check_button.xml index 9c6c7ef92..22e623203 100644 --- a/app/src/main/res/drawable/ic_radio_check_button.xml +++ b/app/src/main/res/drawable/ic_radio_check_button.xml @@ -22,6 +22,27 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_hide_md2.xml b/app/src/main/res/layout/fragment_hide_md2.xml index 30bddc477..a3431b8ff 100644 --- a/app/src/main/res/layout/fragment_hide_md2.xml +++ b/app/src/main/res/layout/fragment_hide_md2.xml @@ -1,23 +1,32 @@ - + + + - - - - - + 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" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_hide_md2.xml b/app/src/main/res/layout/item_hide_md2.xml new file mode 100644 index 000000000..f076fdb93 --- /dev/null +++ b/app/src/main/res/layout/item_hide_md2.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_hide_process_md2.xml b/app/src/main/res/layout/item_hide_process_md2.xml new file mode 100644 index 000000000..fb08d69f9 --- /dev/null +++ b/app/src/main/res/layout/item_hide_process_md2.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file