feat: improve the safeguards (#2038)

This commit is contained in:
Ax333l 2024-07-06 17:43:37 +02:00 committed by GitHub
parent 3f4a234915
commit 1ee1330e47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 136 additions and 218 deletions

View file

@ -15,7 +15,6 @@ class PreferencesManager(
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true) val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false) val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT) val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT) val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
@ -25,8 +24,8 @@ class PreferencesManager(
val firstLaunch = booleanPreference("first_launch", true) val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false) val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val disableSelectionWarning = booleanPreference("disable_selection_warning", false) val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true) val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
} }

View file

@ -1,26 +0,0 @@
package app.revanced.manager.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import kotlinx.coroutines.delay
@Composable
fun Countdown(start: Int, content: @Composable (Int) -> Unit) {
var timer by rememberSaveable(start) {
mutableStateOf(start)
}
LaunchedEffect(timer) {
if (timer == 0) {
return@LaunchedEffect
}
delay(1000L)
timer -= 1
}
content(timer)
}

View file

@ -1,91 +0,0 @@
package app.revanced.manager.ui.component
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
@Composable
fun DangerousActionDialogBase(
onCancel: () -> Unit,
confirmButton: @Composable (Boolean) -> Unit,
@StringRes title: Int,
body: String,
) {
var dismissPermanently by rememberSaveable {
mutableStateOf(false)
}
AlertDialog(
onDismissRequest = onCancel,
confirmButton = {
confirmButton(dismissPermanently)
},
dismissButton = {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.cancel))
}
},
icon = {
Icon(Icons.Outlined.WarningAmber, null)
},
title = {
Text(
text = stringResource(title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface,
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = body,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier
.fillMaxWidth()
.clickable {
dismissPermanently = !dismissPermanently
}
) {
Checkbox(
checked = dismissPermanently,
onCheckedChange = {
dismissPermanently = it
}
)
Text(stringResource(R.string.permanent_dismiss))
}
}
}
)
}

View file

@ -1,23 +0,0 @@
package app.revanced.manager.ui.component
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
@Composable
fun NonSuggestedVersionDialog(suggestedVersion: String, onCancel: () -> Unit, onContinue: (Boolean) -> Unit) {
DangerousActionDialogBase(
onCancel = onCancel,
confirmButton = { dismissPermanently ->
TextButton(
onClick = { onContinue(dismissPermanently) }
) {
Text(stringResource(R.string.continue_))
}
},
title = R.string.non_suggested_version_warning_title,
body = stringResource(R.string.non_suggested_version_warning_description, suggestedVersion),
)
}

View file

@ -0,0 +1,51 @@
package app.revanced.manager.ui.component
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import app.revanced.manager.R
@Composable
fun SafeguardDialog(
onDismiss: () -> Unit,
@StringRes title: Int,
body: String,
) {
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.ok))
}
},
icon = {
Icon(Icons.Outlined.WarningAmber, null)
},
title = {
Text(
text = stringResource(title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
)
},
text = {
Text(body)
}
)
}
@Composable
fun NonSuggestedVersionDialog(suggestedVersion: String, onDismiss: () -> Unit) {
SafeguardDialog(
onDismiss = onDismiss,
title = R.string.non_suggested_version_warning_title,
body = stringResource(R.string.non_suggested_version_warning_description, suggestedVersion),
)
}

View file

