More efficient databinding

This commit is contained in:
topjohnwu 2020-07-12 03:17:50 -07:00
parent b41b2283f4
commit 2c12fe6eb2
26 changed files with 386 additions and 276 deletions

View File

@ -4,6 +4,7 @@ import androidx.annotation.CallSuper
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.utils.DiffObservableList import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.ObservableHost
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
abstract class RvItem { abstract class RvItem {
@ -46,3 +47,5 @@ abstract class ComparableRvItem<in T> : RvItem() {
} }
} }
} }
abstract class ObservableItem<T> : ComparableRvItem<T>(), ObservableHost by ObservableHost.impl

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.Bindable import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ktx.timeDateFormat import com.topjohnwu.magisk.ktx.timeDateFormat
import com.topjohnwu.magisk.ktx.toTime import com.topjohnwu.magisk.ktx.toTime
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
@ -16,13 +17,13 @@ class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
@Bindable get @Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.top) notifyPropertyChanged(BR.top)
} }
var isBottom = false var isBottom = false
@Bindable get @Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.bottom) notifyPropertyChanged(BR.bottom)
} }
override fun itemSameAs(other: LogItem) = item.appName == other.item.appName override fun itemSameAs(other: LogItem) = item.appName == other.item.appName

View File

@ -1,12 +1,16 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.* import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.module.Module import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ui.module.ModuleViewModel import com.topjohnwu.magisk.ui.module.ModuleViewModel
object InstallModule : ComparableRvItem<InstallModule>() { object InstallModule : ComparableRvItem<InstallModule>() {
@ -33,19 +37,19 @@ class SectionTitle(
@Bindable get @Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.button) notifyPropertyChanged(BR.button)
} }
var icon = _icon var icon = _icon
@Bindable get @Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.icon) notifyPropertyChanged(BR.icon)
} }
var hasButton = button != 0 || icon != 0 var hasButton = button != 0 || icon != 0
@Bindable get @Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.hasButton) notifyPropertyChanged(BR.hasButton)
} }
override fun onBindingBound(binding: ViewDataBinding) { override fun onBindingBound(binding: ViewDataBinding) {
@ -66,7 +70,7 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
@Bindable get @Bindable get
protected set(value) { protected set(value) {
field = value field = value
notifyChange(BR.update) notifyPropertyChanged(BR.update)
} }
override fun contentSameAs(other: RepoItem): Boolean = item == other.item override fun contentSameAs(other: RepoItem): Boolean = item == other.item
@ -89,7 +93,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
var repo: Repo? = null var repo: Repo? = null
set(value) { set(value) {
field = value field = value
notifyChange(BR.repo) notifyPropertyChanged(BR.repo)
} }
@get:Bindable @get:Bindable
@ -97,7 +101,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
get() = item.enable get() = item.enable
set(value) { set(value) {
item.enable = value item.enable = value
notifyChange(BR.enabled) notifyPropertyChanged(BR.enabled)
} }
@get:Bindable @get:Bindable
@ -105,7 +109,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
get() = item.remove get() = item.remove
set(value) { set(value) {
item.remove = value item.remove = value
notifyChange(BR.removed) notifyPropertyChanged(BR.removed)
} }
val isUpdated get() = item.updated val isUpdated get() = item.updated
@ -126,21 +130,5 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
&& item.name == other.item.name && item.name == other.item.name
override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id
} }
abstract class ObservableItem<T> : ComparableRvItem<T>(), Observable {
private val list = PropertyChangeRegistry()
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.remove(callback ?: return)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
list.add(callback ?: return)
}
fun notifyChange(id: Int) = list.notifyChange(this, id)
}

View File

@ -11,6 +11,7 @@ import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.utils.TransitiveText import com.topjohnwu.magisk.utils.TransitiveText
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@ -37,7 +38,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
// notify only after the callback invocation; callback can invalidate the backing data, // notify only after the callback invocation; callback can invalidate the backing data,
// which wouldn't be recognized with reverse approach // which wouldn't be recognized with reverse approach
notifyChange(BR.description) notifyPropertyChanged(BR.description)
} }
open fun refresh() {} open fun refresh() {}
@ -60,7 +61,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
) = object : ObservableProperty<T>(initialValue) { ) = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) { override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
setter(newValue) setter(newValue)
notifyChange(fieldId) notifyPropertyChanged(fieldId)
} }
} }
@ -169,7 +170,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
} }
.applyAdapter(entries) { .applyAdapter(entries) {
value = it value = it
notifyChange(BR.selectedEntry) notifyPropertyChanged(BR.selectedEntry)
super.onPressed(view, callback) super.onPressed(view, callback)
} }
.reveal() .reveal()

View File

@ -18,6 +18,8 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ktx.startAnimations import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ui.base.BaseUIActivity import com.topjohnwu.magisk.ui.base.BaseUIActivity
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.ReselectionTarget
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
@ -26,6 +28,8 @@ import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class MainViewModel : BaseViewModel()
open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>() { open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>() {
override val layoutRes = R.layout.activity_main_md2 override val layoutRes = R.layout.activity_main_md2

View File

@ -1,5 +0,0 @@
package com.topjohnwu.magisk.ui
import com.topjohnwu.magisk.ui.base.BaseViewModel
class MainViewModel : BaseViewModel()

View File

@ -1,7 +0,0 @@
package com.topjohnwu.magisk.ui
interface ReselectionTarget {
fun onReselected()
}

View File

@ -20,7 +20,6 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ktx.snackbar import com.topjohnwu.magisk.ktx.snackbar
import com.topjohnwu.magisk.ktx.startAnimations import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.EventHandler import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.events.ViewEvent
@ -116,7 +115,7 @@ abstract class BaseUIActivity<ViewModel : BaseViewModel, Binding : ViewDataBindi
} }
override fun peekSystemWindowInsets(insets: Insets) { override fun peekSystemWindowInsets(insets: Insets) {
viewModel.insets.value = insets viewModel.insets = insets
} }
protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this) protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this)

