fix: comprehensive stability, memory, and installer improvements#3187
fix: comprehensive stability, memory, and installer improvements#3187Anajrim01 wants to merge 14 commits intoReVanced:devfrom
Conversation
Instead of throwing on invalid values, it silently fails and filters the invalid Long out as a null
- fix Koin crash by always passing SelectedApplicationInfo.ViewModelParams when resolving SelectedAppInfoViewModel in nested destinations - serialize patcher ProgressEvent handling through a single channel consumer to prevent out-of-order/concurrent step state updates - improve PatcherWorker resilience: - rethrow UserInteractionException instead of silently swallowing it - timeout downloader loading (30s) to avoid indefinite waits - fail fast if patched APK is missing/empty before signing - delete failed output artifacts after unsuccessful runs - stop silently swallowing setForeground failures on Android 14+ - remove GlobalScope usage in uiSafe and patcher cleanup paths - make patch selection/options writes more atomic and race-safe: - DAO-level get-or-create helpers with conflict-safe inserts - transactional selection import replacement - replace several versionName!! call sites with safe fallbacks to prevent NPEs on malformed APK metadata - replace magic download cache TTL literal with a named constant This reduces startup crashes, improves patching reliability, and hardens data integrity under concurrent updates. # Conflicts: # app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt
|
I've tried to verify that all functionality remains the same without breaking anything and have come across no negative behavior except patcher slowing down a bit when GC is overwhelmed (but this is intentional to prevent it from crashing from too much memory allocation). |
|
|
||
| @Insert | ||
| abstract suspend fun createSelection(selection: PatchSelection) | ||
| @Insert(onConflict = OnConflictStrategy.IGNORE) |
There was a problem hiding this comment.
Why would we be getting conflicts?
There was a problem hiding this comment.
createSelectionIfMissing is used by getOrCreateSelectionId, which follows SELECT -> INSERT -> SELECT flow rather than a single SQL statement. With the unique (patch_bundle, package_name) constraint, concurrent callers could both observe “missing”, then one insert succeeds while the other hits a conflict (and throws).
In the current impl. this is unlikely/rare, but OnConflictStrategy.IGNORE is more of a defensive, wherre it's possible that it may happen in the future so better to avoid unexpected exceptions and handle it appropriately.
Another benefit is, its clearer to future developer that calling createSelectionIfMissing will never throw due to duplicates (from racecondtions) rather only due to other errors.
There was a problem hiding this comment.
Currently, we only save selection when the user clicks the save button, so concurrency isn't a concern in this case.
| companion object { | ||
| private const val CACHE_TTL_MS = 6 * 60 * 60 * 1_000L | ||
| } |
There was a problem hiding this comment.
Most files in the codebase have companion objects at the bottom of the class file.
|
|
||
| companion object { | ||
| fun generateUid() = Random.Default.nextInt() | ||
| fun generateUid() = UUID.randomUUID().mostSignificantBits.toInt() |
There was a problem hiding this comment.
Is there a reason for this change? Just curious
There was a problem hiding this comment.
Now that I look at this, this serves no real benefit as converting to int32 just looses all the benefits of UUID uniqueness (when full) over Random.nextInt. Could be refactored to use full UUID's instead but this might break other things so skipping and reverting instead.
| withTimeout(30_000L) { | ||
| downloaderRepository.loadedDownloadersFlow.first() | ||
| } |
There was a problem hiding this comment.
This operation just retrieves the downloaders themselves. They are already loaded by the time the user starts patching and would have returned an empty list if they for some reason weren't. Why does this have a 30 minute timeout?
There was a problem hiding this comment.
Aha, I had assumed it did some "loading" or initializing of the downloaders here and therefore added a timeout of 30s (not 30 minutes). But if all it does is retrieve a list, then it's indeed redundant as it'll resolve immediately anyways.
There was a problem hiding this comment.
Yeah, the downloaders are downloaded at startup so its redundant.
|
|
||
| if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) { | ||
| GlobalScope.launch(Dispatchers.Main) { | ||
| cleanupScope.launch { |
There was a problem hiding this comment.
There isn't really a difference between this and just using GlobalScope, since it is not being cleaned up. Using GlobalScope is fine since the operation has a timeout anyway.
| packageName, | ||
| input.selectedApp.version ?: withContext(Dispatchers.IO) { | ||
| pm.getPackageInfo(outputFile)?.versionName!! | ||
| pm.getPackageInfo(outputFile)?.versionName ?: "unknown" |
There was a problem hiding this comment.
Use a string resource to make the string translatable.
| for (event in progressEventChannel) { | ||
| applyProgressEvent(event) | ||
| } |
| val parentBackStackEntry = navController.navGraphEntry(it) | ||
| val parentData = | ||
| parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>() | ||
| val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>( | ||
| viewModelStoreOwner = navController.navGraphEntry(it) | ||
| ) | ||
| viewModelStoreOwner = parentBackStackEntry | ||
| ) { parametersOf(parentData) } |
There was a problem hiding this comment.
Please extract this to a @Composable extension function. Remember to use it for SelectedApplicationInfo.Main as well.
This PR introduces a widespread set of stability and performance improvements across the application. It addresses critical crashes (including the Koin initialization failure), fixes memory exhaustion (OOM) and UI lag during heavy patching, properly supports multi-user root installations, and eliminates several data-race conditions.
Bug Fixes & Crash Resolutions
InvocationTargetExceptionoccurring on startup by properly resolving theparentBackStackEntryand passing its data.ChangelogSource.Managerfactory inUpdateViewModel..toLong()with.toLongOrNull()inBasePreferencesManagerto prevent crashes from malformed data.!!non-null assertions acrossversionNamechecks, gracefully falling back to"unknown".KeystoreManagerthat multiplied the validity duration by 24, resulting in incorrect expiry dates.Performance & Memory Improvements
Channelwith a10,000capacity and aBufferOverflow.DROP_OLDESTstrategy.Core, Concurrency, and Database
RandomtoUUID.randomUUID()inAppDatabaseto guarantee unique IDs.OptionDaoandSelectionDao'get-or-create' operations in@Transactionblocks withOnConflictStrategy.IGNOREto resolve duplicate entry exceptions.workerInputsinWorkerRepositoryto aConcurrentHashMap.GlobalScope.launchusage inPatcherViewModelandUtil.kt(uiSafe), replacing them with proper lifecycle-bound scopes and safeLooper.getMainLooper()handlers.REPLACEtoKEEPto prevent the accidental cancellation of actively running patcher/downloader jobs.Root & Installer Enhancements
--user 0. The application now correctly calculates theuserIddynamically viaProcess.myUid() / 100000.greppath matching for mounts by explicitly passing the-Fflag, ensuring file paths are evaluated as literal strings rather than regex.patchedApkis deleted if the patching process fails.Notes for Reviewers
Changelog Fix: HardcodingChangelogSource.Manageracts as a highly effective workaround for the prerelease parsing crash, bypassing the failing source argument entirely.Closes / Fixes
InvocationTargetException, upon invocation. #3153