From 2d98923f50d21dd33bf701821e79232337cc78ff Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 30 Dec 2025 16:15:16 +0100 Subject: [PATCH] feat: Separate version and source selection --- .../java/app/revanced/manager/MainActivity.kt | 116 +++++-- .../room/apps/downloaded/DownloadedAppDao.kt | 4 +- .../revanced/manager/di/ViewModelModule.kt | 2 + .../repository/DownloadedAppRepository.kt | 2 + .../repository/PatchBundleRepository.kt | 13 + .../repository/PatchSelectionRepository.kt | 2 +- .../manager/patcher/patch/PatchInfo.kt | 2 +- .../revanced/manager/ui/component/AppLabel.kt | 5 +- .../manager/ui/model/SelectedSource.kt | 12 + .../manager/ui/model/SelectedVersion.kt | 11 + .../manager/ui/model/navigation/Nav.kt | 22 +- .../manager/ui/screen/AppSelectorScreen.kt | 10 +- .../ui/screen/PatchesSelectorScreen.kt | 2 +- .../ui/screen/SelectedAppInfoScreen.kt | 204 ++++-------- .../manager/ui/screen/SourceSelectorScreen.kt | 38 +++ .../ui/screen/VersionSelectorScreen.kt | 259 ++++++++++++++++ .../manager/ui/viewmodel/MainViewModel.kt | 32 -- .../ui/viewmodel/PatchesSelectorViewModel.kt | 4 +- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 291 ++++++++---------- .../ui/viewmodel/SourceSelectorViewModel.kt | 11 + .../ui/viewmodel/VersionSelectorViewModel.kt | 40 +++ .../java/app/revanced/manager/util/Util.kt | 3 + app/src/main/res/values/strings.xml | 9 +- 23 files changed, 701 insertions(+), 393 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/model/SelectedSource.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/model/SelectedVersion.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 7b6dbd2a..982e7437 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -25,12 +25,13 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.navigation.AppSelector import app.revanced.manager.ui.model.navigation.ComplexParameter import app.revanced.manager.ui.model.navigation.Dashboard import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo import app.revanced.manager.ui.model.navigation.Patcher -import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.ui.model.navigation.SelectedAppInfo import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Update import app.revanced.manager.ui.screen.AppSelectorScreen @@ -41,7 +42,9 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen import app.revanced.manager.ui.screen.RequiredOptionsScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SettingsScreen +import app.revanced.manager.ui.screen.SourceSelectorScreen import app.revanced.manager.ui.screen.UpdateScreen +import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.screen.settings.AboutSettingsScreen import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen @@ -95,23 +98,16 @@ class MainActivity : ComponentActivity() { dynamicColor = dynamicColor, pureBlackTheme = pureBlackTheme ) { - ReVancedManager(vm) + ReVancedManager() } } } } @Composable -private fun ReVancedManager(vm: MainViewModel) { +private fun ReVancedManager() { val navController = rememberNavController() - EventEffect(vm.appSelectFlow) { app -> - navController.navigateComplex( - SelectedApplicationInfo, - SelectedApplicationInfo.ViewModelParams(app) - ) - } - NavHost( navController = navController, startDestination = Dashboard, @@ -142,7 +138,14 @@ private fun ReVancedManager(vm: MainViewModel) { val data = it.toRoute() InstalledAppInfoScreen( - onPatchClick = vm::selectApp, + onPatchClick = { packageName -> + navController.navigateComplex( + SelectedAppInfo, + SelectedAppInfo.ViewModelParams( + SelectedApp.Search(packageName, null) // TODO + ) + ) + }, onBackClick = navController::popBackStack, viewModel = koinViewModel { parametersOf(data.packageName) } ) @@ -150,8 +153,20 @@ private fun ReVancedManager(vm: MainViewModel) { composable { AppSelectorScreen( - onSelect = vm::selectApp, - onStorageSelect = vm::selectApp, + onSelect = { packageName -> + navController.navigateComplex( + SelectedAppInfo, + SelectedAppInfo.ViewModelParams( + SelectedApp.Search(packageName, null) // TODO + ) + ) + }, + onStorageSelect = { app -> + navController.navigateComplex( + SelectedAppInfo, + SelectedAppInfo.ViewModelParams(app) + ) + }, onBackClick = navController::popBackStack ) } @@ -179,11 +194,11 @@ private fun ReVancedManager(vm: MainViewModel) { ) } - navigation(startDestination = SelectedApplicationInfo.Main) { - composable { + navigation(startDestination = SelectedAppInfo.Main) { + composable { val parentBackStackEntry = navController.navGraphEntry(it) val data = - parentBackStackEntry.getComplexArg() + parentBackStackEntry.getComplexArg() val viewModel = koinNavViewModel(viewModelStoreOwner = parentBackStackEntry) { parametersOf(data) @@ -201,8 +216,8 @@ private fun ReVancedManager(vm: MainViewModel) { }, onPatchSelectorClick = { app, patches, options -> navController.navigateComplex( - SelectedApplicationInfo.PatchesSelector, - SelectedApplicationInfo.PatchesSelector.ViewModelParams( + SelectedAppInfo.PatchesSelector, + SelectedAppInfo.PatchesSelector.ViewModelParams( app, patches, options @@ -211,21 +226,40 @@ private fun ReVancedManager(vm: MainViewModel) { }, onRequiredOptions = { app, patches, options -> navController.navigateComplex( - SelectedApplicationInfo.RequiredOptions, - SelectedApplicationInfo.PatchesSelector.ViewModelParams( + SelectedAppInfo.RequiredOptions, + SelectedAppInfo.PatchesSelector.ViewModelParams( app, patches, options ) ) }, + onVersionClick = { packageName, patchSelection, currentSelection -> + navController.navigateComplex( + SelectedAppInfo.VersionSelector, + SelectedAppInfo.VersionSelector.ViewModelParams( + packageName, + patchSelection, + currentSelection, + ) + ) + }, + onSourceClick = { packageName, version -> + navController.navigateComplex( + SelectedAppInfo.SourceSelector, + SelectedAppInfo.SourceSelector.ViewModelParams( + packageName, + version, + ) + ) + }, vm = viewModel ) } - composable { + composable { val data = - it.getComplexArg() + it.getComplexArg() val selectedAppInfoVm = koinNavViewModel( viewModelStoreOwner = navController.navGraphEntry(it) ) @@ -240,9 +274,43 @@ private fun ReVancedManager(vm: MainViewModel) { ) } - composable { + composable { val data = - it.getComplexArg() + it.getComplexArg() + val selectedAppInfoVm = koinNavViewModel( + viewModelStoreOwner = navController.navGraphEntry(it) + ) + + VersionSelectorScreen( + onBackClick = navController::popBackStack, + onSave = { version -> + selectedAppInfoVm.updateVersion(version) + navController.popBackStack() + }, + viewModel = koinViewModel { parametersOf(data) } + ) + } + + composable { + val data = + it.getComplexArg() + val selectedAppInfoVm = koinNavViewModel( + viewModelStoreOwner = navController.navGraphEntry(it) + ) + + SourceSelectorScreen( + onBackClick = navController::popBackStack, + onSave = { source -> + selectedAppInfoVm.updateSource(source) + navController.popBackStack() + }, + viewModel = koinViewModel { parametersOf(data) } + ) + } + + composable { + val data = + it.getComplexArg() val selectedAppInfoVm = koinNavViewModel( viewModelStoreOwner = navController.navGraphEntry(it) ) diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt index 492dbde1..eeb79cae 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt @@ -2,7 +2,6 @@ package app.revanced.manager.data.room.apps.downloaded import androidx.room.Dao import androidx.room.Delete -import androidx.room.Insert import androidx.room.Query import androidx.room.Upsert import kotlinx.coroutines.flow.Flow @@ -12,6 +11,9 @@ interface DownloadedAppDao { @Query("SELECT * FROM downloaded_app") fun getAllApps(): Flow> + @Query("SELECT * FROM downloaded_app WHERE package_name = :packageName") + fun get(packageName: String): Flow> + @Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version") suspend fun get(packageName: String, version: String): DownloadedApp? diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 6970e886..8d3c2f8e 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -24,4 +24,6 @@ val viewModelModule = module { viewModelOf(::InstalledAppInfoViewModel) viewModelOf(::UpdatesSettingsViewModel) viewModelOf(::BundleListViewModel) + viewModelOf(::VersionSelectorViewModel) + viewModelOf(::SourceSelectorViewModel) } diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index e8536de1..839b1eba 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -30,6 +30,8 @@ class DownloadedAppRepository( fun getAll() = dao.getAllApps().distinctUntilChanged() + fun get(packageName: String) = dao.get(packageName) + fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory)) 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 ab8dbb19..e1088528 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 @@ -26,6 +26,7 @@ import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundleInfo +import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast @@ -36,6 +37,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @@ -74,6 +76,17 @@ class PatchBundleRepository( val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } } + fun suggestedVersions(packageName: String, patchSelection: PatchSelection) = + bundleInfoFlow.map { + val allPatches = patchSelection.flatMap { (uid, patches) -> + val bundle = it[uid] ?: return@flatMap emptyList() + bundle.patches.filter { patch -> patches.contains(patch.name) } + .map(PatchInfo::toPatcherPatch) + }.toSet() + + allPatches.mostCommonCompatibleVersions(countUnusedPatches = false)[packageName] + } + val suggestedVersions = bundleInfoFlow.map { val allPatches = it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet() diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt index 22f8187c..7f70c5ce 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt @@ -16,7 +16,7 @@ class PatchSelectionRepository(db: AppDatabase) { packageName = packageName ).also { dao.createSelection(it) }.uid - suspend fun getSelection(packageName: String): Map> = + suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection = dao.getSelectedPatches(packageName).mapValues { it.value.toSet() } suspend fun updateSelection(packageName: String, selection: Map>) = 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 2babc7f4..52b3863e 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 @@ -40,7 +40,7 @@ data class PatchInfo( if (pkg.packageName != packageName) return@any false if (pkg.versions == null) return@any true - versionName != null && versionName in pkg.versions + versionName == null || versionName in pkg.versions } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt index 33a1e201..dcaefcf9 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import app.revanced.manager.R import io.github.fornewid.placeholder.material3.placeholder import kotlinx.coroutines.Dispatchers @@ -47,6 +48,8 @@ fun AppLabel( shape = RoundedCornerShape(100) ) .then(modifier), - style = style + style = style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedSource.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedSource.kt new file mode 100644 index 00000000..8b01dd65 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedSource.kt @@ -0,0 +1,12 @@ +package app.revanced.manager.ui.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class SelectedSource : Parcelable { + data object Auto : SelectedSource() + data object Installed : SelectedSource() + data class Downloaded(val path: String) : SelectedSource() + data class Plugin(val plugin: String) : SelectedSource() // TODO +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedVersion.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedVersion.kt new file mode 100644 index 00000000..ce019ee2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedVersion.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.ui.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class SelectedVersion : Parcelable { + data object Auto : SelectedVersion() + data object Any : SelectedVersion() + data class Specific(val version: String) : SelectedVersion() +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt index 910b8345..8d101604 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt @@ -2,6 +2,7 @@ package app.revanced.manager.ui.model.navigation import android.os.Parcelable import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.SelectedVersion import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import kotlinx.parcelize.Parcelize @@ -23,7 +24,7 @@ data class InstalledApplicationInfo(val packageName: String) data class Update(val downloadOnScreenEntry: Boolean = false) @Serializable -data object SelectedApplicationInfo : ComplexParameter { +data object SelectedAppInfo : ComplexParameter { @Parcelize data class ViewModelParams( val app: SelectedApp, @@ -43,6 +44,25 @@ data object SelectedApplicationInfo : ComplexParameter { + @Parcelize + data class ViewModelParams( + val packageName: String, + val patchSelection: PatchSelection, + val currentSelection: SelectedVersion, + ) : Parcelable + } + + @Serializable + data object SourceSelector : ComplexParameter { + @Parcelize + data class ViewModelParams( + val packageName: String, + val version: String?, + ) : Parcelable + } + @Serializable data object RequiredOptions : ComplexParameter } 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 812c2fa6..ff2ec6e7 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 @@ -115,8 +115,7 @@ fun AppSelectorScreen( ) }, headlineContent = { AppLabel(app.packageInfo) }, - supportingContent = { Text(app.packageName) }, - trailingContent = app.patches?.let { + supportingContent = app.patches?.let { { Text( pluralStringResource( @@ -214,12 +213,7 @@ fun AppSelectorScreen( defaultText = app.packageName ) }, - supportingContent = { - suggestedVersions[app.packageName]?.let { - Text(stringResource(R.string.suggested_version_info, it)) - } - }, - trailingContent = app.patches?.let { + supportingContent = app.patches?.let { { Text( pluralStringResource( 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 035c862e..b0452308 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 @@ -498,7 +498,7 @@ private fun PatchItem( leadingContent = { HapticCheckbox( checked = selected, - onCheckedChange = { onToggle() }, + onCheckedChange = null, enabled = compatible ) }, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 6c668870..6cadd902 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -1,16 +1,11 @@ package app.revanced.manager.ui.screen -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts 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.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.filled.AutoFixHigh @@ -21,7 +16,6 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -37,24 +31,18 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.data.platform.NetworkInfo -import app.revanced.manager.data.room.apps.installed.InstallType -import app.revanced.manager.data.room.apps.installed.InstalledApp -import app.revanced.manager.network.downloader.LoadedDownloaderPlugin -import app.revanced.manager.ui.component.AlertDialogExtended import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar -import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.SelectedVersion import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel -import app.revanced.manager.util.EventEffect import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.enabled import app.revanced.manager.util.toast -import app.revanced.manager.util.transparentListItemColors import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -64,6 +52,8 @@ fun SelectedAppInfoScreen( onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit, onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit, onPatchClick: () -> Unit, + onVersionClick: (packageName: String, patchSelection: PatchSelection, currentSelection: SelectedVersion) -> Unit, + onSourceClick: (packageName: String, version: String?) -> Unit, onBackClick: () -> Unit, vm: SelectedAppInfoViewModel ) { @@ -84,13 +74,12 @@ fun SelectedAppInfoScreen( } val selectedPatchCount = patches.values.sumOf { it.size } - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = vm::handlePluginActivityResult - ) - EventEffect(flow = vm.launchActivityFlow) { intent -> - launcher.launch(intent) - } +// val patches2 by remember { +// derivedStateOf { +// vm.patchSelection(bundles, allowIncompatiblePatches).collectAsStateWithLifecycle() +// } +// } + val composableScope = rememberCoroutineScope() val error by vm.errorFlow.collectAsStateWithLifecycle(null) @@ -142,39 +131,19 @@ fun SelectedAppInfoScreen( ) { paddingValues -> val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList()) - if (vm.showSourceSelector) { - val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null) - - AppSourceSelectorDialog( - plugins = plugins, - installedApp = vm.installedAppData, - searchApp = SelectedApp.Search( - vm.packageName, - vm.desiredVersion - ), - activeSearchJob = vm.activePluginAction, - hasRoot = vm.hasRoot, - onDismissRequest = vm::dismissSourceSelector, - onSelectPlugin = vm::searchUsingPlugin, - requiredVersion = requiredVersion, - onSelect = { - vm.selectedApp = it - vm.dismissSourceSelector() - } - ) - } - ColumnWithScrollbar( modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) { - Text( - version ?: stringResource(R.string.selected_app_meta_any_version), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) + vm.selectedAppInfo?.let { + Text( + it.packageName, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } } PageItem( @@ -182,7 +151,7 @@ fun SelectedAppInfoScreen( stringResource( R.string.patch_selector_item_description, selectedPatchCount - ), + ) + "\n⚠\uFE0F 3 incompatible", onClick = { onPatchSelectorClick( vm.selectedApp, @@ -194,33 +163,55 @@ fun SelectedAppInfoScreen( ) } ) + + if (vm.selectedApp !is SelectedApp.Local || !(vm.selectedApp as SelectedApp.Local).temporary) { + val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle(SelectedVersion.Auto) + val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null) + + val version = resolvedVersion ?: "Any available version" + + val description = if (selectedVersion is SelectedVersion.Auto) + "Auto ($version)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion) + else version + + + PageItem( + R.string.version_selector_item, + description, +// "Auto (${requiredVersion ?: stringResource(R.string.selected_app_meta_any_version)})", // ⚠️ 1 Patch incompatible + onClick = { onVersionClick(packageName, patches, selectedVersion) }, + ) + } + PageItem( R.string.apk_source_selector_item, when (val app = vm.selectedApp) { - is SelectedApp.Search -> stringResource(R.string.apk_source_auto) - is SelectedApp.Installed -> stringResource(R.string.apk_source_installed) - is SelectedApp.Download -> stringResource( - R.string.apk_source_downloader, - plugins.find { it.packageName == app.data.pluginPackageName }?.name - ?: app.data.pluginPackageName - ) + is SelectedApp.Search -> "Auto (Downloaded APK)" // stringResource(R.string.apk_source_auto) + is SelectedApp.Installed -> app.version + " (Installed)" // stringResource(R.string.apk_source_installed) + is SelectedApp.Download -> plugins.find { it.packageName == app.data.pluginPackageName }?.name ?: app.data.pluginPackageName + is SelectedApp.Local -> if (app.temporary) "${app.version} (Local File)" else "Downloaded APK" - is SelectedApp.Local -> stringResource(R.string.apk_source_local) + +// stringResource( +// R.string.apk_source_downloader, +// plugins.find { it.packageName == app.data.pluginPackageName }?.name +// ?: app.data.pluginPackageName +// ) + // stringResource(R.string.apk_source_local) }, - onClick = { - vm.showSourceSelector() - } + onClick = { onSourceClick(packageName, version) }, + enabled = !vm.selectedApp.let { it is SelectedApp.Local && it.temporary }, // Disable for APK from storage ) error?.let { Text( stringResource(it.resourceId), color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier.padding(horizontal = 16.dp) ) } Column( - modifier = Modifier.padding(horizontal = 24.dp), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { val needsInternet = @@ -252,11 +243,11 @@ fun SelectedAppInfoScreen( } @Composable -private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { +private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit, enabled: Boolean = true) { ListItem( modifier = Modifier - .clickable(onClick = onClick) - .padding(start = 8.dp), + .clickable(enabled, onClick = onClick) + .enabled(enabled), headlineContent = { Text( stringResource(title), @@ -275,89 +266,4 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Icon(Icons.AutoMirrored.Outlined.ArrowRight, null) } ) -} - -@Composable -private fun AppSourceSelectorDialog( - plugins: List, - installedApp: Pair?, - searchApp: SelectedApp.Search, - activeSearchJob: String?, - hasRoot: Boolean, - requiredVersion: String?, - onDismissRequest: () -> Unit, - onSelectPlugin: (LoadedDownloaderPlugin) -> Unit, - onSelect: (SelectedApp) -> Unit, -) { - val canSelect = activeSearchJob == null - - AlertDialogExtended( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton(onClick = onDismissRequest) { - Text(stringResource(R.string.cancel)) - } - }, - title = { Text(stringResource(R.string.app_source_dialog_title)) }, - textHorizontalPadding = PaddingValues(horizontal = 0.dp), - text = { - LazyColumn { - item(key = "auto") { - val hasPlugins = plugins.isNotEmpty() - ListItem( - modifier = Modifier - .clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) } - .enabled(hasPlugins), - headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) }, - supportingContent = { - Text( - if (hasPlugins) - stringResource(R.string.app_source_dialog_option_auto_description) - else - stringResource(R.string.app_source_dialog_option_auto_unavailable) - ) - }, - colors = transparentListItemColors - ) - } - - installedApp?.let { (app, meta) -> - item(key = "installed") { - val (usable, text) = when { - // Mounted apps must be unpatched before patching, which cannot be done without root access. - meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource( - R.string.app_source_dialog_option_installed_no_root - ) - // Patching already patched apps is not allowed because patches expect unpatched apps. - meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched) - // Version does not match suggested version. - requiredVersion != null && app.version != requiredVersion -> false to stringResource( - R.string.app_source_dialog_option_installed_version_not_suggested, - app.version - ) - - else -> true to app.version - } - ListItem( - modifier = Modifier - .clickable(enabled = canSelect && usable) { onSelect(app) } - .enabled(usable), - headlineContent = { Text(stringResource(R.string.installed)) }, - supportingContent = { Text(text) }, - colors = transparentListItemColors - ) - } - } - - items(plugins, key = { "plugin_${it.packageName}" }) { plugin -> - ListItem( - modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) }, - headlineContent = { Text(plugin.name) }, - trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName }, - colors = transparentListItemColors - ) - } - } - } - ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt new file mode 100644 index 00000000..03da05bc --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt @@ -0,0 +1,38 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.model.SelectedSource +import app.revanced.manager.ui.viewmodel.SourceSelectorViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SourceSelectorScreen( + onBackClick: () -> Unit, + onSave: (source: SelectedSource) -> Unit, + viewModel: SourceSelectorViewModel, +) { + Scaffold( + topBar = { + AppTopBar( + title = { Text("Select source") }, + onBackClick = onBackClick, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + ListItem( + headlineContent = { Text("Filtering for ${viewModel.input.packageName}: ${viewModel.input.version}") } + ) + } + } +} 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 new file mode 100644 index 00000000..da6db396 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -0,0 +1,259 @@ +package app.revanced.manager.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.data.room.apps.downloaded.DownloadedApp +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.SelectedVersion +import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel +import app.revanced.manager.util.enabled +import app.revanced.manager.util.transparentListItemColors + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VersionSelectorScreen( + onBackClick: () -> Unit, + onSave: (version: SelectedVersion) -> Unit, + viewModel: VersionSelectorViewModel, +) { + val versions by viewModel.availableVersions.collectAsStateWithLifecycle(emptyList()) + + Scaffold( + topBar = { + AppTopBar( + title = { Text("Select version") }, + onBackClick = onBackClick, + ) + }, + floatingActionButton = { + HapticExtendedFloatingActionButton( + text = { Text(stringResource(R.string.save)) }, + icon = { Icon(Icons.Outlined.Save, contentDescription = null) }, + onClick = { onSave(viewModel.selectedVersion) } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + VersionOption( + version = SelectedVersion.Auto, + isSelected = viewModel.selectedVersion is SelectedVersion.Auto, + onSelect = viewModel::selectVersion, + title = { Text("Auto (Recommended)") }, + description = { Text("Automatically select the best available version") } + ) + + if (versions.isNotEmpty()) + HorizontalDivider() + + LazyColumn { + items(versions, key = { it.first.version }) { version -> + VersionOption( + version = version.first, + isSelected = viewModel.selectedVersion == version.first, + onSelect = viewModel::selectVersion, + title = { Text(version.first.version) }, + description = { Text( + "${version.second.let { if (it == 0) "No" else it }} incompatible patches") + } + ) + } + } + + HorizontalDivider() + + VersionOption( + version = SelectedVersion.Any, + isSelected = viewModel.selectedVersion is SelectedVersion.Any, + onSelect = viewModel::selectVersion, + title = { Text("Any available version") }, + description = { Text("Use any available version regardless of compatibility") } + ) + + + } + } +} + +@Composable +private fun VersionOption( + version: SelectedVersion, + isSelected: Boolean, + onSelect: (SelectedVersion) -> Unit, + title: @Composable (() -> Unit), + description: @Composable (() -> Unit)? = null, +) { + ListItem( + modifier = Modifier + .clickable { onSelect(version) }, + leadingContent = { + RadioButton( + selected = isSelected, + onClick = null + ) + }, + headlineContent = title, + supportingContent = description, + colors = transparentListItemColors + ) + +} + +@Composable +fun AppSourceSelectorDialog( + plugins: List, + installedApp: Pair?, + downloadedApps: List, + searchApp: SelectedApp.Search, + activeSearchJob: String?, + hasRoot: Boolean, + requiredVersion: String?, + onDismissRequest: () -> Unit, + onSelectPlugin: (LoadedDownloaderPlugin) -> Unit, + onSelect: (SelectedApp) -> Unit, + onSelectDownloaded: (DownloadedApp) -> Unit = {}, +) { + val canSelect = activeSearchJob == null + + AlertDialogExtended( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + }, + title = { Text(stringResource(R.string.app_source_dialog_title)) }, + textHorizontalPadding = PaddingValues(horizontal = 0.dp), + text = { + Column { + HorizontalDivider() + LazyColumn { + item(key = "auto") { + val hasPlugins = plugins.isNotEmpty() + ListItem( + modifier = Modifier + .clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) } + .enabled(hasPlugins), + headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto) + " (Recommended)") }, + supportingContent = { + Text( + "Automatically choose a suitable source" +// if (hasPlugins) +// stringResource(R.string.app_source_dialog_option_auto_description) +//// "Automatically choose a suitable source" +// else +// stringResource(R.string.app_source_dialog_option_auto_unavailable) + ) + }, + colors = transparentListItemColors + ) + } + + installedApp?.let { (app, meta) -> + item(key = "installed") { + val (usable, text) = when { +// Mounted apps must be unpatched before patching, which cannot be done without root access. + meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource( + R.string.app_source_dialog_option_installed_no_root + ) +// Patching already patched apps is not allowed because patches expect unpatched apps. + meta?.installType == InstallType.DEFAULT -> false to stringResource( + R.string.already_patched + ) +// Version does not match suggested version. + requiredVersion != null && app.version != requiredVersion -> false to "Does not match the selected version" +// stringResource( +// R.string.app_source_dialog_option_installed_version_not_suggested, +// app.version +// ) + + else -> true to null + } + ListItem( + modifier = Modifier + .clickable(enabled = canSelect && usable) { onSelect(app) } + .enabled(usable), + overlineContent = { Text("Installed") }, + headlineContent = { Text(app.version) }, + supportingContent = text?.let { { Text(text) } }, + colors = transparentListItemColors + ) + } + } + + items(downloadedApps, key = { it.version }) { app -> + val (usable, text) = when { +// Version does not match suggested version. + requiredVersion != null && app.version != requiredVersion -> false to "Does not match the selected version" +// stringResource( +// R.string.app_source_dialog_option_installed_version_not_suggested, +// app.version +// ) + + else -> true to null // "Downloaded using downloader plugin" + } + ListItem( + modifier = Modifier + .clickable(enabled = usable) { onSelectDownloaded(app) } + .enabled(usable), + overlineContent = { Text("Downloaded") }, + headlineContent = { Text(app.version) }, + supportingContent = text?.let { { Text(text) } }, + colors = transparentListItemColors + ) + } + + items(plugins, key = { "plugin_${it.packageName}" }) { plugin -> + ListItem( + modifier = Modifier.clickable(enabled = canSelect) { + onSelectPlugin(plugin) + }, + overlineContent = { Text("Plugin") }, + headlineContent = { + Text( + plugin.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName }, + colors = transparentListItemColors + ) + } + } + HorizontalDivider() + } + } + ) +} + diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt index f8ba081f..2b25dfa1 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -12,11 +12,9 @@ import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.PreferencesManager -import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SerializedSelection -import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.theme.Theme import app.revanced.manager.util.tag import app.revanced.manager.util.toast @@ -31,44 +29,14 @@ import kotlinx.serialization.json.Json class MainViewModel( private val patchBundleRepository: PatchBundleRepository, private val patchSelectionRepository: PatchSelectionRepository, - private val downloadedAppRepository: DownloadedAppRepository, private val keystoreManager: KeystoreManager, private val app: Application, val prefs: PreferencesManager, private val json: Json ) : ViewModel() { - private val appSelectChannel = Channel() - val appSelectFlow = appSelectChannel.receiveAsFlow() private val legacyImportActivityChannel = Channel() val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow() - private suspend fun suggestedVersion(packageName: String) = - patchBundleRepository.suggestedVersions.first()[packageName] - - private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? { - if (app !is SelectedApp.Search) return null - - val suggestedVersion = suggestedVersion(app.packageName) ?: return null - - val downloadedApp = - downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true) - ?: return null - return SelectedApp.Local( - downloadedApp.packageName, - downloadedApp.version, - downloadedAppRepository.getApkFileForApp(downloadedApp), - false - ) - } - - fun selectApp(app: SelectedApp) = viewModelScope.launch { - appSelectChannel.send(findDownloadedApp(app) ?: app) - } - - fun selectApp(packageName: String) = viewModelScope.launch { - selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName))) - } - init { viewModelScope.launch { if (!prefs.firstLaunch.get()) return@launch diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index fb458d43..67c3f238 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -20,7 +20,7 @@ import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.ui.model.navigation.SelectedAppInfo import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.saver.Nullable @@ -45,7 +45,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.get @OptIn(SavedStateHandleSaveableApi::class) -class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) : +class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelParams) : ViewModel(), KoinComponent { private val app: Application = get() private val savedStateHandle: SavedStateHandle = get() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 665b353b..6624aa51 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -1,12 +1,7 @@ package app.revanced.manager.ui.viewmodel -import android.app.Activity -import android.app.Application -import android.content.Intent import android.content.pm.PackageInfo import android.os.Parcelable -import android.util.Log -import androidx.activity.result.ActivityResult import androidx.annotation.StringRes import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -20,7 +15,6 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.R 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.DownloaderPluginRepository import app.revanced.manager.domain.repository.InstalledAppRepository @@ -28,31 +22,26 @@ import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.patcher.patch.PatchBundleInfo -import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection -import app.revanced.manager.network.downloader.LoadedDownloaderPlugin -import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet -import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.plugin.downloader.PluginHostApi -import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.model.SelectedSource +import app.revanced.manager.ui.model.SelectedVersion import app.revanced.manager.ui.model.navigation.Patcher -import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo +import app.revanced.manager.ui.model.navigation.SelectedAppInfo import app.revanced.manager.util.Options import app.revanced.manager.util.PM import app.revanced.manager.util.PatchSelection -import app.revanced.manager.util.simpleMessage -import app.revanced.manager.util.tag -import app.revanced.manager.util.toast -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CompletableDeferred +import app.revanced.manager.util.patchCount import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -61,45 +50,109 @@ import org.koin.core.component.get @OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) class SelectedAppInfoViewModel( - input: SelectedApplicationInfo.ViewModelParams + input: SelectedAppInfo.ViewModelParams ) : ViewModel(), KoinComponent { - private val app: Application = get() private val bundleRepository: PatchBundleRepository = get() private val selectionRepository: PatchSelectionRepository = get() private val optionsRepository: PatchOptionsRepository = get() private val pluginsRepository: DownloaderPluginRepository = get() private val installedAppRepository: InstalledAppRepository = get() - private val rootInstaller: RootInstaller = get() private val pm: PM = get() private val savedStateHandle: SavedStateHandle = get() val prefs: PreferencesManager = get() val plugins = pluginsRepository.loadedPluginsFlow - val desiredVersion = input.app.version val packageName = input.app.packageName - private val persistConfiguration = input.patches == null - val hasRoot = rootInstaller.hasRootAccess() - var installedAppData: Pair? by mutableStateOf(null) - private set - private var _selectedApp by savedStateHandle.saveable { - mutableStateOf(input.app) + // User selection + private var selectionFlow = MutableStateFlow( + input.patches?.let { + SelectionState.Customized(input.patches) + } ?: SelectionState.Default + ) + + private val _selectedVersion = MutableStateFlow(SelectedVersion.Auto) + val selectedVersion: StateFlow = _selectedVersion + + private val _selectedSource = MutableStateFlow(SelectedSource.Auto) + val selectedSource: StateFlow = _selectedSource + + fun updateVersion(version: SelectedVersion) { + _selectedVersion.value = version + } + fun updateSource(source: SelectedSource) { + _selectedSource.value = source + } + fun updateConfiguration( + selection: PatchSelection?, + selectedOptions: Options + ) = viewModelScope.launch { + selectionFlow.value = selection?.let(SelectionState::Customized) ?: SelectionState.Default + + val filteredOptions = selectedOptions.filtered(bundleInfoFlow.first()) + options = filteredOptions + + if (persistConfiguration) { + selection?.let { selectionRepository.updateSelection(packageName, it) } + ?: selectionRepository.resetSelectionForPackage(packageName) + + optionsRepository.saveOptions(packageName, filteredOptions) + } } - var selectedAppInfo: PackageInfo? by mutableStateOf(null) - private set - var selectedApp - get() = _selectedApp - set(value) { - _selectedApp = value - invalidateSelectedAppInfo() + + + // All patches for package + val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null) + + // Selection derived from selectionFlow + val patchSelection = combine( + selectionFlow, + bundles, + ) { selection, bundles -> + selection.patches(bundles, allowIncompatible = true) + } + + // Most compatible versions based on patch selection + @OptIn(ExperimentalCoroutinesApi::class) + val mostCompatibleVersions = patchSelection.flatMapLatest { patchSelection -> + bundleRepository.suggestedVersions( + packageName, + patchSelection + ) + } + + // Resolve actual version from user selection + val resolvedVersion = combine( + _selectedVersion, + mostCompatibleVersions, + ) { selected, mostCompatible -> + when (selected) { + is SelectedVersion.Specific -> selected.version + is SelectedVersion.Any -> null + is SelectedVersion.Auto -> { + mostCompatible?.maxByOrNull { it.value }?.key + } } + } + init { invalidateSelectedAppInfo() - viewModelScope.launch(Dispatchers.Main) { + + // Get the previous selection if customization is enabled. + viewModelScope.launch { + if (prefs.disableSelectionWarning.get()) { + val previous = selectionRepository.getSelection(packageName) + if (previous.patchCount == 0) return@launch + selectionFlow.value = SelectionState.Customized(previous) + } + } + + // Get installed app info + viewModelScope.launch { val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) } val installedAppDeferred = async(Dispatchers.IO) { installedAppRepository.get(packageName) } @@ -114,19 +167,6 @@ class SelectedAppInfoViewModel( } } - val requiredVersion = combine( - prefs.suggestedVersionSafeguard.flow, - bundleRepository.suggestedVersions - ) { suggestedVersionSafeguard, suggestedVersions -> - if (!suggestedVersionSafeguard) return@combine null - - suggestedVersions[input.app.packageName] - } - - val bundleInfoFlow by derivedStateOf { - bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version) - } - var options: Options by savedStateHandle.saveable { viewModelScope.launch { if (!persistConfiguration) return@launch // TODO: save options for patched apps. @@ -142,29 +182,39 @@ class SelectedAppInfoViewModel( } private set - private var selectionState: SelectionState by savedStateHandle.saveable { - if (input.patches != null) - return@saveable mutableStateOf(SelectionState.Customized(input.patches)) - // Try to get the previous selection if customization is enabled. - viewModelScope.launch { - if (!prefs.disableSelectionWarning.get()) return@launch - val previous = selectionRepository.getSelection(packageName) - if (previous.values.sumOf { it.size } == 0) return@launch - selectionState = SelectionState.Customized(previous) - } - mutableStateOf(SelectionState.Default) + + + var installedAppData: Pair? by mutableStateOf(null) + private set + + private var _selectedApp by savedStateHandle.saveable { + mutableStateOf(input.app) } - var showSourceSelector by mutableStateOf(false) + var selectedAppInfo: PackageInfo? by mutableStateOf(null) private set - private var pluginAction: Pair? by mutableStateOf(null) - val activePluginAction get() = pluginAction?.first?.packageName - private var launchedActivity by mutableStateOf?>(null) - private val launchActivityChannel = Channel() - val launchActivityFlow = launchActivityChannel.receiveAsFlow() + + var selectedApp + get() = _selectedApp + set(value) { + _selectedApp = value + invalidateSelectedAppInfo() + } + + + val bundleInfoFlow by derivedStateOf { + bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version) + } + + + + + + // TODO: Remove + private var oldSelectionState: SelectionState by savedStateHandle.saveable { mutableStateOf(SelectionState.Default) } val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app -> when { @@ -173,90 +223,13 @@ class SelectedAppInfoViewModel( } } - fun showSourceSelector() { - dismissSourceSelector() - showSourceSelector = true - } - - private fun cancelPluginAction() { - pluginAction?.second?.cancel() - pluginAction = null - } - - fun dismissSourceSelector() { - cancelPluginAction() - showSourceSelector = false - } - - fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) { - cancelPluginAction() - pluginAction = plugin to viewModelScope.launch { - try { - val scope = object : GetScope { - override val hostPackageName = app.packageName - override val pluginPackageName = plugin.packageName - override suspend fun requestStartActivity(intent: Intent) = - withContext(Dispatchers.Main) { - if (launchedActivity != null) error("Previous activity has not finished") - try { - val result = with(CompletableDeferred()) { - launchedActivity = this - launchActivityChannel.send(intent) - await() - } - when (result.resultCode) { - Activity.RESULT_OK -> result.data - Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() - else -> throw UserInteractionException.Activity.NotCompleted( - result.resultCode, - result.data - ) - } - } finally { - launchedActivity = null - } - } - } - - withContext(Dispatchers.IO) { - plugin.get(scope, packageName, desiredVersion) - }?.let { (data, version) -> - if (desiredVersion != null && version != desiredVersion) { - app.toast(app.getString(R.string.downloader_invalid_version)) - return@launch - } - selectedApp = SelectedApp.Download( - packageName, - version, - ParceledDownloaderData(plugin, data) - ) - } ?: app.toast(app.getString(R.string.downloader_app_not_found)) - } catch (e: UserInteractionException.Activity) { - app.toast(e.message!!) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - app.toast(app.getString(R.string.downloader_error, e.simpleMessage())) - Log.e(tag, "Downloader.get threw an exception", e) - } finally { - pluginAction = null - dismissSourceSelector() - } - } - } - - fun handlePluginActivityResult(result: ActivityResult) { - launchedActivity?.complete(result) - } private fun invalidateSelectedAppInfo() = viewModelScope.launch { - val info = when (val app = selectedApp) { + selectedAppInfo = when (val app = selectedApp) { is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } - is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } - else -> null + else -> withContext(Dispatchers.IO) { pm.getPackageInfo(packageName) } +// else -> null } - - selectedAppInfo = info } fun getOptionsFiltered(bundles: List) = options.filtered(bundles) @@ -279,33 +252,15 @@ class SelectedAppInfoViewModel( } fun getPatches(bundles: List, allowIncompatible: Boolean) = - selectionState.patches(bundles, allowIncompatible) + oldSelectionState.patches(bundles, allowIncompatible) fun getCustomPatches( bundles: List, allowIncompatible: Boolean ): PatchSelection? = - (selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible) + (oldSelectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible) - fun updateConfiguration( - selection: PatchSelection?, - options: Options - ) = viewModelScope.launch { - selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default - - val filteredOptions = options.filtered(bundleInfoFlow.first()) - this@SelectedAppInfoViewModel.options = filteredOptions - - if (!persistConfiguration) return@launch - viewModelScope.launch(Dispatchers.Default) { - selection?.let { selectionRepository.updateSelection(packageName, it) } - ?: selectionRepository.resetSelectionForPackage(packageName) - - optionsRepository.saveOptions(packageName, filteredOptions) - } - } - enum class Error(@param:StringRes val resourceId: Int) { NoPlugins(R.string.downloader_no_plugins_available) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt new file mode 100644 index 00000000..cf6b22d8 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt @@ -0,0 +1,11 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.lifecycle.ViewModel +import app.revanced.manager.ui.model.navigation.SelectedAppInfo +import org.koin.core.component.KoinComponent + +class SourceSelectorViewModel( + val input: SelectedAppInfo.SourceSelector.ViewModelParams +) : ViewModel(), KoinComponent { + +} \ No newline at end of file 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 new file mode 100644 index 00000000..0cc6766a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -0,0 +1,40 @@ +package app.revanced.manager.ui.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.ui.model.SelectedVersion +import app.revanced.manager.ui.model.navigation.SelectedAppInfo +import kotlinx.coroutines.flow.map +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class VersionSelectorViewModel( + val input: SelectedAppInfo.VersionSelector.ViewModelParams +) : ViewModel(), KoinComponent { + val patchBundleRepository: PatchBundleRepository = get() + + val patchCount = input.patchSelection.values.sumOf { it.size } + + val availableVersions = patchBundleRepository.suggestedVersions(input.packageName, input.patchSelection) + .map { versions -> + versions.orEmpty() + .map { (key, value) -> SelectedVersion.Specific(key) to patchCount - value } + .sortedWith( + compareBy>{ it.second } + .thenByDescending { it.first.version } + ) + } + + + var selectedVersion by mutableStateOf(input.currentSelection) + private set + + fun selectVersion(version: SelectedVersion) { + selectedVersion = version + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 5b2dfa33..27eb837d 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -57,6 +57,9 @@ import kotlin.reflect.KProperty typealias PatchSelection = Map> typealias Options = Map>> +val PatchSelection.patchCount + get() = this.values.sumOf { it.size } + val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE fun Context.openUrl(url: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35364e02..7699301a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,14 +61,15 @@ Second \"item\" text" Version %s does not match the suggested version Start patching the application - Select patches - %d patches selected + Patches + %d selected No patches selected + Version Your device is not connected to the internet. Downloading will fail later. You are currently on a metered connection. Data charges from your service provider may apply. - Select APK source + APK source Using all APK downloaders Using %s Using installed APK @@ -202,7 +203,7 @@ You will not be able to update the previously installed apps from this source."< Share Patch Select from storage - Select an APK file from storage using file picker + Select an APK file from storage Suggested version: %s Type anything to continue Search patches…