Conversation
4c055f4 to
b28e9a1
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the patching setup flow to be “patch-first”: users choose patches first, then the app derives the most compatible app version and selects an appropriate APK source (with manual overrides for both).
Changes:
- Introduces explicit Version and Source selector screens/models (
SelectedVersion,SelectedSource) and wires them into navigation. - Refactors
SelectedAppInfoViewModel/PatcherWorker/PatcherViewModelto operate on package/version/source instead of the removedSelectedAppmodel. - Updates UI strings/plurals to reflect the new flow and selection counts.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| app/src/main/res/values/strings.xml | Adds version-selector strings and updates selection labels to fit the new flow. |
| app/src/main/res/values/plurals.xml | Adds plurals for available patch count + incompatible patch count messaging. |
| app/src/main/java/app/revanced/manager/util/Util.kt | Adds PatchSelection.patchCount helper used across the new flow. |
| app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt | New VM to compute and present version options from selected patches. |
| app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt | New VM to present source options (installed/local/downloaded/plugin) for a chosen version. |
| app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt | Central refactor: patch selection → resolve version → resolve source → patcher params. |
| app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt | Updates patch selection VM to use new navigation params (package/version/selection). |
| app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt | Refactors storage selection event payload to (packageName, localPath). |
| app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt | Removes legacy “select app then constrain by suggested version” channel flow. |
| app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt | New UI for selecting Auto/Any/Specific versions with incompatibility info. |
| app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt | New UI for selecting Auto/Downloader/Installed/Local/Downloaded sources. |
| app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt | Replaces old source dialog with Version + Source page items and derived descriptions. |
| app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt | Adjusts patch checkbox behavior for the updated selection UX. |
| app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt | Updates list metadata display (available patch count, not-installed indicator). |
| app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt | Adds new routes/params for VersionSelector and SourceSelector; refactors SelectedAppInfo params. |
| app/src/main/java/app/revanced/manager/ui/model/SelectedVersion.kt | Adds SelectedVersion sealed model for Auto/Any/Specific. |
| app/src/main/java/app/revanced/manager/ui/model/SelectedSource.kt | Adds SelectedSource sealed model for Auto/Installed/Downloaded/Local/Plugin. |
| app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt | Removes legacy SelectedApp abstraction in favor of version+source separation. |
| app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt | Ensures app labels truncate cleanly (ellipsis). |
| app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt | Updates worker args and input selection logic to use SelectedSource + resolved version. |
| app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt | Updates patching step generation + worker args to use SelectedSource. |
| app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt | Fixes compatibility check to treat null version as compatible (package-level match). |
| app/src/main/java/app/revanced/manager/domain/repository/PatchSelectionRepository.kt | Uses the shared PatchSelection typealias return type. |
| app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt | Adds suggestedVersions(packageName, patchSelection) to drive patch-first version resolution. |
| app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt | Adds a flow getter for downloaded apps by package. |
| app/src/main/java/app/revanced/manager/data/room/apps/downloaded/DownloadedAppDao.kt | Adds query for downloaded apps by package. |
| app/src/main/java/app/revanced/manager/di/ViewModelModule.kt | Registers the new selector view models in DI. |
| app/src/main/java/app/revanced/manager/MainActivity.kt | Wires new screens/routes into navigation graph. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/screen/SourceSelectorScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/viewmodel/SourceSelectorViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
Outdated
Show resolved
Hide resolved
fd5c431 to
a20ebff
Compare
3911e95 to
b28e9a1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private val unscopedBundles = bundleRepository.scopedBundleInfoFlow(packageName, null) | ||
|
|
||
| val versionPatchSelection = combine(selectionFlow, unscopedBundles) { selection, bundleInfo -> | ||
| selection.patches(bundleInfo, allowIncompatible = true) |
There was a problem hiding this comment.
versionPatchSelection (and downstream patchSelection/customSelection) are computed with allowIncompatible = true unconditionally. This can cause the default selection (based on patch.include) and the patch count shown in the main screen to include version-incompatible patches even when the user has version compatibility checks enabled; later getPatcherParams() recomputes patches using prefs.disablePatchVersionCompatCheck, so the actual patched set may differ from what the UI shows. Consider threading the preference value into these flows (similar to PatchesSelectorViewModel’s allowIncompatiblePatches) so selection/counts stay consistent with what will actually be applied.
| selection.patches(bundleInfo, allowIncompatible = true) | |
| selection.patches( | |
| bundleInfo, | |
| allowIncompatible = prefs.disablePatchVersionCompatCheck | |
| ) |
| installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED | ||
| installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() -> | ||
| DisableReason.NO_ROOT | ||
| input.version != null && packageInfo.versionName != input.version -> DisableReason.VERSION_NOT_MATCHING |
There was a problem hiding this comment.
The installed-source option does not account for split APK installs. SelectedAppInfoViewModel.resolveAutoSource() explicitly avoids choosing Installed when splitSourceDirs is non-empty, but here Installed can still be selected manually, and PatcherWorker will only use applicationInfo.sourceDir (base APK). Consider disabling Installed in this case (and aligning isSourceValid accordingly), or adding a dedicated disable reason/message for split installs.
| installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED | |
| installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() -> | |
| DisableReason.NO_ROOT | |
| input.version != null && packageInfo.versionName != input.version -> DisableReason.VERSION_NOT_MATCHING | |
| packageInfo.applicationInfo.splitSourceDirs?.isNotEmpty() == true -> | |
| DisableReason.FAILED_TO_LOAD | |
| installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED | |
| installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() -> | |
| DisableReason.NO_ROOT | |
| input.version != null && packageInfo.versionName != input.version -> | |
| DisableReason.VERSION_NOT_MATCHING |
| SourceOption( | ||
| isSelected = viewModel.selectedSource == SelectedSource.Auto, | ||
| onSelect = { viewModel.selectSource(SelectedSource.Auto) }, | ||
| headlineContent = { Text(stringResource(R.string.version_selector_auto_title)) }, |
There was a problem hiding this comment.
The Auto option in the source selector uses version_selector_auto_title ("Auto (Recommended)") as its label. This appears version-specific and may be confusing on the source screen; consider using app_source_dialog_option_auto ("Auto") or introducing a dedicated source_selector_auto_title string so the terminology is consistent.
| headlineContent = { Text(stringResource(R.string.version_selector_auto_title)) }, | |
| headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) }, |
app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }, | ||
| headlineContent = headlineContent, | ||
| supportingContent = supportingContent, | ||
| trailingContent = overlineContent, |
There was a problem hiding this comment.
VersionOption passes overlineContent into ListItem as trailingContent, so the state label (Local/Downloaded/Installed) will render on the right instead of the overline slot. Use overlineContent = overlineContent (and keep trailingContent for any true trailing UI) to match the parameter naming and expected layout.
| trailingContent = overlineContent, | |
| overlineContent = overlineContent, |
| installedSource = SourceOption( | ||
| source = SelectedSource.Installed, | ||
| title = packageInfo.versionName.toString(), | ||
| key = input.packageName, | ||
| disableReason = when { | ||
| installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED | ||
| installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() -> | ||
| DisableReason.NO_ROOT | ||
|
|
||
| else -> versionMismatchReason(packageInfo.versionName) | ||
| } |
There was a problem hiding this comment.
installedSource uses packageInfo.versionName.toString(), which will display the literal string "null" if versionName is absent. Prefer a null-safe fallback (e.g., an empty string or a dedicated "Unknown version" label) and pass the nullable value through to versionMismatchReason consistently.
| val downloadedApps = downloadedAppRepository.get(input.packageName) | ||
| .map { apps -> | ||
| apps.sortedByDescending { it.version }.map { app -> | ||
| SourceOption( | ||
| source = SelectedSource.Downloaded( | ||
| path = downloadedAppRepository.getApkFileForApp(app).path, | ||
| version = app.version | ||
| ), | ||
| title = app.version, | ||
| key = app.version, | ||
| disableReason = versionMismatchReason(app.version) |
There was a problem hiding this comment.
Downloaded apps are sorted with sortedByDescending { it.version }, which is lexicographic and can misorder versions (e.g., "10.0" vs "9.9"). If versions are dot-separated numbers, consider sorting with a semantic/version-aware comparator to ensure the newest versions appear first.
| private fun selectedVersionDescription( | ||
| selectedVersion: SelectedVersion, | ||
| resolvedVersion: String?, | ||
| ): String { | ||
| val resolvedText = resolvedVersion ?: stringResource(R.string.selected_app_meta_any_version) | ||
| return if (selectedVersion is SelectedVersion.Auto) { | ||
| stringResource(R.string.selected_app_meta_auto, stringResource(R.string.app_source_dialog_option_auto), resolvedText) | ||
| } else { | ||
| resolvedText | ||
| } | ||
| } |
There was a problem hiding this comment.
selectedVersionDescription formats the Auto label using app_source_dialog_option_auto ("Auto" for sources) rather than the version-specific Auto label (version_selector_auto_title). This makes the Version row text inconsistent with the Version selector screen and can be confusing for translators/users; use a version-specific Auto label here.
| input.localPath?.let { local -> | ||
| pm.getPackageInfo(File(local))?.let { packageInfo -> | ||
| localApp = SourceOption( | ||
| source = SelectedSource.Local(local), | ||
| title = packageInfo.versionName.toString(), | ||
| key = "local", | ||
| disableReason = versionMismatchReason(packageInfo.versionName) | ||
| ) |
There was a problem hiding this comment.
localApp uses packageInfo.versionName.toString(), which can render as "null" if the APK has no versionName. Consider handling the null case explicitly (and keep the disable-reason comparison working with nullable version names).
| allVersions | ||
| .map { (version, supported) -> SelectedVersion.Specific(version) to patchCount - supported } | ||
| .sortedWith(compareBy<Pair<SelectedVersion.Specific, Int>> { it.second }.thenByDescending { it.first.version }) | ||
| } |
There was a problem hiding this comment.
availableVersions is sorted using thenByDescending { it.first.version }, which compares version strings lexicographically and can produce an incorrect ordering for numeric/dot-separated versions. Consider using a version-aware comparator so the list order matches user expectations for "newest" versions.
| when (selected) { | ||
| is SelectedVersion.Specific -> selected.version | ||
| is SelectedVersion.Any -> null | ||
| is SelectedVersion.Auto -> mostCompatible?.maxWithOrNull( | ||
| compareBy<Map.Entry<String, Int>> { it.value } | ||
| .thenBy { it.key } | ||
| )?.key | ||
| } |
There was a problem hiding this comment.
In Auto mode, resolvedVersion breaks ties with .thenBy { it.key }, which compares version names lexicographically. For typical numeric/dot-separated versions this can pick an unexpected "highest" version; consider using a version-aware comparator for the tiebreaker (or reuse the same ordering logic used elsewhere for suggested versions).
| SelectedSource.Installed -> { | ||
| val installedPackage = pm.getPackageInfo(packageName) ?: return false | ||
| val installedApp = installedAppRepository.get(packageName) | ||
|
|
||
| installedApp?.installType != InstallType.DEFAULT && | ||
| !(installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess()) && | ||
| (version == null || installedPackage.versionName == version) | ||
| } |
There was a problem hiding this comment.
resolveAutoSource avoids using the installed APK when the installed app is a split APK (splitSourceDirs not empty), but isSourceValid(SelectedSource.Installed, …) does not include the same check. This allows users to manually select an installed split APK as the patch source, which will likely fail later since patching uses applicationInfo.sourceDir (base APK only). Mirror the split-APK check in isSourceValid (and ideally also hide/disable the Installed option in SourceSelectorViewModel).
| pm.getPackageInfo(input.packageName)?.let { packageInfo -> | ||
| val installedApp = installedAppRepository.get(input.packageName) | ||
|
|
||
| installedSource = SourceOption( | ||
| source = SelectedSource.Installed, | ||
| title = packageInfo.versionName.toString(), | ||
| key = input.packageName, | ||
| disableReason = when { | ||
| installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED | ||
| installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() -> | ||
| DisableReason.NO_ROOT | ||
|
|
||
| else -> versionMismatchReason(packageInfo.versionName) | ||
| } | ||
| ) |
There was a problem hiding this comment.
The Installed source option is created without checking whether the installed app is a split APK (i.e., applicationInfo.splitSourceDirs is non-empty). Since patching later reads applicationInfo.sourceDir (base APK), selecting an installed split APK is likely to fail; consider hiding this option or disabling it with a clear reason when the installed app is split.
# Conflicts: # app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt # app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt
Currently, the app determines a default version at the start of the flow by calculating the most compatible version across all available patches. Patch selection is then restricted to this version. Overriding the version requires manually selecting an APK with a specific version, even though it's not clear to the user which versions are supported.
Instead, the flow should be patch-first. Users explicitly select the patches they want, and the manager automatically calculates the most compatible version based on that patch selection. The calculated version can still be manually overridden to a specific version. Once a version is chosen or calculated, the app automatically determines the best APK source corresponding to that version.
Motivation
Should close #2660, #2504, #2620, #2535