Decouple state from BaseViewModel

This commit is contained in:
topjohnwu 2022-06-10 02:13:25 -07:00
parent aabc36f86b
commit 46d4708386
13 changed files with 57 additions and 113 deletions

View File

@ -4,17 +4,14 @@ import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry 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
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ObservableHost import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.BackPressEvent import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.NavigationEvent import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent import com.topjohnwu.magisk.events.PermissionEvent
@ -25,22 +22,8 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null override var callbacks: PropertyChangeRegistry? = null
enum class State {
LOADED, LOADING, 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 viewEvents: LiveData<ViewEvent> get() = _viewEvents val viewEvents: LiveData<ViewEvent> get() = _viewEvents
var state = State.LOADING
set(value) = set(value, field, { field = it }, 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

View File

@ -2,14 +2,15 @@ package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
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.arch.BaseViewModel import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.di.AppContext import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.databinding.bindExtra import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.filterableListOf import com.topjohnwu.magisk.databinding.filterableListOf
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.ktx.concurrentMap import com.topjohnwu.magisk.ktx.concurrentMap
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@ -43,13 +44,13 @@ class DenyListViewModel : BaseViewModel() {
it.put(BR.viewModel, this) it.put(BR.viewModel, this)
} }
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
if (!Utils.showSuperUser()) { loading = true
state = State.LOADING_FAILED
return@launch
}
state = State.LOADING
val (apps, diff) = withContext(Dispatchers.Default) { val (apps, diff) = withContext(Dispatchers.Default) {
val pm = AppContext.packageManager val pm = AppContext.packageManager
val denyList = Shell.cmd("magisk --denylist ls").exec().out val denyList = Shell.cmd("magisk --denylist ls").exec().out
@ -84,6 +85,6 @@ class DenyListViewModel : BaseViewModel() {
(it.isChecked || (filterSystem() && filterOS())) && filterQuery() (it.isChecked || (filterSystem() && filterOS())) && filterQuery()
} }
state = State.LOADED loading = false
} }
} }

View File