View File

@ -89,3 +89,9 @@ abstract class BaseUIFragment<ViewModel : BaseViewModel, Binding : ViewDataBindi
} }
} }
interface ReselectionTarget {
fun onReselected()
}

View File

@ -5,8 +5,6 @@ import androidx.annotation.CallSuper
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.Observable import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -17,30 +15,33 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.* import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.NavigationWrapper import com.topjohnwu.magisk.model.navigation.NavigationWrapper
import com.topjohnwu.magisk.utils.ObservableHost
import com.topjohnwu.magisk.utils.observable
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
abstract class BaseViewModel( abstract class BaseViewModel(
initialState: State = State.LOADING initialState: State = State.LOADING
) : ViewModel(), Observable, KoinComponent { ) : ViewModel(), ObservableHost by ObservableHost.impl, KoinComponent {
enum class State { enum class State {
LOADED, LOADING, LOADING_FAILED LOADED, LOADING, LOADING_FAILED
} }
val loading @Bindable get() = state == State.LOADING @get:Bindable
val loaded @Bindable get() = state == State.LOADED val loading get() = state == State.LOADING
val loadingFailed @Bindable get() = state == State.LOADING_FAILED @get:Bindable
val loaded get() = state == State.LOADED
@get:Bindable
val loadFailed get() = state == State.LOADING_FAILED
val isConnected get() = Info.isConnected val isConnected get() = Info.isConnected
val viewEvents: LiveData<ViewEvent> get() = _viewEvents val viewEvents: LiveData<ViewEvent> get() = _viewEvents
val insets = ObservableField(Insets.NONE)
var state: State = initialState @get:Bindable
set(value) { var insets by observable(Insets.NONE, BR.insets)
field = value
notifyStateChanged() var state by observable(initialState, BR.loading, BR.loaded, BR.loadFailed)
}
private val _viewEvents = MutableLiveData<ViewEvent>() private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningJob: Job? = null private var runningJob: Job? = null
@ -65,12 +66,6 @@ abstract class BaseViewModel(
protected open fun refresh(): Job? = null protected open fun refresh(): Job? = null
open fun notifyStateChanged() {
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.loaded)
notifyPropertyChanged(BR.loadingFailed)
}
@CallSuper @CallSuper
override fun onCleared() { override fun onCleared() {
isConnected.removeOnPropertyChangedCallback(refreshCallback) isConnected.removeOnPropertyChangedCallback(refreshCallback)
@ -108,41 +103,4 @@ abstract class BaseViewModel(
_viewEvents.postValue(NavigationWrapper(this)) _viewEvents.postValue(NavigationWrapper(this))
} }
// The following is copied from androidx.databinding.BaseObservable
@Transient
private var callbacks: PropertyChangeRegistry? = null
@Synchronized
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
if (callbacks == null) {
callbacks = PropertyChangeRegistry()
}
callbacks?.add(callback)
}
@Synchronized
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks?.remove(callback)
}
/**
* Notifies listeners that all properties of this instance have changed.
*/
@Synchronized
fun notifyChange() {
callbacks?.notifyCallbacks(this, 0, null)
}
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with [androidx.databinding.Bindable] to generate a field in
* `BR` to be used as `fieldId`.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks?.notifyCallbacks(this, fieldId, null)
}
} }

View File

