From 879884a9fa7c8baee796830abd779ba79adf6c07 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 15 Jul 2023 11:52:12 +0200 Subject: [PATCH] feat: switch to Preferences DataStore (#60) --- app/build.gradle.kts | 1 + .../java/app/revanced/manager/MainActivity.kt | 8 +- .../revanced/manager/ManagerApplication.kt | 10 + .../revanced/manager/di/PreferencesModule.kt | 7 +- .../manager/domain/manager/KeystoreManager.kt | 48 ++-- .../domain/manager/PreferencesManager.kt | 23 +- .../manager/base/BasePreferencesManager.kt | 205 ++++++++++-------- .../ui/component/settings/BooleanItem.kt | 50 +++++ .../ui/screen/PatchesSelectorScreen.kt | 4 +- .../settings/DownloadsSettingsScreen.kt | 13 +- .../screen/settings/GeneralSettingsScreen.kt | 47 ++-- .../settings/ImportExportSettingsScreen.kt | 24 +- .../ui/viewmodel/AppDownloaderViewModel.kt | 2 +- .../ui/viewmodel/ImportExportViewModel.kt | 33 +-- .../ui/viewmodel/PatchesSelectorViewModel.kt | 5 +- .../manager/ui/viewmodel/SettingsViewModel.kt | 8 +- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 + 18 files changed, 299 insertions(+), 192 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81deddfa..0a288773 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(libs.compose.activity) implementation(libs.paging.common.ktx) implementation(libs.work.runtime.ktx) + implementation(libs.preferences.datastore) // Compose implementation(platform(libs.compose.bom)) diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 3fb132a9..33065e02 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.destination.Destination @@ -56,9 +57,12 @@ class MainActivity : ComponentActivity() { ) setContent { + val theme by prefs.theme.getAsState() + val dynamicColor by prefs.dynamicColor.getAsState() + ReVancedManagerTheme( - darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK, - dynamicColor = prefs.dynamicColor + darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK, + dynamicColor = dynamicColor ) { val navController = rememberNavController(startDestination = Destination.Dashboard) diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 1396966d..5484918a 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -2,12 +2,18 @@ package app.revanced.manager import android.app.Application 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.androidLogger import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin class ManagerApplication : Application() { + private val scope = MainScope() + private val prefs: PreferencesManager by inject() override fun onCreate() { super.onCreate() @@ -26,5 +32,9 @@ class ManagerApplication : Application() { databaseModule, ) } + + scope.launch { + prefs.preload() + } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt b/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt index 9b8f9933..029ef4ed 100644 --- a/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt +++ b/app/src/main/java/app/revanced/manager/di/PreferencesModule.kt @@ -1,14 +1,9 @@ package app.revanced.manager.di -import android.content.Context import app.revanced.manager.domain.manager.PreferencesManager import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val preferencesModule = module { - fun providePreferences( - context: Context - ) = PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE)) - - singleOf(::providePreferences) + singleOf(::PreferencesManager) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt index 2e588eab..4362caa5 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt @@ -4,8 +4,9 @@ import android.app.Application import android.content.Context import app.revanced.manager.util.signing.Signer import app.revanced.manager.util.signing.SigningOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File -import java.io.InputStream import java.io.OutputStream import java.nio.file.Files import java.nio.file.Path @@ -23,39 +24,46 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { private val keystorePath = app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath() - private fun options( - cn: String = prefs.keystoreCommonName!!, - pass: String = prefs.keystorePass!!, - ) = SigningOptions(cn, pass, keystorePath) - - private fun updatePrefs(cn: String, pass: String) { - prefs.keystoreCommonName = cn - prefs.keystorePass = pass + private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit { + prefs.keystoreCommonName.value = cn + prefs.keystorePass.value = pass } - fun sign(input: File, output: File) = Signer(options()).signApk(input, output) - - init { - if (!keystorePath.exists()) { - regenerate() - } + suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) { + Signer( + SigningOptions( + prefs.keystoreCommonName.get(), + 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) } - 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()) { return false } - Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING) + withContext(Dispatchers.IO) { + Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING) + } updatePrefs(cn, pass) return true } - fun export(target: OutputStream) { - Files.copy(keystorePath, target) + fun hasKeystore() = keystorePath.exists() + + suspend fun export(target: OutputStream) { + withContext(Dispatchers.IO) { + Files.copy(keystorePath, target) + } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index fe1393ad..61e3274f 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -1,22 +1,19 @@ package app.revanced.manager.domain.manager -import android.content.SharedPreferences -import app.revanced.manager.domain.manager.base.BasePreferenceManager +import android.content.Context +import app.revanced.manager.domain.manager.base.BasePreferencesManager import app.revanced.manager.ui.theme.Theme -/** - * @author Hyperion Authors, zt64 - */ class PreferencesManager( - sharedPreferences: SharedPreferences -) : BasePreferenceManager(sharedPreferences) { - var dynamicColor by booleanPreference("dynamic_color", true) - var theme by enumPreference("theme", Theme.SYSTEM) + context: Context +) : BasePreferencesManager(context, "settings") { + val dynamicColor = booleanPreference("dynamic_color", true) + 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) - var keystorePass by stringPreference("keystore_pass", KeystoreManager.DEFAULT) + val preferSplits = booleanPreference("prefer_splits", false) } diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt index c257d24d..57810a05 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -1,98 +1,133 @@ package app.revanced.manager.domain.manager.base -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import kotlin.reflect.KProperty +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +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 -/** - * @author Hyperion Authors, zt64 - */ -abstract class BasePreferenceManager( - private val prefs: SharedPreferences -) { - protected fun getString(key: String, defaultValue: String?) = - prefs.getString(key, defaultValue)!! +abstract class BasePreferencesManager(private val context: Context, name: String) { + private val Context.dataStore: DataStore by preferencesDataStore(name = name) + protected val dataStore get() = context.dataStore - private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue) - private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue) - private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue) - protected inline fun > getEnum(key: String, defaultValue: E) = - enumValueOf(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 > putEnum(key: String, value: E) = - putString(key, value.name) - - protected class Preference( - 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) - } + suspend fun preload() { + dataStore.data.first() } - protected fun stringPreference( - key: String, - defaultValue: String? - ) = Preference( - key = key, - defaultValue = defaultValue, - getter = ::getString, - setter = ::putString - ) + suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block) - protected fun booleanPreference( - key: String, - defaultValue: Boolean - ) = Preference( - key = key, - defaultValue = defaultValue, - getter = ::getBoolean, - setter = ::putBoolean - ) + protected fun stringPreference(key: String, default: String) = + StringPreference(dataStore, key, default) - protected fun intPreference( - key: String, - defaultValue: Int - ) = Preference( - key = key, - defaultValue = defaultValue, - getter = ::getInt, - setter = ::putInt - ) + protected fun booleanPreference(key: String, default: Boolean) = + BooleanPreference(dataStore, key, default) - protected fun floatPreference( - key: String, - defaultValue: Float - ) = Preference( - key = key, - defaultValue = defaultValue, - getter = ::getFloat, - setter = ::putFloat - ) + protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default) + + protected fun floatPreference(key: String, default: Float) = + FloatPreference(dataStore, key, default) protected inline fun > enumPreference( key: String, - defaultValue: E - ) = Preference( - key = key, - defaultValue = defaultValue, - getter = ::getEnum, - setter = ::putEnum - ) + default: E + ) = EnumPreference(dataStore, key, default, enumValues()) + + companion object { + suspend inline fun DataStore.editor(crossinline block: EditorContext.() -> Unit) { + edit { + EditorContext(it).run(block) + } + } + } +} + +class EditorContext(private val prefs: MutablePreferences) { + var Preference.value + get() = prefs.run { read() } + set(value) = prefs.run { write(value) } +} + +abstract class Preference( + private val dataStore: DataStore, + 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>( + dataStore: DataStore, + key: String, + default: E, + private val enumValues: Array +) : Preference(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(dataStore: DataStore, default: T) : + Preference(dataStore, default) { + protected abstract val key: Preferences.Key + override fun Preferences.read() = this[key] ?: default + override fun MutablePreferences.write(value: T) { + this[key] = value + } +} + +class StringPreference( + dataStore: DataStore, + key: String, + default: String +) : BasePreference(dataStore, default) { + override val key = stringPreferencesKey(key) +} + +class BooleanPreference( + dataStore: DataStore, + key: String, + default: Boolean +) : BasePreference(dataStore, default) { + override val key = booleanPreferencesKey(key) +} + +class IntPreference( + dataStore: DataStore, + key: String, + default: Int +) : BasePreference(dataStore, default) { + override val key = intPreferencesKey(key) +} + +class FloatPreference( + dataStore: DataStore, + key: String, + default: Float +) : BasePreference(dataStore, default) { + override val key = floatPreferencesKey(key) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt new file mode 100644 index 00000000..8ed78775 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/BooleanItem.kt @@ -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, + 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, + ) + } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 1f5b9687..cd3771c8 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -183,6 +183,8 @@ fun PatchesSelectorScreen( ) } + val allowExperimental by vm.allowExperimental.getAsState() + LazyColumn( modifier = Modifier.fillMaxSize() ) { @@ -237,7 +239,7 @@ fun PatchesSelectorScreen( patchList( patches = bundle.unsupported, filterFlag = SHOW_UNSUPPORTED, - supported = vm.allowExperimental + supported = allowExperimental ) { ListHeader( title = stringResource(R.string.unsupported_patches), diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt index 701654a1..f13a5dfd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DownloadsSettingsScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -24,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.viewmodel.DownloadsViewModel import org.koin.androidx.compose.getViewModel @@ -58,13 +58,10 @@ fun DownloadsSettingsScreen( .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { - ListItem( - modifier = Modifier.clickable { prefs.preferSplits = !prefs.preferSplits }, - headlineContent = { Text(stringResource(R.string.prefer_splits)) }, - supportingContent = { Text(stringResource(R.string.prefer_splits_description)) }, - trailingContent = { - Switch(checked = prefs.preferSplits, onCheckedChange = { prefs.preferSplits = it }) - } + BooleanItem( + preference = prefs.preferSplits, + headline = R.string.prefer_splits, + description = R.string.prefer_splits_description, ) GroupHeader(stringResource(R.string.downloaded_apps)) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt index 0302502b..6838f927 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt @@ -15,12 +15,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.viewModelScope import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.component.AppTopBar 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.viewmodel.SettingsViewModel +import kotlinx.coroutines.launch import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class) @@ -30,6 +33,7 @@ fun GeneralSettingsScreen( viewModel: SettingsViewModel ) { val prefs = viewModel.prefs + val coroutineScope = viewModel.viewModelScope var showThemePicker by rememberSaveable { mutableStateOf(false) } if (showThemePicker) { @@ -53,22 +57,27 @@ fun GeneralSettingsScreen( .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { - GroupHeader(stringResource(R.string.appearance)) + + val theme by prefs.theme.getAsState() ListItem( modifier = Modifier.clickable { showThemePicker = true }, headlineContent = { Text(stringResource(R.string.theme)) }, supportingContent = { Text(stringResource(R.string.theme_description)) }, trailingContent = { - Button({ - showThemePicker = true - }) { Text(stringResource(prefs.theme.displayName)) } + Button( + onClick = { + showThemePicker = true + } + ) { + Text(stringResource(theme.displayName)) + } } ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BooleanItem( - value = prefs.dynamicColor, - onValueChange = { prefs.dynamicColor = it }, + preference = prefs.dynamicColor, + coroutineScope = coroutineScope, headline = R.string.dynamic_color, description = R.string.dynamic_color_description ) @@ -76,8 +85,8 @@ fun GeneralSettingsScreen( GroupHeader(stringResource(R.string.patcher)) BooleanItem( - value = prefs.allowExperimental, - onValueChange = { prefs.allowExperimental = it }, + preference = prefs.allowExperimental, + coroutineScope = coroutineScope, headline = R.string.experimental_patches, description = R.string.experimental_patches_description ) @@ -91,7 +100,7 @@ private fun ThemePicker( onConfirm: (Theme) -> Unit, prefs: PreferencesManager = koinInject() ) { - var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme) } + var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme.getBlocking()) } AlertDialog( 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, - ) - } -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index e6031530..e51b270e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope import app.revanced.manager.R import app.revanced.manager.ui.viewmodel.ImportExportViewModel 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.sources.SourceSelector import app.revanced.manager.util.toast +import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -40,6 +42,8 @@ fun ImportExportSettingsScreen( onBackClick: () -> Unit, vm: ImportExportViewModel = getViewModel() ) { + val context = LocalContext.current + val importKeystoreLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { it?.let { uri -> vm.startKeystoreImport(uri) } @@ -74,7 +78,12 @@ fun ImportExportSettingsScreen( if (vm.showCredentialsDialog) { KeystoreCredentialsDialog( 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( onClick = { + if (!vm.canExport()) { + context.toast(context.getString(R.string.export_keystore_unavailable)) + return@GroupItem + } exportKeystoreLauncher.launch("Manager.keystore") }, headline = R.string.export_keystore, @@ -144,9 +157,8 @@ private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes @Composable fun KeystoreCredentialsDialog( onDismissRequest: () -> Unit, - tryImport: (String, String) -> Boolean + onSubmit: (String, String) -> Unit ) { - val context = LocalContext.current var cn by rememberSaveable { mutableStateOf("") } var pass by rememberSaveable { mutableStateOf("") } @@ -155,11 +167,7 @@ fun KeystoreCredentialsDialog( confirmButton = { TextButton( onClick = { - if (!tryImport( - cn, - pass - ) - ) context.toast(context.getString(R.string.import_keystore_wrong_credentials)) + onSubmit(cn, pass) } ) { Text(stringResource(R.string.import_keystore_dialog_button)) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt index e2ce849a..c01cc91a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt @@ -118,7 +118,7 @@ class AppDownloaderViewModel( ?: appDownloader.downloadApp( version, savePath, - preferSplit = prefs.preferSplits + preferSplit = prefs.preferSplits.get() ).also { downloadedAppRepository.add( selectedApp.packageName, diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt index 8127e885..87a44d62 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ImportExportViewModel.kt @@ -48,17 +48,20 @@ class ImportExportViewModel( private var keystoreImportPath by mutableStateOf(null) val showCredentialsDialog by derivedStateOf { keystoreImportPath != null } - fun startKeystoreImport(content: Uri) { - val path = File.createTempFile("signing", "ks", app.cacheDir).toPath() - Files.copy( - contentResolver.openInputStream(content)!!, - path, - StandardCopyOption.REPLACE_EXISTING - ) + fun startKeystoreImport(content: Uri) = viewModelScope.launch { + val path = withContext(Dispatchers.IO) { + File.createTempFile("signing", "ks", app.cacheDir).toPath().also { + Files.copy( + contentResolver.openInputStream(content)!!, + it, + StandardCopyOption.REPLACE_EXISTING + ) + } + } knownPasswords.forEach { if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) { - return + return@launch } } @@ -70,10 +73,10 @@ class ImportExportViewModel( keystoreImportPath = null } - fun tryKeystoreImport(cn: String, pass: String) = + suspend fun tryKeystoreImport(cn: String, pass: String) = 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)) { cancelKeystoreImport() return true @@ -88,10 +91,14 @@ class ImportExportViewModel( cancelKeystoreImport() } - fun exportKeystore(target: Uri) = - keystoreManager.export(contentResolver.openOutputStream(target)!!) + fun canExport() = keystoreManager.hasKeystore() - 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)) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index b9cdb86b..987d9712 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -40,10 +40,9 @@ class PatchesSelectorViewModel( val appInfo: AppInfo ) : ViewModel(), KoinComponent { private val selectionRepository: PatchSelectionRepository = get() - private val prefs: PreferencesManager = get() private val savedStateHandle: SavedStateHandle = get() - val allowExperimental get() = prefs.allowExperimental + val allowExperimental = get().allowExperimental val bundlesFlow = get().sources.flatMapLatestAndCombine( combiner = { it } ) { source -> @@ -121,7 +120,7 @@ class PatchesSelectorViewModel( selectionRepository.updateSelection(appInfo.packageName, it) } }.mapValues { it.value.toMutableSet() }.apply { - if (allowExperimental) { + if (allowExperimental.get()) { return@apply } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt index 03726957..1e75bdea 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SettingsViewModel.kt @@ -1,15 +1,15 @@ package app.revanced.manager.ui.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.theme.Theme +import kotlinx.coroutines.launch class SettingsViewModel( val prefs: PreferencesManager ) : ViewModel() { - - fun setTheme(theme: Theme) { - prefs.theme = theme + fun setTheme(theme: Theme) = viewModelScope.launch { + prefs.theme.update(theme) } - } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 048a70e7..73bf0a74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Wrong keystore credentials Export keystore Export the current keystore + No keystore to export Regenerate keystore Generate a new keystore The keystore has been successfully replaced diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2bab9dd..4bdb800d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ viewmodel-lifecycle = "2.6.1" splash-screen = "1.0.1" compose-activity = "1.7.2" paging = "3.1.1" +preferences-datastore = "1.0.0" work-runtime = "2.8.1ō" compose-bom = "2023.06.01" 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" } 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" } +preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" } # Compose compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }