feat: Improve root installation (#2895)

Co-authored-by: Ushie <ushiekane@gmail.com>
This commit is contained in:
Robert
2026-03-10 02:01:49 +01:00
committed by GitHub
parent c59b3e4377
commit 763483b65f
2 changed files with 75 additions and 44 deletions

View File

@@ -54,7 +54,11 @@ class RootInstaller(
await()
}
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
suspend fun execute(vararg commands: String): Shell.Result {
val stdout = mutableListOf<String>()
val stderr = mutableListOf<String>()
return getShell().newJob().add(*commands).to(stdout, stderr).exec()
}
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
@@ -108,20 +112,18 @@ class RootInstaller(
unmount(packageName)
stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo ->
// TODO: get user id programmatically
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
pm.getPackageInfo(patchedAPK)
?: error("Failed to get package info for patched app")
)
)
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
}
// TODO: get user id programmatically
execute("pm uninstall -k --user 0 $packageName")
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"")
.assertSuccess("Failed to install stock app")
}
remoteFS.getFile(modulePath).mkdirs()
remoteFS.getFile(modulePath).apply {
if (!mkdirs() && !exists()) {
throw Exception("Failed to create module directory")
}
}
listOf(
"service.sh",
@@ -142,7 +144,6 @@ class RootInstaller(
}
"$modulePath/$packageName.apk".let { apkPath ->
remoteFS.getFile(patchedAPK.absolutePath)
.also { if (!it.exists()) throw Exception("File doesn't exist") }
.newInputStream().use { inputStream ->
@@ -173,9 +174,48 @@ class RootInstaller(
const val modulesPath = "/data/adb/modules"
private fun Shell.Result.assertSuccess(errorMessage: String) {
if (!isSuccess) throw Exception(errorMessage)
if (!isSuccess) {
throw ShellCommandException(
errorMessage,
code,
out,
err
)
}
}
}
}
class ShellCommandException(
val userMessage: String,
val exitCode: Int,
val stdout: List<String>,
val stderr: List<String>
) : Exception(format(userMessage, exitCode, stdout, stderr)) {
companion object {
private fun format(
message: String,
exitCode: Int,
stdout: List<String>,
stderr: List<String>
): String =
buildString {
appendLine(message)
appendLine("Exit code: $exitCode")
val output = stdout.filter { it.isNotBlank() }
val errors = stderr.filter { it.isNotBlank() }
if (output.isNotEmpty()) {
appendLine("stdout:")
output.forEach(::appendLine)
}
if (errors.isNotEmpty()) {
appendLine("stderr:")
errors.forEach(::appendLine)
}
}
}
}
class RootServiceException : Exception("Root not available")

View File

@@ -3,7 +3,6 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller as AndroidPackageInstaller
import android.net.Uri
import android.os.ParcelUuid
import android.util.Log
@@ -30,18 +29,18 @@ import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.downloader.DownloaderHostApi
import app.revanced.manager.downloader.UserInteractionException
import app.revanced.manager.ui.model.InstallerModel
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.navigation.Patcher
import app.revanced.manager.ui.model.withState
import app.revanced.manager.util.PM
@@ -80,6 +79,7 @@ import ru.solrudev.ackpine.uninstaller.UninstallFailure
import java.io.File
import java.nio.file.Files
import java.time.Duration
import android.content.pm.PackageInstaller as AndroidPackageInstaller
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
class PatcherViewModel(
@@ -404,24 +404,26 @@ class PatcherViewModel(
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
?: throw Exception("Failed to load application info")
// If the app is currently installed
val existingPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
existingPackageInfo
)
) {
// Exit if the selected app version is less than the installed version
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
when (installType) {
InstallType.DEFAULT -> {
// If the app is currently installed
val existingPackageInfo =
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
if (existingPackageInfo != null) {
// Check if the app version is less than the installed version
if (
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
existingPackageInfo
)
) {
// Exit if the selected app version is less than the installed version
packageInstallerStatus =
AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
return@launch
}
}
// Check if the app is mounted as root
// If it is, unmount it first, silently
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
@@ -437,17 +439,6 @@ class PatcherViewModel(
currentPackageInfo.label()
}
// Check for base APK, first check if the app is already installed
if (existingPackageInfo == null) {
// If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package
packageInstallerStatus =
AndroidPackageInstaller.STATUS_FAILURE_INVALID
return@launch
}
}
val inputVersion = input.selectedApp.version
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
?: throw Exception("Failed to determine input APK version")