@ -5,7 +5,6 @@ import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.ActivityExecutor import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.model.events.ContextExecutor import com.topjohnwu.magisk.model.events.ContextExecutor
import com.topjohnwu.magisk.model.events.FragmentExecutor import com.topjohnwu.magisk.model.events.FragmentExecutor
@ -44,7 +43,7 @@ class CompatDelegate internal constructor(
insets.asInsets() insets.asInsets()
.also { view.peekSystemWindowInsets(it) } .also { view.peekSystemWindowInsets(it) }
.let { view.consumeSystemWindowInsets(it) } .let { view.consumeSystemWindowInsets(it) }
?.also { view.viewModel.insets.value = it } ?.also { view.viewModel.insets = it }
?.subtractBy(insets) ?: insets ?.subtractBy(insets) ?: insets
} }
if (ViewCompat.isAttachedToWindow(view.viewRoot)) { if (ViewCompat.isAttachedToWindow(view.viewRoot)) {

View File

@ -3,9 +3,10 @@ package com.topjohnwu.magisk.ui.flash
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.view.MenuItem import android.view.MenuItem
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
@ -19,6 +20,7 @@ import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.diffListOf import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -31,8 +33,10 @@ class FlashViewModel(
private val resources: Resources private val resources: Resources
) : BaseViewModel() { ) : BaseViewModel() {
val showReboot = ObservableField(Shell.rootAccess()) @get:Bindable
val behaviorText = ObservableField(resources.getString(R.string.flashing)) var showReboot by observable(Shell.rootAccess(), BR.showReboot)
@get:Bindable
var behaviorText by observable(resources.getString(R.string.flashing), BR.behaviorText)
val adapter = BindingAdapter<ConsoleItem>() val adapter = BindingAdapter<ConsoleItem>()
val items = diffListOf<ConsoleItem>() val items = diffListOf<ConsoleItem>()
@ -59,7 +63,7 @@ class FlashViewModel(
FlashZip(installer, outItems, logItems).exec() FlashZip(installer, outItems, logItems).exec()
} }
Const.Value.UNINSTALL -> { Const.Value.UNINSTALL -> {
showReboot.value = false showReboot = false
FlashZip.Uninstall(installer, outItems, logItems).exec() FlashZip.Uninstall(installer, outItems, logItems).exec()
} }
Const.Value.FLASH_MAGISK -> { Const.Value.FLASH_MAGISK -> {
@ -70,7 +74,7 @@ class FlashViewModel(
} }
Const.Value.PATCH_FILE -> { Const.Value.PATCH_FILE -> {
uri ?: return@launch uri ?: return@launch
showReboot.value = false showReboot = false
MagiskInstaller.Patch(installer, uri, outItems, logItems).exec() MagiskInstaller.Patch(installer, uri, outItems, logItems).exec()
} }
else -> { else -> {
@ -84,7 +88,7 @@ class FlashViewModel(
private fun onResult(success: Boolean) { private fun onResult(success: Boolean) {
state = if (success) State.LOADED else State.LOADING_FAILED state = if (success) State.LOADED else State.LOADING_FAILED
behaviorText.value = when { behaviorText = when {
success -> resources.getString(R.string.done) success -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure) else -> resources.getString(R.string.failure)
} }

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
@ -18,6 +17,7 @@ import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.Queryable import com.topjohnwu.magisk.ui.base.Queryable
import com.topjohnwu.magisk.ui.base.filterableListOf import com.topjohnwu.magisk.ui.base.filterableListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -28,21 +28,16 @@ class HideViewModel(
override val queryDelay = 1000L override val queryDelay = 1000L
var isShowSystem = false @get:Bindable
@Bindable get var isShowSystem by observable(false, BR.showSystem) {
set(value) { submitQuery()
field = value }
notifyPropertyChanged(BR.showSystem)
submitQuery() @get:Bindable
} var query by observable("", BR.query) {
submitQuery()
}
var query = ""
@Bindable get
set(value) {
field = value
notifyPropertyChanged(BR.query)
submitQuery()
}
val items = filterableListOf<HideItem>() val items = filterableListOf<HideItem>()
val itemBinding = itemBindingOf<HideItem> { val itemBinding = itemBindingOf<HideItem> {
it.bindExtra(BR.viewModel, this) it.bindExtra(BR.viewModel, this)
@ -51,8 +46,6 @@ class HideViewModel(
it.bindExtra(BR.viewModel, this) it.bindExtra(BR.viewModel, this)
} }
val isFilterExpanded = ObservableField(false)
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
state = State.LOADING state = State.LOADING
val apps = magiskRepo.fetchApps() val apps = magiskRepo.fetchApps()
@ -106,10 +99,5 @@ class HideViewModel(
fun resetQuery() { fun resetQuery() {
query = "" query = ""
} }
fun hideFilter() {
isFilterExpanded.value = false
}
} }

View File

@ -1,7 +1,7 @@
package com.topjohnwu.magisk.ui.home package com.topjohnwu.magisk.ui.home
import android.os.Build import android.os.Build
import androidx.databinding.ObservableField import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
@ -27,6 +27,7 @@ import com.topjohnwu.magisk.model.events.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.model.events.dialog.UninstallDialog import com.topjohnwu.magisk.model.events.dialog.UninstallDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.bindingcollectionadapter2.BR import me.tatarka.bindingcollectionadapter2.BR
@ -40,22 +41,26 @@ class HomeViewModel(
private val repoMagisk: MagiskRepository private val repoMagisk: MagiskRepository
) : BaseViewModel() { ) : BaseViewModel() {
val isNoticeVisible = ObservableField(Config.safetyNotice) @get:Bindable
var isNoticeVisible by observable(Config.safetyNotice, BR.noticeVisible)
val stateMagisk = ObservableField(MagiskState.LOADING) @get:Bindable
val stateManager = ObservableField(MagiskState.LOADING) var stateMagisk by observable(MagiskState.LOADING, BR.stateMagisk)
@get:Bindable
val stateMagiskRemoteVersion = ObservableField(R.string.loading.res()) var stateManager by observable(MagiskState.LOADING, BR.stateManager)
val stateMagiskInstalledVersion get() = @get:Bindable
var magiskRemoteVersion by observable(R.string.loading.res(), BR.magiskRemoteVersion)
val magiskInstalledVersion get() =
"${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})" "${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})"
val stateMagiskMode get() = R.string.home_status_normal.res() val magiskMode get() = R.string.home_status_normal.res()
val stateManagerRemoteVersion = ObservableField(R.string.loading.res()) @get:Bindable
val stateManagerInstalledVersion = Info.stub?.let { var managerRemoteVersion by observable(R.string.loading.res(), BR.managerRemoteVersion)
val managerInstalledVersion = Info.stub?.let {
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) (${it.version})" "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) (${it.version})"
} ?: "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" } ?: "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
val statePackageName = packageName val statePackageName = packageName
val stateManagerProgress = ObservableField(0) @get:Bindable
var stateManagerProgress by observable(0, BR.stateManagerProgress)
val items = listOf(DeveloperItem.Mainline, DeveloperItem.App, DeveloperItem.Project) val items = listOf(DeveloperItem.Mainline, DeveloperItem.App, DeveloperItem.Project)
val itemBinding = itemBindingOf<HomeItem> { val itemBinding = itemBindingOf<HomeItem> {
@ -70,28 +75,28 @@ class HomeViewModel(
init { init {
RemoteFileService.progressBroadcast.observeForever { RemoteFileService.progressBroadcast.observeForever {
when (it?.second) { when (it?.second) {
is Manager -> stateManagerProgress.value = it.first.times(100f).roundToInt() is Manager -> stateManagerProgress = it.first.times(100f).roundToInt()
} }
} }
} }
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
repoMagisk.fetchUpdate()?.apply { repoMagisk.fetchUpdate()?.apply {
stateMagisk.value = when { stateMagisk = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED !Info.env.isActive -> MagiskState.NOT_INSTALLED
magisk.isObsolete -> MagiskState.OBSOLETE magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE else -> MagiskState.UP_TO_DATE
} }
stateManager.value = when { stateManager = when {
!app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED !app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE else -> MagiskState.UP_TO_DATE
} }
stateMagiskRemoteVersion.value = magiskRemoteVersion =
"${magisk.version} (${magisk.versionCode})" "${magisk.version} (${magisk.versionCode})"
stateManagerRemoteVersion.value = managerRemoteVersion =
"${app.version} (${app.versionCode}) (${stub.versionCode})" "${app.version} (${app.versionCode}) (${stub.versionCode})"
launch { launch {
@ -122,7 +127,7 @@ class HomeViewModel(
fun hideNotice() { fun hideNotice() {
Config.safetyNotice = false Config.safetyNotice = false
isNoticeVisible.value = false isNoticeVisible = false
} }
private suspend fun ensureEnv() { private suspend fun ensureEnv() {
@ -133,7 +138,7 @@ class HomeViewModel(
// Don't bother checking env when magisk is not installed, loading or already has been shown // Don't bother checking env when magisk is not installed, loading or already has been shown
if ( if (
invalidStates.any { it == stateMagisk.value } || invalidStates.any { it == stateMagisk } ||
shownDialog || shownDialog ||
// don't care for emulators either // don't care for emulators either
Build.DEVICE.orEmpty().contains("generic") || Build.DEVICE.orEmpty().contains("generic") ||

View File

@ -4,7 +4,6 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.RequestFileEvent import com.topjohnwu.magisk.model.events.RequestFileEvent
import com.topjohnwu.magisk.ui.base.BaseUIFragment import com.topjohnwu.magisk.ui.base.BaseUIFragment
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@ -16,7 +15,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
viewModel.data.value = RequestFileEvent.resolve(requestCode, resultCode, data) viewModel.data = RequestFileEvent.resolve(requestCode, resultCode, data)
} }
override fun onStart() { override fun onStart() {

View File

@ -2,21 +2,21 @@ package com.topjohnwu.magisk.ui.install
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.databinding.ObservableField import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.RemoteFileService import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.utils.Utils import com.topjohnwu.magisk.core.utils.Utils
import com.topjohnwu.magisk.data.repository.StringRepository import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.ktx.addOnPropertyChangedCallback
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.entity.internal.Configuration import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.events.RequestFileEvent import com.topjohnwu.magisk.model.events.RequestFileEvent
import com.topjohnwu.magisk.model.events.dialog.SecondSlotWarningDialog import com.topjohnwu.magisk.model.events.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.get import org.koin.core.get
@ -29,11 +29,26 @@ class InstallViewModel(
val isRooted get() = Shell.rootAccess() val isRooted get() = Shell.rootAccess()
val isAB get() = Info.isAB val isAB get() = Info.isAB
val step = ObservableField(0) @get:Bindable
val method = ObservableField(-1) var step by observable(0, BR.step)
val progress = ObservableField(0) @get:Bindable
val data = ObservableField(null as Uri?) var method by observable(-1, BR.method) {
val notes = ObservableField("") when (it) {
R.id.method_patch -> {
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
RequestFileEvent().publish()
}
R.id.method_inactive_slot -> {
SecondSlotWarningDialog().publish()
}
}
}
@get:Bindable
var progress by observable(0, BR.progress)
@get:Bindable
var data by observable(null as Uri?, BR.data)
@get:Bindable
var notes by observable("", BR.notes)
init { init {
RemoteFileService.reset() RemoteFileService.reset()
@ -42,29 +57,18 @@ class InstallViewModel(
if (subject !is DownloadSubject.Magisk) { if (subject !is DownloadSubject.Magisk) {
return@observeForever return@observeForever
} }
this.progress.value = progress.times(100).roundToInt() this.progress = progress.times(100).roundToInt()
if (this.progress.value >= 100) { if (this.progress >= 100) {
state = State.LOADED state = State.LOADED
} }
} }
viewModelScope.launch { viewModelScope.launch {
notes.value = stringRepo.getString(Info.remote.magisk.note) notes = stringRepo.getString(Info.remote.magisk.note)
}
method.addOnPropertyChangedCallback {
when (it!!) {
R.id.method_patch -> {
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
RequestFileEvent().publish()
}
R.id.method_inactive_slot -> {
SecondSlotWarningDialog().publish()
}
}
} }
} }
fun step(nextStep: Int) { fun step(nextStep: Int) {
step.value = nextStep step = nextStep
} }
fun install() = DownloadService(get()) { fun install() = DownloadService(get()) {
@ -73,9 +77,9 @@ class InstallViewModel(
// --- // ---
private fun resolveConfiguration() = when (method.value) { private fun resolveConfiguration() = when (method) {
R.id.method_download -> Configuration.Download R.id.method_download -> Configuration.Download
R.id.method_patch -> Configuration.Patch(data.value!!) R.id.method_patch -> Configuration.Patch(data!!)
R.id.method_direct -> Configuration.Flash.Primary R.id.method_direct -> Configuration.Flash.Primary
R.id.method_inactive_slot -> Configuration.Flash.Secondary R.id.method_inactive_slot -> Configuration.Flash.Secondary
else -> throw IllegalArgumentException("Unknown value") else -> throw IllegalArgumentException("Unknown value")

View File

@ -1,21 +1,23 @@
package com.topjohnwu.magisk.ui.log package com.topjohnwu.magisk.ui.log
import androidx.databinding.ObservableField import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.entity.recycler.LogItem import com.topjohnwu.magisk.model.entity.recycler.LogItem
import com.topjohnwu.magisk.model.entity.recycler.TextItem import com.topjohnwu.magisk.model.entity.recycler.TextItem
import com.topjohnwu.magisk.model.events.SnackbarEvent import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.diffListOf import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -38,19 +40,15 @@ class LogViewModel(
} }
// --- magisk log // --- magisk log
@get:Bindable
val consoleText = ObservableField(" ") var consoleText by observable(" ", BR.consoleText)
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
consoleText.value = repo.fetchMagiskLogs() consoleText = repo.fetchMagiskLogs()
val deferred = withContext(Dispatchers.Default) { val (suLogs, diff) = withContext(Dispatchers.Default) {
async { val suLogs = repo.fetchSuLogs().map { LogItem(it) }
val suLogs = repo.fetchSuLogs().map { LogItem(it) } suLogs to items.calculateDiff(suLogs)
suLogs to items.calculateDiff(suLogs)
}
} }
delay(500)
val (suLogs, diff) = deferred.await()
items.firstOrNull()?.isTop = false items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false items.lastOrNull()?.isBottom = false
items.update(suLogs, diff) items.update(suLogs, diff)

View File

@ -15,7 +15,7 @@ import com.topjohnwu.magisk.ktx.hideKeyboard
import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.model.events.ViewEvent import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.ReselectionTarget import com.topjohnwu.magisk.ui.base.ReselectionTarget
import com.topjohnwu.magisk.ui.base.BaseUIFragment import com.topjohnwu.magisk.ui.base.BaseUIFragment
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.MotionRevealHelper import com.topjohnwu.magisk.utils.MotionRevealHelper

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
@ -25,6 +24,7 @@ import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ui.base.* import com.topjohnwu.magisk.ui.base.*
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.observable
import kotlinx.coroutines.* import kotlinx.coroutines.*
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -53,18 +53,18 @@ class ModuleViewModel(
private var queryJob: Job? = null private var queryJob: Job? = null
private var remoteJob: Job? = null private var remoteJob: Job? = null
var query = "" @get:Bindable
@Bindable get var isRemoteLoading by observable(false, BR.remoteLoading)
set(value) {
if (field == value) return
field = value
notifyPropertyChanged(BR.query)
submitQuery()
// Yes we do lie about the search being loaded
searchLoading.value = true
}
val searchLoading = ObservableField(false) @get:Bindable
var query by observable("", BR.query) {
submitQuery()
// Yes we do lie about the search being loaded
searchLoading = true
}
@get:Bindable
var searchLoading by observable(false, BR.searchLoading)
val itemsSearch = diffListOf<RepoItem>() val itemsSearch = diffListOf<RepoItem>()
val itemSearchBinding = itemBindingOf<RepoItem> { val itemSearchBinding = itemBindingOf<RepoItem> {
it.bindExtra(BR.viewModel, this) it.bindExtra(BR.viewModel, this)
@ -80,13 +80,6 @@ class ModuleViewModel(
private val itemsUpdatable = diffListOf<RepoItem.Update>() private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsRemote = diffListOf<RepoItem.Remote>() private val itemsRemote = diffListOf<RepoItem.Remote>()
var isRemoteLoading = false
@Bindable get
private set(value) {
field = value
notifyPropertyChanged(BR.remoteLoading)
}
val adapter = adapterOf<ComparableRvItem<*>>() val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>() val items = MergeObservableList<ComparableRvItem<*>>()
.insertItem(InstallModule) .insertItem(InstallModule)
@ -261,7 +254,7 @@ class ModuleViewModel(
val diff = withContext(Dispatchers.Default) { val diff = withContext(Dispatchers.Default) {
itemsSearch.calculateDiff(searched) itemsSearch.calculateDiff(searched)
} }
searchLoading.value = false searchLoading = false
itemsSearch.update(searched, diff) itemsSearch.update(searched, diff)
} }
} }

View File

@ -1,13 +1,12 @@
package com.topjohnwu.magisk.ui.safetynet package com.topjohnwu.magisk.ui.safetynet
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.ObservableField
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.CheckSafetyNetEvent import com.topjohnwu.magisk.model.events.CheckSafetyNetEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.safetynet.SafetyNetState.* import com.topjohnwu.magisk.ui.safetynet.SafetyNetState.*
import com.topjohnwu.magisk.utils.observable
import org.json.JSONObject import org.json.JSONObject
enum class SafetyNetState { enum class SafetyNetState {
@ -21,19 +20,23 @@ data class SafetyNetResult(
class SafetynetViewModel : BaseViewModel() { class SafetynetViewModel : BaseViewModel() {
private var currentState = IDLE @get:Bindable
set(value) { var safetyNetTitle by observable(R.string.empty, BR.safetyNetTitle)
field = value @get:Bindable
notifyStateChanged() var ctsState by observable(false, BR.ctsState)
} @get:Bindable
val safetyNetTitle = ObservableField(R.string.empty) var basicIntegrityState by observable(false, BR.basicIntegrityState)
val ctsState = ObservableField(false) @get:Bindable
val basicIntegrityState = ObservableField(false) var evalType by observable("")
val evalType = ObservableField("")
val isChecking @Bindable get() = currentState == LOADING @get:Bindable
val isFailed @Bindable get() = currentState == FAILED val isChecking get() = currentState == LOADING
val isSuccess @Bindable get() = currentState == PASS @get:Bindable
val isFailed get() = currentState == FAILED
@get:Bindable
val isSuccess get() = currentState == PASS
private var currentState by observable(IDLE, BR.checking, BR.failed, BR.success)
init { init {
cachedResult?.also { cachedResult?.also {
@ -41,13 +44,6 @@ class SafetynetViewModel : BaseViewModel() {
} ?: attest() } ?: attest()
} }
override fun notifyStateChanged() {
super.notifyStateChanged()
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.failed)
notifyPropertyChanged(BR.success)
}
private fun attest() { private fun attest() {
currentState = LOADING currentState = LOADING
CheckSafetyNetEvent() { CheckSafetyNetEvent() {
@ -70,26 +66,26 @@ class SafetynetViewModel : BaseViewModel() {
val eval = optString("evaluationType") val eval = optString("evaluationType")
val result = cts && basic val result = cts && basic
cachedResult = this cachedResult = this
ctsState.value = cts ctsState = cts
basicIntegrityState.value = basic basicIntegrityState = basic
evalType.value = if (eval.contains("HARDWARE")) "HARDWARE" else "BASIC" evalType = if (eval.contains("HARDWARE")) "HARDWARE" else "BASIC"
currentState = if (result) PASS else FAILED currentState = if (result) PASS else FAILED
safetyNetTitle.value = safetyNetTitle =
if (result) R.string.safetynet_attest_success if (result) R.string.safetynet_attest_success
else R.string.safetynet_attest_failure else R.string.safetynet_attest_failure
}.onFailure { }.onFailure {
currentState = FAILED currentState = FAILED
ctsState.value = false ctsState = false
basicIntegrityState.value = false basicIntegrityState = false
evalType.value = "N/A" evalType = "N/A"
safetyNetTitle.value = R.string.safetynet_res_invalid safetyNetTitle = R.string.safetynet_res_invalid
} }
} ?: { } ?: {
currentState = FAILED currentState = FAILED
ctsState.value = false ctsState = false
basicIntegrityState.value = false basicIntegrityState = false
evalType.value = "N/A" evalType = "N/A"
safetyNetTitle.value = R.string.safetynet_api_error safetyNetTitle = R.string.safetynet_api_error
}() }()
} }

View File

@ -46,7 +46,7 @@ object Language : SettingsItem.Selector() {
entryValues = values entryValues = values
val selectedLocale = currentLocale.getDisplayName(currentLocale) val selectedLocale = currentLocale.getDisplayName(currentLocale)
value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it } value = names.indexOfFirst { it == selectedLocale }.let { if (it == -1) 0 else it }
notifyChange(BR.selectedEntry) notifyPropertyChanged(BR.selectedEntry)
} }
} }
} }
@ -79,8 +79,8 @@ object Hide : SettingsItem.Input() {
override var value: String = resources.getString(R.string.re_app_name) override var value: String = resources.getString(R.string.re_app_name)
set(value) { set(value) {
field = value field = value
notifyChange(BR.value) notifyPropertyChanged(BR.value)
notifyChange(BR.error) notifyPropertyChanged(BR.error)
} }
@get:Bindable @get:Bindable
@ -112,8 +112,8 @@ object DownloadPath : SettingsItem.Input() {
var result = value var result = value
set(value) { set(value) {
field = value field = value
notifyChange(BR.result) notifyPropertyChanged(BR.result)
notifyChange(BR.path) notifyPropertyChanged(BR.path)
} }
@get:Bindable @get:Bindable
@ -143,7 +143,7 @@ object UpdateChannelUrl : SettingsItem.Input() {
var result = value var result = value
set(value) { set(value) {
field = value field = value
notifyChange(BR.result) notifyPropertyChanged(BR.result)
} }
override fun refresh() { override fun refresh() {

View File

@ -6,8 +6,9 @@ import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.CountDownTimer import android.os.CountDownTimer
import androidx.databinding.ObservableField import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
@ -15,10 +16,10 @@ import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.DENY import com.topjohnwu.magisk.core.model.MagiskPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler import com.topjohnwu.magisk.core.su.SuRequestHandler
import com.topjohnwu.magisk.core.utils.BiometricHelper import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.events.DieEvent import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
@ -32,16 +33,20 @@ class SuRequestViewModel(
private val res: Resources private val res: Resources
) : BaseViewModel() { ) : BaseViewModel() {
val icon = ObservableField<Drawable?>(null) @get:Bindable
val title = ObservableField("") var icon by observable(null as Drawable?, BR.icon)
val packageName = ObservableField("") @get:Bindable
var title by observable("", BR.title)
val denyText = ObservableField(res.getString(R.string.deny)) @get:Bindable
val warningText = ObservableField<CharSequence>(res.getString(R.string.su_warning)) var packageName by observable("", BR.packageName)
@get:Bindable
val selectedItemPosition = ObservableField(0) var denyText by observable(res.getString(R.string.deny), BR.denyText)
@get:Bindable
val grantEnabled = ObservableField(false) var warningText by observable(res.getString(R.string.su_warning), BR.warningText)
@get:Bindable
var selectedItemPosition by observable(0, BR.selectedItemPosition)
@get:Bindable
var grantEnabled by observable(false, BR.grantEnabled)
private val items = res.getStringArray(R.array.allow_timeout).map { SpinnerRvItem(it) } private val items = res.getStringArray(R.array.allow_timeout).map { SpinnerRvItem(it) }
val adapter = BindingListViewAdapter<SpinnerRvItem>(1).apply { val adapter = BindingListViewAdapter<SpinnerRvItem>(1).apply {
@ -89,7 +94,7 @@ class SuRequestViewModel(
fun respond(action: Int) { fun respond(action: Int) {
timer.cancel() timer.cancel()
val pos = selectedItemPosition.value val pos = selectedItemPosition
timeoutPrefs.edit().putInt(policy.packageName, pos).apply() timeoutPrefs.edit().putInt(policy.packageName, pos).apply()
respond(action, Config.Value.TIMEOUT_LIST[pos]) respond(action, Config.Value.TIMEOUT_LIST[pos])
@ -99,16 +104,16 @@ class SuRequestViewModel(
fun cancelTimer() { fun cancelTimer() {
timer.cancel() timer.cancel()
denyText.value = res.getString(R.string.deny) denyText = res.getString(R.string.deny)
} }
override fun onStart() { override fun onStart() {
icon.value = policy.applicationInfo.loadIcon(pm) icon = policy.applicationInfo.loadIcon(pm)
title.value = policy.appName title = policy.appName
packageName.value = policy.packageName packageName = policy.packageName
UiThreadHandler.handler.post { UiThreadHandler.handler.post {
// Delay is required to properly do selection // Delay is required to properly do selection
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0) selectedItemPosition = timeoutPrefs.getInt(policy.packageName, 0)
} }
// Set timer // Set timer
@ -122,14 +127,14 @@ class SuRequestViewModel(
) : CountDownTimer(millis, interval) { ) : CountDownTimer(millis, interval) {
override fun onTick(remains: Long) { override fun onTick(remains: Long) {
if (!grantEnabled.value && remains <= millis - 1000) { if (!grantEnabled && remains <= millis - 1000) {
grantEnabled.value = true grantEnabled = true
} }
denyText.value = "${res.getString(R.string.deny)} (${(remains / 1000) + 1})" denyText = "${res.getString(R.string.deny)} (${(remains / 1000) + 1})"
} }
override fun onFinish() { override fun onFinish() {
denyText.value = res.getString(R.string.deny) denyText = res.getString(R.string.deny)
respond(DENY) respond(DENY)
} }

View File

@ -0,0 +1,170 @@
package com.topjohnwu.magisk.utils
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
* Modified from https://github.com/skoumalcz/teanity/blob/1.2/core/src/main/java/com/skoumal/teanity/observable/Notifyable.kt
*
* Interface that allows user to be observed via DataBinding or manually by assigning listeners.
*
* @see [androidx.databinding.Observable]
* */
interface ObservableHost : Observable {
/**
* Notifies all observers that something has changed. By default implementation this method is
* synchronous, hence observers will never be notified in undefined order. Observers might
* choose to refresh the view completely, which is beyond the scope of this function.
* */
fun notifyChange(host: Observable = this)
/**
* Notifies all observers about field with [fieldId] has been changed. This will happen
* synchronously before or after [notifyChange] has been called. It will never be called during
* the execution of aforementioned method.
* */
fun notifyPropertyChanged(fieldId: Int, host: Observable = this)
companion object {
val impl: ObservableHost get() = ObservableHostImpl()
}
}
private class ObservableHostImpl : ObservableHost {
private var callbacks: PropertyChangeRegistry? = null
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: PropertyChangeRegistry().also { callbacks = it }
}.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: return
}.remove(callback)
}
override fun notifyChange(host: Observable) {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(host, 0, null)
}
override fun notifyPropertyChanged(fieldId: Int, host: Observable) {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(host, fieldId, null)
}
}
/**
* Declares delegated property in [ObservableHost] parent. This property is available for DataBinding
* to be observed as usual. The only caveat is that in order for binding to generate the [fieldId]
* it has to be annotated accordingly.
*
* The annotation however give very strict control over your internal fields and overall reduce
* overhead in notifying observers. (In comparison to [androidx.databinding.ObservableField])
* It helps the kotlin code to feel more,... _native_, while respecting the original functionality.
*
* # Examples:
*
* ## The most basic usage would probably be:
* ```kotlin
* @get:Bindable
* var myField by observable(defaultValue, BR.myField)
* private set
* ```
*
* ## You can use the field as public read/write, of course:
* ```kotlin
* @get:Bindable
* var myField by observable(defaultValue, BR.myField)
* ```
*
* ## Please beware that delegated property instantiates one class per property
* We discourage using simple getters via delegated properties. Instead you can do something like
* this:
*
* ```kotlin
* @get:Bindable
* var myField by observable(defaultValue, BR.myField, BR.myTransformedField)
*
* var myTransformedField
* @Bindable get() {
* return myField.transform()
* }
* set(value) {
* myField = value.transform()
* }
* ```
*
* */
// Optimize for the most common use case
// Generic type is reified to optimize primitive types
inline fun <reified T> ObservableHost.observable(
initialValue: T,
fieldId: Int
) = object : ReadWriteProperty<ObservableHost, T> {
private var field = initialValue
override fun getValue(thisRef: ObservableHost, property: KProperty<*>): T {
return field
}
@Synchronized
override fun setValue(thisRef: ObservableHost, property: KProperty<*>, value: T) {
if (field != value) {
field = value
notifyPropertyChanged(fieldId)
}
}
}
inline fun <reified T> ObservableHost.observable(
initialValue: T,
vararg fieldIds: Int
) = object : ReadWriteProperty<ObservableHost, T> {
private var field = initialValue
override fun getValue(thisRef: ObservableHost, property: KProperty<*>): T {
return field
}
@Synchronized
override fun setValue(thisRef: ObservableHost, property: KProperty<*>, value: T) {
if (field != value) {
field = value
fieldIds.forEach { notifyPropertyChanged(it) }
}
}
}
inline fun <reified T> ObservableHost.observable(
initialValue: T,
vararg fieldIds: Int,
crossinline afterChanged: (T) -> Unit
) = object : ReadWriteProperty<ObservableHost, T> {
private var field = initialValue
override fun getValue(thisRef: ObservableHost, property: KProperty<*>): T {
return field
}
@Synchronized
override fun setValue(thisRef: ObservableHost, property: KProperty<*>, value: T) {
if (field != value) {
field = value
fieldIds.forEach { notifyPropertyChanged(it) }
afterChanged(value)
}
}
}

View File

@ -132,7 +132,7 @@
<TextView <TextView
style="@style/W.Home.ItemContent.Right" style="@style/W.Home.ItemContent.Right"
android:text="@{viewModel.isConnected ? viewModel.stateMagiskRemoteVersion : @string/not_available}" android:text="@{viewModel.isConnected ? viewModel.magiskRemoteVersion : @string/not_available}"
tools:text="20.1 (12345)" /> tools:text="20.1 (12345)" />
</LinearLayout> </LinearLayout>
@ -149,7 +149,7 @@
<TextView <TextView
style="@style/W.Home.ItemContent.Right" style="@style/W.Home.ItemContent.Right"
android:text="@{Info.env.isActive ? viewModel.stateMagiskInstalledVersion : @string/not_available}" android:text="@{Info.env.isActive ? viewModel.magiskInstalledVersion : @string/not_available}"
tools:text="20.1 (12345)" /> tools:text="20.1 (12345)" />
</LinearLayout> </LinearLayout>
@ -166,7 +166,7 @@
<TextView <TextView
style="@style/W.Home.ItemContent.Right" style="@style/W.Home.ItemContent.Right"
android:text="@{Info.env.isActive ? viewModel.stateMagiskMode : @string/not_available}" android:text="@{Info.env.isActive ? viewModel.magiskMode : @string/not_available}"
tools:text="Normal" /> tools:text="Normal" />
</LinearLayout> </LinearLayout>

View File

@ -121,7 +121,7 @@
<TextView <TextView
style="@style/W.Home.ItemContent.Right" style="@style/W.Home.ItemContent.Right"
android:text="@{viewModel.isConnected ? viewModel.stateManagerRemoteVersion : @string/not_available}" android:text="@{viewModel.isConnected ? viewModel.managerRemoteVersion : @string/not_available}"
tools:text="8.0.0 (123) (10)" /> tools:text="8.0.0 (123) (10)" />
</LinearLayout> </LinearLayout>
@ -138,7 +138,7 @@
<TextView <TextView
style="@style/W.Home.ItemContent.Right" style="@style/W.Home.ItemContent.Right"
android:text="@{viewModel.stateManagerInstalledVersion}" android:text="@{viewModel.managerInstalledVersion}"
tools:text="8.0.0 (123) (10)" /> tools:text="8.0.0 (123) (10)" />
</LinearLayout> </LinearLayout>

View File

@ -25,6 +25,7 @@ org.gradle.daemon=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.enableR8.fullMode=true android.enableR8.fullMode=true
android.databinding.incremental=true
android.injected.testOnly=false android.injected.testOnly=false
kapt.incremental.apt=true kapt.incremental.apt=true