feat: check if the version being used is the recommended version (#1675)

This commit is contained in:
Ax333l 2024-03-15 18:57:53 +01:00 committed by GitHub
parent 8d5d86fea8
commit 5d7f9d1387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 365 additions and 78 deletions

View file

@ -25,4 +25,6 @@ class PreferencesManager(
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true)
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
}

View file

@ -3,6 +3,7 @@ package app.revanced.manager.domain.repository
import android.app.Application
import android.content.Context
import android.util.Log
import app.revanced.library.PatchUtils
import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity
@ -12,6 +13,8 @@ import app.revanced.manager.data.room.bundles.Source as SourceInfo
import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.util.flatMapLatestAndCombine
import app.revanced.manager.util.tag
import app.revanced.manager.util.uiSafe
@ -29,6 +32,7 @@ class PatchBundleRepository(
private val app: Application,
private val persistenceRepo: PatchBundlePersistenceRepository,
private val networkInfo: NetworkInfo,
private val prefs: PreferencesManager,
) {
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
@ -47,6 +51,37 @@ class PatchBundleRepository(
it.state.map { state -> it.uid to state }
}
val suggestedVersions = bundles.map {
val allPatches =
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
PatchUtils.getMostCommonCompatibleVersions(allPatches, countUnusedPatches = true)
.mapValues { (_, versions) ->
if (versions.keys.size < 2)
return@mapValues versions.keys.firstOrNull()
// The entries are ordered from most compatible to least compatible.
// If there are entries with the same number of compatible patches, older versions will be first, which is undesirable.
// This means we have to pick the last entry we find that has the highest patch count.
// The order may change in future versions of ReVanced Library.
var currentHighestPatchCount = -1
versions.entries.last { (_, patchCount) ->
if (patchCount >= currentHighestPatchCount) {
currentHighestPatchCount = patchCount
true
} else false
}.key
}
}
suspend fun isVersionAllowed(packageName: String, version: String) =
withContext(Dispatchers.Default) {
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
val suggestedVersion = suggestedVersions.first()[packageName] ?: return@withContext true
suggestedVersion == version
}
/**
* Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed.
*/

View file

@ -1,7 +1,9 @@
package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.options.PatchOption
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
@ -37,6 +39,23 @@ data class PatchInfo(
pkg.versions == null || pkg.versions.contains(versionName)
}
}
/**
* Create a fake [Patch] with the same metadata as the [PatchInfo] instance.
* The resulting patch cannot be executed.
* This is necessary because some functions in ReVanced Library only accept full [Patch] objects.
*/
fun toPatcherPatch(): Patch<*> = object : ResourcePatch(
name = name,
description = description,
compatiblePackages = compatiblePackages
?.map(app.revanced.manager.patcher.patch.CompatiblePackage::toPatcherCompatiblePackage)
?.toSet(),
use = include,
) {
override fun execute(context: ResourceContext) =
throw Exception("Metadata patches cannot be executed")
}
}
@Immutable
@ -48,6 +67,14 @@ data class CompatiblePackage(
pkg.name,
pkg.versions?.toImmutableSet()
)
/**
* Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher.
*/
fun toPatcherCompatiblePackage() = Patch.CompatiblePackage(
name = packageName,
versions = versions,
)
}
@Immutable

View file

