diff --git a/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt index 087a753f1..73ab33b3d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt +++ b/app/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt @@ -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 : RvItem() { ) = oldItem.genericContentSameAs(newItem) } } -} \ No newline at end of file +} + +abstract class ObservableItem : ComparableRvItem(), ObservableHost by ObservableHost.impl diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt index f7f4dcffa..8ea52474d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt @@ -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() { @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 diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt index 05b17887c..59e484b5d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -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() { @@ -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() { @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(), 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(), 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(), 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(), Observable { && item.name == other.item.name override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id - } -abstract class ObservableItem : ComparableRvItem(), 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) - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt index fc372ce9b..fa9ef952d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SettingsItem.kt @@ -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() { // 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() { ) = object : ObservableProperty(initialValue) { override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) { setter(newValue) - notifyChange(fieldId) + notifyPropertyChanged(fieldId) } } @@ -169,7 +170,7 @@ sealed class SettingsItem : ObservableItem() { } .applyAdapter(entries) { value = it - notifyChange(BR.selectedEntry) + notifyPropertyChanged(BR.selectedEntry) super.onPressed(view, callback) } .reveal() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt index ac5d3d147..a8622afff 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -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() { override val layoutRes = R.layout.activity_main_md2 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt deleted file mode 100644 index 5451829da..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.topjohnwu.magisk.ui - -import com.topjohnwu.magisk.ui.base.BaseViewModel - -class MainViewModel : BaseViewModel() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt b/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt deleted file mode 100644 index 2134ecc1b..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/ReselectionTarget.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.topjohnwu.magisk.ui - -interface ReselectionTarget { - - fun onReselected() - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt index b103fbed2..b9253600f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BaseUIActivity.kt @@ -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 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() 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) - } - } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt index 84ec1b91a..75c7a45fc 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatDelegate.kt @@ -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)) { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index e4de9af20..ed33bfeb0 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -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() val items = diffListOf() @@ -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) } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt index 9abb75430..3f7368b53 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt @@ -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() val itemBinding = itemBindingOf { 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 - } - } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt index b7ed04998..763ba293d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt @@ -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 { @@ -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") || diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt index e19012502..a65cc786f 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt @@ -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 { + 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") diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt index b2c35ff21..be5608d17 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt @@ -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) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt index 766899563..418a56068 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 3da68f51f..4168443a5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -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() val itemSearchBinding = itemBindingOf { it.bindExtra(BR.viewModel, this) @@ -80,13 +80,6 @@ class ModuleViewModel( private val itemsUpdatable = diffListOf() private val itemsRemote = diffListOf() - var isRemoteLoading = false - @Bindable get - private set(value) { - field = value - notifyPropertyChanged(BR.remoteLoading) - } - val adapter = adapterOf>() val items = MergeObservableList>() .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) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt index d29e3bf47..7278ce3f6 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt @@ -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 }() } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt index 7f1d9efd7..13a73e302 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt @@ -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() { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt index d097967ff..d6b372684 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt @@ -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(null) - val title = ObservableField("") - val packageName = ObservableField("") - - val denyText = ObservableField(res.getString(R.string.deny)) - val warningText = ObservableField(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(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) } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/ObservableHost.kt b/app/src/main/java/com/topjohnwu/magisk/utils/ObservableHost.kt new file mode 100644 index 000000000..a5254be7f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/ObservableHost.kt @@ -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 ObservableHost.observable( + initialValue: T, + fieldId: Int +) = object : ReadWriteProperty { + 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 ObservableHost.observable( + initialValue: T, + vararg fieldIds: Int +) = object : ReadWriteProperty { + 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 ObservableHost.observable( + initialValue: T, + vararg fieldIds: Int, + crossinline afterChanged: (T) -> Unit +) = object : ReadWriteProperty { + 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) + } + } +} diff --git a/app/src/main/res/layout/include_home_magisk.xml b/app/src/main/res/layout/include_home_magisk.xml index b8a90caaa..07b0f286e 100644 --- a/app/src/main/res/layout/include_home_magisk.xml +++ b/app/src/main/res/layout/include_home_magisk.xml @@ -132,7 +132,7 @@ @@ -149,7 +149,7 @@ @@ -166,7 +166,7 @@ diff --git a/app/src/main/res/layout/include_home_manager.xml b/app/src/main/res/layout/include_home_manager.xml index 8f9d48ff9..007425875 100644 --- a/app/src/main/res/layout/include_home_manager.xml +++ b/app/src/main/res/layout/include_home_manager.xml @@ -121,7 +121,7 @@ @@ -138,7 +138,7 @@ diff --git a/gradle.properties b/gradle.properties index 9d8446412..2865e255b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file +kapt.incremental.apt=true