-
-
Notifications
You must be signed in to change notification settings - Fork 27
feat: Ensure installation of stock APK during root installation #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
f8da7ee
05c7106
91332a3
fa8cbfa
0b071c3
174d7c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package app.revanced.library.installation.installer | ||
|
|
||
| open class InstallerOptions( | ||
| val apk: Installer.Apk | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,35 +28,61 @@ 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<RootInstallerResult, RootInstallation>() { | ||
| ) : Installer<RootInstallerResult, RootInstallation, RootInstallerOptions>() { | ||
|
|
||
| /** | ||
| * The command runner used to run commands on the device. | ||
| */ | ||
| @Suppress("LeakingThis") | ||
| protected val shellCommandRunner = shellCommandRunnerSupplier(this) | ||
|
|
||
| init { | ||
| if (!shellCommandRunner.hasRootPermission()) throw NoRootPermissionException() | ||
| } | ||
|
|
||
| /** | ||
| * 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() | ||
|
Ushie marked this conversation as resolved.
|
||
|
|
||
| // 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 | ||
|
Ushie marked this conversation as resolved.
|
||
|
|
||
| return RootInstallation( | ||
| INSTALLED_APK_PATH(packageName)().output.ifEmpty { null }, | ||
|
|
@@ -97,6 +124,22 @@ abstract class RootInstaller internal constructor( | |
| ) | ||
| } | ||
|
|
||
| fun getInstalledVersionName(packageName: String): String = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why were these APIs added here instead of a utils class? Is it because it needs the shell command runner? In the case of a utils class a command runner can be passed to it for this purpose |
||
| 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") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isnt no root permission simpler |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason this class is inside the RootInstallerOptions.kt class? Class apk is inside the Installer class, so this class should be inside RootInstaller.kt
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, but since the options were moved out of the Installer class, maybe class APK should move to InstallerOptions instead since thats where it's usage is
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be breaking change, refactoring like this can come in another PR, for now put APK in RootInstaller for consistency |
||
Uh oh!
There was an error while loading. Please reload this page.