mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: check if the version being used is the recommended version (#1675)
This commit is contained in:
parent
8d5d86fea8
commit
5d7f9d1387
12 changed files with 365 additions and 78 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue