Updated settings to level functionality with the legacy

This commit is contained in:
Viktor De Pasquale 2019-12-02 18:35:48 +01:00
parent 89da45f9ac
commit 3efea47ca8
7 changed files with 254 additions and 26 deletions

View File

@ -23,7 +23,7 @@ val redesignModule = module {
viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { RequestViewModel() }
viewModel { SafetynetViewModel(get()) }
viewModel { SettingsViewModel() }
viewModel { SettingsViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get()) }
viewModel { ThemeViewModel() }
viewModel { InstallViewModel() }

View File

@ -19,4 +19,9 @@ class SettingsFragment : CompatFragment<SettingsViewModel, FragmentSettingsMd2Bi
activity.title = resources.getString(R.string.section_settings)
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }
}
}

View File

@ -1,21 +1,19 @@
package com.topjohnwu.magisk.redesign.settings
import android.content.Context
import android.os.Build
import android.os.Environment
import android.view.LayoutInflater
import android.widget.Toast
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.magisk.utils.availableLocales
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import java.io.File
// --- Customization
@ -27,6 +25,7 @@ object Customization : SettingsItem.Section() {
object Language : SettingsItem.Selector() {
override var value by dataObservable(0) {
Config.locale = entryValues.getOrNull(it)?.toString() ?: return@dataObservable
refreshLocale()
}
override val title = R.string.language.asTransitive()
@ -61,11 +60,34 @@ object Manager : SettingsItem.Section() {
object ClearRepoCache : SettingsItem.Blank() {
override val title = R.string.settings_clear_cache_title.asTransitive()
override val description = R.string.settings_clear_cache_summary.asTransitive()
override fun refresh() {
isEnabled = Info.env.isActive
}
}
object Hide : SettingsItem.Blank() {
object Hide : SettingsItem.Input() {
override val title = R.string.settings_hide_manager_title.asTransitive()
override val description = R.string.settings_hide_manager_summary.asTransitive()
override val showStrip = false
override var value: String = resources.getString(R.string.re_app_name)
set(value) {
field = value
notifyChange(BR.value)
notifyChange(BR.error)
}
val isError
@Bindable get() = value.length > 14 || value.isBlank()
override val intermediate: String?
get() = if (isError) null else value
override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
override fun refresh() {
isEnabled = Info.env.isActive
}
}
object Restore : SettingsItem.Blank() {
@ -129,11 +151,7 @@ object UpdateChannelUrl : SettingsItem.Input() {
notifyChange(BR.result)
}
init {
updateState()
}
fun updateState() {
override fun refresh() {
isEnabled = UpdateChannel.value == Config.Value.CUSTOM_CHANNEL
}
@ -144,19 +162,43 @@ object UpdateChannelUrl : SettingsItem.Input() {
object UpdateChecker : SettingsItem.Toggle() {
override val title = R.string.settings_check_update_title.asTransitive()
override val description = R.string.settings_check_update_summary.asTransitive()
override var value by dataObservable(Config.checkUpdate) { Config.checkUpdate = it }
override var value by dataObservable(Config.checkUpdate) {
Config.checkUpdate = it
Utils.scheduleUpdateCheck(get())
}
}
// check whether is module already installed beforehand?
object SystemlessHosts : SettingsItem.Blank() {
override val title = R.string.settings_hosts_title.asTransitive()
override val description = R.string.settings_hosts_summary.asTransitive()
override fun refresh() {
isEnabled = Info.env.isActive
}
}
object Biometrics : SettingsItem.Toggle() {
override val title = R.string.settings_su_biometric_title.asTransitive()
override val description = R.string.settings_su_biometric_summary.asTransitive()
override var value by dataObservable(Config.suBiometric) { Config.suBiometric = it }
override fun refresh() {
isEnabled = BiometricHelper.isSupported && Utils.showSuperUser()
if (!isEnabled) {
value = false
}
}
}
object Reauthenticate : SettingsItem.Toggle() {
override val title = R.string.settings_su_reauth_title.asTransitive()
override val description = R.string.settings_su_reauth_summary.asTransitive()
override var value by dataObservable(Config.suReAuth) { Config.suReAuth = it }
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Utils.showSuperUser()
}
}
// --- Magisk
@ -168,13 +210,35 @@ object Magisk : SettingsItem.Section() {
object SafeMode : SettingsItem.Toggle() {
override val title = R.string.settings_core_only_title.asTransitive()
override val description = R.string.settings_core_only_summary.asTransitive()
override var value by dataObservable(Config.coreOnly) { Config.coreOnly = it }
override var value by dataObservable(Config.coreOnly) {
if (Config.coreOnly == it) return@dataObservable
Config.coreOnly = it
when {
it -> runCatching { Const.MAGISK_DISABLE_FILE.createNewFile() }
else -> Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
override fun refresh() {
isEnabled = Info.env.isActive
}
}
object MagiskHide : SettingsItem.Toggle() {
override val title = R.string.magiskhide.asTransitive()
override val description = R.string.settings_magiskhide_summary.asTransitive()
override var value by dataObservable(Config.magiskHide) { Config.magiskHide = it }
override var value by dataObservable(Config.magiskHide) {
Config.magiskHide = it
when {
it -> Shell.su("magiskhide --enable").submit()
else -> Shell.su("magiskhide --disable").submit()
}
}
override fun refresh() {
isEnabled = Info.env.isActive
}
}
// --- Superuser
@ -197,6 +261,10 @@ object AccessMode : SettingsItem.Selector() {
resources.getStringArray(R.array.value_array)
)
}
override fun refresh() {
isEnabled = Utils.showSuperUser()
}
}
object MultiuserMode : SettingsItem.Selector() {
@ -213,6 +281,10 @@ object MultiuserMode : SettingsItem.Selector() {
resources.getStringArray(R.array.value_array)
)
}
override fun refresh() {
isEnabled = Const.USER_ID <= 0 && Utils.showSuperUser()
}
}
object MountNamespaceMode : SettingsItem.Selector() {
@ -229,6 +301,10 @@ object MountNamespaceMode : SettingsItem.Selector() {
resources.getStringArray(R.array.value_array)
)
}
override fun refresh() {
isEnabled = Utils.showSuperUser()
}
}
object AutomaticResponse : SettingsItem.Selector() {
@ -245,6 +321,10 @@ object AutomaticResponse : SettingsItem.Selector() {
resources.getStringArray(R.array.value_array)
)
}
override fun refresh() {
isEnabled = Utils.showSuperUser()
}
}
object RequestTimeout : SettingsItem.Selector() {
@ -263,6 +343,10 @@ object RequestTimeout : SettingsItem.Selector() {
val currentValue = Config.suDefaultTimeout.toString()
value = entryValues.indexOfFirst { it == currentValue }
}
override fun refresh() {
isEnabled = Utils.showSuperUser()
}
}
object SUNotification : SettingsItem.Selector() {
@ -279,4 +363,8 @@ object SUNotification : SettingsItem.Selector() {
resources.getStringArray(R.array.value_array)
)
}
override fun refresh() {
isEnabled = Utils.showSuperUser()
}
}

View File

@ -1,30 +1,47 @@
package com.topjohnwu.magisk.redesign.settings
import android.Manifest
import android.content.Context
import android.content.res.Resources
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.databinding.Bindable
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.ObservableItem
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.RecreateEvent
import com.topjohnwu.magisk.model.events.dialog.BiometricDialog
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.redesign.compat.CompatViewModel
import com.topjohnwu.magisk.redesign.home.itemBindingOf
import com.topjohnwu.magisk.redesign.module.adapterOf
import com.topjohnwu.magisk.redesign.superuser.diffListOf
import com.topjohnwu.magisk.utils.PatchAPK
import com.topjohnwu.magisk.utils.TransitiveText
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.subjects.PublishSubject
import org.koin.core.KoinComponent
import org.koin.core.get
import kotlin.properties.ObservableProperty
import kotlin.reflect.KProperty
class SettingsViewModel : CompatViewModel(), SettingsItem.Callback {
class SettingsViewModel(
private val repositoryDao: RepoDao
) : CompatViewModel(), SettingsItem.Callback {
val adapter = adapterOf<SettingsItem>()
val itemBinding = itemBindingOf<SettingsItem> { it.bindExtra(BR.callback, this) }
@ -34,10 +51,10 @@ class SettingsViewModel : CompatViewModel(), SettingsItem.Callback {
Manager,
UpdateChannel, UpdateChannelUrl, ClearRepoCache, HideOrRestore(), UpdateChecker,
SystemlessHosts, Biometrics,
Biometrics, Reauthenticate,
Magisk,
SafeMode, MagiskHide,
SafeMode, MagiskHide, SystemlessHosts,
Superuser,
AccessMode, MultiuserMode, MountNamespaceMode, AutomaticResponse, RequestTimeout,
@ -45,20 +62,74 @@ class SettingsViewModel : CompatViewModel(), SettingsItem.Callback {
)
override fun onItemPressed(view: View, item: SettingsItem) = when (item) {
is DownloadPath -> requireRWPermission()
else -> Unit
}
override fun onItemChanged(view: View, item: SettingsItem) = when (item) {
// use only instances you want, don't declare everything
is Theme -> Navigation.theme().publish()
is Redesign -> DieEvent().publish()
is Language -> RecreateEvent().publish()
is UpdateChannel -> openUrlIfNecessary(view)
is Biometrics -> authenticateOrRevert()
is ClearRepoCache -> clearRepoCache()
is SystemlessHosts -> createHosts()
is Hide -> updateManager(hide = true)
is Restore -> updateManager(hide = false)
else -> Unit
}
private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.updateState()
UpdateChannelUrl.refresh()
if (UpdateChannelUrl.value.isBlank()) {
UpdateChannelUrl.onPressed(view, this@SettingsViewModel)
}
}
private fun authenticateOrRevert() {
// immediately revert the preference
Biometrics.value = !Biometrics.value
BiometricDialog {
// allow the change on success
onSuccess { Biometrics.value = !Biometrics.value }
}.publish()
}
private fun clearRepoCache() {
Completable.fromAction { repositoryDao.clear() }
.subscribeK { Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT) }
}
private fun createHosts() {
Shell.su("add_hosts_module").submit {
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
private fun requireRWPermission() {
val callback = PublishSubject.create<Boolean>()
callback.subscribeK { if (!it) requireRWPermission() }
PermissionEvent(
listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
), callback
).publish()
}
private fun updateManager(hide: Boolean) {
if (hide) {
PatchAPK.hideManager(get(), Hide.value)
} else {
DownloadService(get()) {
subject = DownloadSubject.Manager(Configuration.APK.Restore)
}
}
}
}
sealed class SettingsItem : ObservableItem<SettingsItem>() {
@ -81,7 +152,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
@CallSuper
open fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
callback.onItemChanged(view, this)
// notify only after the callback invocation; callback can invalidate the backing data,
// which wouldn't be recognized with reverse approach
@ -90,6 +161,8 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
notifyChange(BR.description)
}
open fun refresh() {}
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
if (isFullSpan) {
@ -105,6 +178,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
interface Callback {
fun onItemPressed(view: View, item: SettingsItem)
fun onItemChanged(view: View, item: SettingsItem)
}
// ---
@ -131,6 +205,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
override val layoutRes = R.layout.item_settings_toggle
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
value = !value
super.onPressed(view, callback)
}
@ -147,11 +222,13 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
abstract class Input : Value<String>(), KoinComponent {
override val layoutRes = R.layout.item_settings_input
open val showStrip = true
protected val resources get() = get<Resources>()
protected abstract val intermediate: String?
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
MagiskDialog(view.context)
.applyTitle(title.getText(resources))
.applyView(getView(view.context))
@ -210,6 +287,7 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
override fun onPressed(view: View, callback: Callback) {
if (entries.isEmpty() || entryValues.isEmpty()) return
callback.onItemPressed(view, this)
MagiskDialog(view.context)
.applyTitle(title.getText(resources))
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
@ -229,6 +307,11 @@ sealed class SettingsItem : ObservableItem<SettingsItem>() {
override val layoutRes = R.layout.item_settings_blank
override fun onPressed(view: View, callback: Callback) {
callback.onItemPressed(view, this)
super.onPressed(view, callback)
}
}
abstract class Section : SettingsItem() {

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.topjohnwu.magisk.redesign.settings.Hide" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_generic">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic"
android:hint="@string/settings_app_name_hint"
app:boxStrokeColor="?colorOnSurfaceVariant"
app:counterEnabled="true"
app:counterMaxLength="14"
app:counterOverflowTextColor="@color/colorError"
app:error="@{data.error ? @string/settings_app_name_error : @string/empty}"
app:errorEnabled="true"
app:errorTextColor="?colorError"
app:helperText="@string/settings_app_name_helper"
app:hintEnabled="true"
app:hintTextAppearance="@style/AppearanceFoundation.Tiny"
app:hintTextColor="?colorOnSurfaceVariant">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_custom_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords"
android:text="@={data.value}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorOnSurface"
tools:text="@tools:sample/lorem" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</layout>

View File

@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="@{item.enabled ? 1f : .5f}"
android:onClick="@{(view) -> callback.onItemPressed(view, item)}"
android:onClick="@{(view) -> callback.onItemChanged(view, item)}"
tools:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout

View File

@ -92,7 +92,7 @@
<View
android:id="@+id/input_divider"
gone="@{item.value.empty}"
gone="@{item.value.empty || !item.showStrip}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:alpha=".5"
@ -102,7 +102,7 @@
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/input_selection"
gone="@{item.value.empty}"
gone="@{item.value.empty || !item.showStrip}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/l1"