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 com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.ObservableHost
import me.tatarka.bindingcollectionadapter2.ItemBinding
abstract class RvItem {
@ -45,4 +46,6 @@ abstract class ComparableRvItem<in T> : RvItem() {
) = oldItem.genericContentSameAs(newItem)
}
}
}
}
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 com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ktx.timeDateFormat
import com.topjohnwu.magisk.ktx.toTime
import com.topjohnwu.magisk.model.entity.MagiskLog
@ -16,13 +17,13 @@ class LogItem(val item: MagiskLog) : ObservableItem<LogItem>() {
@Bindable get
set(value) {
field = value
notifyChange(BR.top)
notifyPropertyChanged(BR.top)
}
var isBottom = false
@Bindable get
set(value) {
field = value
notifyChange(BR.bottom)
notifyPropertyChanged(BR.bottom)
}
override fun itemSameAs(other: LogItem) = item.appName == other.item.appName

View File

@ -1,12 +1,16 @@
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 com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.ui.module.ModuleViewModel
object InstallModule : ComparableRvItem<InstallModule>() {
@ -33,19 +37,19 @@ class SectionTitle(
@Bindable get
set(value) {
field = value
notifyChange(BR.button)
notifyPropertyChanged(BR.button)
}
var icon = _icon
@Bindable get
set(value) {
field = value
notifyChange(BR.icon)
notifyPropertyChanged(BR.icon)
}
var hasButton = button != 0 || icon != 0
@Bindable get
set(value) {
field = value
notifyChange(BR.hasButton)
notifyPropertyChanged(BR.hasButton)
}
override fun onBindingBound(binding: ViewDataBinding) {
@ -66,7 +70,7 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
@Bindable get
protected set(value) {
field = value
notifyChange(BR.update)
notifyPropertyChanged(BR.update)
}
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
set(value) {
field = value
notifyChange(BR.repo)
notifyPropertyChanged(BR.repo)
}
@get:Bindable
@ -97,7 +101,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
get() = item.enable
set(value) {
item.enable = value
notifyChange(BR.enabled)
notifyPropertyChanged(BR.enabled)
}
@get:Bindable
@ -105,7 +109,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
get() = item.remove
set(value) {
item.remove = value
notifyChange(BR.removed)
notifyPropertyChanged(BR.removed)
}
val isUpdated get() = item.updated
@ -126,21 +130,5 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
&& item.name == other.item.name
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 com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.utils.TransitiveText
import com.topjohnwu.magisk.view.MagiskDialog
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,
// which wouldn't be recognized with reverse approach
notifyChange(BR.description)
notifyPropertyChanged(BR.description)
}
open fun refresh() {}
@ -60,7 +61,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
) = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
setter(newValue)
notifyChange(fieldId)
notifyPropertyChanged(fieldId)
}
}
@ -169,7 +170,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
}
.applyAdapter(entries) {
value = it
notifyChange(BR.selectedEntry)
notifyPropertyChanged(BR.selectedEntry)
super.onPressed(view, callback)
}
.reveal()

View File