@ -70,8 +70,7 @@ fun AppSelectorScreen(
vm.nonSuggestedVersionDialogSubject?.let { vm.nonSuggestedVersionDialogSubject?.let {
NonSuggestedVersionDialog( NonSuggestedVersionDialog(
suggestedVersion = suggestedVersions[it.packageName].orEmpty(), suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
onCancel = vm::dismissNonSuggestedVersionDialog, onDismiss = vm::dismissNonSuggestedVersionDialog
onContinue = vm::continueWithNonSuggestedVersion,
) )
} }

View file

@ -20,6 +20,7 @@ import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -37,7 +38,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -48,17 +48,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Countdown import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.DangerousActionDialogBase
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
@ -70,7 +69,6 @@ import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
@ -160,10 +158,16 @@ fun PatchesSelectorScreen(
) )
} }
vm.pendingSelectionAction?.let { var showSelectionWarning by rememberSaveable {
SelectionWarningDialog( mutableStateOf(false)
onCancel = vm::dismissSelectionWarning, }
onConfirm = vm::confirmSelectionWarning if (showSelectionWarning) {
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
}
vm.pendingUniversalPatchAction?.let {
UniversalPatchWarningDialog(
onCancel = vm::dismissUniversalPatchWarning,
onConfirm = vm::confirmUniversalPatchWarning
) )
} }
@ -196,9 +200,9 @@ fun PatchesSelectorScreen(
), ),
onToggle = { onToggle = {
if (vm.selectionWarningEnabled) { if (vm.selectionWarningEnabled) {
vm.pendingSelectionAction = { showSelectionWarning = true
vm.togglePatch(uid, patch) } else if (vm.universalPatchWarningEnabled && patch.compatiblePackages == null) {
} vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) }
} else { } else {
vm.togglePatch(uid, patch) vm.togglePatch(uid, patch)
} }
@ -369,36 +373,43 @@ fun PatchesSelectorScreen(
} }
@Composable @Composable
fun SelectionWarningDialog( fun SelectionWarningDialog(onDismiss: () -> Unit) {
onCancel: () -> Unit, SafeguardDialog(
onConfirm: (Boolean) -> Unit onDismiss = onDismiss,
) { title = R.string.warning,
val prefs: PreferencesManager = koinInject() body = stringResource(R.string.selection_warning_description),
DangerousActionDialogBase(
onCancel = onCancel,
confirmButton = { dismissPermanently ->
val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState()
Countdown(start = if (enableCountdown) 3 else 0) { timer ->
LaunchedEffect(timer) {
if (timer == 0) prefs.enableSelectionWarningCountdown.update(false)
}
TextButton(
onClick = { onConfirm(dismissPermanently) },
enabled = timer == 0
) {
val text =
if (timer == 0) stringResource(R.string.continue_) else stringResource(
R.string.selection_warning_continue_countdown, timer
) )
Text(text, color = MaterialTheme.colorScheme.error) }
}
@Composable
fun UniversalPatchWarningDialog(
onCancel: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = onCancel,
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(R.string.continue_))
} }
}, },
title = R.string.selection_warning_title, dismissButton = {
body = stringResource(R.string.selection_warning_description), TextButton(onClick = onCancel) {
Text(stringResource(R.string.cancel))
}
},
icon = {
Icon(Icons.Outlined.WarningAmber, null)
},
title = {
Text(
text = stringResource(R.string.warning),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
)
},
text = {
Text(stringResource(R.string.universal_patch_warning_description))
}
) )
} }

View file

