feat: switch to Preferences DataStore (#60)

This commit is contained in:
Ax333l 2023-07-15 11:52:12 +02:00 committed by GitHub
parent 5d3b963682
commit 879884a9fa
18 changed files with 299 additions and 192 deletions

View File

@ -68,6 +68,7 @@ dependencies {
implementation(libs.compose.activity) implementation(libs.compose.activity)
implementation(libs.paging.common.ktx) implementation(libs.paging.common.ktx)
implementation(libs.work.runtime.ktx) implementation(libs.work.runtime.ktx)
implementation(libs.preferences.datastore)
// Compose // Compose
implementation(platform(libs.compose.bom)) implementation(platform(libs.compose.bom))

View File

@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
@ -56,9 +57,12 @@ class MainActivity : ComponentActivity() {
) )
setContent { setContent {
val theme by prefs.theme.getAsState()
val dynamicColor by prefs.dynamicColor.getAsState()
ReVancedManagerTheme( ReVancedManagerTheme(
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK, darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
dynamicColor = prefs.dynamicColor dynamicColor = dynamicColor
) { ) {
val navController = val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard) rememberNavController<Destination>(startDestination = Destination.Dashboard)

View File

@ -2,12 +2,18 @@ package app.revanced.manager
import android.app.Application import android.app.Application
import app.revanced.manager.di.* import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
class ManagerApplication : Application() { class ManagerApplication : Application() {
private val scope = MainScope()
private val prefs: PreferencesManager by inject()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -26,5 +32,9 @@ class ManagerApplication : Application() {
databaseModule, databaseModule,
) )
} }
scope.launch {
prefs.preload()
}
} }
} }

View File

@ -1,14 +1,9 @@
package app.revanced.manager.di package app.revanced.manager.di
import android.content.Context
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val preferencesModule = module { val preferencesModule = module {
fun providePreferences( singleOf(::PreferencesManager)
context: Context
) = PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
singleOf(::providePreferences)
} }

View File

@ -4,8 +4,9 @@ import android.app.Application
import android.content.Context import android.content.Context
import app.revanced.manager.util.signing.Signer import app.revanced.manager.util.signing.Signer
import app.revanced.manager.util.signing.SigningOptions import app.revanced.manager.util.signing.SigningOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -23,39 +24,46 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
private val keystorePath = private val keystorePath =
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath() app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
private fun options( private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
cn: String = prefs.keystoreCommonName!!, prefs.keystoreCommonName.value = cn
pass: String = prefs.keystorePass!!, prefs.keystorePass.value = pass
) = SigningOptions(cn, pass, keystorePath)
private fun updatePrefs(cn: String, pass: String) {
prefs.keystoreCommonName = cn
prefs.keystorePass = pass
} }
fun sign(input: File, output: File) = Signer(options()).signApk(input, output) suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
Signer(
init { SigningOptions(
if (!keystorePath.exists()) { prefs.keystoreCommonName.get(),
regenerate() prefs.keystorePass.get(),
} keystorePath
)
).signApk(
input,
output
)
} }
fun regenerate() = Signer(options(DEFAULT, DEFAULT)).regenerateKeystore().also { suspend fun regenerate() = withContext(Dispatchers.Default) {
Signer(SigningOptions(DEFAULT, DEFAULT, keystorePath)).regenerateKeystore()
updatePrefs(DEFAULT, DEFAULT) updatePrefs(DEFAULT, DEFAULT)
} }
fun import(cn: String, pass: String, keystore: Path): Boolean { suspend fun import(cn: String, pass: String, keystore: Path): Boolean {
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) { if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
return false return false
} }
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING) withContext(Dispatchers.IO) {
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
}
updatePrefs(cn, pass) updatePrefs(cn, pass)
return true return true
} }
fun export(target: OutputStream) { fun hasKeystore() = keystorePath.exists()
Files.copy(keystorePath, target)
suspend fun export(target: OutputStream) {
withContext(Dispatchers.IO) {
Files.copy(keystorePath, target)
}
} }
} }

View File

