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 disableSelectionWarning = booleanPreference("disable_selection_warning", false)
val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true) 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.app.Application
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import app.revanced.library.PatchUtils
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.data.room.bundles.PatchBundleEntity 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.LocalPatchBundle
import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource 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.flatMapLatestAndCombine
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
@ -29,6 +32,7 @@ class PatchBundleRepository(
private val app: Application, private val app: Application,
private val persistenceRepo: PatchBundlePersistenceRepository, private val persistenceRepo: PatchBundlePersistenceRepository,
private val networkInfo: NetworkInfo, private val networkInfo: NetworkInfo,
private val prefs: PreferencesManager,
) { ) {
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE) private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
@ -47,6 +51,37 @@ class PatchBundleRepository(
it.state.map { state -> it.uid to state } 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. * 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 package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.options.PatchOption import app.revanced.patcher.patch.options.PatchOption
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@ -37,6 +39,23 @@ data class PatchInfo(
pkg.versions == null || pkg.versions.contains(versionName) 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 @Immutable
@ -48,6 +67,14 @@ data class CompatiblePackage(
pkg.name, pkg.name,
pkg.versions?.toImmutableSet() pkg.versions?.toImmutableSet()
) )
/**
* Converts this [CompatiblePackage] into a [Patch.CompatiblePackage] from patcher.
*/
fun toPatcherCompatiblePackage() = Patch.CompatiblePackage(
name = packageName,
versions = versions,
)
} }
@Immutable @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.material.icons.outlined.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -18,7 +19,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.AppTopBar
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator 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.model.SelectedApp
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.toast
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -43,19 +43,17 @@ fun AppSelectorScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: AppSelectorViewModel = getViewModel() vm: AppSelectorViewModel = getViewModel()
) { ) {
val context = LocalContext.current SideEffect {
vm.onStorageClick = onStorageClick
}
val pickApkLauncher = val pickApkLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { apkUri -> uri?.let(vm::handleStorageResult)
vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(
context.getString(
R.string.failed_to_load_apk
)
)
}
} }
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle(emptyMap())
var filterText by rememberSaveable { mutableStateOf("") } var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) } 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 // TODO: find something better for this
if (search) { if (search) {
SearchBar( SearchBar(
@ -193,8 +199,17 @@ fun AppSelectorScreen(
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) }, modifier = Modifier.clickable { onAppClick(app.packageName) },
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = { AppLabel(app.packageInfo) }, headlineContent = {
supportingContent = { Text(app.packageName) }, AppLabel(
app.packageInfo,
defaultText = app.packageName
)
},
supportingContent = {
suggestedVersions[app.packageName]?.let {
Text(stringResource(R.string.suggested_version_info, it))
}
},
trailingContent = app.patches?.let { trailingContent = app.patches?.let {
{ {
Text( 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.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.Countdown
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.patches.OptionItem import app.revanced.manager.ui.component.patches.OptionItem
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
@ -390,13 +391,10 @@ fun SelectionWarningDialog(
onConfirm: (Boolean) -> Unit onConfirm: (Boolean) -> Unit
) { ) {
val prefs: PreferencesManager = rememberKoinInject() val prefs: PreferencesManager = rememberKoinInject()
var dismissPermanently by rememberSaveable {
mutableStateOf(false)
}
AlertDialog( DangerousActionDialogBase(
onDismissRequest = onCancel, onCancel = onCancel,
confirmButton = { confirmButton = { dismissPermanently ->
val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState() val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState()
Countdown(start = if (enableCountdown) 3 else 0) { timer -> Countdown(start = if (enableCountdown) 3 else 0) { timer ->
@ -416,49 +414,8 @@ fun SelectionWarningDialog(
} }
} }
}, },
dismissButton = { title = R.string.selection_warning_title,
TextButton(onClick = onCancel) { body = stringResource(R.string.selection_warning_description),
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))
}
}
}
) )
} }

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

View File

@ -92,6 +92,12 @@ 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.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description
)
BooleanItem( BooleanItem(
preference = vm.prefs.multithreadingDexFileWriter, preference = vm.prefs.multithreadingDexFileWriter,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,

View File

@ -3,24 +3,74 @@ 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.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel 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.ui.model.SelectedApp
import app.revanced.manager.util.PM 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.io.File
import java.nio.file.Files 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 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()
} }
val appList = pm.appList 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 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 -> app.contentResolver.openInputStream(uri)?.use { stream ->
with(inputFile) { with(inputFile) {
delete() delete()

View File

@ -2,6 +2,7 @@ package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
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
@ -9,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller 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.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
@ -22,6 +24,7 @@ import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -35,6 +38,7 @@ class VersionSelectorViewModel(
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject() private val patchBundleRepository: PatchBundleRepository by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val prefs: PreferencesManager by inject()
private val appDownloader: AppDownloader = APKMirror() private val appDownloader: AppDownloader = APKMirror()
val rootInstaller: RootInstaller by inject() val rootInstaller: RootInstaller by inject()
@ -45,9 +49,34 @@ class VersionSelectorViewModel(
var errorMessage: String? by mutableStateOf(null) var errorMessage: String? by mutableStateOf(null)
private set 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 var patchesWithoutVersions = 0
bundles.flatMap { (_, bundle) -> bundles.flatMap { (_, bundle) ->
@ -65,16 +94,32 @@ class VersionSelectorViewModel(
count + patchesWithoutVersions count + patchesWithoutVersions
} }
} }
}.flowOn(Dispatchers.Default)
init {
viewModelScope.launch {
requiredVersion = requiredVersionAsync.await()
}
} }
val downloadableVersions = mutableStateSetOf<SelectedApp.Download>()
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps -> 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 { init {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) } 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 = installedApp =
packageInfo.await()?.let { 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="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">Disable version compatibility check</string>
<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_description">Enforce selection of the suggested app version</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>
@ -113,6 +115,7 @@
<string name="patch">Patch</string> <string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</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="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="type_anything">Type anything to continue</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="apply">Apply</string> <string name="apply">Apply</string>
@ -167,6 +170,8 @@
<string name="universal_patches">Universal patches</string> <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_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_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_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">You may encounter issues when not using the default patch selection and options.</string>
<string name="selection_warning_continue_countdown">Continue (%ds)</string> <string name="selection_warning_continue_countdown">Continue (%ds)</string>