@ -69,8 +69,7 @@ fun VersionSelectorScreen(
if (viewModel.showNonSuggestedVersionDialog) if (viewModel.showNonSuggestedVersionDialog)
NonSuggestedVersionDialog( NonSuggestedVersionDialog(
suggestedVersion = viewModel.requiredVersion.orEmpty(), suggestedVersion = viewModel.requiredVersion.orEmpty(),
onCancel = viewModel::dismissNonSuggestedVersionDialog, onDismiss = viewModel::dismissNonSuggestedVersionDialog
onContinue = viewModel::continueWithNonSuggestedVersion,
) )
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()

View file

@ -126,12 +126,24 @@ fun AdvancedSettingsScreen(
headline = R.string.patch_compat_check, headline = R.string.patch_compat_check,
description = R.string.patch_compat_check_description description = R.string.patch_compat_check_description
) )
BooleanItem(
preference = vm.prefs.disableUniversalPatchWarning,
coroutineScope = vm.viewModelScope,
headline = R.string.universal_patches_safeguard,
description = R.string.universal_patches_safeguard_description
)
BooleanItem( BooleanItem(
preference = vm.prefs.suggestedVersionSafeguard, preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard, headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description description = R.string.suggested_version_safeguard_description
) )
BooleanItem(
preference = vm.prefs.disableSelectionWarning,
coroutineScope = vm.viewModelScope,
headline = R.string.patch_selection_safeguard,
description = R.string.patch_selection_safeguard_description
)
GroupHeader(stringResource(R.string.device)) GroupHeader(stringResource(R.string.device))
SettingsListItem( SettingsListItem(

View file

@ -3,14 +3,12 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -25,8 +23,7 @@ import java.nio.file.Files
class AppSelectorViewModel( class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
private val patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository
private val prefs: PreferencesManager,
) : ViewModel() { ) : ViewModel() {
private val inputFile = File(app.cacheDir, "input.apk").also { private val inputFile = File(app.cacheDir, "input.apk").also {
it.delete() it.delete()
@ -46,13 +43,6 @@ class AppSelectorViewModel(
nonSuggestedVersionDialogSubject = null nonSuggestedVersionDialogSubject = null
} }
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
nonSuggestedVersionDialogSubject?.let(onStorageClick)
dismissNonSuggestedVersionDialog()
}
fun handleStorageResult(uri: Uri) = viewModelScope.launch { fun handleStorageResult(uri: Uri) = viewModelScope.launch {
val selectedApp = withContext(Dispatchers.IO) { val selectedApp = withContext(Dispatchers.IO) {
loadSelectedFile(uri) loadSelectedFile(uri)

View file

@ -47,10 +47,12 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
private val packageName = input.app.packageName private val packageName = input.app.packageName
val appVersion = input.app.version val appVersion = input.app.version
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null) var pendingUniversalPatchAction by mutableStateOf<(() -> Unit)?>(null)
var selectionWarningEnabled by mutableStateOf(true) var selectionWarningEnabled by mutableStateOf(true)
private set private set
var universalPatchWarningEnabled by mutableStateOf(true)
private set
val allowIncompatiblePatches = val allowIncompatiblePatches =
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
@ -59,6 +61,8 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
init { init {
viewModelScope.launch { viewModelScope.launch {
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
if (prefs.disableSelectionWarning.get()) { if (prefs.disableSelectionWarning.get()) {
selectionWarningEnabled = false selectionWarningEnabled = false
return@launch return@launch
@ -131,21 +135,15 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
customPatchSelection = selection.put(bundle, newPatches) customPatchSelection = selection.put(bundle, newPatches)
} }
fun confirmSelectionWarning(dismissPermanently: Boolean) { fun confirmUniversalPatchWarning() {
selectionWarningEnabled = false universalPatchWarningEnabled = false
pendingSelectionAction?.invoke() pendingUniversalPatchAction?.invoke()
pendingSelectionAction = null pendingUniversalPatchAction = null
if (!dismissPermanently) return
viewModelScope.launch {
prefs.disableSelectionWarning.update(true)
}
} }
fun dismissSelectionWarning() { fun dismissUniversalPatchWarning() {
pendingSelectionAction = null pendingUniversalPatchAction = null
} }
fun reset() { fun reset() {

View file

@ -162,12 +162,6 @@ class VersionSelectorViewModel(
nonSuggestedVersionDialogSubject = null nonSuggestedVersionDialogSubject = null
} }
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
selectedVersion = nonSuggestedVersionDialogSubject
dismissNonSuggestedVersionDialog()
}
fun select(app: SelectedApp) { fun select(app: SelectedApp) {
if (requiredVersion != null && app.version != requiredVersion) { if (requiredVersion != null && app.version != requiredVersion) {
nonSuggestedVersionDialogSubject = app nonSuggestedVersionDialogSubject = app

View file

@ -76,6 +76,10 @@
<string name="patch_compat_check_description">The check restricts patches to supported app versions</string> <string name="patch_compat_check_description">The check restricts patches to supported app versions</string>
<string name="suggested_version_safeguard">Require suggested app version</string> <string name="suggested_version_safeguard">Require suggested app version</string>
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string> <string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
<string name="patch_selection_safeguard">Allow changing patch selection</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string>
<string name="universal_patches_safeguard">Disable universal patch warning</string>
<string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select universal patches</string>
<string name="import_keystore">Import keystore</string> <string name="import_keystore">Import keystore</string>
<string name="import_keystore_description">Import a custom keystore</string> <string name="import_keystore_description">Import a custom keystore</string>
<string name="import_keystore_dialog_title">Enter keystore credentials</string> <string name="import_keystore_dialog_title">Enter keystore credentials</string>
@ -134,6 +138,7 @@
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="warning">Warning</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="system">System</string> <string name="system">System</string>
@ -190,10 +195,10 @@
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string> <string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string> <string name="patch_options_reset_toast">Patch options have been reset</string>
<string name="non_suggested_version_warning_title">Non suggested version</string> <string name="non_suggested_version_warning_title">Non suggested version</string>
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s</string> <string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
<string name="selection_warning_title">Stop using defaults?</string> <string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">You may encounter issues when not using the default patch selection and options.</string> <string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
<string name="selection_warning_continue_countdown">Continue (%ds)</string> <string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings.</string>
<string name="supported">Supported</string> <string name="supported">Supported</string>
<string name="universal">Universal</string> <string name="universal">Universal</string>
<string name="unsupported">Unsupported</string> <string name="unsupported">Unsupported</string>