@ -1,22 +1,19 @@
package app.revanced.manager.domain.manager package app.revanced.manager.domain.manager
import android.content.SharedPreferences import android.content.Context
import app.revanced.manager.domain.manager.base.BasePreferenceManager import app.revanced.manager.domain.manager.base.BasePreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
/**
* @author Hyperion Authors, zt64
*/
class PreferencesManager( class PreferencesManager(
sharedPreferences: SharedPreferences context: Context
) : BasePreferenceManager(sharedPreferences) { ) : BasePreferencesManager(context, "settings") {
var dynamicColor by booleanPreference("dynamic_color", true) val dynamicColor = booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM) val theme = enumPreference("theme", Theme.SYSTEM)
var allowExperimental by booleanPreference("allow_experimental", false) val allowExperimental = booleanPreference("allow_experimental", false)
var preferSplits by booleanPreference("prefer_splits", false) val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
var keystoreCommonName by stringPreference("keystore_cn", KeystoreManager.DEFAULT) val preferSplits = booleanPreference("prefer_splits", false)
var keystorePass by stringPreference("keystore_pass", KeystoreManager.DEFAULT)
} }

View File

@ -1,98 +1,133 @@
package app.revanced.manager.domain.manager.base package app.revanced.manager.domain.manager.base
import android.content.SharedPreferences import android.content.Context
import androidx.compose.runtime.getValue import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.datastore.core.DataStore
import androidx.core.content.edit import androidx.datastore.preferences.core.*
import kotlin.reflect.KProperty import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
/** abstract class BasePreferencesManager(private val context: Context, name: String) {
* @author Hyperion Authors, zt64 private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name)
*/ protected val dataStore get() = context.dataStore
abstract class BasePreferenceManager(
private val prefs: SharedPreferences
) {
protected fun getString(key: String, defaultValue: String?) =
prefs.getString(key, defaultValue)!!
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue) suspend fun preload() {
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue) dataStore.data.first()
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
enumValueOf<E>(getString(key, defaultValue.name))
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) =
putString(key, value.name)
protected class Preference<T>(
private val key: String,
defaultValue: T,
getter: (key: String, defaultValue: T) -> T,
private val setter: (key: String, newValue: T) -> Unit
) {
@Suppress("RedundantSetter")
var value by mutableStateOf(getter(key, defaultValue))
private set
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
setter(key, newValue)
}
} }
protected fun stringPreference( suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block)
key: String,
defaultValue: String?
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getString,
setter = ::putString
)
protected fun booleanPreference( protected fun stringPreference(key: String, default: String) =
key: String, StringPreference(dataStore, key, default)
defaultValue: Boolean
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getBoolean,
setter = ::putBoolean
)
protected fun intPreference( protected fun booleanPreference(key: String, default: Boolean) =
key: String, BooleanPreference(dataStore, key, default)
defaultValue: Int
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getInt,
setter = ::putInt
)
protected fun floatPreference( protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default)
key: String,
defaultValue: Float protected fun floatPreference(key: String, default: Float) =
) = Preference( FloatPreference(dataStore, key, default)
key = key,
defaultValue = defaultValue,
getter = ::getFloat,
setter = ::putFloat
)
protected inline fun <reified E : Enum<E>> enumPreference( protected inline fun <reified E : Enum<E>> enumPreference(
key: String, key: String,
defaultValue: E default: E
) = Preference( ) = EnumPreference(dataStore, key, default, enumValues())
key = key,
defaultValue = defaultValue, companion object {
getter = ::getEnum, suspend inline fun DataStore<Preferences>.editor(crossinline block: EditorContext.() -> Unit) {
setter = ::putEnum edit {
) EditorContext(it).run(block)
}
}
}
}
class EditorContext(private val prefs: MutablePreferences) {
var <T> Preference<T>.value
get() = prefs.run { read() }
set(value) = prefs.run { write(value) }
}
abstract class Preference<T>(
private val dataStore: DataStore<Preferences>,
protected val default: T
) {
internal abstract fun Preferences.read(): T
internal abstract fun MutablePreferences.write(value: T)
val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged()
suspend fun get() = flow.first()
fun getBlocking() = runBlocking { get() }
@Composable
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
getBlocking()
})
suspend fun update(value: T) = dataStore.editor {
this@Preference.value = value
}
}
class EnumPreference<E : Enum<E>>(
dataStore: DataStore<Preferences>,
key: String,
default: E,
private val enumValues: Array<E>
) : Preference<E>(dataStore, default) {
private val key = stringPreferencesKey(key)
override fun Preferences.read() =
this[key]?.let { name ->
enumValues.find { it.name == name }
} ?: default
override fun MutablePreferences.write(value: E) {
this[key] = value.name
}
}
abstract class BasePreference<T>(dataStore: DataStore<Preferences>, default: T) :
Preference<T>(dataStore, default) {
protected abstract val key: Preferences.Key<T>
override fun Preferences.read() = this[key] ?: default
override fun MutablePreferences.write(value: T) {
this[key] = value
}
}
class StringPreference(
dataStore: DataStore<Preferences>,
key: String,
default: String
) : BasePreference<String>(dataStore, default) {
override val key = stringPreferencesKey(key)
}
class BooleanPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Boolean
) : BasePreference<Boolean>(dataStore, default) {
override val key = booleanPreferencesKey(key)
}
class IntPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Int
) : BasePreference<Int>(dataStore, default) {
override val key = intPreferencesKey(key)
}
class FloatPreference(
dataStore: DataStore<Preferences>,
key: String,
default: Float
) : BasePreference<Float>(dataStore, default) {
override val key = floatPreferencesKey(key)
} }

View File

@ -0,0 +1,50 @@
package app.revanced.manager.ui.component.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.domain.manager.base.Preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun BooleanItem(
preference: Preference<Boolean>,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
@StringRes headline: Int,
@StringRes description: Int
) {
val value by preference.getAsState()
BooleanItem(
value = value,
onValueChange = { coroutineScope.launch { preference.update(it) } },
headline = headline,
description = description
)
}
@Composable
fun BooleanItem(
value: Boolean,
onValueChange: (Boolean) -> Unit,
@StringRes headline: Int,
@StringRes description: Int
) = ListItem(
modifier = Modifier.clickable { onValueChange(!value) },
headlineContent = { Text(stringResource(headline)) },
supportingContent = { Text(stringResource(description)) },
trailingContent = {
Switch(
checked = value,
onCheckedChange = onValueChange,
)
}
)

View File

@ -183,6 +183,8 @@ fun PatchesSelectorScreen(
) )
} }
val allowExperimental by vm.allowExperimental.getAsState()
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@ -237,7 +239,7 @@ fun PatchesSelectorScreen(
patchList( patchList(
patches = bundle.unsupported, patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED, filterFlag = SHOW_UNSUPPORTED,
supported = vm.allowExperimental supported = allowExperimental
) { ) {
ListHeader( ListHeader(
title = stringResource(R.string.unsupported_patches), title = stringResource(R.string.unsupported_patches),

View File

@ -13,7 +13,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -24,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.viewmodel.DownloadsViewModel import app.revanced.manager.ui.viewmodel.DownloadsViewModel
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@ -58,13 +58,10 @@ fun DownloadsSettingsScreen(
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
ListItem( BooleanItem(
modifier = Modifier.clickable { prefs.preferSplits = !prefs.preferSplits }, preference = prefs.preferSplits,
headlineContent = { Text(stringResource(R.string.prefer_splits)) }, headline = R.string.prefer_splits,
supportingContent = { Text(stringResource(R.string.prefer_splits_description)) }, description = R.string.prefer_splits_description,
trailingContent = {
Switch(checked = prefs.preferSplits, onCheckedChange = { prefs.preferSplits = it })
}
) )
GroupHeader(stringResource(R.string.downloaded_apps)) GroupHeader(stringResource(R.string.downloaded_apps))

View File

@ -15,12 +15,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.SettingsViewModel import app.revanced.manager.ui.viewmodel.SettingsViewModel
import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -30,6 +33,7 @@ fun GeneralSettingsScreen(
viewModel: SettingsViewModel viewModel: SettingsViewModel
) { ) {
val prefs = viewModel.prefs val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope
var showThemePicker by rememberSaveable { mutableStateOf(false) } var showThemePicker by rememberSaveable { mutableStateOf(false) }
if (showThemePicker) { if (showThemePicker) {
@ -53,22 +57,27 @@ fun GeneralSettingsScreen(
.padding(paddingValues) .padding(paddingValues)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
GroupHeader(stringResource(R.string.appearance)) GroupHeader(stringResource(R.string.appearance))
val theme by prefs.theme.getAsState()
ListItem( ListItem(
modifier = Modifier.clickable { showThemePicker = true }, modifier = Modifier.clickable { showThemePicker = true },
headlineContent = { Text(stringResource(R.string.theme)) }, headlineContent = { Text(stringResource(R.string.theme)) },
supportingContent = { Text(stringResource(R.string.theme_description)) }, supportingContent = { Text(stringResource(R.string.theme_description)) },
trailingContent = { trailingContent = {
Button({ Button(
showThemePicker = true onClick = {
}) { Text(stringResource(prefs.theme.displayName)) } showThemePicker = true
}
) {
Text(stringResource(theme.displayName))
}
} }
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BooleanItem( BooleanItem(
value = prefs.dynamicColor, preference = prefs.dynamicColor,
onValueChange = { prefs.dynamicColor = it }, coroutineScope = coroutineScope,
headline = R.string.dynamic_color, headline = R.string.dynamic_color,
description = R.string.dynamic_color_description description = R.string.dynamic_color_description
) )
@ -76,8 +85,8 @@ fun GeneralSettingsScreen(
GroupHeader(stringResource(R.string.patcher)) GroupHeader(stringResource(R.string.patcher))
BooleanItem( BooleanItem(
value = prefs.allowExperimental, preference = prefs.allowExperimental,
onValueChange = { prefs.allowExperimental = it }, coroutineScope = coroutineScope,
headline = R.string.experimental_patches, headline = R.string.experimental_patches,
description = R.string.experimental_patches_description description = R.string.experimental_patches_description
) )
@ -91,7 +100,7 @@ private fun ThemePicker(
onConfirm: (Theme) -> Unit, onConfirm: (Theme) -> Unit,
prefs: PreferencesManager = koinInject() prefs: PreferencesManager = koinInject()
) { ) {
var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme) } var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme.getBlocking()) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@ -122,22 +131,4 @@ private fun ThemePicker(
} }
} }
) )
} }
@Composable
private fun BooleanItem(
value: Boolean,
onValueChange: (Boolean) -> Unit,
@StringRes headline: Int,
@StringRes description: Int
) = ListItem(
modifier = Modifier.clickable { onValueChange(!value) },
headlineContent = { Text(stringResource(headline)) },
supportingContent = { Text(stringResource(description)) },
trailingContent = {
Switch(
checked = value,
onCheckedChange = onValueChange,
)
}
)

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.viewmodel.ImportExportViewModel import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
@ -32,6 +33,7 @@ import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.PasswordField import app.revanced.manager.ui.component.PasswordField
import app.revanced.manager.ui.component.sources.SourceSelector import app.revanced.manager.ui.component.sources.SourceSelector
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -40,6 +42,8 @@ fun ImportExportSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: ImportExportViewModel = getViewModel() vm: ImportExportViewModel = getViewModel()
) { ) {
val context = LocalContext.current
val importKeystoreLauncher = val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
it?.let { uri -> vm.startKeystoreImport(uri) } it?.let { uri -> vm.startKeystoreImport(uri) }
@ -74,7 +78,12 @@ fun ImportExportSettingsScreen(
if (vm.showCredentialsDialog) { if (vm.showCredentialsDialog) {
KeystoreCredentialsDialog( KeystoreCredentialsDialog(
onDismissRequest = vm::cancelKeystoreImport, onDismissRequest = vm::cancelKeystoreImport,
tryImport = vm::tryKeystoreImport onSubmit = { cn, pass ->
vm.viewModelScope.launch {
val result = vm.tryKeystoreImport(cn, pass)
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
}
}
) )
} }
@ -102,6 +111,10 @@ fun ImportExportSettingsScreen(
) )
GroupItem( GroupItem(
onClick = { onClick = {
if (!vm.canExport()) {
context.toast(context.getString(R.string.export_keystore_unavailable))
return@GroupItem
}
exportKeystoreLauncher.launch("Manager.keystore") exportKeystoreLauncher.launch("Manager.keystore")
}, },
headline = R.string.export_keystore, headline = R.string.export_keystore,
@ -144,9 +157,8 @@ private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes
@Composable @Composable
fun KeystoreCredentialsDialog( fun KeystoreCredentialsDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
tryImport: (String, String) -> Boolean onSubmit: (String, String) -> Unit
) { ) {
val context = LocalContext.current
var cn by rememberSaveable { mutableStateOf("") } var cn by rememberSaveable { mutableStateOf("") }
var pass by rememberSaveable { mutableStateOf("") } var pass by rememberSaveable { mutableStateOf("") }
@ -155,11 +167,7 @@ fun KeystoreCredentialsDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
if (!tryImport( onSubmit(cn, pass)
cn,
pass
)
) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
} }
) { ) {
Text(stringResource(R.string.import_keystore_dialog_button)) Text(stringResource(R.string.import_keystore_dialog_button))

View File

@ -118,7 +118,7 @@ class AppDownloaderViewModel(
?: appDownloader.downloadApp( ?: appDownloader.downloadApp(
version, version,
savePath, savePath,
preferSplit = prefs.preferSplits preferSplit = prefs.preferSplits.get()
).also { ).also {
downloadedAppRepository.add( downloadedAppRepository.add(
selectedApp.packageName, selectedApp.packageName,

View File

@ -48,17 +48,20 @@ class ImportExportViewModel(
private var keystoreImportPath by mutableStateOf<Path?>(null) private var keystoreImportPath by mutableStateOf<Path?>(null)
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null } val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
fun startKeystoreImport(content: Uri) { fun startKeystoreImport(content: Uri) = viewModelScope.launch {
val path = File.createTempFile("signing", "ks", app.cacheDir).toPath() val path = withContext(Dispatchers.IO) {
Files.copy( File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
contentResolver.openInputStream(content)!!, Files.copy(
path, contentResolver.openInputStream(content)!!,
StandardCopyOption.REPLACE_EXISTING it,
) StandardCopyOption.REPLACE_EXISTING
)
}
}
knownPasswords.forEach { knownPasswords.forEach {
if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) { if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) {
return return@launch
} }
} }
@ -70,10 +73,10 @@ class ImportExportViewModel(
keystoreImportPath = null keystoreImportPath = null
} }
fun tryKeystoreImport(cn: String, pass: String) = suspend fun tryKeystoreImport(cn: String, pass: String) =
tryKeystoreImport(cn, pass, keystoreImportPath!!) tryKeystoreImport(cn, pass, keystoreImportPath!!)
private fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean { private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
if (keystoreManager.import(cn, pass, path)) { if (keystoreManager.import(cn, pass, path)) {
cancelKeystoreImport() cancelKeystoreImport()
return true return true
@ -88,10 +91,14 @@ class ImportExportViewModel(
cancelKeystoreImport() cancelKeystoreImport()
} }
fun exportKeystore(target: Uri) = fun canExport() = keystoreManager.hasKeystore()
keystoreManager.export(contentResolver.openOutputStream(target)!!)
fun regenerateKeystore() = keystoreManager.regenerate().also { fun exportKeystore(target: Uri) = viewModelScope.launch {
keystoreManager.export(contentResolver.openOutputStream(target)!!)
}
fun regenerateKeystore() = viewModelScope.launch {
keystoreManager.regenerate()
app.toast(app.getString(R.string.regenerate_keystore_success)) app.toast(app.getString(R.string.regenerate_keystore_success))
} }

View File

@ -40,10 +40,9 @@ class PatchesSelectorViewModel(
val appInfo: AppInfo val appInfo: AppInfo
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val prefs: PreferencesManager = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
val allowExperimental get() = prefs.allowExperimental
val allowExperimental = get<PreferencesManager>().allowExperimental
val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine( val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
combiner = { it } combiner = { it }
) { source -> ) { source ->
@ -121,7 +120,7 @@ class PatchesSelectorViewModel(
selectionRepository.updateSelection(appInfo.packageName, it) selectionRepository.updateSelection(appInfo.packageName, it)
} }
}.mapValues { it.value.toMutableSet() }.apply { }.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental) { if (allowExperimental.get()) {
return@apply return@apply
} }

View File

@ -1,15 +1,15 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import kotlinx.coroutines.launch
class SettingsViewModel( class SettingsViewModel(
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch {
fun setTheme(theme: Theme) { prefs.theme.update(theme)
prefs.theme = theme
} }
} }

View File

@ -45,6 +45,7 @@
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string> <string name="import_keystore_wrong_credentials">Wrong keystore credentials</string>
<string name="export_keystore">Export keystore</string> <string name="export_keystore">Export keystore</string>
<string name="export_keystore_description">Export the current keystore</string> <string name="export_keystore_description">Export the current keystore</string>
<string name="export_keystore_unavailable">No keystore to export</string>
<string name="regenerate_keystore">Regenerate keystore</string> <string name="regenerate_keystore">Regenerate keystore</string>
<string name="regenerate_keystore_description">Generate a new keystore</string> <string name="regenerate_keystore_description">Generate a new keystore</string>
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string> <string name="regenerate_keystore_success">The keystore has been successfully replaced</string>

View File

@ -4,6 +4,7 @@ viewmodel-lifecycle = "2.6.1"
splash-screen = "1.0.1" splash-screen = "1.0.1"
compose-activity = "1.7.2" compose-activity = "1.7.2"
paging = "3.1.1" paging = "3.1.1"
preferences-datastore = "1.0.0"
work-runtime = "2.8.1ō" work-runtime = "2.8.1ō"
compose-bom = "2023.06.01" compose-bom = "2023.06.01"
accompanist = "0.30.1" accompanist = "0.30.1"
@ -35,6 +36,7 @@ splash-screen = { group = "androidx.core", name = "core-splashscreen", version.r
compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" } compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" }
paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "paging" } paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "paging" }
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" } work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
# Compose # Compose
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }