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 f8472a1..1f06c57 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 @@ -26,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) @@ -56,13 +55,14 @@ class LocalInstaller( ) } - override suspend fun install(apk: Apk) { - logger.info("Installing ${apk.file.name}") + override suspend fun install(options: InstallerOptions) { + val patchedApk = options.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/command/AdbShellCommandRunner.kt b/library/src/commonMain/kotlin/app/revanced/library/installation/command/AdbShellCommandRunner.kt index 19e9c83..28272fa 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 } } } 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 c5b0183..25f025a 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 = this } 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 f781f04..24b19f6 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,11 +30,13 @@ class AdbInstaller( logger.fine("Connected to $deviceSerial") } - override suspend fun install(apk: Apk): AdbInstallerResult { + override suspend fun install(options: InstallerOptions): AdbInstallerResult { + val patchedApk = options.apk + 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 dc2d020..11c8071 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,12 @@ 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 $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\" && " + 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 da709ea..b036f32 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 @@ -10,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]. */ @@ -19,11 +18,11 @@ abstract class Installer interna /** * Installs the [Apk] file. * - * @param apk The [Apk] file. + * @param options The [InstallerOptions]. * * @return The result of the installation. */ - abstract suspend fun install(apk: Apk): TInstallerResult + abstract suspend fun install(options: TInstallerOptions): TInstallerResult /** * Uninstalls the package. @@ -43,11 +42,5 @@ abstract class Installer interna */ abstract suspend fun getInstallation(packageName: String): TInstallation? - /** - * Apk file for [Installer]. - * - * @param file The [Apk] file. - * @param packageName The package name of the [Apk] file. - */ - class Apk(val file: File, val packageName: String? = null) + open class Apk(val file: File) } 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 0000000..0ff7131 --- /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 InstallerOptions( + val apk: Installer.Apk +) \ 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 27992f6..e48c594 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,8 +4,11 @@ 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.GET_INSTALLED_VERSION_NAME 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 @@ -16,8 +19,6 @@ 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.invoke -import app.revanced.library.installation.installer.Installer.Apk -import app.revanced.library.installation.installer.RootInstaller.NoRootPermissionException import java.io.File /** @@ -27,15 +28,14 @@ import java.io.File * * @throws NoRootPermissionException If the device does not have root permission. */ -@Suppress("MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate", "unused") abstract class RootInstaller internal constructor( shellCommandRunnerSupplier: (RootInstaller) -> ShellCommandRunner, -) : Installer() { +) : Installer() { /** * The command runner used to run commands on the device. */ - @Suppress("LeakingThis") protected val shellCommandRunner = shellCommandRunnerSupplier(this) init { @@ -43,19 +43,46 @@ abstract class RootInstaller internal constructor( } /** - * Installs the given [apk] by mounting. + * Installs the given patched APK by mounting it over a regular installation of the stock APK. * - * @param apk The [Apk] to install. + * 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. * - * @throws PackageNameRequiredException If the [Apk] does not have a package name. + * @param options The installer options containing the patched APK to mount and the stock APK + * used as the installation base. + * + * @throws PackageVersionMismatchException If the installed app version does not match the + * expected stock APK version. */ - override suspend fun install(apk: Apk): RootInstallerResult { - logger.info("Installing ${apk.packageName} by mounting") + override suspend fun install(options: RootInstallerOptions): RootInstallerResult { + val stockApk = options.stockApk + val packageName = stockApk.packageName + logger.info("Installing $packageName by mounting") + + // Ensure the installed base app matches the stock APK version. + val installedVersionName = try { + getInstalledVersionName(packageName) + } catch (_: PackageNotInstalledException) { + null + } - val packageName = apk.packageName?.also { it.assertInstalled() } ?: throw PackageNameRequiredException() + 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. - apk.file.move(TMP_FILE_PATH) + options.stockApk.file.move(TMP_FILE_PATH) CREATE_INSTALLATION_PATH().waitFor() MOUNT_APK(packageName)().waitFor() @@ -88,7 +115,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 }, @@ -97,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. */ @@ -114,21 +157,26 @@ 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. * - * @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) } } - internal class FailedToFindInstalledPackageException internal constructor(packageName: String) : Exception("Failed to resolve installed APK path for package \"$packageName\"") + internal class PackageVersionMismatchException internal constructor(packageName: String) : + Exception("Package $packageName does not match the expected version") + + internal class PackageNotInstalledException internal constructor(packageName: String) : + Exception("Package $packageName is not installed") - 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("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 new file mode 100644 index 0000000..cb353ef --- /dev/null +++ b/library/src/commonMain/kotlin/app/revanced/library/installation/installer/RootInstallerOptions.kt @@ -0,0 +1,10 @@ +package app.revanced.library.installation.installer + +import java.io.File + +class RootInstallerOptions( + patchedApk: Installer.Apk, + val stockApk: Apk, +) : InstallerOptions(patchedApk) + +class Apk(file: File, val packageName: String, val versionName: String) : Installer.Apk(file) \ No newline at end of file