@ -0,0 +1,91 @@
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
inline fun DangerousActionDialogBase(
noinline onCancel: () -> Unit,
crossinline 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

@ -0,0 +1,23 @@
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

@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -18,7 +19,6 @@ 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.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -29,10 +29,10 @@ import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.toast
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@ -43,19 +43,17 @@ fun AppSelectorScreen(
onBackClick: () -> Unit,
vm: AppSelectorViewModel = getViewModel()
) {
val context = LocalContext.current
SideEffect {
vm.onStorageClick = onStorageClick
}
val pickApkLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { apkUri ->
vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(
context.getString(
R.string.failed_to_load_apk
)
)
}
uri?.let(vm::handleStorageResult)
}
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle(emptyMap())
var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) }
@ -69,6 +67,14 @@ fun AppSelectorScreen(
}
}
vm.nonSuggestedVersionDialogSubject?.let {
NonSuggestedVersionDialog(
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
onCancel = vm::dismissNonSuggestedVersionDialog,
onContinue = vm::continueWithNonSuggestedVersion,
)
}
// TODO: find something better for this
if (search) {
SearchBar(
@ -193,8 +199,17 @@ fun AppSelectorScreen(
ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) },
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = { AppLabel(app.packageInfo) },
supportingContent = { Text(app.packageName) },
headlineContent = {
AppLabel(
app.packageInfo,
defaultText = app.packageName
)
},
supportingContent = {
suggestedVersions[app.packageName]?.let {
Text(stringResource(R.string.suggested_version_info, it))
}
},
trailingContent = app.patches?.let {
{
Text(

View file

@ -62,6 +62,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Countdown
import app.revanced.manager.ui.component.DangerousActionDialogBase
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
@ -390,13 +391,10 @@ fun SelectionWarningDialog(
onConfirm: (Boolean) -> Unit
) {
val prefs: PreferencesManager = rememberKoinInject()
var dismissPermanently by rememberSaveable {
mutableStateOf(false)
}
AlertDialog(
onDismissRequest = onCancel,
confirmButton = {
DangerousActionDialogBase(
onCancel = onCancel,
confirmButton = { dismissPermanently ->
val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState()
Countdown(start = if (enableCountdown) 3 else 0) { timer ->
@ -416,49 +414,8 @@ fun SelectionWarningDialog(
}
}
},
dismissButton = {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.cancel))
}
},
icon = {
Icon(Icons.Outlined.WarningAmber, null)
},
title = {
Text(
text = stringResource(R.string.selection_warning_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 = stringResource(R.string.selection_warning_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.clickable {
dismissPermanently = !dismissPermanently
}
) {
Checkbox(
checked = dismissPermanently,
onCheckedChange = {
dismissPermanently = it
}
)
Text(stringResource(R.string.permanent_dismiss))
}
}
}
title = R.string.selection_warning_title,
body = stringResource(R.string.selection_warning_description),
)
}

View file

@ -37,6 +37,7 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
import app.revanced.manager.util.isScrollingUp
@ -53,7 +54,7 @@ fun VersionSelectorScreen(
val list by remember {
derivedStateOf {
(downloadedVersions + viewModel.downloadableVersions)
val apps = (downloadedVersions + viewModel.downloadableVersions)
.distinctBy { it.version }
.sortedWith(
compareByDescending<SelectedApp> {
@ -61,10 +62,19 @@ fun VersionSelectorScreen(
}.thenByDescending { supportedVersions[it.version] }
.thenByDescending { it.version }
)
viewModel.requiredVersion?.let { requiredVersion ->
apps.filter { it.version == requiredVersion }
} ?: apps
}
}
var selectedVersion: SelectedApp? by rememberSaveable { mutableStateOf(null) }
if (viewModel.showNonSuggestedVersionDialog)
NonSuggestedVersionDialog(
suggestedVersion = viewModel.requiredVersion.orEmpty(),
onCancel = viewModel::dismissNonSuggestedVersionDialog,
onContinue = viewModel::continueWithNonSuggestedVersion,
)
val lazyListState = rememberLazyListState()
Scaffold(
@ -79,7 +89,7 @@ fun VersionSelectorScreen(
text = { Text(stringResource(R.string.select_version)) },
icon = { Icon(Icons.Default.Check, null) },
expanded = lazyListState.isScrollingUp,
onClick = { selectedVersion?.let(onAppClick) }
onClick = { viewModel.selectedVersion?.let(onAppClick) }
)
}
) { paddingValues ->
@ -98,8 +108,8 @@ fun VersionSelectorScreen(
item {
SelectedAppItem(
selectedApp = it,
selected = selectedVersion == it,
onClick = { selectedVersion = it },
selected = viewModel.selectedVersion == it,
onClick = { viewModel.select(it) },
patchCount = supportedVersions[it.version],
enabled =
!(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
@ -121,8 +131,8 @@ fun VersionSelectorScreen(
) {
SelectedAppItem(
selectedApp = it,
selected = selectedVersion == it,
onClick = { selectedVersion = it },
selected = viewModel.selectedVersion == it,
onClick = { viewModel.select(it) },
patchCount = supportedVersions[it.version]
)
}
@ -156,7 +166,7 @@ fun SelectedAppItem(
onClick: () -> Unit,
patchCount: Int?,
enabled: Boolean = true,
alreadyPatched: Boolean = false
alreadyPatched: Boolean = false,
) {
ListItem(
leadingContent = { RadioButton(selected, null) },
@ -175,9 +185,11 @@ fun SelectedAppItem(
else -> null
},
trailingContent = patchCount?.let { {
Text(pluralStringResource(R.plurals.patch_count, it, it))
} },
trailingContent = patchCount?.let {
{
Text(pluralStringResource(R.plurals.patch_count, it, it))
}
},
modifier = Modifier
.clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
.run {

View file

@ -92,6 +92,12 @@ fun AdvancedSettingsScreen(
headline = R.string.patch_compat_check,
description = R.string.patch_compat_check_description
)
BooleanItem(
preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description
)
BooleanItem(
preference = vm.prefs.multithreadingDexFileWriter,
coroutineScope = vm.viewModelScope,

View file

@ -3,24 +3,74 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.pm.PackageInfo
import android.net.Uri
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.Files
class AppSelectorViewModel(
private val app: Application,
private val pm: PM
private val pm: PM,
private val patchBundleRepository: PatchBundleRepository,
private val prefs: PreferencesManager,
) : ViewModel() {
private val inputFile = File(app.cacheDir, "input.apk").also {
it.delete()
}
val appList = pm.appList
var onStorageClick: (SelectedApp.Local) -> Unit = {}
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
private set
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
fun loadSelectedFile(uri: Uri) =
fun dismissNonSuggestedVersionDialog() {
nonSuggestedVersionDialogSubject = null
}
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
nonSuggestedVersionDialogSubject?.let(onStorageClick)
dismissNonSuggestedVersionDialog()
}
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
val selectedApp = withContext(Dispatchers.IO) {
loadSelectedFile(uri)
}
if (selectedApp == null) {
app.toast(app.getString(R.string.failed_to_load_apk))
return@launch
}
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
onStorageClick(selectedApp)
} else {
nonSuggestedVersionDialogSubject = selectedApp
}
}
private fun loadSelectedFile(uri: Uri) =
app.contentResolver.openInputStream(uri)?.use { stream ->
with(inputFile) {
delete()

View file

@ -2,6 +2,7 @@ package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import android.util.Log
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -9,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
@ -22,6 +24,7 @@ import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -35,6 +38,7 @@ class VersionSelectorViewModel(
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val pm: PM by inject()
private val prefs: PreferencesManager by inject()
private val appDownloader: AppDownloader = APKMirror()
val rootInstaller: RootInstaller by inject()
@ -45,9 +49,34 @@ class VersionSelectorViewModel(
var errorMessage: String? by mutableStateOf(null)
private set
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
var requiredVersion: String? by mutableStateOf(null)
private set
var selectedVersion: SelectedApp? by mutableStateOf(null)
private set
private var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp?>(null)
val showNonSuggestedVersionDialog by derivedStateOf { nonSuggestedVersionDialogSubject != null }
private val requiredVersionAsync = viewModelScope.async(Dispatchers.Default) {
if (!prefs.suggestedVersionSafeguard.get()) return@async null
patchBundleRepository.suggestedVersions.first()[packageName]
}
val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles ->
requiredVersionAsync.await()?.let { version ->
// It is mandatory to use the suggested version if the safeguard is enabled.
return@supportedVersions mapOf(
version to bundles
.asSequence()
.flatMap { (_, bundle) -> bundle.patches }
.flatMap { it.compatiblePackages.orEmpty() }
.filter { it.packageName == packageName }
.count { it.versions.isNullOrEmpty() || version in it.versions }
)
}
val supportedVersions = patchBundleRepository.bundles.map { bundles ->
var patchesWithoutVersions = 0
bundles.flatMap { (_, bundle) ->
@ -65,16 +94,32 @@ class VersionSelectorViewModel(
count + patchesWithoutVersions
}
}
}.flowOn(Dispatchers.Default)
init {
viewModelScope.launch {
requiredVersion = requiredVersionAsync.await()
}
}
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, downloadedAppRepository.getApkFileForApp(it), false) }
downloadedApps.filter { it.packageName == packageName }.map {
SelectedApp.Local(
it.packageName,
it.version,
downloadedAppRepository.getApkFileForApp(it),
false
)
}
}
init {
viewModelScope.launch(Dispatchers.Main) {
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val installedAppDeferred = async(Dispatchers.IO) { installedAppRepository.get(packageName) }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
installedApp =
packageInfo.await()?.let {
@ -112,4 +157,23 @@ class VersionSelectorViewModel(
}
}
}
fun dismissNonSuggestedVersionDialog() {
nonSuggestedVersionDialogSubject = null
}
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
selectedVersion = nonSuggestedVersionDialogSubject
dismissNonSuggestedVersionDialog()
}
fun select(app: SelectedApp) {
if (requiredVersion != null && app.version != requiredVersion) {
nonSuggestedVersionDialogSubject = app
return
}
selectedVersion = app
}
}

View file

@ -69,6 +69,8 @@
<string name="multithreaded_dex_file_writer_description">Use multiple cores to write DEX files. This is faster, but uses more memory</string>
<string name="patch_compat_check">Disable version compatibility check</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_description">Enforce selection of the suggested app version</string>
<string name="import_keystore">Import keystore</string>
<string name="import_keystore_description">Import a custom keystore</string>
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
@ -113,6 +115,7 @@
<string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</string>
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
<string name="suggested_version_info">Suggested version: %s</string>
<string name="type_anything">Type anything to continue</string>
<string name="search">Search</string>
<string name="apply">Apply</string>
@ -167,6 +170,8 @@
<string name="universal_patches">Universal patches</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="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="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_continue_countdown">Continue (%ds)</string>