@ -35,7 +35,7 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
setHasOptionsMenu(true) setHasOptionsMenu(true)
activity?.setTitle(R.string.flash_screen_title) activity?.setTitle(R.string.flash_screen_title)
viewModel.flashState.observe(this) { viewModel.state.observe(this) {
activity?.supportActionBar?.setSubtitle( activity?.supportActionBar?.setSubtitle(
when (it) { when (it) {
FlashViewModel.State.FLASHING -> R.string.flashing FlashViewModel.State.FLASHING -> R.string.flashing

View File

@ -32,9 +32,9 @@ class FlashViewModel : BaseViewModel() {
FLASHING, SUCCESS, FAILED FLASHING, SUCCESS, FAILED
} }
private val _flashState = MutableLiveData(State.FLASHING) private val _state = MutableLiveData(State.FLASHING)
val flashState: LiveData<State> get() = _flashState val state: LiveData<State> get() = _state
val flashing = Transformations.map(flashState) { it == State.FLASHING } val flashing = Transformations.map(state) { it == State.FLASHING }
@get:Bindable @get:Bindable
var showReboot = Info.isRooted var showReboot = Info.isRooted
@ -89,7 +89,7 @@ class FlashViewModel : BaseViewModel() {
} }
private fun onResult(success: Boolean) { private fun onResult(success: Boolean) {
_flashState.value = if (success) State.SUCCESS else State.FAILED _state.value = if (success) State.SUCCESS else State.FAILED
} }
fun onMenuItemClicked(item: MenuItem): Boolean { fun onMenuItemClicked(item: MenuItem): Boolean {

View File

@ -26,14 +26,14 @@ import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
enum class MagiskState {
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}
class HomeViewModel( class HomeViewModel(
private val svc: NetworkService private val svc: NetworkService
) : BaseViewModel() { ) : BaseViewModel() {
enum class State {
LOADING, INVALID, OUTDATED, UP_TO_DATE
}
val magiskTitleBarrierIds = val magiskTitleBarrierIds =
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button) intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
val appTitleBarrierIds = val appTitleBarrierIds =
@ -43,16 +43,16 @@ class HomeViewModel(
var isNoticeVisible = Config.safetyNotice var isNoticeVisible = Config.safetyNotice
set(value) = set(value, field, { field = it }, BR.noticeVisible) set(value) = set(value, field, { field = it }, BR.noticeVisible)
val stateMagisk val magiskState
get() = when { get() = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED !Info.env.isActive -> State.INVALID
Info.env.versionCode < BuildConfig.VERSION_CODE -> MagiskState.OBSOLETE Info.env.versionCode < BuildConfig.VERSION_CODE -> State.OUTDATED
else -> MagiskState.UP_TO_DATE else -> State.UP_TO_DATE
} }
@get:Bindable @get:Bindable
var stateManager = MagiskState.LOADING var appState = State.LOADING
set(value) = set(value, field, { field = it }, BR.stateManager) set(value) = set(value, field, { field = it }, BR.appState)
val magiskInstalledVersion val magiskInstalledVersion
get() = Info.env.run { get() = Info.env.run {
@ -84,13 +84,11 @@ class HomeViewModel(
} }
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
state = State.LOADING appState = State.LOADING
Info.getRemote(svc)?.apply { Info.getRemote(svc)?.apply {
state = State.LOADED appState = when {
BuildConfig.VERSION_CODE < magisk.versionCode -> State.OUTDATED
stateManager = when { else -> State.UP_TO_DATE
BuildConfig.VERSION_CODE < magisk.versionCode -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
} }
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
@ -98,7 +96,6 @@ class HomeViewModel(
("${magisk.version} (${magisk.versionCode}) (${stub.versionCode})" + ("${magisk.version} (${magisk.versionCode}) (${stub.versionCode})" +
if (isDebug) " (D)" else "").asText() if (isDebug) " (D)" else "").asText()
} ?: run { } ?: run {
state = State.LOADING_FAILED
managerRemoteVersion = R.string.not_available.asText() managerRemoteVersion = R.string.not_available.asText()
} }
ensureEnv() ensureEnv()
@ -119,14 +116,14 @@ class HomeViewModel(
fun onDeletePressed() = UninstallDialog().publish() fun onDeletePressed() = UninstallDialog().publish()
fun onManagerPressed() = when (state) { fun onManagerPressed() = when (magiskState) {
State.LOADED -> withExternalRW { State.LOADING -> SnackbarEvent(R.string.loading).publish()
State.INVALID -> SnackbarEvent(R.string.no_connection).publish()
else -> withExternalRW {
withInstallPermission { withInstallPermission {
ManagerInstallDialog().publish() ManagerInstallDialog().publish()
} }
} }
State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).publish()
} }
fun onMagiskPressed() = withExternalRW { fun onMagiskPressed() = withExternalRW {
@ -139,7 +136,7 @@ class HomeViewModel(
} }
private suspend fun ensureEnv() { private suspend fun ensureEnv() {
if (MagiskState.NOT_INSTALLED == stateMagisk || checkedEnv) return if (magiskState == State.INVALID || checkedEnv) return
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}" val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
if (!Shell.cmd(cmd).await().isSuccess) { if (!Shell.cmd(cmd).await().isSuccess) {
EnvFixDialog(this).publish() EnvFixDialog(this).publish()

View File

@ -95,7 +95,6 @@ class InstallViewModel(
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true) R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true)
else -> error("Unknown value") else -> error("Unknown value")
} }
state = State.LOADING
} }
override fun onSaveState(state: Bundle) { override fun onSaveState(state: Bundle) {

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui.module package com.topjohnwu.magisk.ui.module
import android.net.Uri import android.net.Uri
import androidx.databinding.Bindable
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
@ -10,10 +11,7 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.MergeObservableList import com.topjohnwu.magisk.databinding.*
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffListOf
import com.topjohnwu.magisk.events.GetContentEvent import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
@ -36,6 +34,10 @@ class ModuleViewModel : BaseViewModel() {
val data get() = uri val data get() = uri
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
init { init {
if (Info.env.isActive && LocalModule.loaded()) { if (Info.env.isActive && LocalModule.loaded()) {
items.insertItem(InstallModule) items.insertItem(InstallModule)
@ -45,9 +47,9 @@ class ModuleViewModel : BaseViewModel() {
override fun refresh(): Job { override fun refresh(): Job {
return viewModelScope.launch { return viewModelScope.launch {
state = State.LOADING loading = true
loadInstalled() loadInstalled()
state = State.LOADED loading = false
loadUpdateInfo() loadUpdateInfo()
} }
} }

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.os.Process import android.os.Process
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
@ -14,10 +15,7 @@ import com.topjohnwu.magisk.core.di.AppContext
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.utils.BiometricHelper import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.AnyDiffRvItem import com.topjohnwu.magisk.databinding.*
import com.topjohnwu.magisk.databinding.MergeObservableList
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffListOf
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
@ -45,15 +43,17 @@ class SuperuserViewModel(
it.put(BR.listener, this) it.put(BR.listener, this)
} }
// --- @get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
if (!Utils.showSuperUser()) { if (!Utils.showSuperUser()) {
state = State.LOADING_FAILED loading = false
return@launch return@launch
} }
state = State.LOADING loading = true
val (policies, diff) = withContext(Dispatchers.IO) { val (policies, diff) = withContext(Dispatchers.IO) {
db.deleteOutdated() db.deleteOutdated()
db.delete(AppContext.applicationInfo.uid) db.delete(AppContext.applicationInfo.uid)
@ -98,7 +98,7 @@ class SuperuserViewModel(
itemsHelpers.clear() itemsHelpers.clear()
else if (itemsHelpers.isEmpty()) else if (itemsHelpers.isEmpty())
itemsHelpers.add(itemNoData) itemsHelpers.add(itemNoData)
state = State.LOADED loading = false
} }
// --- // ---

View File

@ -24,7 +24,7 @@
android:paddingTop="@dimen/internal_action_bar_size" android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom" app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:invisibleUnless="@{viewModel.loaded}" app:invisible="@{viewModel.loading}"
app:items="@{viewModel.items}" app:items="@{viewModel.items}"
app:extraBindings="@{viewModel.extraBindings}" app:extraBindings="@{viewModel.extraBindings}"
tools:listitem="@layout/item_hide_md2" tools:listitem="@layout/item_hide_md2"
@ -52,24 +52,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout
goneUnless="@{viewModel.loadFailed}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_available"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
</LinearLayout>
</FrameLayout> </FrameLayout>
</layout> </layout>

View File

@ -7,8 +7,6 @@
<import type="com.topjohnwu.magisk.core.Info" /> <import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" />
<import type="com.topjohnwu.magisk.ui.home.DeveloperItem" /> <import type="com.topjohnwu.magisk.ui.home.DeveloperItem" />
<import type="com.topjohnwu.magisk.ui.home.IconLink" /> <import type="com.topjohnwu.magisk.ui.home.IconLink" />

View File

@ -19,7 +19,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/superuser_list" android:id="@+id/superuser_list"
goneUnless="@{viewModel.loaded || !viewModel.items.empty}" gone="@{viewModel.loading}"
app:items="@{viewModel.items}" app:items="@{viewModel.items}"
app:extraBindings="@{viewModel.extraBindings}" app:extraBindings="@{viewModel.extraBindings}"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -33,7 +33,7 @@
tools:listitem="@layout/item_policy_md2" /> tools:listitem="@layout/item_policy_md2" />
<LinearLayout <LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}" goneUnless="@{viewModel.loading}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -54,24 +54,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout
goneUnless="@{viewModel.loadFailed}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
tools:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_available"
android:textAppearance="@style/AppearanceFoundation.Title"
android:textStyle="bold" />
</LinearLayout>
</FrameLayout> </FrameLayout>
</layout> </layout>

View File

@ -7,7 +7,7 @@
<import type="com.topjohnwu.magisk.core.Info" /> <import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" /> <import type="com.topjohnwu.magisk.ui.home.HomeViewModel.State" />
<variable <variable
name="viewModel" name="viewModel"
@ -63,7 +63,7 @@
<Button <Button
style="@style/WidgetFoundation.Button" style="@style/WidgetFoundation.Button"
gone="@{viewModel.stateMagisk != MagiskState.OBSOLETE}" gone="@{viewModel.magiskState != State.OUTDATED}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.onMagiskPressed()}" android:onClick="@{() -> viewModel.onMagiskPressed()}"
@ -74,7 +74,7 @@
<Button <Button
style="@style/WidgetFoundation.Button.Text" style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.stateMagisk == MagiskState.OBSOLETE}" gone="@{viewModel.magiskState == State.OUTDATED}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"

View File

@ -7,7 +7,7 @@
<import type="com.topjohnwu.magisk.core.Info" /> <import type="com.topjohnwu.magisk.core.Info" />
<import type="com.topjohnwu.magisk.ui.home.MagiskState" /> <import type="com.topjohnwu.magisk.ui.home.HomeViewModel.State" />
<variable <variable
name="viewModel" name="viewModel"
@ -64,7 +64,7 @@
<Button <Button
style="@style/WidgetFoundation.Button" style="@style/WidgetFoundation.Button"
gone="@{viewModel.stateManager != MagiskState.OBSOLETE}" gone="@{viewModel.appState != State.OUTDATED}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.onManagerPressed()}" android:onClick="@{() -> viewModel.onManagerPressed()}"
@ -75,7 +75,7 @@
<Button <Button
style="@style/WidgetFoundation.Button.Text" style="@style/WidgetFoundation.Button.Text"
gone="@{viewModel.stateManager != MagiskState.UP_TO_DATE}" gone="@{viewModel.appState != State.UP_TO_DATE}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"