@ -18,6 +18,8 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ktx.startAnimations
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.utils.HideBottomViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
@ -26,6 +28,8 @@ import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
class MainViewModel : BaseViewModel()
open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>() {
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.ktx.snackbar
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.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent
@ -116,7 +115,7 @@ abstract class BaseUIActivity<ViewModel : BaseViewModel, Binding : ViewDataBindi
}
override fun peekSystemWindowInsets(insets: Insets) {
viewModel.insets.value = insets
viewModel.insets = insets
}
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.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.model.events.*
import com.topjohnwu.magisk.model.navigation.NavigationWrapper
import com.topjohnwu.magisk.utils.ObservableHost
import com.topjohnwu.magisk.utils.observable
import kotlinx.coroutines.Job
import org.koin.core.KoinComponent
abstract class BaseViewModel(
initialState: State = State.LOADING
) : ViewModel(), Observable, KoinComponent {
) : ViewModel(), ObservableHost by ObservableHost.impl, KoinComponent {
enum class State {
LOADED, LOADING, LOADING_FAILED
}
val loading @Bindable get() = state == State.LOADING
val loaded @Bindable get() = state == State.LOADED
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
@get:Bindable
val loading get() = state == State.LOADING
@get:Bindable
val loaded get() = state == State.LOADED
@get:Bindable
val loadFailed get() = state == State.LOADING_FAILED
val isConnected get() = Info.isConnected
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
val insets = ObservableField(Insets.NONE)
var state: State = initialState
set(value) {
field = value
notifyStateChanged()
}
@get:Bindable
var insets by observable(Insets.NONE, BR.insets)
var state by observable(initialState, BR.loading, BR.loaded, BR.loadFailed)
private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningJob: Job? = null
@ -65,12 +66,6 @@ abstract class BaseViewModel(
protected open fun refresh(): Job? = null
open fun notifyStateChanged() {
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.loaded)
notifyPropertyChanged(BR.loadingFailed)
}
@CallSuper
override fun onCleared() {
isConnected.removeOnPropertyChangedCallback(refreshCallback)
@ -108,41 +103,4 @@ abstract class BaseViewModel(
_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.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.model.events.ContextExecutor
import com.topjohnwu.magisk.model.events.FragmentExecutor
@ -44,7 +43,7 @@ class CompatDelegate internal constructor(
insets.asInsets()
.also { view.peekSystemWindowInsets(it) }
.let { view.consumeSystemWindowInsets(it) }
?.also { view.viewModel.insets.value = it }
?.also { view.viewModel.insets = it }
?.subtractBy(insets) ?: insets
}
if (ViewCompat.isAttachedToWindow(view.viewRoot)) {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package com.topjohnwu.magisk.ui.home
import android.os.Build
import androidx.databinding.ObservableField
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BuildConfig
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.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
import me.tatarka.bindingcollectionadapter2.BR
@ -40,22 +41,26 @@ class HomeViewModel(
private val repoMagisk: MagiskRepository
) : BaseViewModel() {
val isNoticeVisible = ObservableField(Config.safetyNotice)
val stateMagisk = ObservableField(MagiskState.LOADING)
val stateManager = ObservableField(MagiskState.LOADING)
val stateMagiskRemoteVersion = ObservableField(R.string.loading.res())
val stateMagiskInstalledVersion get() =
@get:Bindable
var isNoticeVisible by observable(Config.safetyNotice, BR.noticeVisible)
@get:Bindable
var stateMagisk by observable(MagiskState.LOADING, BR.stateMagisk)
@get:Bindable
var stateManager by observable(MagiskState.LOADING, BR.stateManager)
@get:Bindable
var magiskRemoteVersion by observable(R.string.loading.res(), BR.magiskRemoteVersion)
val magiskInstalledVersion get() =
"${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())
val stateManagerInstalledVersion = Info.stub?.let {
@get:Bindable
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})"
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 itemBinding = itemBindingOf<HomeItem> {
@ -70,28 +75,28 @@ class HomeViewModel(
init {
RemoteFileService.progressBroadcast.observeForever {
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 {
repoMagisk.fetchUpdate()?.apply {
stateMagisk.value = when {
stateMagisk = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED
magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
stateManager.value = when {
stateManager = when {
!app.isUpdateChannelCorrect && isConnected.value -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
stateMagiskRemoteVersion.value =
magiskRemoteVersion =
"${magisk.version} (${magisk.versionCode})"
stateManagerRemoteVersion.value =
managerRemoteVersion =
"${app.version} (${app.versionCode}) (${stub.versionCode})"
launch {
@ -122,7 +127,7 @@ class HomeViewModel(
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible.value = false
isNoticeVisible = false
}
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
if (
invalidStates.any { it == stateMagisk.value } ||
invalidStates.any { it == stateMagisk } ||
shownDialog ||
// don't care for emulators either
Build.DEVICE.orEmpty().contains("generic") ||

View File

@ -4,7 +4,6 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.ktx.value
import com.topjohnwu.magisk.model.events.RequestFileEvent
import com.topjohnwu.magisk.ui.base.BaseUIFragment
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?) {
super.onActivityResult(requestCode, resultCode, data)
viewModel.data.value = RequestFileEvent.resolve(requestCode, resultCode, data)
viewModel.data = RequestFileEvent.resolve(requestCode, resultCode, data)
}
override fun onStart() {

View File

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

View File

@ -1,21 +1,23 @@
package com.topjohnwu.magisk.ui.log
import androidx.databinding.ObservableField
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
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.TextItem
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.ui.base.BaseViewModel
import com.topjohnwu.magisk.ui.base.diffListOf
import com.topjohnwu.magisk.ui.base.itemBindingOf
import com.topjohnwu.magisk.utils.observable
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 java.io.File
import java.io.IOException
@ -38,19 +40,15 @@ class LogViewModel(
}
// --- magisk log
val consoleText = ObservableField(" ")
@get:Bindable
var consoleText by observable(" ", BR.consoleText)
override fun refresh() = viewModelScope.launch {
consoleText.value = repo.fetchMagiskLogs()
val deferred = withContext(Dispatchers.Default) {
async {
val suLogs = repo.fetchSuLogs().map { LogItem(it) }
suLogs to items.calculateDiff(suLogs)
}
consoleText = repo.fetchMagiskLogs()
val (suLogs, diff) = withContext(Dispatchers.Default) {
val suLogs = repo.fetchSuLogs().map { LogItem(it) }
suLogs to items.calculateDiff(suLogs)
}
delay(500)
val (suLogs, diff) = deferred.await()
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
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.ViewEvent
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.utils.EndlessRecyclerScrollListener
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.ObservableArrayList
import androidx.databinding.ObservableField
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
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.ui.base.*
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.observable
import kotlinx.coroutines.*
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import kotlin.math.roundToInt
@ -53,18 +53,18 @@ class ModuleViewModel(
private var queryJob: Job? = null
private var remoteJob: Job? = null
var query = ""
@Bindable get
set(value) {
if (field == value) return
field = value
notifyPropertyChanged(BR.query)
submitQuery()
// Yes we do lie about the search being loaded
searchLoading.value = true
}
@get:Bindable
var isRemoteLoading by observable(false, BR.remoteLoading)
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 itemSearchBinding = itemBindingOf<RepoItem> {
it.bindExtra(BR.viewModel, this)
@ -80,13 +80,6 @@ class ModuleViewModel(
private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsRemote = diffListOf<RepoItem.Remote>()
var isRemoteLoading = false
@Bindable get
private set(value) {
field = value
notifyPropertyChanged(BR.remoteLoading)
}
val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>()
.insertItem(InstallModule)
@ -261,7 +254,7 @@ class ModuleViewModel(
val diff = withContext(Dispatchers.Default) {
itemsSearch.calculateDiff(searched)
}
searchLoading.value = false
searchLoading = false
itemsSearch.update(searched, diff)
}
}

View File

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

View File

@ -46,7 +46,7 @@ object Language : SettingsItem.Selector() {
entryValues = values
val selectedLocale = currentLocale.getDisplayName(currentLocale)
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)
set(value) {
field = value
notifyChange(BR.value)
notifyChange(BR.error)
notifyPropertyChanged(BR.value)
notifyPropertyChanged(BR.error)
}
@get:Bindable
@ -112,8 +112,8 @@ object DownloadPath : SettingsItem.Input() {
var result = value
set(value) {
field = value
notifyChange(BR.result)
notifyChange(BR.path)
notifyPropertyChanged(BR.result)
notifyPropertyChanged(BR.path)
}
@get:Bindable
@ -143,7 +143,7 @@ object UpdateChannelUrl : SettingsItem.Input() {
var result = value
set(value) {
field = value
notifyChange(BR.result)
notifyPropertyChanged(BR.result)
}
override fun refresh() {

View File

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

View File

@ -121,7 +121,7 @@
<TextView
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)" />
</LinearLayout>
@ -138,7 +138,7 @@
<TextView
style="@style/W.Home.ItemContent.Right"
android:text="@{viewModel.stateManagerInstalledVersion}"
android:text="@{viewModel.managerInstalledVersion}"
tools:text="8.0.0 (123) (10)" />
</LinearLayout>

View File

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