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 a8a64d71..cebcdcd7 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 @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 2f7a8fe3..73debb82 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -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. */ diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt index 4914a07a..0754756b 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt b/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt new file mode 100644 index 00000000..32804012 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt @@ -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)) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt new file mode 100644 index 00000000..b55dd5f5 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt @@ -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), + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt index 0e86c8eb..658a90f2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt @@ -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( 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 cfd3655d..7856b71c 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 @@ -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), ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt index 02d2c5de..dc7a7dd7 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -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 { @@ -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 { diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 99382f98..5063b9b7 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -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, diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index abb69a3f..24a63960 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -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(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() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt index cae25116..306397ad 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -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() + var requiredVersion: String? by mutableStateOf(null) + private set + + var selectedVersion: SelectedApp? by mutableStateOf(null) + private set + + private var nonSuggestedVersionDialogSubject by mutableStateOf(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() + 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 + } } \ 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 bae85e41..dab22c3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ Use multiple cores to write DEX files. This is faster, but uses more memory Disable version compatibility check The check restricts patches to supported app versions + Require suggested app version + Enforce selection of the suggested app version Import keystore Import a custom keystore Enter keystore credentials @@ -113,6 +115,7 @@ Patch Select from storage Select an APK file from storage using file picker + Suggested version: %s Type anything to continue Search Apply @@ -167,6 +170,8 @@ Universal patches Patch selection and options has been reset to recommended defaults Patch options have been reset + Non suggested version + The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s Stop using defaults? You may encounter issues when not using the default patch selection and options. Continue (%ds)