From f8da7ee25085a5486f06e6016cbc3ac2eb103002 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sun, 5 Apr 2026 22:08:00 +0300 Subject: [PATCH 1/6] feat: Ensure stock APK is installed in RootInstaller --- .../installation/installer/LocalInstaller.kt | 7 +++--- .../installation/installer/AdbInstaller.kt | 6 ++--- .../installation/installer/Constants.kt | 2 ++ .../installation/installer/Installer.kt | 5 ++-- .../installation/installer/RootInstaller.kt | 23 ++++++++++++------- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt index f8472a18..2c6c9449 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt @@ -10,7 +10,6 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.os.Build import androidx.core.content.ContextCompat -import app.revanced.library.installation.installer.Installer.Apk import java.io.Closeable import java.io.File @@ -56,13 +55,13 @@ class LocalInstaller( ) } - override suspend fun install(apk: Apk) { - logger.info("Installing ${apk.file.name}") + override suspend fun install(patchedApk: Apk, stockApk: Apk?) { + logger.info("Installing ${patchedApk.file.name}") val packageInstaller = context.packageManager.packageInstaller packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> - session.writeApk(apk.file) + session.writeApk(patchedApk.file) session.commit(intentSender) } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt index f781f046..bc8626b9 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt @@ -30,11 +30,11 @@ class AdbInstaller( logger.fine("Connected to $deviceSerial") } - override suspend fun install(apk: Apk): AdbInstallerResult { + override suspend fun install(patchedApk: Apk, stockApk: Apk?): AdbInstallerResult { return runPackageManager { val sdkVersion = shellCommandRunner(GET_SDK_VERSION).output.toInt() - if (sdkVersion < 34) install(apk.file) - else installWithOptions(apk.file, listOf(UPDATE_OWNERSHIP)) + if (sdkVersion < 34) install(patchedApk.file) + else installWithOptions(patchedApk.file, listOf(UPDATE_OWNERSHIP)) } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index dc2d0202..ef7b254d 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -19,6 +19,8 @@ internal object Constants { const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER" const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH" const val GET_SDK_VERSION = "getprop ro.build.version.sdk" + const val UNINSTALL_KEEP_DATA = "pm uninstall -k --user 0 $PLACEHOLDER" + const val INSTALL_STOCK_APK = "pm install -r -d --user 0 $PLACEHOLDER" const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index da709ea9..19c31a4f 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -1,6 +1,5 @@ package app.revanced.library.installation.installer -import app.revanced.library.installation.installer.Installer.Apk import java.io.File import java.util.logging.Logger @@ -19,11 +18,11 @@ abstract class Installer interna /** * Installs the [Apk] file. * - * @param apk The [Apk] file. + * @param patchedApk The [Apk] file. * * @return The result of the installation. */ - abstract suspend fun install(apk: Apk): TInstallerResult + abstract suspend fun install(patchedApk: Apk, stockApk: Apk? = null): TInstallerResult /** * Uninstalls the package. diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 27992f64..3ec58178 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -6,6 +6,7 @@ import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT +import app.revanced.library.installation.installer.Constants.INSTALL_STOCK_APK import app.revanced.library.installation.installer.Constants.KILL import app.revanced.library.installation.installer.Constants.MOUNTED_APK_PATH import app.revanced.library.installation.installer.Constants.MOUNT_APK @@ -15,9 +16,8 @@ import app.revanced.library.installation.installer.Constants.MOUNT_SCRIPT_PATH import app.revanced.library.installation.installer.Constants.RESTART import app.revanced.library.installation.installer.Constants.TMP_FILE_PATH import app.revanced.library.installation.installer.Constants.UMOUNT +import app.revanced.library.installation.installer.Constants.UNINSTALL_KEEP_DATA import app.revanced.library.installation.installer.Constants.invoke -import app.revanced.library.installation.installer.Installer.Apk -import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException import java.io.File /** @@ -43,19 +43,26 @@ abstract class RootInstaller internal constructor( } /** - * Installs the given [apk] by mounting. + * Installs the given [patchedApk] by mounting. * - * @param apk The [Apk] to install. + * @param patchedApk The [Apk] to install. * * @throws PackageNameRequiredException If the [Apk] does not have a package name. */ - override suspend fun install(apk: Apk): RootInstallerResult { - logger.info("Installing ${apk.packageName} by mounting") + override suspend fun install(patchedApk: Apk, stockApk: Apk?): RootInstallerResult { + logger.info("Installing ${patchedApk.packageName} by mounting") - val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() + val packageName = patchedApk.packageName ?: throw PackageNameRequiredException() + + // Ensure stock APK is installed + if (stockApk != null) { + UNINSTALL_KEEP_DATA(packageName)().waitFor() + logger.info("Installing stock APK for $packageName") + INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor() + } else packageName.assertInstalled() // Setup files. - apk.file.move(TMP_FILE_PATH) + patchedApk.file.move(TMP_FILE_PATH) CREATE_INSTALLATION_PATH().waitFor() MOUNT_APK(packageName)().waitFor() From 05c710647b7572f4481dde53bd1c62badb64ba35 Mon Sep 17 00:00:00 2001 From: Ushie Date: Mon, 6 Apr 2026 14:47:40 +0300 Subject: [PATCH 2/6] feat: Cleanup and avoid unnecessary uninstallation or update --- .../installation/installer/LocalInstaller.kt | 5 +- .../library/installation/command/RunResult.kt | 2 +- .../installation/installer/AdbInstaller.kt | 6 ++- .../installation/installer/Constants.kt | 3 +- .../installation/installer/Installer.kt | 18 +++++-- .../installer/InstallerOptions.kt | 5 ++ .../installation/installer/RootInstaller.kt | 52 +++++++++++++------ .../installer/RootInstallerOptions.kt | 6 +++ 8 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 library/src/commonMain/kotlin/app/revanced/library/installation/installer/InstallerOptions.kt create mode 100644 library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt index 2c6c9449..b201f161 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt @@ -25,7 +25,7 @@ import java.io.File class LocalInstaller( private val context: Context, onResult: (result: LocalInstallerResult) -> Unit, -) : Installer(), Closeable { +) : Installer(), Closeable { private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val pmStatus = intent.getIntExtra(LocalInstallerService.EXTRA_STATUS, -999) @@ -55,7 +55,8 @@ class LocalInstaller( ) } - override suspend fun install(patchedApk: Apk, stockApk: Apk?) { + override suspend fun install(options: InstallOptions) { + val patchedApk = options.patchedApk logger.info("Installing ${patchedApk.file.name}") val packageInstaller = context.packageManager.packageInstaller diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt index c5b01832..4a81f63a 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt @@ -22,5 +22,5 @@ interface RunResult { /** * Waits for the command to finish. */ - fun waitFor() {} + fun waitFor(): RunResult } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt index bc8626b9..d7cdee8b 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt @@ -18,7 +18,7 @@ import se.vidstige.jadb.managers.PackageManager.UPDATE_OWNERSHIP */ class AdbInstaller( deviceSerial: String? = null, -) : Installer() { +) : Installer() { private val shellCommandRunner: ShellCommandRunner private val packageManager: PackageManager @@ -30,7 +30,9 @@ class AdbInstaller( logger.fine("Connected to $deviceSerial") } - override suspend fun install(patchedApk: Apk, stockApk: Apk?): AdbInstallerResult { + override suspend fun install(options: InstallOptions): AdbInstallerResult { + val patchedApk = options.patchedApk + return runPackageManager { val sdkVersion = shellCommandRunner(GET_SDK_VERSION).output.toInt() if (sdkVersion < 34) install(patchedApk.file) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index ef7b254d..b5e951e2 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -21,7 +21,8 @@ internal object Constants { const val GET_SDK_VERSION = "getprop ro.build.version.sdk" const val UNINSTALL_KEEP_DATA = "pm uninstall -k --user 0 $PLACEHOLDER" const val INSTALL_STOCK_APK = "pm install -r -d --user 0 $PLACEHOLDER" - + const val GET_INSTALLED_VERSION_CODE = + "dumpsys package $PLACEHOLDER | sed -n 's/.*versionCode=\\([0-9]*\\).*/\\1/p' | head -n 1" const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + $$"mv $$TMP_FILE_PATH $base_path && " + diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index 19c31a4f..63aa18e8 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -9,7 +9,7 @@ import java.util.logging.Logger * @param TInstallerResult The type of the result of the installation. * @param TInstallation The type of the installation. */ -abstract class Installer internal constructor() { +abstract class Installer internal constructor() { /** * The [Logger]. */ @@ -22,7 +22,7 @@ abstract class Installer interna * * @return The result of the installation. */ - abstract suspend fun install(patchedApk: Apk, stockApk: Apk? = null): TInstallerResult + abstract suspend fun install(options: TInstallOptions): TInstallerResult /** * Uninstalls the package. @@ -48,5 +48,17 @@ abstract class Installer interna * @param file The [Apk] file. * @param packageName The package name of the [Apk] file. */ - class Apk(val file: File, val packageName: String? = null) + sealed class Apk { + abstract val file: File + + class Patched( + override val file: File + ) : Apk() + + class Stock( + override val file: File, + val packageName: String, + val versionCode: Int + ) : Apk() + } } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/InstallerOptions.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/InstallerOptions.kt new file mode 100644 index 00000000..582537da --- /dev/null +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/InstallerOptions.kt @@ -0,0 +1,5 @@ +package app.revanced.library.installation.installer + +open class InstallOptions( + val patchedApk: Installer.Apk.Patched +) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt index 3ec58178..20f9fd21 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstaller.kt @@ -4,6 +4,7 @@ import app.revanced.library.installation.command.ShellCommandRunner import app.revanced.library.installation.installer.Constants.CREATE_INSTALLATION_PATH import app.revanced.library.installation.installer.Constants.DELETE import app.revanced.library.installation.installer.Constants.EXISTS +import app.revanced.library.installation.installer.Constants.GET_INSTALLED_VERSION_CODE import app.revanced.library.installation.installer.Constants.INSTALLED_APK_PATH import app.revanced.library.installation.installer.Constants.INSTALL_MOUNT_SCRIPT import app.revanced.library.installation.installer.Constants.INSTALL_STOCK_APK @@ -30,7 +31,7 @@ import java.io.File @Suppress("MemberVisibilityCanBePrivate") abstract class RootInstaller internal constructor( shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, -) : Installer() { +) : Installer() { /** * The command runner used to run commands on the device. @@ -43,23 +44,34 @@ abstract class RootInstaller internal constructor( } /** - * Installs the given [patchedApk] by mounting. + * Installs the given APK by mounting it over a stock installation. * - * @param patchedApk The [Apk] to install. + * The stock APK is used to ensure a valid base for mounting, the app will be reinstalled if necessary. + * + * @param options The install options containing: + * - the patched APK to mount + * - the stock APK to install beforehand if necessary * - * @throws PackageNameRequiredException If the [Apk] does not have a package name. */ - override suspend fun install(patchedApk: Apk, stockApk: Apk?): RootInstallerResult { - logger.info("Installing ${patchedApk.packageName} by mounting") + override suspend fun install(options: RootInstallOptions): RootInstallerResult { + val patchedApk = options.patchedApk + val stockApk = options.stockApk + val packageName = stockApk.packageName + val expectedVersionCode = stockApk.versionCode + logger.info("Installing $packageName by mounting") + + // Ensure the installed base app matches the stock APK version. + val installedVersionCode = getInstalledVersionCode(packageName) - val packageName = patchedApk.packageName ?: throw PackageNameRequiredException() + if (installedVersionCode != expectedVersionCode) { + if (installedVersionCode != null && installedVersionCode > expectedVersionCode) { + UNINSTALL_KEEP_DATA(packageName)().waitFor() + } - // Ensure stock APK is installed - if (stockApk != null) { - UNINSTALL_KEEP_DATA(packageName)().waitFor() logger.info("Installing stock APK for $packageName") INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor() - } else packageName.assertInstalled() + } + packageName.assertInstalled() // Setup files. patchedApk.file.move(TMP_FILE_PATH) @@ -121,7 +133,8 @@ abstract class RootInstaller internal constructor( * * @param content The content of the file. */ - protected fun String.write(content: String) = shellCommandRunner.write(content.byteInputStream(), this) + protected fun String.write(content: String) = + shellCommandRunner.write(content.byteInputStream(), this) /** * Asserts that the package is installed. @@ -134,8 +147,17 @@ abstract class RootInstaller internal constructor( } } - internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : Exception("Failed to resolve installed APK path for package \"$packageName\"") + private fun getInstalledVersionCode(packageName: String): Int? { + val result = GET_INSTALLED_VERSION_CODE(packageName)().waitFor() + + if (result.exitCode != 0) return null + + return result.output.trim().toIntOrNull() + } + + internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : + Exception("Failed to resolve installed APK path for package \"$packageName\"") - internal class PackageNameRequiredException internal constructor() : Exception("Package name is required") - internal class NoRootPermissionException internal constructor() : Exception("No root permission") + internal class NoRootPermissionException internal constructor() : + Exception("No root permission") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt new file mode 100644 index 00000000..37ebbfdc --- /dev/null +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt @@ -0,0 +1,6 @@ +package app.revanced.library.installation.installer + +class RootInstallOptions( + patchedApk: Installer.Apk.Patched, + val stockApk: Installer.Apk.Stock +) : InstallOptions(patchedApk) \ No newline at end of file From 91332a32dc66e1c7733d6d2dbfbd67754bfb9bac Mon Sep 17 00:00:00 2001 From: Ushie Date: Mon, 6 Apr 2026 18:14:15 +0300 Subject: [PATCH 3/6] feat: Throw on downgrade required --- .../installation/installer/Installer.kt | 22 ++-------------- .../installer/InstallerOptions.kt | 2 +- .../installation/installer/RootInstaller.kt | 26 ++++++++++++------- .../installer/RootInstallerOptions.kt | 10 ++++--- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index 63aa18e8..6be59725 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -18,7 +18,7 @@ abstract class Installer expectedVersionCode) { - UNINSTALL_KEEP_DATA(packageName)().waitFor() - } + if (installedVersionCode != null && installedVersionCode > expectedVersionCode) { + throw PackageDowngradeRequiredException(packageName) + } + if (installedVersionCode != expectedVersionCode) { logger.info("Installing stock APK for $packageName") INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor() } @@ -107,7 +110,7 @@ abstract class RootInstaller internal constructor( val patchedApkPath = MOUNTED_APK_PATH(packageName) val patchedApkExists = EXISTS(patchedApkPath)().exitCode == 0 - if (patchedApkExists) return null + if (!patchedApkExists) return null return RootInstallation( INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, @@ -155,6 +158,9 @@ abstract class RootInstaller internal constructor( return result.output.trim().toIntOrNull() } + internal class PackageDowngradeRequiredException internal constructor(packageName: String) : + Exception("$packageName requires reinstallation for a downgrade, data wipe is likely") + internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : Exception("Failed to resolve installed APK path for package \"$packageName\"") diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt index 37ebbfdc..c887cd00 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt @@ -1,6 +1,10 @@ package app.revanced.library.installation.installer +import java.io.File + class RootInstallOptions( - patchedApk: Installer.Apk.Patched, - val stockApk: Installer.Apk.Stock -) : InstallOptions(patchedApk) \ No newline at end of file + patchedApk: Installer.Apk, + val stockApk: StockApk +) : InstallOptions(patchedApk) + +class StockApk(file: File, val packageName: String, val versionCode: Int) : Installer.Apk(file) \ No newline at end of file From fa8cbfa73dc30052a2bf0a7f14a8b83018252114 Mon Sep 17 00:00:00 2001 From: Ushie Date: Mon, 6 Apr 2026 18:18:56 +0300 Subject: [PATCH 4/6] fix waitfor --- .../library/installation/command/AdbShellCommandRunner.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt index 19e9c83e..28272fa9 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt @@ -38,8 +38,9 @@ class AdbShellCommandRunner : ShellCommandRunner { override val output by lazy { process.inputStream.bufferedReader().readText().removeSuffix("\n") } override val error by lazy { process.errorStream.bufferedReader().readText() } - override fun waitFor() { + override fun waitFor(): RunResult { process.waitFor() + return this } } } From 0b071c3c839cf8bdb45f6efddcf7549574955daa Mon Sep 17 00:00:00 2001 From: Ushie Date: Mon, 6 Apr 2026 18:22:55 +0300 Subject: [PATCH 5/6] make waitFor return this --- .../app/revanced/library/installation/command/RunResult.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt index 4a81f63a..25f025a6 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/command/RunResult.kt @@ -22,5 +22,5 @@ interface RunResult { /** * Waits for the command to finish. */ - fun waitFor(): RunResult + fun waitFor(): RunResult = this } From 174d7c2cd919e2f5eb24d6a3bbd5fdda7c948d48 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sat, 11 Apr 2026 00:02:28 +0300 Subject: [PATCH 6/6] reviews --- .../installation/installer/LocalInstaller.kt | 6 +- .../installation/installer/AdbInstaller.kt | 6 +- .../installation/installer/Constants.kt | 5 +- .../installation/installer/Installer.kt | 6 +- .../installer/InstallerOptions.kt | 4 +- .../installation/installer/RootInstaller.kt | 89 +++++++++++-------- .../installer/RootInstallerOptions.kt | 8 +- 7 files changed, 70 insertions(+), 54 deletions(-) diff --git a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt index b201f161..1f06c579 100644 --- a/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt +++ b/library/src/androidMain/kotlin/app/revanced/library/installation/installer/LocalInstaller.kt @@ -25,7 +25,7 @@ import java.io.File class LocalInstaller( private val context: Context, onResult: (result: LocalInstallerResult) -> Unit, -) : Installer(), Closeable { +) : Installer(), Closeable { private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val pmStatus = intent.getIntExtra(LocalInstallerService.EXTRA_STATUS, -999) @@ -55,8 +55,8 @@ class LocalInstaller( ) } - override suspend fun install(options: InstallOptions) { - val patchedApk = options.patchedApk + override suspend fun install(options: InstallerOptions) { + val patchedApk = options.apk logger.info("Installing ${patchedApk.file.name}") val packageInstaller = context.packageManager.packageInstaller diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt index d7cdee8b..24b19f60 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/AdbInstaller.kt @@ -18,7 +18,7 @@ import se.vidstige.jadb.managers.PackageManager.UPDATE_OWNERSHIP */ class AdbInstaller( deviceSerial: String? = null, -) : Installer() { +) : Installer() { private val shellCommandRunner: ShellCommandRunner private val packageManager: PackageManager @@ -30,8 +30,8 @@ class AdbInstaller( logger.fine("Connected to $deviceSerial") } - override suspend fun install(options: InstallOptions): AdbInstallerResult { - val patchedApk = options.patchedApk + override suspend fun install(options: InstallerOptions): AdbInstallerResult { + val patchedApk = options.apk return runPackageManager { val sdkVersion = shellCommandRunner(GET_SDK_VERSION).output.toInt() diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt index b5e951e2..11c80716 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Constants.kt @@ -19,10 +19,13 @@ internal object Constants { const val INSTALLED_APK_PATH = "pm path $PLACEHOLDER" const val CREATE_INSTALLATION_PATH = "$CREATE_DIR $MOUNT_PATH" const val GET_SDK_VERSION = "getprop ro.build.version.sdk" - const val UNINSTALL_KEEP_DATA = "pm uninstall -k --user 0 $PLACEHOLDER" + const val UNINSTALL_KEEP_DATA = "pm uninstall -k $PLACEHOLDER" const val INSTALL_STOCK_APK = "pm install -r -d --user 0 $PLACEHOLDER" const val GET_INSTALLED_VERSION_CODE = "dumpsys package $PLACEHOLDER | sed -n 's/.*versionCode=\\([0-9]*\\).*/\\1/p' | head -n 1" + const val GET_INSTALLED_VERSION_NAME = + "dumpsys package $PLACEHOLDER | sed -n 's/.*versionName=//p' | head -n 1" + const val MOUNT_APK = "base_path=\"$MOUNTED_APK_PATH\" && " + $$"mv $$TMP_FILE_PATH $base_path && " + diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt index 6be59725..b036f326 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/Installer.kt @@ -9,7 +9,7 @@ import java.util.logging.Logger * @param TInstallerResult The type of the result of the installation. * @param TInstallation The type of the installation. */ -abstract class Installer internal constructor() { +abstract class Installer internal constructor() { /** * The [Logger]. */ @@ -18,11 +18,11 @@ abstract class Installer ShellCommandRunner, -) : Installer() { +) : Installer() { /** * The command runner used to run commands on the device. */ - @Suppress("LeakingThis") protected val shellCommandRunner = shellCommandRunnerSupplier(this) init { @@ -43,41 +43,46 @@ abstract class RootInstaller internal constructor( } /** - * Installs the given APK by mounting it over a stock installation. + * Installs the given patched APK by mounting it over a regular installation of the stock APK. * - * The stock APK is used to ensure a valid base for mounting. If the currently - * installed version differs from the expected stock version, the stock APK is - * installed first. If the installed version is newer than the expected version, - * reinstallation may be required. + * The stock APK is used to ensure a valid base installation for mounting. If the app is not + * currently installed, the stock APK is installed first. If the installed app version does not + * match the expected stock APK version, installation is aborted. * - * @param options The install options containing the patched APK to mount and the - * stock APK to use as the installation base. + * @param options The installer options containing the patched APK to mount and the stock APK + * used as the installation base. * - * @throws PackageDowngradeRequiredException If the installed app version is - * newer than the expected stock version. + * @throws PackageVersionMismatchException If the installed app version does not match the + * expected stock APK version. */ - override suspend fun install(options: RootInstallOptions): RootInstallerResult { - val patchedApk = options.patchedApk + override suspend fun install(options: RootInstallerOptions): RootInstallerResult { val stockApk = options.stockApk val packageName = stockApk.packageName - val expectedVersionCode = stockApk.versionCode logger.info("Installing $packageName by mounting") // Ensure the installed base app matches the stock APK version. - val installedVersionCode = getInstalledVersionCode(packageName) - - if (installedVersionCode != null && installedVersionCode > expectedVersionCode) { - throw PackageDowngradeRequiredException(packageName) + val installedVersionName = try { + getInstalledVersionName(packageName) + } catch (_: PackageNotInstalledException) { + null } - if (installedVersionCode != expectedVersionCode) { - logger.info("Installing stock APK for $packageName") - INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor() + when { + installedVersionName == null -> { + logger.info("Installing stock APK for $packageName") + INSTALL_STOCK_APK(stockApk.file.absolutePath)().waitFor() + packageName.assertInstalled() + } + + installedVersionName != stockApk.versionName -> { + throw PackageVersionMismatchException(packageName) + } } + packageName.assertInstalled() // Setup files. - patchedApk.file.move(TMP_FILE_PATH) + options.stockApk.file.move(TMP_FILE_PATH) CREATE_INSTALLATION_PATH().waitFor() MOUNT_APK(packageName)().waitFor() @@ -119,6 +124,22 @@ abstract class RootInstaller internal constructor( ) } + fun getInstalledVersionName(packageName: String): String = + GET_INSTALLED_VERSION_NAME(packageName)() + .waitFor() + .output + .trim() + .takeIf { it.isNotEmpty() } + ?: throw PackageNotInstalledException(packageName) + + fun getInstalledVersionCode(packageName: String): Int = + GET_INSTALLED_VERSION_CODE(packageName)() + .waitFor() + .output + .trim() + .toIntOrNull() + ?: throw PackageNotInstalledException(packageName) + /** * Runs a command on the device. */ @@ -142,28 +163,20 @@ abstract class RootInstaller internal constructor( /** * Asserts that the package is installed. * - * @throws FailedToFindInstalledPackageException If the package is not installed. + * @throws PackageNotInstalledException If the package is not installed. */ private fun String.assertInstalled() { if (INSTALLED_APK_PATH(this)().output.isEmpty()) { - throw FailedToFindInstalledPackageException(this) + throw PackageNotInstalledException(this) } } - private fun getInstalledVersionCode(packageName: String): Int? { - val result = GET_INSTALLED_VERSION_CODE(packageName)().waitFor() - - if (result.exitCode != 0) return null - - return result.output.trim().toIntOrNull() - } - - internal class PackageDowngradeRequiredException internal constructor(packageName: String) : - Exception("$packageName requires reinstallation for a downgrade, data wipe is likely") + internal class PackageVersionMismatchException internal constructor(packageName: String) : + Exception("Package $packageName does not match the expected version") - internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : - Exception("Failed to resolve installed APK path for package \"$packageName\"") + internal class PackageNotInstalledException internal constructor(packageName: String) : + Exception("Package $packageName is not installed") internal class NoRootPermissionException internal constructor() : - Exception("No root permission") + Exception("Root permission is not granted") } diff --git a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt index c887cd00..cb353ef7 100644 --- a/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt @@ -2,9 +2,9 @@ package app.revanced.library.installation.installer import java.io.File -class RootInstallOptions( +class RootInstallerOptions( patchedApk: Installer.Apk, - val stockApk: StockApk -) : InstallOptions(patchedApk) + val stockApk: Apk, +) : InstallerOptions(patchedApk) -class StockApk(file: File, val packageName: String, val versionCode: Int) : Installer.Apk(file) \ No newline at end of file +class Apk(file: File, val packageName: String, val versionName: String) : Installer.Apk(file) \ No newline at end of file